無印吉澤

Site Reliability Engineering(SRE)、ソフトウェア開発、クラウドコンピューティングなどについて、吉澤が調べたり試したことを書いていくブログです。

Building Secure and Reliable Systems 読書メモ - Chapter 6

f:id:muziyoshiz:20200430014145p:plain:w320

月一連載になっている SRS 本の読書メモです。

Chapter 6 の "Design for Understandability" は、最初にタイトルを見たときから気になっている章でした。実際読んでみて、勉強になる考え方が多かったです。

じっくり英訳しようとすると全然先に進めなくなってしまうので、今回から若干翻訳の手を抜いて、ほとんど意訳で書いてるのと、一部は英語のまま引用してます。また、細部はかなり省略してしまってます。興味が湧いた部分については原文をあたってみてください。

本章で頻出する "Understandability" という単語は、以下のメモでは「わかりやすさ」と訳しました。「理解可能性」のように訳したほうがいいかとも思ったのですが、文章が固くなりすぎるように感じたので、このメモでは「わかりやすさ」にしました。

また、本章には、"reason about" という表現も頻出します。これは「すべてのコードを深く読んで理解しなくても、システムの不変条件やメンタルモデルからそのことを説明できる」というような意味で使われています。適切な訳という自信はありませんが、以下では「論証する」と訳しました。

SRS 本について

SRS 本はこちらから無償でダウンロードできます。

前回までの読書メモは SRS Book タグ を参照のこと。

Chapter 6. Design for Understandability

本章では、システムのわかりやすさ(understandability)について議論する。

最初に、不変条件(invariants)とメンタルモデル(mental models)に従って、あなたのシステムを分析し、理解する方法についての議論から始める。そして、アイデンティティ、認可、アクセス制御のための標準フレームワークに基づく階層化されたシステムアーキテクチャが、わかりやすいシステムの設計を助けることを示す。

そして、セキュリティ境界(security boundaries)に関する話題を題材として、どのようにソフトウェア設計(特に、アプリケーションフレームワークとAPIの利用)が、セキュリティおよび信頼性に関する特性(security and reliability properties)に対する論証(reason about)を助けるかを示す。

本書では、システムのわかりやすさ(understandability)とは、関連分野の専門知識を持つ人が、以下の両方を、正確かつ自信を持って論証できる度合いと定義する。

  • The operational behavior of the system(システムの運用上の振る舞い)
  • The system’s invariants, including security and availability(セキュリティや可用性を含む、システムの不変条件)

Why Is Understandability Important?

わかりやすいシステム(understandable system)を設計し、そのわかりやすさを維持するには努力を要する。わかりやすいシステムは、プロジェクトのベロシティを維持するという(4章で示した)利点もあるが、それ以上に、以下のような明確な利点がある。

  • Decreases the likelihood of security vulnerabilities or resilience failures
    • わかりにくいシステムでは、修正時にバグや脆弱性を生みやすい。またそれらに気づきにくい
  • Facilitates effective incident response
    • わかりにくいシステムは、インシデント発生時の対応〜根本原因の解決を妨げる
  • Increases confidence in assertions about a system’s security posture
    • システムのセキュリティに対するアサーション(表明)は、一般に、不変条件(invariants)と呼ばれる。不変条件とは、そのシステムの、すべてのありうる振る舞いに対して成立しなければならない特性(property)のことである
    • 例えば、外部からの入力(悪意あるものを含む)に対して必ず特定の検査が行われる、というのは不変条件である。そのような抽象化がなければ、システムが外部からの入力に対して安全かどうかの論証が難しい

System Invariants

システムの不変条件(system invariant)とは、そのシステムを取り巻く環境が正しく振る舞うか間違って振る舞うかに関係なく、常に真である特性のこと。ここで言う環境は、あなたが直接コントロールできないものもすべて含む。

以下は、セキュリティと信頼性に関する望ましい特性の例:

  • Only authenticated and properly authorized users can access a system’s persistent data store.
  • All operations on sensitive data in a system’s persistent data store are recorded in an audit log in accordance with the system’s auditing policy.
  • All values received from outside a system’s trust boundary are appropriately validated or encoded before being passed to APIs that are prone to injection vulnerabilities (e.g., SQL query APIs or APIs for constructing HTML markup).
  • The number of queries received by a system’s backend scales relative to the number of queries received by the system’s frontend.
  • If a system’s backend fails to respond to a query after a predetermined amount of time, the system’s frontend gracefully degrades—for example, by responding with an approximate answer.
  • When the load on any component is greater than that component can handle, in order to reduce the risk of cascading failure, that component will serve overload errors rather than crashing.
  • A system can only receive RPCs from a set of designated systems and can only send RPCs to a set of designated systems.

あなたのシステムがこれらの望ましいセキュリティ特性に反している、つまり不変条件が実際には不変条件ではない、という場合には、そのシステムはセキュリティ上の弱点や脆弱性を持つということになる。

上記リストの4番目にあるような、信頼性に関する特性についても同様である。フロントエンドが外部から受け取った以上のリクエストを、バックエンドに対して生成するようなシステムは、可用性に関する潜在的な弱点を持つといえる。

Analyzing Invariants

意図した特性が実際に不変条件として成立しているかは、解析しないとわからない。不変条件が成立していないことによって起こりうる悪影響と、不変条件が成立するかを検証するために割く労力は、トレードオフの関係にある。

両極端な例を上げると、例えば少しのテストとバグチェックのためのソースコードレビューをするくらいでは、不変条件の違反を見逃す恐れがある。一方で、マイクロカーネルの実装で行われているように、セキュリティ特性を形式論理で証明できれば完璧だが、そのようなことを大規模なアプリケーション開発に適用するのは難しい。

本章では、その両極端な例の間にある、現実的な妥協案を示す。わかりやすさについての明確な目標を持ってシステムを設計することで、システムが特定の不変条件を満たすことを(形式的にとまではいかなくても)原理的に示すことができ、それらのアサーションをかなり高い度合いで信頼できる。Google では、このアプローチが、大規模ソフトウェア開発に対して実用的で、かつよくある種類の脆弱性の発生を効果的に減らすことができることを発見した。テストと検証についての詳細は Chapter 13 で示す。

Mental Models

複雑なシステムをそのまま全体的に理解することは難しいため、エンジニアや特定分野のエキスパートは、不必要な詳細を省いたメンタルモデルを構築する。複雑なシステムでは、メンタルモデルが複数になることもある。

メンタルモデルは有用だが、限界もある。もし、典型的な運用シナリオに基づいてメンタルモデルを作ったら、それは通常でない場合のシナリオでのシステムの振る舞いの予測には使えない。

例えば、通常はリクエスト数の増加に応じて段階的に負荷が増えるシステムが、ある閾値を超えると、全く違う振る舞いを見せることがある。そのようなシステムでは、単純すぎるメンタルモデルは、トラブルシューティングを阻害する。

極端な状況、または通常でない状況でも、システムが予測可能な振る舞いをして、メンタルモデルが保たれるようにシステムを設計すべきである。そのようなシステムであれば、障害発生時にも、そのメンタルモデルは有用なままになる。

※本文ではメモリのスラッシングを例に挙げて、システムの振る舞いを予測可能にする方法を例示している。

Designing Understandable Systems

この章の残りでは、システムをよりわかりやすくし、そのシステムが日々進化するなかでもわかりやすさを維持し続けるために使える、いくつかの具体的な指標について議論する。

Complexity Versus Understandability

わかりやすさに対する最初の敵は、管理されていない複雑さ(unmanaged complexity)である

いくつかの複雑さは、現在のソフトウェアシステム(特に分散システム)の規模と、それらのシステムが解決する問題による生来のもので、回避できない。Google では数万のエンジニアが、10億行を超えるコードを含むソースコードリポジトリを編集しているが、そこまでの規模でない組織でも、1つのプロダクトで数百を超える機能を提供することはざらにあるだろう。

例えば、Gmail は多くの機能を提供している(p.94 のリスト)。このように多数の機能を提供するシステムは、機能がより少ないシステムよりも本質的に複雑である。しかしこれらの機能は価値を生み出しているので、複雑さを下げるためにそれらの機能を削除しろとは言えない。私達がそれらの複雑さを管理できれば、システムを十分にセキュアかつ高信頼にすることができる。

私達の目標は、この本質的な複雑さをより小さい部品・コンパートメントに分割して含むように、システムの設計を構成することである。そして、それは具体的かつ重要なシステム特性や振る舞い(specific, relevant system properties and behaviors)を、高い確度で人間が論証できるような方法で行わなければならない。言い換えると、私達はわかりやすさの観点に立って、複雑さという特性を管理しなければならない。(※なんとなく言いたいことはわかるがうまく訳せない)

本書ではセキュリティと信頼性の観点で、わかりやすさについて議論している。しかし、私達が議論するパターンの話は、この2つの観点に限ったものではなく、一般的なソフトウェア設計テクニック全般に渡る。システム及びソフトウェア設計に関する全般的な読み物としては、John Ousterhout の A Philosophy of Software Design を参照のこと。

Breaking Down Complexity

複雑なシステムの振る舞いのすべての側面を理解するためには、巨大な1個のメンタルモデルを内面化して維持する必要がある。しかし、そのようなことは人間には難しい。

システムを複数の小さなコンポーネントにわけて、それを組み合わせることで、システムをわかりやすくすることができる。そして、個々のコンポーネントの特性を論証可能にして、そこからシステム全体の特性を引き出せるようにする。

このアプローチは、現実には単純明快ではない。そのようなことが可能かどうかは、そのシステム全体がどのように複数のコンポーネントへと分解されているか、そしてそれらのコンポーネント間のインターフェイスと信頼関係の性質に依存する。この点については p.97 からの "System Architecture" にて見ていく。

Centralized Responsibility for Security and Reliability Requirements

セキュリティおよび信頼性に関する要求(例えば何らかのチェック処理)を個々のコンポーネントで実装していたら、そのシステム全体がそれらの要求を満たしているか判断するのは難しい。

Chapter 4 で議論したように、そのような共通タスクを実装する責任は、その組織共通のコンポーネント(ライブラリやフレームワーク)に集約させることができる。

例えば、サービスで共通して必要なセキュリティ機能(認証、認可、ロギング)は RPC サービスフレームワークが実装する。また、信頼性に関する機能(リクエストのタイムアウトなど)も RPC サービスフレームワークに任せる。

セキュリティと信頼性に関する要求に関する責任を1箇所に集中させることで、以下の利点が得られる。

  • Improved understandability of the system
    • レビュアーは、システムの1箇所を見るだけで、セキュリティおよび信頼性に関する要求が満たされているかどうかを判断できる
  • Increased likelihood that the resulting system is actually correct
    • アプリケーション上での要求のアドホックな実装が間違っていたり、実装し忘れている可能性について考えずに済むようになる

System Architecture

システムを複数のレイヤーとコンポーネントに分割することは、複雑さを管理するためのキーツールである。

しかし、この分割をどのように行うかは、慎重に考える必要がある。もしコンポーネント同士が密結合していたら、モノリシックシステムと同様に、理解が難しいシステムになってしまう。システムをわかりやすくするためには、境界とインターフェイスに注意を払う必要がある。

経験豊富なソフトウェアエンジニアは、外部の環境から来た入力は信頼すべきでなく、それらの入力についてシステムはなんの仮定も置いてはいけない、ということを知っている。その一方で、システム内部からの呼び出しについては信頼し、想定通りの呼び出し方をされると期待しがちである。

しかし、システムのセキュリティ特性が実現されるかどうかは、実際にはシステム内部からの入力が正しいかどうかに依存する。そのため、セキュリティ特性を論証可能にするためには、システム内部からの呼び出しについてもなんの仮定も置けない。

コンポーネントが呼び出し元に対して置く仮定を減らせば減らすほど、そのコンポーネントの特性の論証が容易になる。理想的には、そのような仮定は全く無くしたい。

もし、呼び出し元について仮定を置くことを強制される場合は、その仮定を、インターフェイスの設計に明示的に加える事が重要である。例えば、呼び出し元のプリンシパルを制限する。

Understandable Interface Specifications

構造化されたインターフェイス(structured interfaces)、一貫したオブジェクトモデル(consistent object models)、そして冪等なオペレーション(idempotent operations)は、システムのわかりやすさに貢献する。

  • Prefer narrow interfaces that offer less room for interpretation
    • フレームワークは、RPC メソッドの入出力に対する型定義の機能を(gRPC, Thrift, OpenAPI のように)持っていたほうがいい
    • OpenAPI や Protocol buffer が持つようなバージョニング機能の有無は、将来のアップグレードのしやすさに影響する(アップデートに関する Protocol buffer のガイドライン
    • 任意の文字列(JSON の string 型)を受け付けるような API は、入出力が制約されないため、わかりやすさを損ないやすい。また、例えばクライアントとサーバが別のタイミングでアップデートされるときに、片方の処理を壊すことがありうる
    • 明確な API 仕様がないと、例えば Istio Authorization Policy のような認可フレームワーク上のポリシーと、サービスが公開する実際の境界面(actual surface area)を関連付けるための、自動的なセキュリティ監査システムの構築は難しくなる
  • Prefer interfaces that enforce a common object model
    • 複数のタイプのリソースを管理するシステムは、Kubernetes Objects のような共通オブジェクトモデル(common object model)から恩恵を得られる
    • そのオブジェクトで各リソースを取り扱えるようになるだけでなく、エンジニアが巨大なシステムを理解するのに役立つ単一のメンタルモデルを提供する(p.99 にその利点についての箇条書きあり)
    • Google は resource-oriented APIs を設計するための一般的なガイドライン を公開している
  • Pay attention to idempotent operations
    • API 呼び出しは失敗したり、成功したあとでその応答が届かないことがある。API が冪等(idempotent)であれば、呼び出し元は再実行すればいい。もし、冪等でなければ、呼び出し結果を確認するためのポーリングが必要になる
    • 冪等性は、エンジニアのメンタルモデルにも影響する。例えば、エンジニアがなにかデータを作成する API を冪等だと思って呼び出しているのに、実際にはそうでなかった場合、そのデータが意図せず重複して作られる、ということが起こりうる
    • 本質的に冪等でない処理であっても、サーバ側で冪等にすることができる。上記の例では、リクエストに一意な識別子(例:UUID)を含めることで、2回目の API 呼び出しをサーバ側で検出して、重複したデータの作成を防げる

Understandable Identities, Authentication, and Access Control

どんなシステムでも、機密性の高い(highly sensitive)リソースに、誰がアクセスしたかは特定できるようにすべきである。例えば課金システムでは、PII(personally identifiable information)データに、社内の誰がアクセスしたかを、監査できる必要がある。監査フレームワークは、アクセスのログを取り、そのログを自動的に解析し、定期的なチェックや、インシデント調査に活用できるべきである。

Identities

アイデンティティ(identity)とは、属性の集合、またはあるエンティティに関連する識別子のことである。クレデンシャル(credentials)は、特定のエンティティのアイデンティティを主張する。クレデンシャルは、認証プロトコル(authentication protocol)を使って送信される。

システムがどのように人間のエンティティ(顧客および管理者)識別するかを論証することは相対的に簡単な一方で、巨大なシステムは人間以外のエンティティも識別できる必要がある。マイクロサービスの集合から構成された巨大システムは、人が介在する場合もしない場合も含めて、相互に呼び出し合う。エンティティには、人、ソフトウェアコンポーネント、ハードウェアコンポーネントが含まれる。

一般に、アイデンティティと認証のサブシステム(identity and authentication subsystems)は、アイデンティティのモデル化と、それらのアイデンティティを表すためのクレデンシャルの提供に責任を持つ。アイデンティティが意味があるもの("meaningful")であるためには、以下の条件を満たす必要がある。

  • Have understandable identifiers
    • 人間にとってわかりやすい文字列であること(例:widget-store-frontend-prod のような文字列)
    • わかりやすい文字列には、設定ミスを防げたり、異常なアクセスにすぐ気づけたりするメリットがある
    • これが 24245223 のような無意味な数字だと、間違った設定をしていても気づけない
  • Be robust against spoofing
    • ID/password を clear-text channel でやり取りするようなものは簡単に spoofing できてしまう
  • Have nonreusable identifiers
    • 例えば、社員のメールアドレスを管理ツールのアイデンティティに使っていて、その管理者が退職したあとで同じメールアドレスを新入社員に割り当てたら、いろいろな権限が渡ってしまう可能性がある

意味のあるアイデンティティを、システム上のすべてのアクティブなエンティティに割り当てることが、わかりやすさへの基本的な第一歩である。組織内で統一されたアイデンティティシステム(organization-wide identity system)は、エンティティを参照するための共通の方法を提供し、全従業員が共通のメンタルモデルを持つことを助ける。

Example: Identity model for the Google production system.

例として、Google のシステムでは、異なる種類のアクティブなエンティティを以下のように分類している。

  • Administrators
    • システムの状態を変更する(例えば新しいリリースを行ったり、設定を変更する)権限を持つエンジニア
    • Administrator は、システムの状態を直接変更せず、そのような一連のワークロードを起動する
    • そのアイデンティティは global directory service で管理されて、シングルサインオンに使われる
  • Machines
    • Google データセンタ内の物理マシン
    • global inventory service/machine database で管理されている
    • Google プロダクション環境では、マシンは DNS 名で解決可能
    • そのマシン上で動作するソフトウェアの修正権限を表すために、administrators と machines の間の関連を管理する
  • Workloads
    • Borg オーケストレーションによってスケジューリングされたワークロード
    • ワークロードと、それが動作するマシンのアイデンティティは、大抵の場合異なる
    • オーケストレーションシステムは、ワークロードと、それが動作可能なマシンの関係(制約)を強制する
  • Customers
    • Google が提供するサービスにアクセスする顧客
    • Customers のアイデンティティは、専用のアイデンティティサブシステムで管理される
    • Google は OpenID Connect workflows を提供しており、顧客は Google identity を、Google 外のエンドポイントで使えるようにしている
Authentication and transport security

認証およびトランスポートセキュリティの分野は難しいので、すべてのエンジニアが完全に理解することは期待できない。その代わりに、それらの抽象化とAPIを理解すべき。

Google ではエンジニアに Application Layer Transport Security (ALTS) を提供している。ALTS は、以下のようなシンプルなメンタルモデルを提供する。

  • An application is run as a meaningful identity:
    • A tool on an administrator’s workstation to access production typically runs as that administrator’s identity.
    • A privileged process on a machine typically runs as that machine’s identity.
    • An application deployed as a workload using an orchestration framework typ‐ ically runs as a workload identity specific to the environment and service pro‐ vided (such as myservice-frontend-prod).
  • ALTS provides zero-config transport security on the wire.
  • An API for common access control frameworks retrieves authenticated peer information.

ALTS に類似のシステムとして、Istio のセキュリティモデル がある。

このようなシステムがない場合、セキュリティ対策が正しく行われるか確認するためには、すべてのアプリケーションコードに目を通す必要が出てきてしまう。

Access control

アクセス制御の部分をフレームワークで提供することは、システム全体のわかりやすさを大きく向上させる。フレームワークはアクセス制御に関する共通の知識、共通の方法をエンジニアに提供する。

フレームワークは、複数のコンポーネント(Ingress → Frontend → Backend)を渡るワークロードに対するアクセス権限、といった難しい問題の扱いを助けることができる。このようなワークロードチェインでは、リクエストを送った Customer の権限と、ワークロードを実行する Machine の権限があり、両方を考慮する必要がある。

Security Boundaries

あるシステムの信頼されたコンピューティングベース(trusted computing base, TCB)とは、「セキュリティポリシーが適用されることを確実にするのに十分な正確な機能を持つ(ハードウェア、ソフトウェア、人間、...)の集合であり、より端的に言えば、その失敗がセキュリティポリシーの違反を引き起こす可能性があるもの」である(Anderson, Ross J. 2008. Security Engineering: A Guide to Building Dependable Distributed Systems. Hoboken, NJ: Wiley. からの引用)。

TCB と「それ以外のすべて」の間のインターフェイスを、セキュリティ境界(security boundary)と呼ぶ。TCB は、そのセキュリティ境界を超えてくるすべての入力を、信頼できないものとして扱わなければならない。

Chapter 4 の web application(ウィジェット販売サイト)を例に挙げる。このサイトは発送先住所を扱う。1個のサーバがすべての機能を提供するモノリシックシステムでは、発送先住所の TCB(TCB_AddressData)はそのシステム全体になる。例えば、発送先住所とは関係ない機能(例えばカタログ検索機能)に SQL インジェクション脆弱性があると、それは即座に発送先住所の漏洩に繋がってしまう。

※ 以下の TCB の具体例については、図を見たほうがわかりやすいと思う。p.107 以降を参照のこと。

アプリケーションをマイクロサービスに分割することで、TCB を小さくし、セキュリティを改善することができる。上記の例では、カタログ検索機能のバックエンドおよびデータベースを、(発送先住所を含む)販売機能のバックエンドおよびデータベースと分けることで、発送先住所の TCB を小さくできる。

ただ、このようにマイクロサービスに分割しても、フロントエンドに全ユーザーの発送先住所を表示できる機能の実装を許すと、TCB の範囲は web frontend まで広がる。Web frontend の脆弱性が、全ユーザーの発送先住所の漏洩に繋がるためである。

フロントエンドに強すぎる権限を与える代わりに、フロントエンドとバックエンドの間で、OAuth2 のトークンのような エンドユーザーコンテキストチケット(EUC) をやり取りするという方法がある。この方法では、フロントエンドは任意のユーザーの EUC を取得することができず、フロントエンドの脆弱性があっても、攻撃者がアクセスできるデータは限定される。

また、カタログ検索機能と販売機能のフロントエンドが同一のドメインで動いていたら、片方の XSS 脆弱性が、もう一方にも影響してしまう。つまり TCB は再びシステム全体にまで広がってしまう。両者のフロントエンドが別ドメインで動いていれば、TCB は狭まる。

TCB にはセキュリティ上の利点だけでなく、システムをわかりやすくするというメリットもある。TCB として成り立つことを示すためには、コンポーネントはそれ以外の部分から独立していなければならない。そのためには、コンポーネントは明確に定義された、きれいなインターフェイスを持たなければならない。もし、コンポーネントの正しさが、そのコンポーネント外の環境に対する仮定に依存するなら、それは定義上 TCB とは言えない。

TCB はしばしばそれ自身の failure domain を持ち、バグの発生や DoS 攻撃、またはその他の運用上の影響に対して、どのように振る舞えばいいかについての理解を与える。Chapter 8 で、システムを区分することによる利点について、さらに詳しく議論する。

Software Design

巨大なシステムを一度、セキュリティ境界によって区分された複数のコンポーネントによって構造化したあとも、あなたはそのすべてのコードとサブコンポーネントを引き続き論証し続ける必要がある。そして大抵の場合、分割後のコードも十分に巨大かつ複雑なソフトウェアになる。

この節では、不変条件を、より小さいソフトウェアコンポーネント(モジュールやライブラリ、API)のレベルでさらに論証できるように、ソフトウェアを構成するための技術について議論する。

Using Application Frameworks for Service-Wide Requirements

これまでに議論したように、フレームワークは再利用可能な機能の部品を提供する。あるシステムは、認証フレームワーク、認可フレームワーク、RPC フレームワーク、オーケストレーションフレームワーク、監視フレームワーク、ソフトウェアリリースフレームワーク、などなど……から成るということがありうる。これらのフレームワークは柔軟性を持つが、柔軟性を持ちすぎる。あり得るフレームワークの組み合わせや、その設定方法は無数にあり、エンジニアを圧倒するほどである。

Googleでは、この複雑さを管理するために、より高レベルのフレームワークを作ることが有用だと気づいた。これをGoogleでは "application frameworks" と呼ぶ。あるいは、"full-stack" または "batteries-included frameworks" とも呼ばれる。

Application frameworks は、サブフレームワークの標準的なセットを、妥当なデフォルト値と、それらのサブフレームワークが相互動作することの保証とともに提供する。

例えば、開発者が自分たちのお気に入りのフレームワークを使った開発したとする。そして、認証のための設定はしたが、認可やアクセス制御のための設定をし忘れるということがありうる。Application framework は、すべてのアプリケーションが有効な(valid)認可ポリシーを持つことを保証し、明示的な許可を持たないすべてのアクセスを拒否するというデフォルト設定を持つ。したがって、設定し忘れのような問題を回避できる。

全般的に、application frameworks は、アプリケーション開発者やサービスオーナーが必要とするすべての機能(下記)を有効化および設定するための、こうあるべきという方法(an opinionated way)を提供できなければならない。

  • Request dispatching, request forwarding, and deadline propagation
  • User input sanitization and locale detection
  • Authentication, authorization, and data access auditing
  • Logging and error reporting
  • Health management, monitoring, and diagnostics
  • Quota enforcement
  • Load balancing and traffic management
  • Binary and configuration deployments
  • Integration, prerelease, and load testing
  • Dashboards and alerting
  • Capacity planning and provisioning
  • Handling of planned infrastructure outages

※ "deadline" は SRE 本では「タイムアウト」と訳されている。deadline propagationは「タイムアウトの伝播」。例えば、全体のタイムアウトが30秒で、あるリクエストで10秒かかったら、後続処理のタイムアウト時間は20秒にする、というように伝播させる。

Understanding Complex Data Flows

多くのセキュリティ特性は、システムの中を流れる値(values)についてのアサーションに依存している。

例えば、多くの Web サービスは様々な目的に URL を使っている。URL パラメータは string 型。string 型の値は、その URL が well-formed かどうかに関わらず、その値に関する明示的なアサーションを渡すことができない。したがって、セキュリティ特性を論証するためには、システムのなかで、その文字列が正しく渡されていくかを、上流のコード(upstream code)すべてを読んで理解する必要がある。

型を用いれば、検証すべきコードを大幅に減らすことができて、システムをわかりやすくする助けになる。URL を型として表現し、例えば Url.parse() のようなコンストラクタにデータ検証処理を実装すれば、あなたはその Url.parse() のコードを理解するだけでセキュリティ特性を論証できるようになる。

このような型の実装は、TCB のように振る舞う(すべての URL が well-formed である、という特性に責任を持つ)。しかし、これはその型(のモジュール)を呼び出す側のコードに悪意がなく、周りの環境に欠陥がある(compromised)状態ではない、という仮定があったうえでの話である。そのような仮定が置けない場合は、セキュリティチームは結果として発生する最悪のシナリオに対処する必要がある(Part IV を参照)。

より複雑なセキュリティ特性に対して、型を用いることもできる。例えば、インジェクション脆弱性(XSS または SQL インジェクション)は、外部から渡された信用できない入力に対する検証やエンコーディングに依存する。このような脆弱性を防ぐための効果的な方法の一つが、SQL クエリや HTML マークアップのような injection sink context(※なんて訳せばいい??)で安全に使えるとすでにわかっている値とそうでない値を区別するために、型を使うという方法である。

  • 内部的に、SafeSql または SafeHtml のような型を用意する。安全だと検証される前の String と区別する
  • SQL や HTML を受け取る側が、String ではなく、SafeSql または SafeHtml 型を受け取るようにする
  • 引き続き String 型を受け取るかもしれないが、その値の安全性に対してなんのアサーションも持ってはならない

このようにすることで、アプリケーション全体が XSS 脆弱性に対して安全かどうかを調べるためには、type と、type-safe sink API の実装のみを理解すればよくなる。Chapter 12 では、呼び出し元についての仮定をなにも持たずに、型の contract を保証するためのアプローチについて議論する。

Considering API Usability

API の適用および利用が、あなたの組織の開発者とその生産性に与える影響について考えるのは良いアイディアだ。もし API が使いづらければ、開発者はその採用に気乗りせず、動きが遅くなってしまう。

Secure-by-construction API(※なんて訳せばいい?? 構造による保証?)は、コードをわかりやすくし、エンジニアがそのアプリケーションのロジックに集中できるようにする、という二重のメリットを持つ。加えて、セキュアなアプローチをあなたの組織文化へと自動的に組み込む。

開発者にとって利益のあるライブラリやフレームワーク(secure-by-construction APIs のようなもの)を設計することは、可能である。また、これはセキュリティと信頼性の文化を促進する。

例として、エスケープを自動的に行う HTML テンプレートシステムを考える。これは、XSS 脆弱性を起こさないことを保証するセキュリティ不変条件であると同時に、開発者の観点では通常の HTML テンプレートと変わらず利用できる。

暗号化技術を正しく使うのはとても難しく、微妙なミスが脆弱性を生むことが多い。そのような経験から、Google では Tink という名前のライブラリを開発した。Tink は、Google が開発するアプリケーション内で、暗号化技術を安全に使う ことを簡単にする(間違って使うことを難しくする)ような API を提供する。

Tink の設計および開発を導いた原則:

  • Secure by default
  • Usability
  • Readability and auditability
  • Extensibility
  • Agility
  • Interoperability
    • Tink is available in many languages and on many platforms.

通常の暗号化ライブラリは、ローカルディスク上に秘密鍵を持つことを簡単にしているが、そのようなライブラリを使うと、鍵管理に関するインシデントを完全に防ぐことは難しい。対象的に、Tink の API は生の鍵情報(raw key material)を受け付けない。その代わりに、Tink はCloud Key Management Service (KMS)、AWS Key Management Service、Android Keystore のような鍵管理サービスと統合された鍵管理機能を提供することで、鍵管理サービスの利用を促進する。

注意点として、データの重要さに合わせて、適切な暗号化方法を選ぶのは Tink を利用するエンジニアの責任である。そのような設計レベルのミスを Tink で防ぐことはできない。ソフトウェア開発者とレビュアーは、セキュリティおよび信頼性に関する特性のうち、ライブラリやフレームワークが保証するものとしないものをしっかり理解しなければならない。

Tink に限界があるのと同様に、 secure-by-construction web framework も、XSS 脆弱性を防ぐことはできるが、アプリケーションのビジネスロジックに含まれるセキュリティバグは防げない。

Conclusion

システムのわかりやすさと、信頼性およびセキュリティは深く結びついている。

わかりやすいシステムを構築するための私たちからの第一のガイダンスは、システムを、明確かつ限られた目的を持つコンポーネントを用いて構築することである。それらのコンポーネントのなかにはそれぞれの TCB を構成するものもあり、セキュリティリスクへの対応に集中するものもある。

また、私達は望ましい特性(例えばセキュリティ不変条件や、構造的な回復力、データ耐久性)を、それらのコンポーネント内やコンポーネント間で強制するための戦略についても議論した。これらの戦略は以下を含む。

  • Narrow, consistent, typed interfaces: 狭く、一貫性があり、型を持つインターフェイス
  • Consistent and carefully implemented authentication, authorization, and accounting strategies: 一貫性があり、注意深く実装された認証、認可、課金の戦略
  • Clear assignment of identities to active entities, whether they are software com‐ ponents or human administrators: アクティブなエンティティ(ソフトウェアコンポーネントや人間の管理者)に対するアイデンティティの明快な割り当て
  • Application framework libraries and data types that encapsulate security invari‐ ants to ensure that components follow best practices consistently: コンポーネントがベストプラクティスに一貫して従うことを保証するように、セキュリティ不変条件を埋め込んだ、アプリケーションフレームワークライブラリやデータ型

あなたの最も重要なシステムが誤動作したときに、システムのわかりやすさが、それをささいなインシデントにするか、あるいは大災害にするかの明暗を分ける。SRE は自らの職務のために、そのシステムのセキュリティ不変条件を知っておかなければならない。極端なケースでは、SRE はセキュリティインシデントの発生時に、セキュリティのために可用性を犠牲にして、サービスをダウンさせなければならないかもしれない。

ここまでの感想

6章も、5章に続いて読み応えのある内容でした。タイトルにある Understandability(わかりやすさ)という言葉からは予想できなかった、説得力のある議論が次々と続いて、読んでいて飽きませんでした。

システムのわかりやすさ(understandability)について、明確な定義がなされていて、正直言って一度読んだだけでは理解しきれてない気がしますが、うまく身につけたら今後役立ちそうです。

文中で触れられていた A Philosophy of Software Design は、日本語訳がないようですが、ちょっと読んでみたいですね。検索したところ、日本語の解説記事がいくつかあったので、まずはそのあたりから読んでみます。

詳しい説明のなかでは、通常時と障害発生時でメンタルモデルが変わってしまうとシステムがわかりにくくなる、という指摘は、なるほどと思いました。

また、コンポーネントの責務を明確にするというのは、つまり特定の不変条件に対して責任を持つコンポーネントを明確にすることで、その結果、ある不変条件が成立していることを論証するために読むべきコード量が減る、というのはうまい説明方法だと思いました。現実には、なかなかそんなふうにきれいな構造のコードになってないことってありますよね……。この説明方法は、今後うまく活用していきたいです。