突然の「Operation not permitted」—Dockerが採用するセキュリティ機構「Seccomp」とは何か? #docker #seccomp #mirantis
この記事は1年以上前に投稿されました。情報が古い可能性がありますので、ご注意ください。
ある日、あなたはいつものようにコンテナを立ち上げ、おもむろにlsコマンドを実行しました。普段であればファイル一覧が表示されたはずですが、今日は様子がおかしい…。
[vagrant@master ~]$ docker container run --rm -it nginx:1.20.2 /bin/bash root@6a62dea033f3:/# ls -l ls: cannot access 'bin': Operation not permitted ls: cannot access 'boot': Operation not permitted ls: cannot access 'dev': Operation not permitted ls: cannot access 'etc': Operation not permitted ls: cannot access 'home': Operation not permitted ls: cannot access 'lib': Operation not permitted ls: cannot access 'lib64': Operation not permitted ls: cannot access 'media': Operation not permitted ls: cannot access 'mnt': Operation not permitted ls: cannot access 'opt': Operation not permitted ls: cannot access 'proc': Operation not permitted ls: cannot access 'root': Operation not permitted ls: cannot access 'run': Operation not permitted ls: cannot access 'sbin': Operation not permitted ls: cannot access 'srv': Operation not permitted ls: cannot access 'sys': Operation not permitted ls: cannot access 'tmp': Operation not permitted ls: cannot access 'usr': Operation not permitted ls: cannot access 'var': Operation not permitted ls: cannot access 'docker-entrypoint.d': Operation not permitted ls: cannot access 'docker-entrypoint.sh': Operation not permitted total 0 d????????? ? ? ? ? ? bin d????????? ? ? ? ? ? boot d????????? ? ? ? ? ? dev d????????? ? ? ? ? ? docker-entrypoint.d -????????? ? ? ? ? ? docker-entrypoint.sh d????????? ? ? ? ? ? etc d????????? ? ? ? ? ? home d????????? ? ? ? ? ? lib d????????? ? ? ? ? ? lib64 d????????? ? ? ? ? ? media d????????? ? ? ? ? ? mnt d????????? ? ? ? ? ? opt d????????? ? ? ? ? ? proc d????????? ? ? ? ? ? root d????????? ? ? ? ? ? run d????????? ? ? ? ? ? sbin d????????? ? ? ? ? ? srv d????????? ? ? ? ? ? sys d????????? ? ? ? ? ? tmp d????????? ? ? ? ? ? usr d????????? ? ? ? ? ? var
「Operation not permitted」がズラッと並び、lsコマンドの結果も「?」だらけです。一体何が起こっているのでしょうか?
この現象の謎を解き明かすために、Dockerが採用しているセキュリティ機構である「Seccomp」を知ることから始めましょう。
Seccompとは
通常、プロセスは直接カーネルの機能を利用することはできず、主にシステムコールを介して利用する仕組みとなっています。システムコールには多くの種類があり、中にはreboot(2)のようにホストの停止・再起動を行うなど、自由に使えては危険なのもあります。
Seccompとは、Linuxカーネルが持つセキュリティ機構の一つで、Secure Computing Modeの略です。簡単に言うと、Seccompはシステムコールの許可・不許可を設定できるようにし、危険なシステムコールを実行できなくするためのものです。
注: 危険なシステムコールの例としてわかりやすいrebootシステムコールを挙げましたが、実際にコンテナ内でrebootシステムコールを実行しても、コンテナが終了するのみでホストを停止・再起動させることはありません。
DockerにおけるSeccomp
Dockerでは、Seccompをデフォルトで有効とし、コンテナ内で危険なシステムコールの実行を禁止するようになっています。Seccompはカーネルの機能であるため、ホストのカーネルでSeccompが有効になっていることと、Seccompを処理するためのlibseccompライブラリがホストにインストールされている必要があります。
さらにDockerでは、コンテナ内で利用可能なシステムコール一覧 (プロファイル)をあらかじめ組み込んでいるため、Seccompプロファイルを別途準備する必要がなく、安全な状態でコンテナを実行することができます。
お使いのDockerがSeccomp有効かどうかは、docker system infoコマンドで調べることができます。
[vagrant@master ~]$ docker system info (略) Server: (略) Security Options: seccomp Profile: default (略) [vagrant@master ~]$ docker system info --format '{{json .SecurityOptions}}' ["name=seccomp,profile=default"]
セキュリティオプションとしてseccompが有効で、内蔵のデフォルトプロファイルを利用していることがわかります。デフォルトで無効化されているおもなシステムコールについてはSignificant syscalls blocked by the default profileをご覧ください。
Seccompプロファイルの変更
デフォルトで無効化されているシステムコールを利用したい、という場合にはいくつか方法があります。
まずはdocker container runコマンドに--privilegedオプションを付与して実行することです。これによりコンテナは特権モードで実行されるため、Seccompで禁止されたシステムコールが利用可能となります。ただし、特権モードではホストのカーネル機能の利用やホストのデバイスへのアクセスなどほぼあらゆることが可能となるため、禁止されたシステムコールを利用する目的に比して過剰であり、そもそもむやみに利用してよいオプションではありません。
次はdocker container runコマンドに--security-opt seccomp=unconfinedオプションを付与して実行することです。これはデフォルトのSeccompを完全に無効化してしまうオプションで、すべてのシステムコールが利用可能となります。できれば利用したいシステムコールをピンポイントで指定したいので、やはりこれも過剰対応でしょう。
最後にdocker container runコマンドに--cap-addオプションと、利用したいシステムコールに対応したケーパビリティを付与して実行します。例えばstraceコマンドは、デフォルトのSeccompプロファイルではptraceシステムコールを禁止されているため利用できません。
[vagrant@master ~]$ docker container run --rm -it python:3.7.10 /bin/bash root@9caa75e64a09:/# apt update && apt install -y strace (略) root@9caa75e64a09:/# strace ls strace: test_ptrace_get_syscall_info: PTRACE_TRACEME: Operation not permitted strace: ptrace(PTRACE_TRACEME, ...): Operation not permitted +++ exited with 1 +++
デフォルトで無効化されているシステムコールを見ると、ptraceシステムコールはCAP_SYS_PTRACEケーパビリティを禁止することでも利用できなくなっていることがわかります。よってstraceコマンドを使いたい場合は、--cap-add CAP_SYS_PTRACEオプションを付与すればよいのです。
[vagrant@master ~]$ docker container run --rm -it --cap-add CAP_SYS_PTRACE python:3.7.10 /bin/bash root@4072ffbcb1b8:/# apt update && apt install -y strace (略) root@4072ffbcb1b8:/# strace ls execve("/bin/ls", ["ls"], 0x7ffebc490a30 /* 13 vars */) = 0 brk(NULL) = 0x55c76040a000 (略)
ここまでは禁止されているシステムコールを利用したいという場合でした。
では、デフォルトのSeccompプロファイルよりもさらにシステムコールを禁止したい場合はどうすればよいでしょうか? docker container runコマンドに--security-opt seccomp=...オプションを利用します。seccomp=の引数には、カスタマイズしたプロファイルを記載したファイルを指定します。まず、デフォルトのSeccompプロファイルをダウンロードしましょう。
[vagrant@master ~]$ curl -LO https://raw.githubusercontent.com/moby/moby/master/profiles/seccomp/default.json
ここでファイルの詳細情報を取得するシステムコールである「lstat」を禁止してみます。禁止するには、このファイルから該当行を削除するだけです。
[vagrant@master ~]$ cp -a default.json no-lstat.json [vagrant@master ~]$ vi no-lstat.json [vagrant@master ~]$ diff -u default.json no-lstat.json --- default.json 2021-11-26 06:25:50.336790052 +0000 +++ no-lstat.json 2021-11-26 06:24:15.784301565 +0000 @@ -195,7 +195,6 @@ "lremovexattr", "lseek", "lsetxattr", - "lstat", "lstat64", "madvise", "membarrier",
まずはデフォルトのSeccompプロファイルでコンテナを起動します。
[vagrant@master ~]$ docker container run --rm -it python:3.7.10 /bin/bash root@063438deb89a:/# ls -l total 0 drwxr-xr-x. 1 root root 179 Jun 23 00:54 bin drwxr-xr-x. 2 root root 6 Jun 13 10:30 boot drwxr-xr-x. 5 root root 360 Nov 26 07:10 dev drwxr-xr-x. 1 root root 66 Nov 26 07:10 etc drwxr-xr-x. 2 root root 6 Jun 13 10:30 home drwxr-xr-x. 1 root root 41 Jun 23 00:54 lib drwxr-xr-x. 2 root root 34 Jun 21 00:00 lib64 drwxr-xr-x. 2 root root 6 Jun 21 00:00 media drwxr-xr-x. 2 root root 6 Jun 21 00:00 mnt drwxr-xr-x. 2 root root 6 Jun 21 00:00 opt dr-xr-xr-x. 101 root root 0 Nov 26 07:10 proc drwx------. 1 root root 24 Jun 23 15:30 root drwxr-xr-x. 3 root root 30 Jun 21 00:00 run drwxr-xr-x. 1 root root 20 Jun 23 00:53 sbin drwxr-xr-x. 2 root root 6 Jun 21 00:00 srv dr-xr-xr-x. 13 root root 0 Nov 26 05:37 sys drwxrwxrwt. 1 root root 25 Jun 29 01:46 tmp drwxr-xr-x. 1 root root 19 Jun 21 00:00 usr drwxr-xr-x. 1 root root 19 Jun 21 00:00 var
問題なくファイルのパーミッションや所有者、サイズや最終更新日時などの情報が取得できました。
では、「lstat」システムコールを禁止するように変更したSeccompプロファイルを指定してコンテナを起動してみましょう。
[vagrant@master ~]$ docker container run --rm -it --security-opt seccomp=no-lstat.json python:3.7.10 /bin/bash root@d39f912f6f6c:/# ls -l ls: cannot access 'bin': Operation not permitted ls: cannot access 'boot': Operation not permitted ls: cannot access 'dev': Operation not permitted ls: cannot access 'etc': Operation not permitted ls: cannot access 'home': Operation not permitted ls: cannot access 'lib': Operation not permitted ls: cannot access 'lib64': Operation not permitted ls: cannot access 'media': Operation not permitted ls: cannot access 'mnt': Operation not permitted ls: cannot access 'opt': Operation not permitted ls: cannot access 'proc': Operation not permitted ls: cannot access 'root': Operation not permitted ls: cannot access 'run': Operation not permitted ls: cannot access 'sbin': Operation not permitted ls: cannot access 'srv': Operation not permitted ls: cannot access 'sys': Operation not permitted ls: cannot access 'tmp': Operation not permitted ls: cannot access 'usr': Operation not permitted ls: cannot access 'var': Operation not permitted total 0 d????????? ? ? ? ? ? bin d????????? ? ? ? ? ? boot d????????? ? ? ? ? ? dev d????????? ? ? ? ? ? etc d????????? ? ? ? ? ? home d????????? ? ? ? ? ? lib d????????? ? ? ? ? ? lib64 d????????? ? ? ? ? ? media d????????? ? ? ? ? ? mnt d????????? ? ? ? ? ? opt d????????? ? ? ? ? ? proc d????????? ? ? ? ? ? root d????????? ? ? ? ? ? run d????????? ? ? ? ? ? sbin d????????? ? ? ? ? ? srv d????????? ? ? ? ? ? sys d????????? ? ? ? ? ? tmp d????????? ? ? ? ? ? usr d????????? ? ? ? ? ? var
想定通り、ファイルのパーミッションや所有者、サイズや最終更新日時などの情報の取得に失敗しました。
コンテナごとではなく、docker全体に変更したSeccompプロファイルを指定したい場合は /etc/docker/daemon.json にて seccomp-profile キーを指定してdockerを再起動します。
[vagrant@master ~]$ sudo cp no-lstat.json /etc/docker/ [vagrant@master ~]$ sudo vi /etc/docker/daemon.json { "seccomp-profile": "/etc/docker/no-lstat.json" } [vagrant@master ~]$ sudo systemctl restart docker
docker system infoを実行すると、デフォルトでないSeccompプロファイルを使っているという表示が確認できます。
[vagrant@master ~]$ docker system info (略) Server: (略) Security Options: seccomp WARNING: You're not using the default seccomp profile Profile: /etc/docker/no-lstat.json (略) [vagrant@master ~]$ docker system info --format '{{json .SecurityOptions}}' ["name=seccomp,profile=/etc/docker/no-lstat.json"]
これで--security-opt seccomp=no-lstat.jsonオプションを付与しなくても「lstat」システムコールが禁止されます。
[vagrant@master ~]$ docker container run --rm -it python:3.7.10 /bin/bash root@c2e99b57de61:/# ls -l ls: cannot access 'bin': Operation not permitted ls: cannot access 'boot': Operation not permitted ls: cannot access 'dev': Operation not permitted (略)
突然の「Operation not permitted」の原因とは?
長々とDockerのSeccompについて見てきましたが、そもそもの発端は、いつもは問題なかったlsコマンドが突然「Operation not permitted」となってしまったことでした。ここまで見てきた方であれば「原因はSeccompの設定だろう?」と予想がついているかもしれません。
しかし、docker container runには特にオプションは与えていません。
[vagrant@master ~]$ docker container run --rm -it nginx:1.20.2 /bin/bash root@96d2431fb81e:/# ls -l ls: cannot access 'bin': Operation not permitted ls: cannot access 'boot': Operation not permitted ls: cannot access 'dev': Operation not permitted (略)
Seccompプロファイルもデフォルトです。
[vagrant@master ~]$ docker system info --format '{{json .SecurityOptions}}' ["name=seccomp,profile=default"]
では一体何が? 「いつもは問題なかったlsコマンド」…ですが、よくよく考えてみると、一つ変えたことがあります。使用するnginxイメージを 1.20.1 から 1.20.2 にバージョンアップしていました。1.20.1 に戻してみると、
[vagrant@master ~]$ docker container run --rm -it nginx:1.20.1 /bin/bash root@51c4476dee49:/# ls -l total 12 drwxr-xr-x. 2 root root 4096 Oct 11 00:00 bin drwxr-xr-x. 2 root root 6 Oct 3 09:00 boot drwxr-xr-x. 5 root root 360 Nov 26 07:39 dev (略)
問題なく表示されています。
イメージのパッチレベルバージョンの違いで、Seccompにひっかかかったりひっかからなかったりするのでしょうか? そもそもSeccompの問題なのでしょうか? Seccompを無効化して確認してみましょう。
[vagrant@master ~]$ docker container run --rm -it --security-opt seccomp=unconfined nginx:1.20.2 /bin/bash root@59aa1e5c54c3:/# ls -l total 12 drwxr-xr-x. 2 root root 4096 Nov 15 00:00 bin drwxr-xr-x. 2 root root 6 Oct 3 09:15 boot drwxr-xr-x. 5 root root 360 Nov 26 07:46 dev (略)
Seccompを無効化すると、nginx:1.20.2 でも問題がなくなることがわかりました。ということはSeccompが原因で間違いなさそうです。
次はどのシステムコールが禁止されているのか、システムコールを追跡する strace コマンドを使って調べてみましょう。straceコマンドはデフォルトで禁止されているptraceシステムコールを使用するため、--cap-add CAP_SYS_PTRACEオプションで許可しておくことをお忘れなく。
まずは問題が起きていない nginx:1.20.1 で調べます。
[vagrant@master ~]$ docker container run --rm -it --cap-add CAP_SYS_PTRACE nginx:1.20.1 /bin/bash root@4a90aa7969fb:/# apt update && apt install -y strace (略) root@4a90aa7969fb:/# strace ls -l execve("/bin/ls", ["ls", "-l"], 0x7ffe830aea08 /* 10 vars */) = 0 (略) lstat("bin", {st_mode=S_IFDIR|0755, st_size=4096, ...}) = 0 (略)
「lstat」というシステムコールでファイルの情報を取得しているようです。Seccompデフォルトプロファイルで許可されているので、問題ない動作です。
では問題が起きている nginx:1.20.2 を調べてみましょう。
[vagrant@master ~]$ docker container run --rm -it --cap-add CAP_SYS_PTRACE nginx:1.20.2 /bin/bash root@3173e81ea71d:/# apt update && apt install -y strace (略) root@3173e81ea71d:/# strace ls -l execve("/bin/ls", ["ls", "-l"], 0x7ffcf52ec3a8 /* 10 vars */) = 0 (略) statx(AT_FDCWD, "bin", AT_STATX_SYNC_AS_STAT|AT_SYMLINK_NOFOLLOW, STATX_MODE|STATX_NLINK|STATX_UID|STATX_GID|STATX_MTIME|STATX_SIZE, 0x7ffcd3c9df30) = -1 EPERM (Operation not permitted) (略)
「lstat」ではなく「statx」というシステムコールを使っていて、これが禁止されているようです。
nginx:1.20.1 と nginx:1.20.2 のlsコマンドを比べてみると、バージョンが上がっています。
[vagrant@master ~]$ docker container run --rm -it nginx:1.20.1 /bin/bash root@94c53ae74d15:/# ls --version ls (GNU coreutils) 8.30 Copyright (C) 2018 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by Richard M. Stallman and David MacKenzie.
[vagrant@master ~]$ docker container run --rm -it nginx:1.20.2 /bin/bash root@fc847357a710:/# ls --version ls (GNU coreutils) 8.32 Copyright (C) 2020 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later https://gnu.org/licenses/gpl.html. This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Written by Richard M. Stallman and David MacKenzie.
ls (GNU coreutils)の更新履歴を見てみると、バージョン 8.32にて「lstat」システムコールの代わりに「statx」システムコールを使うように変更されています。
さらに nginx:1.20.1のDockerfileとnginx:1.20.2のDockerfileを比べてみると、ベースイメージが debian:buster-slim から debian:bullseye-slim に更新されています。おそらくベースイメージのバージョンアップに伴い、ls (GNU coreutils)もバージョンアップされたのでしょう。
つまり、nginx:1.20.1からnginx:1.20.2は、nginxとしてはパッチレベルのバージョンアップであったが、ベースイメージがメジャーバージョンアップだったためlsコマンドの「lstat」システムコールが「statx」システムコールに置き換わり、非互換が発生したのだろう…と結論づけたくなりますが、そうは問屋が卸しません。Seccompデフォルトプロファイルを見ると「statx」システムコールは許可済みであるからです。
では、何が問題なのでしょうか…。
これまで見てきたことをもう一度振り返ってみましょう。Seccompはそもそもカーネルの機能であり、Dockerはカーネルとの仲介にlibseccompライブラリを使っている、ということでした。
もしかするとlibseccomp側で「statx」システムコールに対応していないため? …と探してみると、ありました。
どうやら libseccomp 2.3.3 で「statx」システムコール対応がなされたようです。この問題が発生しているホストの libseccomp を確認してみると、
[vagrant@master ~]$ rpm -q libseccomp libseccomp-2.3.1-3.el7.x86_64
2.3.1という「statx」システムコールに対応していないバージョンでした。
libseccompをソースから入れ直さないといけないのか…と調べてみると、2.3.1-4にて「statx」システムコール対応がバックポートされたようです。早速アップデートしましょう。
[vagrant@master ~]$ sudo yum install libseccomp (略) [vagrant@master ~]$ rpm -q libseccomp libseccomp-2.3.1-4.el7.x86_64
そしてnginx:1.20.2でlsコマンドを実行してみると…
[vagrant@master ~]$ docker container run --rm -it nginx:1.20.2 /bin/bash root@9a71c723a080:/# ls -l total 12 drwxr-xr-x. 2 root root 4096 Nov 15 00:00 bin drwxr-xr-x. 2 root root 6 Oct 3 09:15 boot drwxr-xr-x. 5 root root 360 Nov 26 08:51 dev drwxr-xr-x. 1 root root 41 Nov 17 10:39 docker-entrypoint.d -rwxrwxr-x. 1 root root 1202 Nov 17 10:39 docker-entrypoint.sh drwxr-xr-x. 1 root root 66 Nov 26 08:51 etc drwxr-xr-x. 2 root root 6 Oct 3 09:15 home drwxr-xr-x. 1 root root 45 Nov 15 00:00 lib drwxr-xr-x. 2 root root 34 Nov 15 00:00 lib64 drwxr-xr-x. 2 root root 6 Nov 15 00:00 media drwxr-xr-x. 2 root root 6 Nov 15 00:00 mnt drwxr-xr-x. 2 root root 6 Nov 15 00:00 opt dr-xr-xr-x. 100 root root 0 Nov 26 08:51 proc drwx------. 2 root root 37 Nov 15 00:00 root drwxr-xr-x. 3 root root 30 Nov 15 00:00 run drwxr-xr-x. 2 root root 4096 Nov 15 00:00 sbin drwxr-xr-x. 2 root root 6 Nov 15 00:00 srv dr-xr-xr-x. 13 root root 0 Nov 26 05:37 sys drwxrwxrwt. 1 root root 6 Nov 17 10:39 tmp drwxr-xr-x. 1 root root 66 Nov 15 00:00 usr drwxr-xr-x. 1 root root 41 Nov 15 00:00 var
無事、ファイルの情報が正常に取得できるようになりました!
まとめ
本稿では「コンテナ内で実行したlsコマンドでOperation not permittedが出るようになった」という問題から、Dockerがデフォルトで採用しているSeccompというセキュリティ機構の仕組みや使い方について見ていき、原因を突き止めて解決してみました。
今回の問題の原因をまとめると、
- nginxイメージを1.20.1から1.20.2にバージョンアップした際、nginxだけでなくベースイメージがdebian:buster-slimからdebian:bullseye-slimに更新され、ls (GNU coreutils)コマンドがバージョンアップされた結果、ファイル情報を取得するために「lstat」システムコールに代わって「statx」システムコールを使うようになっていた。
- DockerのデフォルトSeccompプロファイルは「lstat」「statx」両システムコールを許可していたが、Dockerとカーネルを仲介するlibseccompのバージョンが古く、「statx」システムコールに対応しておらず、Seccompで禁止されてしまった。
となります。それぞれ、
- Docker HubにあるDocker社公式イメージであっても、どのようなイメージなのか精査する必要がある。
- Dockerはカーネルの機能を用いてコンテナを実現しているため、カーネルに関するパッケージはきちんと更新する。
という対応が必要となるでしょう。
もし同様の症状で困っている方がいらっしゃれば、本稿が参考となれば幸いです。