はじめに
Docker やコンテナのセキュリティ対策というと、まず思い浮かぶのは「イメージを小さくする」「脆弱性スキャンを回す」「root 実行をやめる」「read-only にする」といった“作り方・動かし方”の改善です。これらは確かに効きます。ただ、本番運用で本当に強くなるのは 「侵入後の行動を縛る」設計です。
たとえば攻撃者がコンテナ内で任意コード実行を得たとしても、
- カーネルへの危険なシステムコールが叩けない
- 予期しないファイルアクセスや権限操作ができない
- コンテナ外へ広がる足場を作れない
こうした状態に持ち込めれば、被害は大きく縮みます。
そのための中核が seccomp / AppArmor / SELinux です。
ただし、ここでつまずきやすいのが「一度設定したら終わり」という発想です。現実にはアプリも依存も増減し、要件も変わります。だから必要なのは “プロファイル運用”です。
- プロファイルを コード(ファイル)として管理
- いつ、誰が、どの変更を入れたか 差分が追える
- 検証環境で ログから不足権限を特定
- 本番に段階適用し、 壊れない導入手順を確立
この記事では、3つの仕組みを「概念で理解」したうえで、ハンズオンとして seccomp と AppArmor を実際に適用し、最後に SELinux を運用の視点でどう扱うかまで整理します。
座学
1) 3兄弟の役割分担を押さえる(混ぜると迷子になる)
同じ“コンテナ防御”でも、守っている層が違います。
seccomp:システムコールのフィルタ(カーネルに届く直前で止める)
Linux では、ユーザー空間のプログラムが最終的にカーネル機能を使うとき system call(syscall) を叩きます。seccomp はここを絞る仕組みです。
- 「このプロセスは
mount()を呼べない」 - 「
ptrace()(デバッグ/追跡)を禁止」 - 「
clone()の一部を制限」
コンテナ内のアプリが“何をしようとしても”、カーネル呼び出しの入り口で落とせるので強力です。
Docker には デフォルト seccomp プロファイルがあり、通常はそれが効いています(意識していなくても効いていることが多い)。
AppArmor:パス中心のMAC(このバイナリはこのパスへ触れるな)
AppArmor は「このプログラム(プロファイル名)は、このパスに read だけ許可、write は禁止」といった形で縛ります。Ubuntu 系などでよく使われます。
/etc/shadowを読み取り禁止/proc/*の特定領域を禁止- ネットワークや capability と組み合わせて制限
“ファイルとプロセスの行動”に寄った制御で、ログも比較的追いやすいです。
SELinux:ラベル中心のMAC(このタイプはこのタイプへ触れない)
SELinux は “パス” ではなく “ラベル(type)” を基準にアクセス制御します。RHEL/CentOS/Fedora 系で強く、コンテナ世界でも重要です。
- ファイルに
container_file_tのようなラベルが付く - プロセスに
container_tのようなタイプが付く - 「このタイプからこのタイプへのアクセスは拒否」というポリシー
SELinux は慣れるまで難しいですが、ラベル運用がハマると 強力で一貫した制御になります。
2) “プロファイル運用”のゴール:最小権限を「維持」する
導入で終わると、だいたいこうなります。
- ある日アプリ更新で動かなくなる
- 原因が分からず
--security-opt seccomp=unconfinedに逃げる - そのまま戻さず、本番が“穴あき状態”で固定される
これを防ぐには、プロファイルを「一発芸」ではなく、次のように扱います。
- 段階導入:まず検証で enforce、ログを見て調整 → 本番へ
- 差分管理:Git 管理で PR でレビュー(誰が何を許したか残す)
- 観測:拒否ログが出たら、必要最小限だけ追加
- 例外の扱い:例外は“期限付き/理由付き”にして借金化させない
“最小権限”はゴールではなく、継続的に保つ状態です。
3) どれから始めるべきか(現場で失敗しにくい順)
おすすめの入り方はこうです。
- seccomp(まずは default を把握 → カスタムは小さく)
- AppArmor(Ubuntu系ならログから調整しやすい)
- SELinux(RHEL系なら避けられないので運用設計と一体で)
全部いっぺんに入れるより、1つずつ“運用できる形”にするほうが結局早いです。
ハンズオン
ここでは次をやります。
- seccomp:危険な syscall を明示的に拒否するカスタムプロファイルを当てる
- AppArmor:コンテナ用の簡易プロファイルを作って適用する
- SELinux:ボリュームラベルと運用上の注意点を実地で押さえる(環境依存が大きいので“要点重視”)
前提:Linux(Ubuntu 系を想定)。Docker が動く環境。
可能なら検証VMで。
0) サンプルコンテナを用意(動けばOK)
まずは何でもいいので nginx を起動。
docker run --rm -d --name demo -p 8080:80 nginx:alpine
curl -I http://localhost:8080
docker rm -f demo
1) seccomp:カスタムプロファイルを当てて挙動を確認
1-1) 最小の seccomp ルールを作る(例:unshare を拒否)
攻撃の足場作りで使われがちな syscall を、あえて拒否してみます。
seccomp-deny-unshare.json を作成:
{
"defaultAction": "SCMP_ACT_ERRNO",
"archMap": [
{ "architecture": "SCMP_ARCH_X86_64", "subArchitectures": ["SCMP_ARCH_X86", "SCMP_ARCH_X32"] }
],
"syscalls": [
{
"names": [
"read","write","open","openat","close","fstat","mmap","mprotect","munmap",
"brk","rt_sigaction","rt_sigprocmask","ioctl","pread64","pwrite64",
"readv","writev","access","pipe","select","sched_yield","mremap",
"clone","execve","exit","wait4","kill","uname","getpid","getuid","getgid"
],
"action": "SCMP_ACT_ALLOW"
},
{
"names": ["unshare"],
"action": "SCMP_ACT_ERRNO"
}
]
}
ポイント:
- default を拒否(ERRNO)にし、必要なものだけ allow する “強い” 形です
- 実務で最初からこれをやると壊しやすいので、ハンズオン用として割り切っています
- 実務では「default を土台に、deny を少し足す」ほうが安全です(後述)
1-2) このプロファイルを付けて実行
unshare を叩くテスト:
docker run --rm -it --security-opt seccomp=./seccomp-deny-unshare.json alpine:3.20 sh -lc "unshare -m true; echo done"
期待:unshare が失敗します(Operation not permitted など)。
これが seccomp の “カーネル入口で落とす” 感覚です。
1-3) 実務での現実解:default をベースに “deny 追加”で始める
ハンズオンのように allowlist 方式は強い反面、アプリ更新のたび壊れやすいです。現場で現実的なのは、
- まず Docker の default seccomp を使う
- そこに 追加で禁止したい syscall だけ deny する
- 拒否ログや稼働状況を見ながら強化する
という進め方です。最初の導入目標は「強いプロファイル」ではなく、“戻さず運用できる強化”です。
2) AppArmor:プロファイルを作って適用(Ubuntu系で試しやすい)
2-1) AppArmor が有効か確認
sudo aa-status
有効なら profiles が表示されます。
2-2) 超簡易プロファイルを作る
例として「危険なパスアクセスを拒否」するだけのイメージを作ります。
docker-demo-apparmor(ファイル)を作成:
#include <tunables/global>profile docker-demo-apparmor flags=(attach_disconnected,mediate_deleted) {
# 基本許可(かなり緩め)
network,
capability,
file,
umount, # 代表的に守りたい領域を拒否(例)
deny /etc/shadow r,
deny /root/** rwklx,
deny /proc/kcore r, # 最低限の実行に必要なもの(ざっくり)
/usr/** rix,
/bin/** rix,
/sbin/** rix,
/lib/** mr,
/lib64/** mr, # 一時領域
/tmp/** rw,
}
これは「AppArmor の雰囲気を掴む」ための例です。実務ではアプリごとにパス要件が違うので、ログを見て絞ります。
2-3) 読み込む
sudo apparmor_parser -r -W ./docker-demo-apparmor
sudo aa-status | grep docker-demo-apparmor || true
2-4) コンテナに付けて実行
docker run --rm -it --security-opt apparmor=docker-demo-apparmor alpine:3.20 sh -lc "cat /etc/shadow || true; echo ok"
期待:/etc/shadow の読み取りが拒否されます。
これが AppArmor の “パス中心で縛る” 感覚です。
2-5) 重要:AppArmor は「ログから整える」が基本
本番でやるなら、まず complain(学習)→ enforce(強制)の流れが安全です。拒否が出たら、必要最小限だけ許可を足していきます。
(ここを雑にやると、結局 “全部許可” に戻ってしまいます)
3) SELinux:コンテナ運用で外せない「ボリュームラベル」を押さえる
SELinux は環境依存が大きいので、ここでは “運用で死にやすい点” に絞ります。
3-1) SELinux が有効か確認(RHEL系など)
getenforce
Enforcing なら強制中です。
3-2) ボリュームマウント時の :Z / :z が重要
SELinux 環境でホストディレクトリをコンテナへマウントすると、ラベルが合わずに “Permission denied” になりがちです。
そのときに使うのが :Z / :z です。
:Z:そのコンテナ専用にラベル付け(基本はこっちが安全):z:複数コンテナで共有できるラベル付け(共有が必要なときだけ)
例:
mkdir -p ./data
docker run --rm -it -v "$(pwd)/data:/data:Z" alpine:3.20 sh -lc "touch /data/x && ls -l /data"
この「ボリュームのラベル設計」は、SELinux 導入というより SELinux 前提の運用そのものです。
“動かないから SELinux を Permissive にする” ではなく、ラベルで解決するのが筋になります。
まとめ
seccomp / AppArmor / SELinux は、コンテナ防御を「侵入前提の設計」に引き上げる武器です。ポイントは “入れること” より “運用すること” にあります。
- seccomp:syscall を止める。まずは default を理解し、禁止を小さく足すところから
- AppArmor:パス中心で縛る。ログで不足権限を特定し、段階的に enforce へ
- SELinux:ラベル中心で縛る。特にボリュームの
:Z/:zは運用の必須知識
そして、プロファイル運用として重要なのは次です。
- プロファイルは Git 管理し、レビューできる形にする
- 検証環境で拒否ログを拾い、必要最小限を積み上げる
- “例外で unconfined に逃げる” を設計で防ぐ(期限付き例外・戻す手順)
コンテナは便利ですが、便利さはしばしば“権限の濃さ”と表裏です。プロファイル運用は、その濃さを薄めて「突破されても致命傷にしない」ための現実的な一歩になります。
コメントを残す