Skip to main content

特権付きDockerコンテナ—本当に必要ですか?

2020年11月5日

0 分で読めます

今週、Podmanでテストをしているうちに、あるコンテナをrootless構成で実行するのに--privilegedフラグが必要な理由が気になり、調べ始めたら深みにはまってしまいました。同僚のEric Smallingが、そのフラグが必要な理由を尋ねたのも、もっともなことです。

結局のところ、--privilegedは「あらゆるもの」を許可するための簡略表記です。rootlessで実行するならそれほど問題ないと思うかもしれませんが、最小権限とゼロトラストの原則をある程度損ないます。rootlessで実行する場合でも、コンテナが実際に必要とするものを正確に把握し、必要最低限の権限だけを付与することが大切です。

rootlessで実行するプロセスにこのフラグを設定しても、ユーザーが持つ以上の権限がプロセスに与えられるわけではありません。それでも、この設定が推奨される理由となったプロセスの必要なケイパビリティが何なのか、興味をそそられました。探究心に駆られ、探検の装備を身にまとって暗闇の奥へと足を踏み入れました。

詳しく見ていきましょう

まず、コンテナをrootlessで実行するとはどういうことかを見てみましょう。これは、Linuxカーネルのユーザー名前空間の機能によるものです。非特権ユーザーでも新しいユーザー名前空間を作成できます。ユーザーが新しいユーザー名前空間を作成して入ると、その名前空間の中ではrootになり、コンテナを正常に起動するのに必要な権限の大部分を得ます。ユーザー名前空間内のrootは、システム全体のrootと同じ権限を持つわけではありません。しかし、これにより昇格した権限を必要とせずにコンテナを実行できるため、脆弱性が悪用される可能性のある攻撃対象領域を大幅に縮小できます。

今回使うシステムは、最小構成のCentOS 8をインストールした仮想マシンで、MacBook上のVirtualBoxで動作しています。最初にdnfを使ってpodmanをインストールしました。

dnf install podman

次に、ユーザーのホームディレクトリにディレクトリを作成し、そこで自己署名証明書を生成しました。

mkdir certs
openssl req -newkey rsa:4096 -nodes -sha256 -keyout certs/domain.key -x509 -days 365 -out certs/domain.crt

次に、この証明書を使ってテスト用のローカルDockerレジストリを動かします。ここからが本題です。この方法について調べたドキュメントは、どれも--privilegedフラグの使用を勧めていました。

[matt@localhost ~]$ podman run -d --name registry -p 5000:5000 -v "$(pwd)"/certs:/certs  --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key --privileged registry:2

上のコマンドを見ると、タグ2のレジストリイメージを実行し、現在のディレクトリにあるcertsディレクトリをコンテナ内の/certsにバインドマウントするボリュームを作成し、レジストリを設定する環境変数を渡しています。そして、podmanにこのコンテナを特権モードで実行するよう指示する--privilegedフラグを、ためらいなく追加しています。

podmanでレジストリを使うには、/etc/containers/registries.confにエントリを追加する必要があります。

[registries.insecure]
registries = ['localhost:5000']

特権モードでは問題なく動作します。そこでまず、非特権モードで実行するとどうなるかを確認しました。調べるのは簡単でした。コンテナは起動した後にクラッシュして再起動を繰り返し、ログには証明書にアクセスできないと表示されます。

[matt@localhost log]$ podman logs bd323f90c60b

time="2020-10-20T18:24:27.806128235Z" level=fatal msg="open /certs/domain.crt: permission denied"

この時点では、原因はLinuxのケイパビリティに関係していると思いました。--privilegedフラグが行う主なことの一つは、コンテナがカーネルの提供するすべてのケイパビリティにアクセスできるようにすることだからです。このコンテナを特権モードで実行したときの状態は、podmanで確認できます。

[matt@localhost ~]$ podman top -l capeff
EFFECTIVE CAPS
full

次に、Linuxのmanページで各ケイパビリティがプロセスに許可する操作を詳しく調べました。ここでもpodmanを使って、実行中の非特権コンテナが持つケイパビリティを確認できます。

[matt@localhost ~]$ podman top b7cea04eb70e
USER   PID   PPID   %CPU    ELAPSED        TTY   TIME   COMMAND
root   1     0      0.000   1.400417458s   ?     0s     registry serve /etc/docker/registry/config.yml

[matt@localhost ~]$ podman top -l capeff
EFFECTIVE CAPS
AUDIT_WRITE,CHOWN,DAC_OVERRIDE,FOWNER,FSETID,KILL,MKNOD,NET_BIND_SERVICE,NET_RAW,SETFCAP,SETGID,SETPCAP,SETUID,SYS_CHROOT

この一覧をLinuxのmanページと照らし合わせると、非特権モードで有効なケイパビリティがあれば、コンテナはファイルを読み取れるはずです。実際、この一覧にはこのコンテナが必要とする以上のケイパビリティが含まれているようです。この点には後ほど戻ります。

次に確認するのはホストのaudit.logです。予想どおり、次の記録がありました。

type=AVC msg=audit(1603218166.554:4674): avc:  denied  { read } for  pid=223435 comm="registry" name="domain.crt" dev="dm-0" ino=6582402 scontext=system_u:system_r:container_t:s0:c127,c779 tcontext=unconfined_u:object_r:user_home_t:s0 tclass=file permissive=0

この記録から、SELinuxがdomain.crtファイルへの読み取り呼び出しをブロックしたことが分かります。CentOSおよびRHELシステムでは、SELinuxはデフォルトでEnforcingモードに設定されています。sestatusツールで確認できます。

[matt@localhost ~]$ sudo sestatus
[sudo] password for matt: 
SELinux status:                 enabled
SELinuxfs mount:                /sys/fs/selinux
SELinux root directory:         /etc/selinux
Loaded policy name:             targeted
Current mode:                   enforcing
Mode from config file:          enforcing
Policy MLS status:              enabled
Policy deny_unknown status:     allowed
Memory protection checking:     actual (secure)
Max kernel policy version:      31

SELinuxはEnforcingモードでファイルやディレクトリにラベルを付け、アクセスを管理します。このラベル付けは、コンテナエンジンの動作にも反映されます。コンテナエンジンはcontainer_tラベルでプロセスを起動し、コンテナ自体にはcontainer_file_tラベルを付けます。SELinuxは、プロセスがこのラベルの付いたファイルとのみやり取りできるようにし、コンテナ外のファイルへのアクセスをデフォルトで拒否します。--privilegedフラグを付けて実行すると、ラベルが無効になり、コンテナはコンテナエンジンの起動時に使われたラベルで実行されます。podmanを使ってコンテナを確認すると、この違いが分かります。こちらは特権コンテナです。

[matt@localhost ~]$ podman top -l label
LABEL
unconfined_u:system_r:container_runtime_t:s0

こちらは非特権で実行しているコンテナです。

[matt@localhost ~]$ podman top -l label
LABEL
System_u:system_r:container_t:s0:c23,c603

ホストのファイルシステムにあるcertsディレクトリを見ると、user_home_tラベルが付いていることが分かります。

[matt@localhost ~]$ ls -lZ certs
total 8
-rw-rw-r--. 1 matt matt unconfined_u:object_r:user_home_t:s0 1944 Oct 20 17:54 domain.crt
-rw-------. 1 matt matt unconfined_u:object_r:user_home_t:s0 3272 Oct 20 17:53 domain.key

つまり、非特権モードでは、ファイルをイメージにバインドマウントしていてもコンテナからアクセスできません。

そこで、コンテナを--privilegedで実行する代わりに、いくつかの方法で解決できます。まず、podmanコマンドラインで--security-opts label=disableを指定すれば、ラベルを完全に無効化できます。セキュリティの観点からは明らかに望ましくありません。そこでpodmanとDockerには、マウントのラベルを付け直す仕組みがあります。マウントを個別に使う場合はZスイッチを、共有する場合はzスイッチを使います。

コンテナを修正するには、次のように実行するだけです。

podman run -d --name registry -p 5000:5000 -v "$(pwd)"/certs:/certs:Z  --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key registry:2

最後にケイパビリティについてですが、コンテナを有効なケイパビリティの絶対的な最小限で実行するのは、常に良い考えです。今回のレジストリコンテナは、私のテスト環境では、すべてのケイパビリティを削除して実行できます。

podman run -d --name registry -p 5000:5000 -v "$(pwd)"/certs:/certs:Z  --restart=always -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key --cap-drop=all registry:2

コンテナに必要なケイパビリティを調べる方法はいくつかあります。たとえば、動作中にSELinuxがブロックするケイパビリティを監査ログで確認し、必要なものを--cap-add引数で追加できます。最小権限の原則に従うのが、常に最善の選択です。この話の教訓は、必要なものまで一緒に捨ててはいけない、ということです。

まとめ

ユーザー名前空間内であっても、コンテナに--privilegedを指定するのは望ましい方法ではなく、最小権限とゼロトラストの原則に反します。SELinuxなどのツールの出力を活用して、コンテナイメージが求めているケイパビリティや権限を監査し、実行前に実際に必要なものを確認しましょう。そして、本番環境でコンテナを実行する際は、それらだけを設定してください。必要な権限は、実際にはごくわずかであることがよくあります。

開発者ファーストのコンテナセキュリティ

Snykは、コンテナイメージとKubernetesワークロードの脆弱性を検出し、自動的に修正します。

続きを読む

feature customer snowflake
Article

開発初期からセキュリティを確保:Snowflake Cortex Code向けSnyk Studioインテグレーションを発表

Snyk StudioがSnowflake Cortex Codeと連携し、開発中にAI生成コード、依存関係、コンテナの脆弱性をスキャンします。

blog feature pypi spoof
Article

オープンソースのメンテナーに、Snyk AI Security Platformをすべて無料で提供

オープンソースのメンテナーは、実際の脆弱性レポートに追われ、優先順位付けや修正、修正版のリリースを迅速に進めるための支援を必要としています。SnykのSecure Developer Programでは、条件を満たすプロジェクトにSnyk AI Security Platformを無料で提供します。

feature insights announcement
Blog

Node-gypサプライチェーン侵害:binding.gypに潜む自己増殖型npmワーム

新たなnpmワームがbinding.gypを悪用し、インストール時にnode-gypを起動。ライフサイクルスクリプトを使わずに悪意あるパッケージからコードを実行します。認証情報を窃取し、GitHub上に潜伏し、メンテナー間で自己増殖します。