コンテナ防御を“設定”から“運用”へ:seccomp / AppArmor / SELinux をプロファイル管理で回す実践ガイド

はじめに

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) どれから始めるべきか(現場で失敗しにくい順)

おすすめの入り方はこうです。

  1. seccomp(まずは default を把握 → カスタムは小さく)
  2. AppArmor(Ubuntu系ならログから調整しやすい)
  3. 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 に逃げる” を設計で防ぐ(期限付き例外・戻す手順)

コンテナは便利ですが、便利さはしばしば“権限の濃さ”と表裏です。プロファイル運用は、その濃さを薄めて「突破されても致命傷にしない」ための現実的な一歩になります。


投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です