皆さん、こんにちは!セキュリティバイブルの主筆を務めるホワイトハッカーです。
突然ですが、皆さんの目の前にあるPCや、日々開発しているシステム、安心して使えていますか?「大丈夫、ちゃんと動いているし」と思っているかもしれませんが、サイバー攻撃者は常に皆さんの「盲点」を探しています。特に、現代のインフラに欠かせないコンテナ技術は、便利さの裏側で、知らず知らずのうちに大きなセキュリティリスクを抱えていることがあるんです。
今日は、そんなコンテナセキュリティの深淵に、新人のIT担当者さんやセキュリティに初めて触れる開発者さんでも安心してついてこられるよう、泥棒や家の防犯に例えながら、とことん優しく、そして実践的に解説していきたいと思います。
コンテナの「特権昇格」って、一体何者?
まず、今日のテーマである「特権昇格」という言葉、ちょっと難しく聞こえますよね。でも、心配いりません。これを理解するために、皆さんの「家」を想像してみましょう。
- あなたの家全体: これが「ホストOS」や「物理サーバー」です。家の鍵は厳重に管理されていますよね。
- 各部屋: これが「コンテナ」です。リビング、寝室、書斎…それぞれの部屋で異なる活動ができます。
普段、皆さんがコンテナ(部屋)を使うとき、その部屋の中だけで作業をするのが普通ですよね。リビングで料理をし、寝室で眠る。しかし、もしリビングに侵入した泥棒が、そのリビングの鍵を不正に手に入れ、さらに他の部屋の鍵まで勝手に作ったり、家のマスターキーを奪って家全体を乗っ取ろうとしたらどうでしょう?
これが、コンテナにおける「特権昇格攻撃」のイメージです。
コンテナ(部屋)の中に入り込んだ攻撃者(泥棒)が、本来そのコンテナには許されていない「特権」(他の部屋の鍵や家のマスターキー)を不正に手に入れて、ホストOS(家全体)にまで影響を及ぼそうとする行為を指します。
「コンテナは隔離されているから安全でしょ?」と思われがちですが、実はこの「隔離」には、攻撃者が目を光らせる隙があるんです。
攻撃者の狙いは「システムコール」!
では、攻撃者はどうやって特権を昇格させようとするのでしょうか?そのカギとなるのが、「システムコール」という存在です。
システムコールとは、簡単に言えば「アプリ(部屋の住人)からOS(大家さん)へのお願い」のこと。
- 「ファイルを保存したい!」(
writeシステムコール) - 「新しいプロセスを立ち上げたい!」(
forkシステムコール) - 「ネットワークに接続したい!」(
socketシステムコール)
…といった具合に、アプリケーションがOSのカーネル(家の心臓部)に直接頼みごとをするためのインターフェースなんです。
コンテナの中で動くアプリケーションも、当然このシステムコールを使ってOSに様々な要求をします。しかし、もし攻撃者がコンテナ内で脆弱性を見つけ、本来は使われるべきでない、危険なシステムコールを悪用しようとしたら?
例えば、あるアプリはファイル操作しかしないはずなのに、OSの深い部分にアクセスできるようなシステムコール(例えば、mountでファイルシステムをマウントする、ptraceで他のプロセスを監視・操作する、perf_event_openでカーネル情報を盗むなど)を叩かれたらどうでしょう。これは、リビングの住人が、大家さんに「合鍵作成機を貸してくれ」とお願いするようなもの。大家さんが何も考えずに貸してしまったら、大変なことになりますよね。
攻撃者は、まさにこの「本来不要なシステムコール」を狙って、コンテナの境界を突破し、ホストOSの権限を奪おうとするのです。
鍵穴を絞る!Seccompプロファイルでシステムコールを制限しよう
では、この危険なシステムコールをどうやって防ぐか?その一つが、Seccomp(Secure Computing mode)プロファイルです。
Seccompは、Linuxカーネルのセキュリティ機能の一つで、プロセスが利用できるシステムコールを制限するための仕組みです。先ほどの例えで言えば、「部屋の鍵穴を、その部屋で使う最低限の鍵しか通さないように絞り込む」イメージです。
デフォルトのDockerコンテナでも、実はある程度のSeccompプロファイルが適用されています。しかし、それはあくまで「一般的な」アプリケーションを動かすための制限であり、特定のアプリケーションにとっては不要な、しかし攻撃者には悪用されかねないシステムコールが許可されている場合があります。
だからこそ、私たちは一歩踏み込んで、自分たちのアプリケーションが必要とするシステムコールだけを厳選し、それ以外はすべてブロックするという設定をしていく必要があるんです。
Seccompプロファイルの構造を見てみよう
Seccompプロファイルは、JSON形式で記述されます。ちょっと難しそうに見えるかもしれませんが、大丈夫。一つずつ見ていきましょう。
{
"defaultAction": "SCMP_ACT_ERRNO", // 許可されていないシステムコールが呼ばれた場合のデフォルト動作
// SCMP_ACT_ERRNO: エラーを返す
// SCMP_ACT_KILL: プロセスを強制終了する(より厳格)
"syscalls": [ // 個別に許可または拒否するシステムコールのリスト
{
"names": [ "read", "write", "openat", "close", "fstat", "lseek", "mmap", "munmap", "brk", "exit_group" ],
"action": "SCMP_ACT_ALLOW" // 上記のシステムコールは許可する
},
{
"names": [ "mount", "umount2", "pivot_root", "setuid", "setgid" ],
"action": "SCMP_ACT_ERRNO" // これらのシステムコールは明示的に拒否し、エラーを返す
}
]
}
defaultAction: ここで、リストに記載されていないシステムコールが呼び出された場合の「デフォルトの対応」を決めます。SCMP_ACT_ERRNOはエラーを返す、SCMP_ACT_KILLはプロセスを強制終了する、という設定です。基本的には、SCMP_ACT_ERRNOで必要なものを許可し、それ以外はエラーにするというアプローチが一般的です。syscalls: ここに、個別に許可したい、あるいは明示的に拒否したいシステムコールの名前と、それに対するactionを指定します。
こうすることで、「このアプリケーションはファイル読み書きしかしないから、mountのようなファイルシステム操作系のシステムコールは不要だよね?」と判断し、ピンポイントで制限できるわけです。
DockerでSeccompプロファイルを適用する方法
作成したSeccompプロファイル(例えば my_seccomp_profile.json という名前で保存)をDockerコンテナに適用するのはとても簡単です。
# Seccompプロファイルを指定してDockerコンテナを起動する例
docker run \
--rm \
--security-opt seccomp=/path/to/my_seccomp_profile.json \
my_image:latest
--security-opt seccomp=/path/to/my_seccomp_profile.json を追加するだけ。これにより、コンテナは指定されたプロファイルに基づいてシステムコールが制限された状態で実行されます。
注意点: Seccompプロファイルは非常に強力なため、アプリケーションが必要とするシステムコールを誤ってブロックしてしまうと、コンテナが正常に動作しなくなります。 実際の運用に入る前に、十分なテストを行うことが不可欠です。
もう一段階ガード!AppArmorで「部屋の利用規則」を定める
Seccompでシステムコールを制限するだけでもかなりの防御力になりますが、さらに一歩踏み込んでAppArmorという機能も活用してみましょう。
AppArmor(Application Armor)は、Linuxカーネルのセキュリティモジュールの一つで、アプリケーションごとにアクセス制御(ファイルアクセス、ネットワークアクセス、機能利用など)を設定できます。先ほどの例えで言えば、「部屋ごとの利用規則」のようなものです。
- 「リビングでは料理道具しか使えない」
- 「寝室ではネット接続できない」
- 「書斎では特定のファイルしか開けない」
といった具体的なルールをアプリケーションに適用することで、万が一コンテナ内に侵入されたとしても、攻撃者ができることを極限まで絞り込むことができます。
Seccompが「大家さんへのお願い(システムコール)の種類」を制限するのに対し、AppArmorは「部屋の中での具体的な行動(ファイルアクセスやネットワーク接続など)」を制限する、と考えると分かりやすいでしょう。両者を組み合わせることで、より強固な多層防御を築けます。
AppArmorプロファイルの構造を見てみよう
AppArmorプロファイルは、テキストファイルで記述されます。これも例を見てみましょう。
#include <tunables/global>
# プロファイル名(任意に設定)
profile my_apparmor_profile flags=(attach_disconnected, complain) {
# 継承するルール
#include <abstractions/base> # 基本的なファイルアクセスやネットワークアクセスを許可
# 明示的に拒否するルール
deny /etc/shadow rwk, # /etc/shadowファイルへの読み書き実行を拒否
deny /proc/** rwk, # /procディレクトリ配下への読み書き実行を拒否 (一部例外を除く)
# 明示的に許可するルール
# アプリケーションが動作に必要なパスのみを許可
/var/www/html/** r, # Webコンテンツディレクトリへの読み取りを許可
/usr/bin/nginx ix, # Nginxの実行ファイルを許可
/tmp/** rw, # /tmpディレクトリへの読み書きを許可
# ネットワークアクセス制限
network, # 全てのネットワークアクセスを許可(デフォルト)
# deny network, # 全てのネットワークアクセスを拒否する場合
# network tcp, # TCP接続のみ許可する場合
# network udp, # UDP接続のみ許可する場合
# その他の機能制限
# capability net_raw, # rawソケットの利用を許可
# deny capability net_raw, # rawソケットの利用を拒否
}
profile my_apparmor_profile flags=(...): プロファイル名と、モード(complainは違反をログに記録するがブロックしない、enforceはブロックする)を指定します。最初はcomplainモードで試運転し、問題がなければenforceに切り替えるのがおすすめです。deny: 明示的に拒否するルールです。例えば/etc/shadowのような重要なシステムファイルへのアクセスを拒否することで、攻撃者が情報を盗み出すのを防ぎます。- パス指定とアクセス権限:
/var/www/html/** rは、/var/www/html以下のすべてのファイルに対して読み取り(r)を許可します。rwkは読み書き実行、ixは実行、などがあります。 network: ネットワーク通信の許可・拒否を設定できます。
AppArmorは、どのファイルにアクセスできるか、どのネットワーク通信ができるか、といった「アプリケーションの振る舞い」を細かく制御できるため、Seccompと合わせて利用することで、攻撃者がコンテナ内でできることを大幅に制限できます。
DockerでAppArmorプロファイルを適用する方法
AppArmorプロファイル(例えば my_apparmor_profile という名前で保存)をDockerコンテナに適用するには、まずホストOSにプロファイルをロードする必要があります。
# AppArmorプロファイルをホストOSにロードする
sudo apparmor_parser -r -W /path/to/my_apparmor_profile
そして、Dockerコンテナを起動する際に、このロードしたプロファイルを指定します。
# AppArmorプロファイルを指定してDockerコンテナを起動する例
docker run \
--rm \
--security-opt apparmor=my_apparmor_profile \
my_image:latest
--security-opt apparmor=my_apparmor_profile を追加するだけですね。
注意点: AppArmorプロファイルもSeccompと同様に強力です。アプリケーションが必要とするアクセス権限を誤って拒否してしまうと、コンテナが正常に動作しなくなります。 ロードする前に文法チェックを行い、適用後もログを注意深く監視し、complainモードで十分にテストを行ってからenforceモードに切り替えるようにしましょう。
実践! Dockerでの設定例
さあ、ここまででSeccompとAppArmorの重要性を理解できたと思います。次は、具体的な設定例を通して、これらの防御をどのように組み込むかを見ていきましょう。
ここでは、非常にシンプルなNginxコンテナを例に取ります。NginxはWebサーバーなので、基本的に「Webコンテンツを読み込み、HTTPリクエストに応答する」以上のことをする必要はありませんよね。この原則に基づいてプロファイルを作成してみましょう。
1. Seccompプロファイルの作成(nginx_seccomp.json)
NginxがWebサーバーとして動作するために必要な最低限のシステムコールを許可します。ファイルシステム操作やネットワーク操作は許可しますが、mountやptraceといった危険なものはブロックします。
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": [
"read", "write", "openat", "close", "fstat", "lseek", "mmap", "munmap", "brk", "exit_group",
"access", "stat", "lstat", "getdents64", "fcntl", "ioctl", "sendfile",
"socket", "bind", "listen", "accept4", "connect", "sendto", "recvfrom", "getsockname", "getpeername", "setsockopt",
"clock_gettime", "getpid", "getppid", "gettid", "getuid", "getgid", "geteuid", "getegid", "getresuid", "getresgid",
"rt_sigaction", "rt_sigprocmask", "epoll_create1", "epoll_ctl", "epoll_wait",
"futex", "setrlimit", "prlimit64", "sysinfo", "uname"
],
"action": "SCMP_ACT_ALLOW"
},
{
"names": [
"mount", "umount2", "pivot_root", "setuid", "setgid", "setresuid", "setresgid",
"chown", "fchown", "lchown", "chmod", "fchmod",
"add_key", "request_key", "keyctl",
"ptrace", "perf_event_open",
"kexec_load", "init_module", "finit_module", "delete_module"
],
"action": "SCMP_ACT_ERRNO"
}
]
}
このプロファイルは、NginxがWebサーバーとして基本的なファイルI/O、ネットワーク通信、プロセス管理を行うために必要なシステムコールを許可しつつ、特権昇格につながる可能性のあるシステムコールを拒否しています。
2. AppArmorプロファイルの作成(nginx_apparmor_profile)
Nginxコンテナがアクセスすべきでないファイルやディレクトリを制限します。特に/etc/shadowのような機密情報や、/proc以下のカーネル情報への直接アクセスを制限します。
#include <tunables/global>
profile nginx_apparmor_profile flags=(attach_disconnected) {
# 基本的な抽象化ルールをインクルード
# これにより、一般的なファイルアクセスやネットワークアクセスが許可されます
# ただし、後述のdenyルールが優先されます
#include <abstractions/base>
#include <abstractions/nameservice> # DNS解決などのために必要になる場合があります
# 明示的に拒否するルール - 非常に重要!
deny /etc/shadow rwk, # /etc/shadow へのアクセスを拒否 (rootのパスワード情報など)
deny /etc/sudoers rwk, # /etc/sudoers へのアクセスを拒否 (sudo権限設定)
deny /boot/** rwk, # ブート関連ファイルへのアクセスを拒否
deny /proc/kcore rwk, # カーネルメモリダンプへのアクセスを拒否
deny /sys/** rwk, # /sys 以下のシステム情報へのアクセスを拒否 (一部例外を除く)
deny /dev/mem rwk, # 物理メモリへの直接アクセスを拒否
deny /dev/kmem rwk, # カーネルメモリへの直接アクセスを拒否
# Nginxがアクセスする必要があるパスの許可
/usr/sbin/nginx ix, # Nginx実行ファイルの実行を許可
/etc/nginx/** r, # Nginx設定ファイルへの読み取りアクセスを許可
/var/log/nginx/** rw, # Nginxログファイルへの読み書きアクセスを許可
/var/cache/nginx/** rw, # Nginxキャッシュディレクトリへの読み書きアクセスを許可
/var/www/html/** r, # Webコンテンツディレクトリへの読み取りアクセスを許可
/tmp/** rw, # /tmp ディレクトリへの読み書きアクセスを許可
# ネットワークアクセス
network inet stream, # IPv4 TCPストリームソケットを許可 (HTTP/HTTPS通信用)
network inet6 stream, # IPv6 TCPストリームソケットを許可
# 任意のポートへの listen を許可
# Nginxが80/443ポートなどで待機できるようにします
capability net_bind_service, # 1024番以下のポートでbindすることを許可
# その他の機能制限/許可
capability chown, # chownを許可 (必要なければdeny)
capability setgid, # setgidを許可 (必要なければdeny)
capability setuid, # setuidを許可 (必要なければdeny)
# これらのcapabiltyは、Nginxがワーカープロセスを非rootユーザーで実行するために必要になる場合があります。
# 実際のユースケースに合わせて厳密に調整してください。
}
このプロファイルは、Nginxの動作に必要なファイルやネットワークへのアクセスを許可しつつ、ホストOSの重要な部分へのアクセスを厳しく制限しています。
3. Dockerコンテナの起動
まず、AppArmorプロファイルをホストOSにロードします。
sudo apparmor_parser -r -W ./nginx_apparmor_profile
次に、これらのプロファイルを指定してNginxコンテナを起動します。
docker run -d \
--name my-secure-nginx \
-p 80:80 \
--security-opt seccomp=./nginx_seccomp.json \
--security-opt apparmor=nginx_apparmor_profile \
nginx:latest
これで、皆さんのNginxコンテナは、SeccompとAppArmorによって強力にガードされた状態で稼働するようになります!
まとめ:一歩ずつ、セキュアなシステムを築き上げよう
今日は、コンテナの「特権昇格」という攻撃のメカニズムから、SeccompやAppArmorといったLinuxカーネルの強力な防御機能まで、泥棒と家の防犯に例えながら解説してきました。
最初は難しく感じるかもしれませんが、「不要なものはすべて閉じ込める」という防犯の基本原則は、サイバーセキュリティの世界でもまったく同じです。アプリケーションが必要とする最低限の機能だけを許可し、それ以外はすべて拒否する。この考え方を身につけることが、攻撃者の盲点を突き、安全なシステムを構築するための第一歩になります。
「これって本当に全部必要なの?」「この機能がなければ、どんなリスクがあるんだろう?」という疑問を常に持ち、一つずつ検証しながら、セキュアなシステムを築き上げていきましょう。完璧なセキュリティは存在しませんが、知識と対策を積み重ねることで、リスクを限りなくゼロに近づけることは可能です。
皆さんのシステムが、今日も明日も安全に稼働することを願って。また次のセキュリティバイブルでお会いしましょう!
コメント