正しく届いたbaggageも権限の証明ではない
一言でいうと
baggageの伝播は、情報を運ぶ機能です。このラボでは、外部の値をローカルのリクエストと混ぜずに読み取り、そのうえで、決められた宛先へ、決められたキーと値だけを送り出します。読み取れるという事実を、認証・認可や個人情報の安全性の保証に置き換えることはしません。
なぜ必要なのか
在庫サービスに向かう注文リクエストに、regionとchannelを付けたとしましょう。運用者は、地域・流入経路ごとに、遅い区間を分けて見たいと考えています。ところが、同じbaggageに、誰かがrole=adminやメールアドレスの形の値も入れていました。全体を別のサーバーへ注入するコードは、文法上は正しいですが、必要のない情報まで境界を越えさせます。
さらに危険な誤解は、role=adminが見えるから管理者のリクエストだと判断することです。このモジュールの入力は、誰でも作れる合成のヘッダーです。値を読む関数と、ユーザーの権限を検証するシステムは、完全に別物で、このラボには認証サーバー自体がありません。ヘッダーを通過させたという観測から、権限検証の成功を主張することはできません。
OpenTelemetryのドキュメントは、baggageには整合性を確認する機能が組み込まれておらず、伝わる情報の露出に注意する必要があると説明しています。また、baggageがスパン属性に自動で入るわけではありません。公式のBaggageの説明を参照してください。このモジュールは、その警告を、小さな入出力関数の反例で確認します。
どう動くのか
入ってきたContextと、すでに付いていたContext
事前実験で、呼び出し側にrequest=alphaを付けてから、外部のbaggageを抽出しました。デフォルトの抽出結果には、外部の値だけでなく、既存のローカルのrequestも一緒に残りました。空のOpenTelemetry Contextを明示した抽出では、外部の値だけが残りました。最初は2つの結果が同じになると予想しましたが、実際の実行で外れ、その違いを課題にしました。
| このラボの抽出条件 | 観測したbaggage |
|---|---|
| ローカルのrequestがある状態でのデフォルトの抽出 | ローカルのrequestと外部の値が一緒に残ります |
| 空のContextを指定した抽出 | 外部の値だけがあります |
| 空のContextと空のヘッダー | 空のbaggageです |
ステップ6のinboundは、外部ヘッダーを別のコンテキストで読む関数です。入力のdictと呼び出し側の現在の状態を変更せず、新しいOTel Contextを返す必要があります。外部のrole=adminも、このステップでは読み取った結果に残ります。これは、信頼したり使用したりするという意味ではなく、「抽出」と「ポリシーに従った選択」を、別々の動作として観測するための条件です。
ここでの空のContextは、OpenTelemetryのContextです。前にexecutorで使ったcontextvars.Contextと名前が似ていますが、返り値の契約まで同じオブジェクトではありません。関数ごとに、何を返すよう求められているかを確認してください。型が間違っていると、値がたまたま同じでも、そのあとの伝播APIにうまくつながりません。
空のコンテキストで読めば、既存のリクエストとの混合は防げます。しかし、外部の値そのものの真偽がわかるようになるわけではありません。この限界をはっきり書いておかないと、「新しいContextだから安全だ」という、また別の万能ルールが生まれます。業務上の権限は、別途、検証済みの本人情報とポリシーで判断する必要があります。
キーだけを許可すれば十分か
ステップ7は、outbound(source, destination)関数を直します。regionという名前だけを許可したからといって、その中に入った任意の文字列を送ってよいわけではありません。誰かがregionの値として長い説明や識別子を入れたなら、名前は許可リストにあっても、内容は課題の分類体系から外れます。そのため、今回の課題は、値の集合まで閉じます。
| 条件 | この課題で許可するもの |
|---|---|
| 宛先の文字列 | warehouse.internalと完全に同じであること |
| regionの値 | test-eastまたはtest-west |
| channelの値 | webまたはbatch |
| それ以外のキー・値・宛先 | 伝播しません |
名前と値は、実際の運用情報ではなく、ラボ用の合成の分類です。許可される値が有限の短い文字列なので、課題の中では、任意の長さの文字列も除外されます。しかし、これをすべてのサービスに合う一般的な個人情報ポリシーと呼ぶことはしません。実際のポリシーは、情報の目的、受信者、保持期間、アクセス主体とあわせて決める必要があり、このラボはそのシステム全体を試験しません。ここで検証するのは、上の表を関数が正確に守ったかどうかです。
大文字・小文字や空白も、勝手に直しません。WEBやtest-eastのあとの空白は、この契約で許可される値ではありません。入力を正規化するか、拒否するかは、インターフェース設計で決めるべき選択です。この課題は、黙って正規化する代わりに、許可された文字列だけを渡すよう明示しています。
宛先の境界は、文字列の接頭辞ではない
warehouse.internal.evilは、warehouse.internalで始まりますが、同じ宛先ではありません。ラボは、この反例を実際の関数に入れます。startswith1つで許可すると、地域の値のフィルターが合っていても、宛先ポリシーで失敗します。別の宛先には空のdictを返す必要があり、許可された宛先でも、残る値がなければ、空のbaggageヘッダーすら作りません。
重要な範囲の制限があります。ここでdestinationは、論理的な宛先の識別子です。DNS照会、URLのパース、TLS証明書、HTTPリダイレクトは実行しません。したがって、この関数の文字列比較が通ったからといって、実際のHTTPクライアントの宛先検証やSSRF対策を完了したと言ってはいけません。ネットワーク接続があるシステムなら、その境界は別に設計して試験する必要があります。
元を消す代わりに、送るコンテキストを作る
受講者の関数は、sourceから必要な値だけを選んで新しいContextに入れ、W3Cのプロパゲーターでヘッダーを作ります。元のroleやほかの値が不要だからといって、元そのものを変更する課題ではありません。元は、別のポリシーの処理でも使われることがあるため、この関数の契約は保全です。採点は、伝播の結果を再度抽出して比較し、元も前後で照合します。
ヘッダー文字列の順序が、正解と完全に同じである必要はありません。意味が同じW3Cのbaggageなら、キーの順序を理由に不合格にしません。代わりに、許可されていないキーが別のヘッダー名で漏れると失敗します。返すdictは、baggageの文字列ヘッダー1つか、空のdictだけを許可します。順序に厳しく、リークに甘い採点にならないようにしたものです。
baggageとスパン属性は別の観測である
事前実験では、baggageを付けた状態で、実際のSDKのスパンを終了し、エクスポーターで読み取りました。baggageは、スパン属性に自動では現れませんでした。ステップ8のレポートの該当する仮説は、この観測と公式の説明に基づいて答えます。現在のラボのステップ1–7は、Context関数の動作を検証するもので、毎ステップでスパンを作ったりCollectorに保存したりするわけではありません。
伝播する情報と、スパンに記録する情報を区別すると、調査の計画もより明確になります。どのキーがヘッダーとして出ていったか、受信側のContextに入ったか、スパン属性に選択的に記録されたか、ストレージに残ったかは、それぞれ別の質問です。1か所で値を見たからといって、残りの境界を確認したかのように報告しないでください。
現場での姿
この練習をもとにコードレビューを行うなら、正常なヘッダー1つだけを見ません。空のヘッダー、不明なキー、既知のキーの誤った値、別の宛先、許可されたホストのように見える接頭辞、元のリクエストのローカルの値がある場合を、あわせて照合します。実際のデータがなくても、各境界の反例を作れます。合成データで失敗条件を先に再現すれば、不要な運用情報を収集せずに、コードの責任範囲を絞れます。
次のラボですること
ステップ6は、入ってきた値を、既存のリクエストと分離して読みます。ステップ7は、表の宛先・キー・値のポリシーで、送り出すヘッダーを作ります。ステップ8は、伝播の時点・復元・信頼の範囲に関する8つの仮説を判定します。レポートだけ合っていれば合格する課題ではありません。前の7つの関数の実際の動作がすべて合っていてはじめて総合検証に合格し、合格の範囲を外部認証やネットワークセキュリティ全体に広げることはしません。