これは何?
「コンテナセキュリティ〜コンテナ化されたアプリケーションを保護する要素技術〜」を読んで学んだこと、読む上で必要だった前提知識の整理などを雑に書くメモ
個人的なメモですが、誰かの役に立てば幸いです。
また、もし記載内容の誤りや理解の誤りを見つけたら、コメントをいただけると嬉しいです。
なお、あくまでも個人的なメモなので全く触れてない箇所などもあります。
非常におすすめの本なので、気になった方はぜひ購入してみてください!
本書の全体像
Chapter 1では、そもそもコンテナにおけるセキュリティ的な脅威には何があるのか、全体を通してどんな観点を持てば良いのかについて書かれています。
そしてChapter2〜5では、コンテナに関連するLinuxの技術要素について深掘りし、各技術要素の特徴からセキュリティリスクを考えるという内容になっています。
その後、Chapter6〜14でさまざまなコンテナセキュリティにおける脅威に対して、どのように対策すべきなのかが書かれています。
個人的にはChapter2〜5に書かれている内容が一番重要だと感じました。
コンテナセキュリティを考える上で必須である技術要素についてかなり深掘りされているので、「そもそもコンテナとはなんなのか」を理解することができました。
このChapter2〜5を読むだけでもいいと思うくらい、重要だと感じました。
Chapter 1:コンテナセキュリティの脅威
- コンテナ脅威モデル:コンテナに対する脅威を特定して列挙
- 開発者が書いたアプリケーションコードの脆弱性
- コンテナイメージの設定不備や侵害
- 脆弱性のあるビルド環境
- リポジトリにおける設定不備や侵害
- ホストマシンの脆弱性
- シークレット漏洩
- コンテナにおけるネットワークの侵害
- コンテナエスケープ(コンテナを経由してホストマシンに侵入すること)
- コンテナオーケストレータの脆弱性
- コンテナはVM同様、セキュリティ境界(リソース・ユーザー・データを分離する境界)として機能する
- ただし、コンテナは基本的にホストのカーネルを共有するため、VMよりも隔離度合いが低い
- コンテナにおけるセキュリティの原則
- 最小権限
- 各コンテナに対して適切な権限を付与する
- 多層防御
- 様々なレイヤーでの対策
- 攻撃対象領域の縮小
- マイクロサービスの思想に寄せてサービス間のインターフェイス(≒攻撃対象領域)を減らす
- 影響範囲の制限
- コンテナが侵害された際に、別のコンテナに影響を与えないようにする
- 職務分掌
- コンポーネントやユーザーごとに権限・シークレットを使い分ける
- 最小権限
前提知識・関連情報
カーネル
- OSの中核となる機能
- ハードウェアとソフトウェアの橋渡し的な役割を持つ
- プログラムで利用するハードウェアリソース(CPU、メモリ、ストレージ、ネットワークインターフェイスなど)の割り当てや、プログラムの実行状態の管理を行う
Chapter 2:Linuxシステムコール、パーミッション、capability
- ファイルのデータRead/Write、プログラム実行などの処理はカーネルが実行している
- アプリケーションがこれらの処理をする際、システムコールをすることでカーネルに対して命令をしている
- コンテナはカーネルを共有するので、複数のコンテナが一つのカーネルに対してシステムコールする
- コンテナごとに権限を適切に割り振らないと過剰なシステムコールができてしまう、セキュリティリスクが高まる
- setuid、setgidを有効にすることでroot権限を使うことができるので、コンテナセキュリティを考える上で重要な観点になる
- docker runコマンドの
--no-new-privilegesオプションでの制限、コンテナイメージスキャンでのsetuidビット検知などの対処方法がある
- docker runコマンドの
- 特権をよりきめ細かく制御するために使うのがcapability
- capabilityの割り当てによって、実行できるアクションを制御することができる
- (AWSで言うところのIAMポリシーに近いかも)
-
getcapsコマンドでcapabilityの確認、setcapsコマンドでcapability付与を行う
- 攻撃者がsetuidを付与したり、capabilityを追加したりして権限昇格ができると、コンテナエスケープにつながってしまう
前提知識・関連情報
setuid、setgid、スティッキービート
- setuid、setgid
- 一般ユーザーがroot権限でプログラムを実行できる仕組みで、有効になっている場合はファイルパーミッションで
sと表現される - 厳密には違うだろうが、AWSでいうところのスイッチロールみたいな仕組み
- setuidはユーザー単位、setgidはグループ単位
- 一般ユーザーがroot権限でプログラムを実行できる仕組みで、有効になっている場合はファイルパーミッションで
- スティッキービート
- ディレクトリに設定される特殊なアクセス権
- 有効にすると全てのユーザーが書き込み可能だが、削除は所有者とrootしかできないという状態になる
- ファイルパーミッションでは
tと表現される
Chapter 3:コントロールグループ
- コントロールグループ(cgroup)はプロセスが利用するCPU、メモリ、ネットワークインターフェイスなどのリソース制限を行う
- Linuxのプロセスは何らかのcgroupに属し、子プロセスは親プロセスのcgroupを継承する
- 通常、
/sys/fs/cgroupに設定情報が格納されている -
lscgroupコマンドで定義されているcgroupの一覧表示が可能
- コンテナを起動すると新しいコンテナのための新しいcgroupを、コンテナランタイムが作成する
- 新しいcgroupを編集することで、コンテナが利用できるリソースに制限をかける
- ただし、デフォルトでは制限されてないことがある
- Dockerの
config.jsonに記載した設定などがあればcgroupに反映される
- cgroupにはv1とv2がある
Chapter 4:コンテナの分離
- Linuxのnamespaceは、あるプロセスから見えるリソースに制限をかける仕組み
- 例えば、コンテナ内で
psコマンドを実行してもホスト上で動作しているプロセスは見えないが、これはPID namespaceのおかげ
- 例えば、コンテナ内で
- namespace化されているリソースは以下
- UTS(UTS namespaceはホスト名とドメイン名を隔離する仕組みを指す)
- プロセスID(PID)
- マウントポイント
- ネットワーク
- ユーザーID、グループID
- プロセス間通信(IPC)
- cgroup
- 各namespaceは自動的に作成され、新しいnamespaceを追加することも可能
- 新しいnamespaceを作成するにはrootユーザーになる必要がある
- user namespaceは例外で、一般ユーザーでも作成可能
- コンテナを新しく作成するときはnamespaceの作成が必要なので、Dockerデーモンはrootユーザーで実行される必要がある
- 新しいnamespaceを作成するにはrootユーザーになる必要がある
- PID namespace
-
unshare --pid --forkコマンドを使うと新しいPID namespaceを作成できるが、それだけでは不十分-
psコマンドは/proc配下のファイルを読み込むため、rootディレクトリの変更も合わせて行う必要がある
-
-
chrootコマンドでrootディレクトリを変更する際は、変更先のディレクトリ配下に実行したいコマンドのファイル(/bin/bashなど)が存在しないと変更できない- 実際にコンテナを使う際は
chrootコマンドよりもpivot_rootコマンドが推奨される
- 実際にコンテナを使う際は
-
- mount namespace
- mount namespaceを新しく作った場合も、デフォルトの動作としては
/proc/<PID>/mountsファイルからマウントポイントの情報を読み取るので、コンテナからホストのマウントポイントが全て見える状態になる- rootファイルシステムも一緒に作ることで、完全にマウントセットを分離することができる
- mount namespaceを新しく作った場合も、デフォルトの動作としては
- user namespace
- user namespaceはホストのrootユーザーと非rootユーザーのマッピングという重要な機能を持つ
- 新しいuser namespaceには基本すべてのcapabilityが付与されるため、非rootユーザーがコンテナの中のプロセスを通してrootユーザー同等の権限を持つことができる
- user namespaceはDockerではデフォルトで無効になっている
- 同じプロセスを指していたとしても、コンテナから見たプロセスIDとホストから見たプロセスIDは異なる
- つまり、ホストが侵害されるとホスト上のコンテナのプロセスが全て筒抜けになる
- コンテナの実行に必要なものだけを抽出したThin OSディストリビューションを使う、などの対策が可能だがリスクが無くなるわけではない
前提知識・関連情報
KubernetesのNamespaceとLinuxのnamespace
ややこしいがそれぞれ意味が異なる。Linuxのnamespaceの方が低レイヤーの仕組み。
- KubernetesのNamespace
- Kubernetesクラスター内の論理的な分割単位で、同一クラスター内でリソースを論理的に分割する
- Linuxのnamespace
- プロセスごとに異なるリソースを割り当てる仕組み
- PID(プロセスID)、Network、Mount、UTSなど様々なnamespaceの種類が存在する
unshareコマンド
- Linuxで新しいnamespaceを作成し、その中でプログラムを実行するコマンド
-
--pid:新しいPID namespaceを作成 -
--net:新しいNetwork namespaceを作成 -
--mount:新しいMount namespaceを作成 -
--uts:新しいUTS namespaceを作成 -
--ipc:新しいIPC namespaceを作成 -
--user:新しいUser namespaceを作成 -
--fork:新しいプロセスをforkしてからコマンドを実行する
-
バインドマウント
- 既存のディレクトリやファイルを別の場所にマウントする仕組みのこと
- 通常のマウントとの違い
- 通常のマウント:デバイスを特定のディレクトリにマウントする
- バインドマウント:既にマウントされているディレクトリを、別の場所からもアクセスできるようにマウントする
Chapter 5:仮想マシン
- 仮想マシンとコンテナの違いは、ホストマシンのカーネルを共有するかどうか
- 物理マシンを起動すると、BIOS起動 → 識別できるハードウェアの確認 → カーネルがリソースを割り当て、というプロセスを踏む
- カーネルはrootファイルシステムのマウント、ネットワークセットアップ、デーモン起動などを行う
- 仮想マシンを利用する際は、VMM(virtual machine monitor)がゲストカーネルへのリソース割り当てを行う
- コンテナもnamespace、cgroup、rootファイルシステムの変更によってプロセスを分離する
- コンテナとVMの構造は似ているが、論理的な分割が行われるレイヤーや仕組みが異なる
- コンテナはカーネルを共有しているのでVMよりも脆弱性が高い
- 仮想マシンの方がリソースの隔離度合いは高いが、軌道にかかる時間オーバーヘッド、ポータビリティの高さという観点ではコンテナの方がメリットがある
- コンテナは、仮想マシンのリソースの隔離度合いを犠牲に便利さを高めたもの
Chapter 6:コンテナイメージ
- コンテナイメージはrootファイルシステムと設定情報ファイルの2つで構成される
- OCI(Open Container Initiative)はコンテナのイメージ仕様、ランタイム仕様、配布仕様を定めたプロジェクト
- Dockerデーモンはroot権限を持っているため、docker buildには十分注意する必要がある
- Dockerデーモンに頼らない(≒非root権限でビルドする)代替ツールを利用することでリスクを低くすることができる
- コンテナイメージのビルドは大部分をDockerfileに頼っているので、Dockerfileに機密情報を記載したりするのはNG
- Dockerfileの中でファイルを削除したとしても痕跡が残る
- イメージの改ざんを検知するにはハッシュ参照
- タグは修正可能なので改ざん検知には使えない
- ソースコードからビルドしてデプロイするまでの過程で、潜在的な弱点が複数存在する
- リポジトリに保存した後、ソースコード・Dockerfileが改ざんされていないか
- ビルドする際に使うベースイメージ・依存関係に脆弱性は無いか
- ビルド環境そのものに脆弱性は無いか
- レジストリに保管されているイメージが改ざんされていないか
- イメージプルのリクエストを盗聴されて無いか
- デプロイ定義に悪意ある定義がされていないか
- それぞれ対策するべきだが、デプロイまでの流れを自動化すると攻撃者が介入できる領域を減らすことができる
前提知識・関連情報
ランタイムファイルシステムバンドル、umociコマンド
- ランタイムファイルシステムバンドル
- OCI(Open Container Initiative)で定義された、コンテナにおける標準的なディレクトリ構造のこと
-
config.jsonとコンテナのルートファイルシステム(rootfsディレクトリ)で構成される
-
umociコマンド- OCI準拠のイメージを操作するためのコマンド
Chapter 7:イメージに含まれるソフトウェアの脆弱性
- 仮想マシン上のアプリケーションが別のアプリケーションと依存関係を持っていて、そのアプリケーションの脆弱性が波及することがある
- コンテナでrootファイルシステムを個別に持っておくことで、依存関係を分離することができる
- 一つのコンテナイメージから大量のコンテナを起動するのが一般的なので、コンテナイメージそのものをスキャンするのが最も効率な方法
- ただし、そうするとコンテナが起動した後に追加されたソフトウェアがスキャンされない
- コンテナ起動後にソフトウェアが追加されないよう、コンテナはイミュータブルに設計することが推奨される
- イミュータブルに設計しても、デプロイ済みのイメージに対するスキャンは定期的に行うことが推奨される
- いくら対策しても、脆弱性への対策が行われるまでの間に行われる「ゼロデイ攻撃」のリスクは潜在的に存在している
- 実行中のコンテナにおける異常な振る舞いを監視する仕組みを合わせて導入することで、リスクを低減することが可能
Chapter 8:コンテナ分離の強化
- サンドボックス:アプリケーションを分離してリソースへのアクセスを制限する手法(cgroupやnamespaceの考え方と同様)
- アプリケーションで干渉しないように分離する、アプリケーション間でのアクセスに制限を設ける、2つの方法がある
- コンテナそのもののパーミッションを制御する機能
- seccomp
- アプリケーションが実行できるシステムコールを制限する機能
- seccompプロファイルはとりあえず有効化しておくことが推奨される
- AppArmor
- LSM(Linux Security Module)の一つで、ファイルの実行権限を制御する
- Linuxで一般的なDAC(任意アクセス制御)とは異なるMAC(強制アクセス制御)を実装しており、MACでは管理者のみユーザーのアクセス権限を設定することができる
- SELinux
- LSMの一つで、プロセスがもつアクセス権を制御することができる
- seccomp
- コンテナとVMの中間に位置する機能
- gVisor
- コンテナからのシステムコールをサンドボックス化する機能
- カーネルを再実装しているため強力な分離を実現するが、かなり複雑な仕組みであり、独自の脆弱性を含む可能性が比較的高い
- Kata Containers
- VMと同じように分離された状態でコンテナを起動させることができる
- Firecracker
- コンテナに必要な機能のみを載せた超軽量なVM
- AWSのLambdaやFargateに利用されている
- Unikernel
- コンテナに必要なOSの機能のみを使うという思想で設計された
- gVisor
前提知識・関連情報
LSM(Linux Security Module)
- Linuxのセキュリティをさらに強化するために追加されたセキュリティモジュールの総称
- AppArmor、SELinux、Smack、TOMOYO Linuxなどがある
Chapter 9:コンテナエスケープ
- コンテナイメージは指定をしない限りは、rootとして実行される
- rootへの権限昇格ができること自体は問題ではないが、非rootユーザーでコンテナを実行できる方法もある
- コンテナの設定で非rootユーザーのユーザーIDを指定する
- user namespaceを使用して実行する
- rootへの権限昇格ができること自体は問題ではないが、非rootユーザーでコンテナを実行できる方法もある
- 比較的新しい仕組みとして、rootlessコンテナというものもある
- user namespaceを利用した仕組みで、ホスト上の非rootユーザーをコンテナ内のrootユーザーにマッピングできる
- ただし、コンテナラインタイムによってはまだサポートしてないことがある
-
--privilegedオプションを付与してコンテナを実行すると、コンテナがrootで実行される場合よりも過剰なcapabilityを付与してしまうのでリスクがさらに高まる- 「
--privilegedオプションをつけるとroot権限を持つ」は厳密には誤り、「さらに高い権限を持ってしまう」が正しい
- 「
-
-vオプションを使うとホストのディレクトリを簡単にコンテナへマウントすることができてしまう-
/etcなどの重要なディレクトリをマウントするとセキュリティリスクが高まる
-
- CIツールでDockerソケットをコンテナにマウントさせることがあるが、「Dockerソケットのマウント ≒ root権限を持つ」なので注意
- 本番環境ではCI/CDパイプラインにDockerソケットをマウントしない
- サイドカーコンテナを利用すると、複数のコンテナでnamespaceを共有して機能を共有することができる
- namespaceの共有はセキュリティリスクが高まるが、それに引き換えコンテナの機能拡張を簡単に行うことができる
前提知識・関連情報
CI/CDツールにおけるDockerソケットのマウント
本書ではDockerソケットをマウントせずにCI/CDパイプラインを構築する方法が書かれてなかったため調べてみました
- Dockerソケットを使わない(デーモンレス)なビルドツールをすることが推奨
- 例としてKaniko、Buildsh、Buildkit with rootless mode、img、Podman、Makisu、Sysboxなどがある
- 違いはざっくりと以下の通り
- 小〜中規模な環境でシンプルさを重視するなら、最も広く使われているKaniko
- エンタープライズ環境ならRed HatのサポートがあるBuildah
- 複雑なビルドやパフォーマンスを重視する場合は、Docker公式のBuildKit
- 既存のDockerワークフローを維持したい場合は、Dockerと互換性が高いPodman
- Kubernetes環境であればKaniko または Buildah
- AWSサービスを使ってCI/CDパイプラインを構築する際に、Dockerソケットのマウントを考慮する必要はあるのか?
- CodeBuildを使う場合は、AWSマネージドなCI/CDパイプラインを使えるので特に気にしなくてOK
- Dockerソケットのマウントは不要、環境も自動的にクリーンアップされる、IAMロールによる権限管理も可能
- EKS上でCI/CDパイプラインを組む場合は注意が必要、デーモンレスツールの使用が推奨される
- CodeBuildを使う場合は、AWSマネージドなCI/CDパイプラインを使えるので特に気にしなくてOK
Chapter 10:コンテナネットワークセキュリティ
- コンテナはホストのIPを共有しつつ、network namespaceを使ってネットワークを論理的に分割することが可能
- k8sでは各PodがIPを持ち、k8sのServiceがNATに近い役割を果たす
- Serviceはルーティングをするだけで、ServiceとPodの間のインターフェイスは別途設ける必要がある
- L3/L4のルーティングやルールの制御
- netfilterというカーネル機能に依存している
- iptablesを使うことでnetfilterのネットワークポリシーを設定できる
- Podが増えたりポリシーが複雑になるとパフォーマンスが落ちるので、その際はIPVS(IP virtual service)を代わりに利用できる
- iptablesを使うことでnetfilterのネットワークポリシーを設定できる
- k8sのネットワークにおけるベスプラは
- デフォルト拒否:Ingress通信をホワイトリスト形式にする
- 外向きデフォルト拒否:Egress通信をホワイトリスト形式にする
- Pod間通信制限:許可されたPod間の通信のみ許可する
- ポート制限:特定のポートしか利用しないように制限する
- netfilterというカーネル機能に依存している
- L7のネットワーク制御
- サービスメッシュを使ってL7のネットワークポリシーを設定する
- アプリケーションコンテナと同じPodにサービスメッシュのサイドカーコンテナを稼働させることが必要、
- ただし、Istio Ambient Meshのようにサイドカーを利用しないサービスメッシュも存在する
- サービスメッシュでmTLSを使うことでよりセキュアにすることができる
Chapter 11:TLSによるコンポーネントの安全な接続
TLSに関する一般的な技術的解説がほとんどであるため割愛
Chapter 12:コンテナへのシークレットの受け渡し
- コンテナイメージにシークレットを格納することは避ける
- コンテナイメージとシークレットは切り離して管理するのが基本方針
- ネットワークインターフェイス経由でシークレットを受け渡す方法が、一般的に使われている方法
- サービスメッシュによるmTLSによって暗号化をする
- 環境変数にシークレットを格納する方法は避けるべき
- コンテナがマウントされたボリュームを介してシークレットを受け渡す際は、ディスクではなくメモリ上の一時的なディレクトリを利用することが推奨される
- k8sを利用している場合は、ネイティブにサポートされているKubernetes Secretを利用するのが推奨
- AWS、Azure、Google Cloud、HashiCorpなどが提供しているシークレット管理サービスが使えるのであれば、そっちを使うのが推奨
- ただし、ホストのrootユーザーはホスト上のコンテナで利用しているシークレットや鍵情報を簡単に入手できるということを忘れてはいけない
- もしもホストが侵害されたら…
Chapter 13:コンテナのランタイム保護
- 通常、コンテナで起動されるプログラムはそのコンテナが提供する機能に必要なプログラムだけが起動するべき
- 例えば、nginxコンテナであればnginxプロセスのみ実行されるべき
- コンテナで起動される実行ファイルが適切かどうかを見分ける技術の一つにeBPFがある
- Traceeを使うとコンテナ内で起動してるプロセスを可視化できるが、TraceeはeBPFを使って実装されている
- プロセス以外にもネットワークトラフィックやプロセスを実行するUID、などから不正な動きを検知できる
- ただし、eBPFは検知できても変更はできない
- アラートを発砲した後の対応までセットで考えないといけない
前提知識・関連情報
TraceeとeBPF
- Tracee
- コンテナ内のイベントをトレースするOSSプロジェクト
- eBPF
- Linuxカーネル内で安全にプログラムを実行できる技術
Chapter 14:コンテナとOWASPトップ10
- 2017年版のOWASP Top 10に対する対策方針
- インジェクション
- コンテナイメージのスキャンに加えて、コンテナ内のアプリケーションコードのレビューによって対策
- 認証の不備
- Chapter11に記載のサービスメッシュ, Chapter 12に記載のシークレット管理によって対策
- 機密情報の露出
- Chapter 12に記載のシークレット管理にて対策
- XML外部エンティティ参照
- コンテナイメージのスキャンに加えて、コンテナ内のアプリケーションコードのレビューによって対策
- アクセス制御の不備
- Chapter 9に記載のコンテナエスケープへの対策が有効
- セキュリテイの設定ミス
- CSPMの活用、CIS benchmarkなどのガイドラインをもとにしたレビューによって対策
- 環境変数に機密情報を持たせない対策も有効
- クロスサイトスクリプティング
- コンテナイメージのスキャンにて対策
- 安全でないデシリアライザーション
- デシリアライズのプロセスを分離する
- デシリアライズが目的の通信を制限する
- 既知の脆弱性を持つコンポーネントの使用
- コンテナイメージのスキャンによって対策
- 不十分なログとモニタリング
- コンテナイベントログの記録、SIEMの導入によって対策
- インジェクション
- 「コンテナイメージのスキャン」が非常に効果的な方法である