午前2時14分のポストモーテム
ページャーが鳴ったのは、ちょうど午前2時14分でした。その夜、すでに1,200回以上のSSHブルートフォース攻撃をブロックしていたため、最悪の事態は去ったと思っていました。しかし、それは間違いでした。見落としていた小さなツールであるPython製のウェブスクレイパーが、リモートコード実行(RCE)の脆弱性を突かれたのです。そのツールは非ルートユーザーで実行されていましたが、攻撃者は/tmpに1.2MBの悪意のあるバイナリを送り込むことに成功し、データベースの認証情報を求めて/etcのスキャンを開始しました。
このインシデントは、標準的なユーザー権限だけでは不十分であることを証明しました。サービスがシステムの他の部分に触れる必要がないのであれば、それを見ることさえできないようにすべきです. Systemdのサンドボックス化は、Linuxの名前空間(namespace)を使用してアプリケーションを制限された環境に閉じ込めることで、この問題を解決します。これにより、システム全体に及ぶ可能性のある侵害を、封じ込められた軽微なインシデントへと変えることができます。
クイックスタート:5分でサービスを要塞化する
[Service]ブロックにわずか6行を追加するだけで、攻撃表面(アタックサーフェス)を大幅に削減できます。/etc/systemd/system/my-app.serviceにある典型的なサービスファイルの移行例を見てみましょう。
脆弱な設定
[Unit]
Description=脆弱なアプリ
[Service]
ExecStart=/usr/bin/python3 /opt/my-app/app.py
User=myappuser
Restart=always
[Install]
WantedBy=multi-user.target
要塞化された設定
[Unit]
Description=セキュアなアプリ
[Service]
ExecStart=/usr/bin/python3 /opt/my-app/app.py
User=myappuser
Restart=always
# セキュリティの要塞化
PrivateTmp=true
ProtectSystem=full
ProtectHome=true
NoNewPrivileges=true
PrivateDevices=true
DevicePolicy=closed
[Install]
WantedBy=multi-user.target
systemctl daemon-reloadを実行してからsystemctl restart my-appを実行して、これらの変更を適用します。これで、ドアに鍵をかけたことになります。
サンドボックスの仕組み
Systemdは、カーネルレベルの名前空間(特にmount、UTS、IPC、PID)を使用して、これらの制限を強制します。それぞれのディレクティブが内部で実際に何を行っているのかを解説します。
1. PrivateTmp=true
/tmpおよび/var/tmpディレクトリは、全ユーザーが書き込み可能であるため、攻撃者にとって格好の遊び場となります。PrivateTmp=trueを設定すると、サービスは独自のファイルシステム名前空間を取得します。ホストの他の部分から完全に隔離されたプライベートな/tmpが表示されるようになります。サービスが停止すると、Systemdはプライベートディレクトリを消去し、残された悪意のあるペイロードを即座に削除します。
2. ProtectSystem=full
このディレクティブは、サービスがOSのどの部分を変更できるかを制御します。主に3つのレベルがあります。
- true:
/usr、/boot、/efiを読み取り専用としてマウントします。 - full: 上記に加え、
/etcも読み取り専用にします。 - strict: ファイルシステム全体を読み取り専用にします。その後、
ReadWritePaths=を使用して特定のフォルダに手動で書き込み権限を付与する必要があります。
ほとんどのAPIにとって、fullが最適なバランスです。これにより、攻撃者が設定ファイルやバイナリを書き換えるのを防ぐことができます。
3. NoNewPrivileges=true
これにより、サービスとその子プロセスが新しい権限を取得するのを防ぎます。具体的にはPR_SET_NO_NEW_PRIVSフラグを設定し、SUID/SGIDビットを無効にします。たとえ攻撃者がアプリ内部からsudoやpkexecのようなSUIDバイナリのバグを見つけたとしても、それを利用してルート権限に昇格することはできません。
4. ProtectHome=true
あなたのアプリは本当に/home/adminを見る必要がありますか?おそらく必要ないでしょう。これをtrueに設定すると、/home、/root、/run/userが完全に空に見えるようになります。これにより、アプリケーションが侵害された場合に、SSHキーや個人データが流出するのを防ぎます。
高度な要塞化
サービスが外部に公開されている場合は、カーネルへの露出を制限するためにさらに厳格な制約を適用する必要があります。
システムコールのフィルタリング
標準的なウェブアプリがreboot()やkexec_load()を呼び出す必要はありません。ホワイトリストを使用して、許可されるシステムコールを制限できます。Systemdは、一般的なニーズをカバーする便利な@system-serviceグループを提供しています。
# 危険または不要なカーネルコールをブロック
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
カーネルケーパビリティの削除
Linuxでは、ルート権限は「ケーパビリティ(capabilities)」に細分化されています。例えば、CAP_NET_BIND_SERVICEはプロセスが443番ポートにバインドすることを許可します。アプリが8080番ポートで動作しているなら、ケーパビリティは一切必要ありません。次の1行ですべて破棄できます。
CapabilityBoundingSet=
ネットワークの隔離
ファイルを処理するだけのローカルワーカーを実行している場合は、ネットワークを完全に無効にします。PrivateNetwork=trueは、ローカルループバックインターフェース(lo)以外のすべてからサービスを切断します。
本番環境への導入チップス
注意して行わないと、要塞化によって機能が壊れることがあります。安全に変更を導入する方法を紹介します。
systemd-analyzeによる監査
推測に頼らず、結果を確認しましょう。Systemdにはセキュリティ監査機能が組み込まれています。次のコマンドを実行して、現在の露出状況を確認してください。
systemd-analyze security my-app.service
ほとんどのデフォルトサービスは、10点満点中9.6点という危険なスコアを叩き出します。本番グレードのサービスを目指すなら、スコアを2.5以下に下げることを目標にしてください。
ファイルシステムアクセスの処理
ProtectSystem=strictを使用すると、ログを書き込もうとしたときにアプリが失敗します。パスを自動的に管理するために、以下のモダンなディレクティブを使用してください。
RuntimeDirectory=my-app # /run/my-app を作成
StateDirectory=my-app # /var/lib/my-app を作成
LogsDirectory=my-app # /var/log/my-app を作成
Systemdがディレクトリ作成を処理し、所有者をサービスファイルで定義されたUser=に設定します。これは手動でchownコマンドを実行するよりもクリーンで安全です。
ロギングの効率化
サンドボックス内のフラットファイルへの書き込みは避けましょう。アプリケーションがstdout(標準出力)またはstderr(標準エラー出力)にログを出力するように設定します。Systemdのjournaldがこれらのストリームを自動的に収集するため、サービスが/var/logへの書き込み権限を持つ必要がなくなります。
結論
セキュリティとは多層防御のことです。Systemdのサンドボックス化により、単一の脆弱性がシステム全体の完全な乗っ取りにつながるのを防ぐことができます。攻撃者をネットワークがなく権限昇格もできない読み取り専用の箱に閉じ込めることで、対応のための時間を稼ぐことができます。まずはPrivateTmpとProtectSystemから始め、systemd-analyzeを使用して残りのギャップを埋めていきましょう。

