午前2時14分:決して受けたくない電話
夜中の2時14分、枕元のスマホが激しく震えた。監視ダッシュボードを見ると、エッジゲートウェイが真っ赤に染まっている。本番クラスター内のレガシーなWebサービスが何者かに突破されたのだ。私がターミナルを開く頃には、攻撃者はすでにラテラルムーブメント(横展開)を開始しており、より価値の高いターゲットを求めて内部の10.0.0.0/8ネットワークをスキャンしていた。
ログには見慣れた光景が記録されていた。侵入口はPHP-FPM 7.4構成の既知の脆弱性だった。攻撃者はwww-dataとしてシェルを奪取した。通常、これは制限された低権限のアカウントだ。しかし、どれほど権限が低いユーザーであっても、何百ものシステムコール(syscall)を通じて Linuxカーネルと対話することができてしまう。攻撃者はunshareやptraceを駆使して名前空間の脆弱性を探り、最終的にコンテナを脱出してホストを侵害する経路を見つけ出したのだ。
これは単なるPHPコードの不備ではない。OSレベルの境界設定の失敗だ。本来触れる必要のないカーネルの機能に、アプリケーションがアクセスすることを許していたのが根本的な原因だった。
根本的な問題:「全か無か」の罠
かつてのLinuxセキュリティはバイナリ(二者択一)だった。<a href="https://itnotes.dev/ja/linux-sudoers%e3%81%ae%e5%a0%85%e7%89%a2%e5%8c%96%ef%bc%9a%e3%80%8c%e5%85%a8%e5%93%a1%e3%81%abroot%e6%a8%a9%e9%99%90%e3%80%8d%e3%81%a7%e6%9c%ac%e7%95%aa%e7%92%b0%e5%a2%83%e3%82%92%e5%b4%a9%e5%a3%8a/">root</a>(UID 0)として何でもできるか、あるいは標準的なuserとしてほとんど何もできないかのどちらかだ。Webサーバーをポート80にバインドさせるために、プロセスをrootで実行することがよくあった。これは極めて大きなリスクだ。そのプロセスが侵害されれば、攻撃者は管理者と同等の全権限を手にすることになる。
権限昇格インシデントの多くは、プロセスに必要以上のパワーを与えているために発生する。Webサーバーに必要なのは、静的ファイルの読み取り theater ネットワークソケットのリスニングだ。カーネルモジュールのロードやシステムクロックの変更、マシンの再起動などは**不要**である。それにもかかわらず、デフォルトの設定では、多くのシステムがあらゆるプロセスに対して全システムコールへのアクセスを許可しており、その多くがカーネルのバグを突くために悪用され得るのだ。
このインシデントの後、二次認証ノードを要塞化する際、 toolcraft.app/ja/tools/security/password-generator のツールを使ってサーバーのシークレットを生成した。これは完全にブラウザ内で動作するため、ローカルマシンから外へデータが送信されることはない。私は、アプリケーションとカーネルの関係においても、これと同じレベルの厳格で局所的な隔離を実現したいと考えた。
戦略1:Linux Capabilitiesによる権限の細分化
Linux Capabilities(カーネル2.2で導入)は、rootの絶対的な権限を小さく個別の権限に分割する機能だ。現在、約40種類のCapabilityが存在する。例えば:
CAP_NET_BIND_SERVICE: 1024番未満のポートへのバインドを許可する。CAP_CHOWN: ファイルの所有者変更を許可する。CAP_SYS_TIME: システムクロックの設定を許可する。
これらを使用することで、プロセスに必要最小限の権限だけを正確に付与できる。ネットワークパケットをキャプチャする必要がある診断ツールがある場合、root権限を与えるのではなく、CAP_NET_RAWだけを付与する。これにより、万が一ツールが乗っ取られた際の影響範囲(爆発半径)を最小限に抑えることができる。
実践的な実装
現在のファイルのCapabilityを確認するにはgetcapを、適用するにはsetcapを使用する。以下は、インシデント後のクリーンアップ中に、バイナリから不要な権限を削ぎ落とした際の手順だ:
# すべてのroot権限を剥奪し、低いポートへのバインドのみを許可する
sudo setcap 'cap_net_bind_service=+ep' /usr/bin/my-web-app
# 権限を確認する
getcap /usr/bin/my-web-app
コンテナ環境では、「ゼロトラスト」の立場から設定を始めるべきだ。すべての権限を一旦ドロップし、必要なものだけを明示的に追加する:
# コンテナを起動する最も安全な方法
docker run --cap-drop=ALL --cap-add=NET_BIND_SERVICE my-secure-app
戦略2:Seccompによるインターフェース의フィルタリング
Capabilitiesが「権限」を制御するのに対し、Seccomp(Secure Computing Mode)は「インターフェース」を制御する。これは、いわばシステムコールのファイアウォールだ。Capabilityを持たない非rootユーザーであっても、依然としてexecve、fork、openなどは呼び出せる。現代のLinuxカーネル(v6.x)には450以上のシステムコールがあるが、一般的なアプリケーションが使用するのはせいぜい40〜60個だ。残りの400個は、不要な攻撃対象領域(アタックサーフェス)に過ぎない。
SeccompではJSONプロファイルを定義し、「このホワイトリストにないシステムコールをプロセスが実行しようとしたら、即座に終了させる」ようカーネルに指示できる。
Seccompプロファイルの構築
以下は、Nginxフリート向けに開発した要塞化プロファイルのスニペットだ。「デフォルト拒否(default deny)」戦略を採用している。安全性を確保するには、この方法しかない。
{
"defaultAction": "SCMP_ACT_ERRNO",
"architectures": [ "SCMP_ARCH_X86_64" ],
"syscalls": [
{
"names": [ "accept4", "epoll_wait", "pwrite64", "read", "write", "close" ],
"action": "SCMP_ACT_ALLOW"
}
]
}
このプロファイル下でアプリケーションを実行すると、リストにない操作を試みた際にカーネルはEPERMエラーを返す。これにより、socket呼び出しに依存するリバースシェルや、mountを使ってホストの機密ファイルシステムを覗こうとする攻撃を効果的にブロックできる。
Capabilities vs. Seccomp:ツールの使い分け
これらは競合するものではなく、異なるレイヤーを保護するものだ。エンジニアリングチームには、次のように説明している:
| 機能 | Linux Capabilities | Seccomp |
|---|---|---|
| 主な焦点 | 権限(何ができるか?) | システムコール(カーネルとどう対話するか?) |
| 粒度 | 粗い(約40カテゴリ) | きめ細かい(450以上の個別呼び出し) |
| 使いやすさ | 直感的なマッピング | 複雑。アプリのプロファイリングが必要 |
| 最適な用途 | SUID/Rootバイナリの置き換え | カーネルのゼロデイ脆弱性攻撃の緩和 |
多層防御:要塞化チェックリスト
午前2時のインシデントは、単一のセキュリティ層に頼るのがギャンブルであることを証明した。回復力のあるシステムを構築するには、これらの技術を組み合わせる必要がある。以下は、新しいサービスを導入する際の私の標準チェックリストだ:
1. Rootを捨てる
決してプロセスをUID 0で実行してはならない。Dockerfileには必ずUSERディレクティブを含めること。これが最も基本的な防御策だ。
2. すべてのCapabilityを剥奪する
ゼロベースで始める。Dockerでは--cap-drop=ALLを、SystemdユニットではCapabilityBoundingSet=を使用する。他に手段がない場合のみ、CAP_NET_BIND_SERVICEなどの特定の権限を追加する。
3. プロファイリングとSeccompの適用
straceを使用して、通常の起動・負荷サイクル中にアプリがどのシステムコールを使用しているかを正確に観察する。その上で、許可された呼び出しのみを定義したプロファイルを作成する。
# アプリを追跡して必要なシステムコールを特定する
strace -c -f ./my-app
4. Systemdサンドボックスの活用
コンテナ化されていないサービスの場合、最新のSystemdはこれらの技術を優れたラッパーとして提供している。.serviceファイルに以下の行を追加しよう:
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
NoNewPrivileges=yes
PrivateDevices=yes
ProtectSystem=strict
特にNoNewPrivileges=yesを有効にすることは極めて重要だ。これにより、そのプロセス(およびフォークされた子プロセス)がexecveを通じて新たな権限を獲得することを確実に防げる。これは、権限昇格の経路としてのSUIDバイナリを効果的に無効化する。
あの午前2時のインシデントは、バックアップからの復旧と48時間にわたるデプロイ用マニフェストの書き換えという苦い結末を迎えた。手痛い教訓だった。しかし現在、私たちのシステムは単に「コードにバグがないこと」を祈るような脆弱なものではない。コードには脆弱性があることを前提とし、たとえ攻撃が成功しても行き止まりになるような、極めて強固なサンドボックスを構築しているのだ。

