ルートレスコンテナの実行方法

ルートレスコンテナ(非特権コンテナ)を使用することは、最も効果的なコンテナセキュリティのベストプラクティスの1つです。

実際のところ、ほとんどの場合、プロセスをrootとして実行する必要はありません。ベアメタルサーバーでは、多くのサービスはrootで動作していません。では、なぜコンテナの80%がrootで動いているのでしょうか?

諸悪の根源(しゃれです)はDockerのデフォルト設定にあります。特に指定しない限り、DockerはコンテナのプロセスをUID 0、つまりrootユーザーとして実行します。開発者はこれに慣れてしまい、イメージの非特権版を提供しなくなりました。また、互換性を維持するために他のコンテナランタイムもこの挙動を踏襲しました。

幸いなことに、非特権で実行することは時間とともに簡単になってきました。

この記事では、次の内容を扱います。

  • rootとして実行するリスクを説明します。
  • イメージを非特権ユーザーで実行できるように適応させる方法を確認します。
  • ユーザーネームスペースを活用して特権コンテナを分離します。

rootとして実行する危険性

要するに、コンテナが侵害された場合、攻撃者にとって物事を簡単にしてしまうということです。

そうなると、攻撃者はコンテナ内でroot権限(つまり完全な制御)を得ます。これにより、次のことが可能になります。

  • プロセスを実行し、バイナリを改変する。
  • 安全だと思っていたファイルから認証情報を見つけ、ラテラルムーブメントを可能にする。

そして全体として、脆弱性を悪用してホストへ脱出することが容易になります。以下のブログ記事で確認できます。

だからこそ、コンテナをルートレスで実行することは非常に良いプラクティスです。ワークロードをさらに分離でき、侵害されたコンテナによる被害を最小化できます。

ルートレス実行のトレードオフ

時間とともに簡単になってきているとはいえ、コンテナを非特権で実行するのは簡単ではありません。

プロセスが非特権で動作できるように、イメージを適応させる必要があります。これにはポートの変更やファイル権限の見直しが含まれます。この手順は次のセクションで説明します。

また、一部のリソースに対する低レベルのアクセスを失います。Sysdig Agentのようなセキュリティプローブは、ホスト上で何が動いているかについて詳細なコンテキストを取得するために、このアクセスを必要とします。これらのワークロードをrootで実行しても問題ありません。コンテナ全体のごく一部であり、追加の制御を実装するのも容易だからです。

さらに、ユーザーネームスペースとケーパビリティを使って、制限された権限でコンテナを実行することもできます。方法は後述します。これらの機能のトレードオフは、プロセスが依然としてある程度特権的であることです。特にCAP_SYS_ADMINCAP_NET_ADMINのケーパビリティを過度に許可している場合はなおさらです。このトピックは「Falcoでコンテナの脱出ケーパビリティを検知する方法」という記事で取り上げました。

イメージをルートレス対応に準備する

理論上、イメージを非特権で実行できるように準備するのは簡単です。

  1. 1024より大きいポートのみを使用する。
  2. 非特権ユーザーが必要なものにアクセスできるよう、ファイル権限を見直す。

しかし実際には、これを実装するのが常に簡単とは限りません。そこで、実例で学びましょう。 nginx-unprivilegedイメージを使用します。これはコードが公開されており、十分なドキュメントもあるためです。

nginx-unprivilegedイメージは、いくつかの変更を加えることでルートレスを実現しています。対象は
/etc/nginx/conf.d/default.conf 設定ファイルです。

# implement changes required to run NGINX as an unprivileged user
RUN sed -i 's,listen       80;,listen       8080;,' /etc/nginx/conf.d/default.conf \
    && sed -i '/user  nginx;/d' /etc/nginx/nginx.conf \
    && sed -i 's,\(/var\)\{0\,1\}/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf \
    && sed -i "/^http {/a \    proxy_temp_path /tmp/proxy_temp;\n    client_body_temp_path /tmp/client_temp;\n    fastcgi_temp_path /tmp/fastcgi_temp;\n    uwsgi_temp_path /tmp/uwsgi_temp;\n    scgi_temp_path /tmp/scgi_temp;\n" /etc/nginx/nginx.conf \
    && sed -i 's,PIDFILE=${PIDFILE:-/run/nginx.pid},PIDFILE=${PIDFILE:-/tmp/nginx.pid},' /etc/init.d/nginx \

これらを確認していきましょう。

まず、デフォルトのポートとして8080を設定しています。1024より大きいポートはどのユーザーでも待ち受けできますが、ポート80を待ち受けできるのは特権ユーザーだけだからです。

sed -i 's,listen       80;,listen       8080;,' /etc/nginx/conf.d/default.conf

次に、コンテナランタイムでポートを適切にマッピングできます。

docker run -p 8080:80 nginx-unprivileged

続いて、userディレクティブを削除して、ユーザー変更が行われないことを保証します。

&& sed -i '/user  nginx;/d' /etc/nginx/nginx.conf \

さらに、非特権ユーザーがアクセスできるように、いくつかのフォルダを「tmp」に変更します。

&& sed -i 's,\(/var\)\{0\,1\}/run/nginx.pid,/tmp/nginx.pid,' /etc/nginx/nginx.conf \
    && sed -i "/^http {/a \    proxy_temp_path /tmp/proxy_temp;\n    client_body_temp_path /tmp/client_temp;\n    fastcgi_temp_path /tmp/fastcgi_temp;\n    uwsgi_temp_path /tmp/uwsgi_temp;\n    scgi_temp_path /tmp/scgi_temp;\n" /etc/nginx/nginx.conf \
    && sed -i 's,PIDFILE=${PIDFILE:-/run/nginx.pid},PIDFILE=${PIDFILE:-/tmp/nginx.pid},' /etc/init.d/nginx \

変更されるファイルはPIDファイルと一時ファイルです。

最後に、設定フォルダとキャッシュフォルダをユーザーが書き込み可能にします

# nginx user must own the cache and etc directory to write cache and tweak the nginx config
    && chown -R $UID:0 /var/cache/nginx \
    && chmod -R g+w /var/cache/nginx \
    && chown -R $UID:0 /etc/nginx \
    && chmod -R g+w /etc/nginx

すでにお気づきかもしれませんが、イメージを非特権で動作するように変更するのは簡単です。ただしそれは、コンテナ内のサービスがその前提で準備されている場合に限ります。NGINXサーバーは高度にパラメータ化されており、ソースコードを変更せずに設定ファイルから挙動を変えられます。

幸い、多くのソフトウェアはこれらの原則に従っています。コツは、/tmpディレクトリへ移動する必要があるファイルの一覧を把握することです。

ケーパビリティとユーザーネームスペース

前述のとおり、ネットワークスタックへ直接アクセスしたりptraceを使用したりといった、特権操作を行う必要があるコンテナもあります。

ケーパビリティ

これを許可する最も安全な方法は、Linuxのケーパビリティを使用することです。ケーパビリティを使えば、通常ユーザーに特定の特権を付与できます。

Dockerでは–cap-addフラグで設定できます。

$ docker run --cap-add=SYS_PTRACE […]

KubernetesのPodに対しては、securityContext内でケーパビリティを定義できます。

apiVersion: v1
kind: Pod
metadata:
  name: security-context-demo-4spec:
  containers:
  - name: sec-ctx-4image: gcr.io/google-samples/hello-app:2.0securityContext:
      capabilities:
        add: ["NET_ADMIN", "SYS_TIME"]

ユーザーネームスペース

しかし、コンテナイメージを非特権で動作するように適応させたり、ケーパビリティを使ったりする時間がない場合もあります。

そのような場合は、ユーザーネームスペースを使って、コンテナ内で動作するユーザーと、ホスト上でワークロードを実行するユーザーを抽象化できます。これにより、コンテナ内ではrootとしてプロセスを実行しつつ、ホスト上では通常ユーザーとして実行できます。

ユーザーネームスペースを使うと、攻撃者がコンテナから脱出してもホスト上で特別な権限を持てないため、与えられる被害を限定できます。

Dockerでユーザーネームスペースを有効化するには、/etc/subuidおよび/etc/subgidファイルでマッピングを設定する必要があります。これらの手順は他のコンテナランタイムでも同様で、この例の直後にそれぞれのドキュメントへのリンクがあります。

次のIDを持つユーザーがいるとします。

$ id testuser
uid=1001(testuser) gid=1001(testuser) groups=1001(testuser)

/etc/subuid/etc/subgidの両方に、次のようなエントリを追加することでユーザーネームスペースを作成できます。

testuser:231072:65536

このネームスペースは、231072.から始まる65536個のユーザーIDを予約します。この範囲内では、すべてのユーザーIDがtestuserにマッピングされ、231072 はネームスペース内のUID 0 にマッピングされます。

次に、コンテナがデフォルトでどのホストユーザーとして実行されるかを指定するため、--userns-remapフラグを付けてdockerdを起動します。

$ dockerd --userns-remap="testuser:testuser"

Linuxでは、PodmanCRI-Orunccontainerdなど、ほとんどのコンテナランタイムがユーザーネームスペースをサポートしています。

KubernetesPodのユーザーネームスペースをサポートしています。PodのデプロイメントyamlでhostUsers: falseを設定することで、この機能を有効化できます。

apiVersion: v1
kind: Pod
metadata:
  name: userns
spec:
  hostUsers: false[…]

Kubernetesにおけるユーザーネームスペースのサポートは継続的に改善されています。たとえば、Kubernetes 1.35のアルファ機能強化では、ユーザーネームスペースをホストのネットワークスタックへのアクセスから切り離しています。

結論

ルートレスコンテナを使用することで、コンテナの1つが侵害された場合に備えた防御線を構築できます。

非特権での実行は時間とともに容易になってきており、ルートレスが選べない場合でも、ケーパビリティやユーザーネームスペースといったツールを利用できます。

今ほど試してみるのに良いタイミングはありません。

翻訳元: https://www.sysdig.com/blog/how-to-run-rootless-containers

ソース: sysdig.com