コンテナセキュリティの考え方
コンテナの比較対象としてよく挙がるのはベアメタル環境や仮想化環境です。
ベアメタル環境は、ひとつのOS上でミドルウェアやアプリケーションが動作するレガシーな構成です。一方、コンテナ技術や仮想化技術では、コンテナエンジンやハイパーバイザの上で別のOSが動作します。これにより、アプリケーションとホストOSが分離され、セキュリティ境界として機能します。
多層防御の視点から見ると、一般的に仮想化環境が最もセキュリティレイヤの強度が強く、ベアメタル環境が弱いとされています。なお、コンテナ環境はこれらの中間に位置しています。
仮想化環境やベアメタル環境との違い
仮想化環境は、CPUやメモリなどの物理リソースを主に仮想化することによって、ハードウェアレベルの分離を実現しています。そのため、ゲストOS上とホストOSは強く分離されており、一般的にハイパーバイザ上のプログラムがホストOSに直接アクセスすることは困難です。
一方、コンテナ環境は、ホストOS内のカーネルや物理リソースに直接アクセスして動作します。ただし、コンテナは、後述するnamespaceやcgroupなどの技術により、ホストOSへのアクセスが強く制限されています。コンテナの実体はベアメタル環境と同じプロセスやスレッドになりますが、他プロセスやホストOSへのアクセス権が制限されているため、脅威にさらされてもコンテナ内で被害を抑えることができます。そのため、ベアメタル環境よりは強固な分離が実現できます。
多層防御とコンテナ
コンテナ化されたプロセスは、通常のプロセスと同じように、システムコールを使用し、パーミッションや特権を必要とします。コンテナでは、実行時やコンテナイメージの構築時に、パーミッションをどのように割り当てるかがセキュリティに大きな影響を与えます。コンテナは要素技術がそのままセキュリティに繋がるので、コンテナ技術を理解する上で知っておくべきLinux OSの機能と合わせて紹介します。
コンテナの要素技術
Namespace
namespaceはリソースの分離を行います。例えばデフォルトではコンテナは自身のプロセス情報かしか見えませんが、dockerの--pidオプションを指定して他のコンテナと PID namespace を共有することで別のコンテナのプロセス情報(PIDなど)が見えます。
- mount namespace
- UTS namespace
- IPC namespace
- PID namespace
- network namespace
- user namespace
- cgroup
dockerはデフォルトでmount、uts、ipc、pid、networkを使用します。
コンテナはnamespaceでコンテナプロセスを隔離しますが、オプションを指定することで隔離を弱めることができます。
具体的には、docker runに--pid=hostオプションを指定することによって、ホストのプロセスIDをコンテナ内から認識されるようにできます。
上記のようなnamespaceを緩和する設定は、コンテナの隔離機能が低下するため、基本的に非推奨となります。
pivot_root
pivot_rootはプロセスのルートファイルシステムを隔離する目的で使用されるコマンドです。ホストOSのファイルシステム上にコンテナ用の新しいルートファイルシステムを作成できます。プログラムの実体はファイルとメモリであることを考えれば、ファイルシステムの隔離技術はコンテナの根幹といえます。コンテナ技術では、プロセスのファイルシステムを隔離したうえで、namespaceで論理リソースを隔離、cgroupでcpuやメモリなどを制限して、コンテナを生成しています。
cgroups
コンテナは論理的には強力な権限分離がされていますが、物理的にはホストOSのCPUやメモリを共有しています。そのため、一つのコンテナに過剰な物理リソースを割り当てると、他コンテナの障害などに繋がります。
cgroup はCPUやメモリの仕様を制限できます。dockerでは、--cpu-shares オプションで操作が可能です。実際にはcgroup のcpu.shares ファイルによって制限されています。
同様にKubernetesのポッド構成ファイルでも制御ができます。
Linux capability
現在のLinuxカーネルには、30種類以上のケイパビリティが搭載されています。ケイパビリティは特権を細分化する仕組みで、割り当てたスレッドが特定のアクションを実行できるかどうかが決まります。例えば、スレッドが低番号(1024以下)のポートにバインドするには、CAP_NET_BIND_SERVICEケイパビリティが必要です。CAP_SYS_BOOT は、任意の実行ファイルがシステムを再起動する許可を得られないようにするために存在します。CAP_SYS_MODULEは、カーネルモジュールをロードまたはアンロードするために必要です。先ほど、pingツールがrootとして実行され、スレッドが生のネットワークソケットを開くために必要なケイパビリティを自分自身に与えるのに十分な時間があると述べました。この特定のケイパビリティは CAP_NET_RAW と呼ばれています。
プロセスに割り当てられたケイパビリティは、getpcapsコマンドで確認することができます。
Dockerでは、デフォルトでコンテナに割り当てられるケーパビリティが決まっていますが、--cap-add及び--cap-dropオプションでこれらの制御ができます。
許可されたケーパビリティ一覧
Capability Key | Capability Description |
---|---|
AUDIT_WRITE | Write records to kernel auditing log. |
CHOWN | Make arbitrary changes to file UIDs and GIDs (see chown(2)). |
DAC_OVERRIDE | Bypass file read, write, and execute permission checks. |
FOWNER | Bypass permission checks on operations that normally require the file system UID of the process to match the UID of the file. |
FSETID | Don’t clear set-user-ID and set-group-ID permission bits when a file is modified. |
KILL | Bypass permission checks for sending signals. |
MKNOD | Create special files using mknod(2). |
NET_BIND_SERVICE | Bind a socket to internet domain privileged ports (port numbers less than 1024). |
NET_RAW | Use RAW and PACKET sockets. |
SETFCAP | Set file capabilities. |
SETGID | Make arbitrary manipulations of process GIDs and supplementary GID list. |
SETPCAP | Modify process capabilities. |
SETUID | Make arbitrary manipulations of process UIDs. |
SYS_CHROOT | Use chroot(2), change root directory. |
Kubernetesでも、 ポッド構成ファイルやポッドセキュリティで制御することができます。
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo-4
spec:
containers:
- name: sec-ctx-4
image: gcr.io/google-samples/node-hello:1.0
securityContext:
capabilities:
add: ["NET_ADMIN", "SYS_TIME"]
コンテナの種類
Docker runコマンドに--privilegedオプションを指定すると、これらのケーパビリティがすべて割り当てられます。これがいわゆる特権コンテナと呼ばれるものです。
現在主流の特権コンテナと通常コンテナはどちらもroot権限で動作していますが、特権コンテナと通常コンテナの主な違いは付与される特権の範囲になります。
ルートレスコンテナと呼ばれる技術も開発が進んでおり、コンテナエンジンやコンテナをホストOSの一般ユーザ権限で動作させます。これには、User Namespaceが利用されています。ルートレスコンテナの利点は、コンテナが乗っ取られたとしても、ホストOSのroot権限まで窃取されにくいところです。開発が進めばルートレスコンテナが主流になるかもしれません。
特権コンテナの危険性
特権コンテナは、ホストOSとの権限境界がnamespaceのみになり、ホストOSに特権アクセスできてしまうため、非常に危険です。
コンテナの運用では、--privilegedオプションを付与せず、コンテナに必要なケーパビリティのみを付与することが重要となります。
ファイルパーミッション
コンテナを運用しているかどうかに関わらず、Linux システムでは、ファイルパーミッションがセキュリティの要となります。Linuxでは、すべてがファイルであるという言葉があります。アプリケーションコード、データ、設定情報、ログなど、すべてがファイルに収められています。画面やプリンターなどの物理的なデバイスもファイルとして表現されます。ファイルに対するパーミッションは、どのユーザがそのファイルへのアクセスを許可され、ファイルに対してどのようなアクションを実行できるかを決定します。
ls -lコマンドを実行するとファイルとその属性に関する情報を取得できます。
-rw-rw-r-- 1 user user 0 4月 11 10:17 abc
最初の3文字のグループは、ファイルを所有するユーザーに対するパーミッションを記述します。
2番目のグループは、そのファイルのグループのメンバーに対するパーミッションを示しています。
最後のグループは、他のユーザーの権限を示しています。
ユーザーがこのファイルに対して実行できるアクションは、r、w、xの各ビットが設定されているかどうかによって、読み取り、書き込み、実行の3つに分かれます。各グループの3つの文字は、オンまたはオフになっているビットを表し、これら3つのアクションのうちどれが許可されているかを示します。
r、w、xの各ビットについては、すでにご存知の方も多いと思いますが、それだけではありません。パーミッションは、setuidビット、setgidビット、stickyビットの使用によっても影響を受けます。最初の2つのビットは、セキュリティの観点から重要です。なぜなら、攻撃者が悪意のある目的で使用する可能性のある追加のパーミッションをプロセスに取得させることができるからです。
通常、ファイルを実行すると、起動したプロセスは自分のユーザーIDを継承します。ファイルにsetuidビットが設定されている場合、プロセスはファイルの所有者のユーザーID(権限)を持つことになります。
以下のようなファイルの場合、root以外のユーザがファイルを実行した際に実行ファイルがrootの権限で動作します。このファイルに任意のコードや操作を実行する脆弱性ががあった場合、一般ユーザがroot権限(権限昇格)でコマンドを実行したりできるようになります。
ls -l
-rwsr-xr-x 1 root vpsuser 6579 3月 15 16:38 2016 setuid_test
setuidは特権昇格への危険な経路を提供するため、コンテナ・イメージ・スキャナの中には、setuid ビットが設定されたファイルの存在を報告するものがあります。また、docker run コマンドで --no-new-privileges オプションを指定すると、setuid ビットが使用されないようになります。
Kubernetesでは、 ポッド構成ファイルやポッドセキュリティのallowPrivilegeEscalationで制御することができます。
apiVersion: v1
kind: Pod
metadata:
name: security-context-demo
spec:
securityContext:
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
volumes:
- name: sec-ctx-vol
emptyDir: {}
containers:
- name: sec-ctx-demo
image: busybox
command: [ "sh", "-c", "sleep 1h" ]
volumeMounts:
- name: sec-ctx-vol
mountPath: /data/demo
securityContext:
allowPrivilegeEscalation: false
システムコール
アプリケーションは、ユーザースペースと呼ばれる、オペレーティングシステムのカーネルよりも低いレベルの権限を持つ空間で動作します。アプリケーションが、ファイルへのアクセス、ネットワークでの通信、時刻の確認などを行いたい場合には、アプリケーションに代わってカーネルに必ず要求しなければなりません。ユーザー空間のコードがカーネルにこれらの要求をするために使用するプログラムインターフェースは、システムコールまたはsyscallインターフェースとして知られています。システムコールには300以上の種類があり、その数はLinuxカーネルのバージョンによって異なります。以下が例です。
read ファイルからのデータ読み込み
write ファイルへのデータ書き込み
open ファイルを開いて後から読み書きできるようにする
execve 実行プログラムの実行
chown ファイルの所有者を変更
clone 新しいプロセスの作成
システムコールは通常、より高レベルのプログラミング抽象化に包まれているため、アプリケーション開発者がシステムコールを直接気にする必要はほとんどありません。。。が、コンテナが利用できるシステムコールを制限すれば、コンテナの隔離を強化することができます。
dockerでは、seccompを使ってシステムコールの制限を行えます。設計上はseccompを使わなくてもcontainer breakoutを防げるはずですが、システムコールを制限しておくことで脆弱性に関する処理を防げる可能性があります。
しかしながら、実際のアプリケーションでは必要となるシステムコールの一覧は自明ではないため、手動のプロファイル作成は困難です。現実的な方法としては、straceなどで必要なシステムコールを洗い出す方法や、dockersilmでプロファイルを行う方法があります。
dockerでは、コンテナ実行時に--security-opt オプションでseccompのポリシーファイルを指定することで禁止されたシステムコールをブロックします。
docker run --rm -it --security-opt seccomp=/path/to/seccomp/profile.json hello-world
また、dockerでは、デフォルトのseccompポリシーファイルが用意されています。これは経験則から最適化されたポリシーになるため、新しいseccompプロファイルを適用する場合には、このポリシーを拡張するという方法が一般的です。
Kubernetesでは、ポッド構成ファイルにseccompのプロファイル(ワーカノードに保存されたもの)を指定することでコンテナランタイム(dockerなど)で適用されます。
securityContext:
seccompProfile:
type: RuntimeDefault|Localhost|Unconfined
localhostProfile: my-profiles/profile-allow.json
ユニオンファイルシステム
pivot_rootの概念図を見ると、コンテナのファイルシステムがホストOS上にそのまま配置されているだけに見えますが、実はそこまで単純ではありません。コンテナのルートファイルシステムはユニオンファイルシステムという仕組みで構成されています。
コンテナのユニオンファイルシステムは、ホストOS上に散らばったディレクトリ(ハッシュblob)を束ねてひとつのルートファイルシステムにしています。
なぜこのような仕組みなっているかというと、コンテナイメージ(コンテナのファイルバンドル)は、レイヤという論理単位で分割されていて、各レイヤごとにディレクトリ管理されているためです。
静的なファイルのコンテナイメージからコンテナ(プロセス)が起動するとリードオンリーのコンテナイメージ(レイヤ)の内容がコピーされた、読書き可能なレイヤ(コンテナレイヤ)が新しく生成されます。コンテナはそこにディスクへの変更を記録して動作します。このとき、コンテナイメージが変更されることはありません。また、コンテナが破棄されると同時に、読書き可能なレイヤ(コンテナレイヤ)は破棄されるため、実行時の情報は揮発します。
コンテナイメージの仕様は、(OCI)Open Container Initiativeという団体が決めています。