Linux OSのセキュリティ機能とコンテナ技術

コンテナセキュリティの考え方

コンテナの比較対象としてよく挙がるのはベアメタル環境や仮想化環境です。

ベアメタル環境は、ひとつのOS上でミドルウェアやアプリケーションが動作するレガシーな構成です。一方、コンテナ技術や仮想化技術では、コンテナエンジンやハイパーバイザの上で別のOSが動作します。これにより、アプリケーションとホストOSが分離され、セキュリティ境界として機能します。

多層防御の視点から見ると、一般的に仮想化環境が最もセキュリティレイヤの強度が強く、ベアメタル環境が弱いとされています。なお、コンテナ環境はこれらの中間に位置しています。

f:id:FallenPigeon:20210425161220p:plain

仮想化環境やベアメタル環境との違い

仮想化環境は、CPUやメモリなどの物理リソースを主に仮想化することによって、ハードウェアレベルの分離を実現しています。そのため、ゲストOS上とホストOSは強く分離されており、一般的にハイパーバイザ上のプログラムがホストOSに直接アクセスすることは困難です。

一方、コンテナ環境は、ホストOS内のカーネルや物理リソースに直接アクセスして動作します。ただし、コンテナは、後述するnamespaceやcgroupなどの技術により、ホストOSへのアクセスが強く制限されています。コンテナの実体はベアメタル環境と同じプロセスやスレッドになりますが、他プロセスやホストOSへのアクセス権が制限されているため、脅威にさらされてもコンテナ内で被害を抑えることができます。そのため、ベアメタル環境よりは強固な分離が実現できます。

 

f:id:FallenPigeon:20210425161318p:plain

f:id:FallenPigeon:20210425164649p:plain

多層防御とコンテナ

コンテナ化されたプロセスは、通常のプロセスと同じように、システムコールを使用し、パーミッションや特権を必要とします。コンテナでは、実行時やコンテナイメージの構築時に、パーミッションをどのように割り当てるかがセキュリティに大きな影響を与えます。コンテナは要素技術がそのままセキュリティに繋がるので、コンテナ技術を理解する上で知っておくべきLinux OSの機能と合わせて紹介します。

f:id:FallenPigeon:20210425161747p:plain

コンテナの要素技術

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を緩和する設定は、コンテナの隔離機能が低下するため、基本的に非推奨となります。f:id:FallenPigeon:20210425160940p:plain

pivot_root

pivot_rootはプロセスのルートファイルシステムを隔離する目的で使用されるコマンドです。ホストOSのファイルシステム上にコンテナ用の新しいルートファイルシステムを作成できます。プログラムの実体はファイルとメモリであることを考えれば、ファイルシステムの隔離技術はコンテナの根幹といえます。コンテナ技術では、プロセスのファイルシステムを隔離したうえで、namespaceで論理リソースを隔離、cgroupでcpuやメモリなどを制限して、コンテナを生成しています。

f:id:FallenPigeon:20210425161028p:plain

 

cgroups

コンテナは論理的には強力な権限分離がされていますが、物理的にはホストOSのCPUやメモリを共有しています。そのため、一つのコンテナに過剰な物理リソースを割り当てると、他コンテナの障害などに繋がります。

cgroup はCPUやメモリの仕様を制限できます。dockerでは、--cpu-shares オプションで操作が可能です。実際にはcgroup のcpu.shares ファイルによって制限されています。

同様にKubernetesのポッド構成ファイルでも制御ができます。

 

f:id:FallenPigeon:20210425160858p:plain

 

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オプションでこれらの制御ができます。

docs.docker.com

許可されたケーパビリティ一覧

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"]

コンテナの種類

f:id:FallenPigeon:20210425162000p:plain

Docker runコマンドに--privilegedオプションを指定すると、これらのケーパビリティがすべて割り当てられます。これがいわゆる特権コンテナと呼ばれるものです。

現在主流の特権コンテナと通常コンテナはどちらもroot権限で動作していますが、特権コンテナと通常コンテナの主な違いは付与される特権の範囲になります。

ルートレスコンテナと呼ばれる技術も開発が進んでおり、コンテナエンジンやコンテナをホストOSの一般ユーザ権限で動作させます。これには、User Namespaceが利用されています。ルートレスコンテナの利点は、コンテナが乗っ取られたとしても、ホストOSのroot権限まで窃取されにくいところです。開発が進めばルートレスコンテナが主流になるかもしれません。

特権コンテナの危険性

特権コンテナは、ホストOSとの権限境界がnamespaceのみになり、ホストOSに特権アクセスできてしまうため、非常に危険です。

f:id:FallenPigeon:20210425162042p:plain

コンテナの運用では、--privilegedオプションを付与せず、コンテナに必要なケーパビリティのみを付与することが重要となります。

ファイルパーミッション

コンテナを運用しているかどうかに関わらず、Linux システムでは、ファイルパーミッションがセキュリティの要となります。Linuxでは、すべてがファイルであるという言葉があります。アプリケーションコード、データ、設定情報、ログなど、すべてがファイルに収められています。画面やプリンターなどの物理的なデバイスもファイルとして表現されます。ファイルに対するパーミッションは、どのユーザがそのファイルへのアクセスを許可され、ファイルに対してどのようなアクションを実行できるかを決定します。

 

f:id:FallenPigeon:20210425160639p:plain

 

ls -lコマンドを実行するとファイルとその属性に関する情報を取得できます。

$ 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 新しいプロセスの作成

 

f:id:FallenPigeon:20210425160519p:plain

システムコールは通常、より高レベルのプログラミング抽象化に包まれているため、アプリケーション開発者がシステムコールを直接気にする必要はほとんどありません。。。が、コンテナが利用できるシステムコールを制限すれば、コンテナの隔離を強化することができます。

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 

dzone.com

 

ユニオンファイルシステム

pivot_rootの概念図を見ると、コンテナのファイルシステムがホストOS上にそのまま配置されているだけに見えますが、実はそこまで単純ではありません。コンテナのルートファイルシステムはユニオンファイルシステムという仕組みで構成されています。

コンテナのユニオンファイルシステムは、ホストOS上に散らばったディレクトリ(ハッシュblob)を束ねてひとつのルートファイルシステムにしています。

なぜこのような仕組みなっているかというと、コンテナイメージ(コンテナのファイルバンドル)は、レイヤという論理単位で分割されていて、各レイヤごとにディレクトリ管理されているためです。

静的なファイルのコンテナイメージからコンテナ(プロセス)が起動するとリードオンリーのコンテナイメージ(レイヤ)の内容がコピーされた、読書き可能なレイヤ(コンテナレイヤ)が新しく生成されます。コンテナはそこにディスクへの変更を記録して動作します。このとき、コンテナイメージが変更されることはありません。また、コンテナが破棄されると同時に、読書き可能なレイヤ(コンテナレイヤ)は破棄されるため、実行時の情報は揮発します。

f:id:FallenPigeon:20210425161556p:plain

f:id:FallenPigeon:20210425161130p:plain

 


コンテナイメージの仕様は、(OCI)Open Container Initiativeという団体が決めています。

github.com

www.itbook.info

namu-r21.hatenablog.com