LinuxでAppArmorを導入する:カスタムセキュリティプロファイルを構築してプロセスを隔離し、ゼロデイ攻撃を阻止する

Security tutorial - IT technology blog
Security tutorial - IT technology blog

現場での現実的な課題:RCE発生時に標準権限が機能しない理由

昨年、Nginxゲートウェイの背後で稼働していた社内のFastAPIマイクロサービスが、ゼロデイのパストラバーサル脆弱性による直撃を受けました。わずか12分でその欠陥はリモートコード実行(RCE)へとエスカレートしました。私たちは標準的なセキュリティ強化チェックリストに従い、svc-workerという名前の専用非特権ユーザーの下でプロセスを実行していました。理論上は、それによって影響範囲(ブラスト半径)が限定されるはずでした。

しかし、防ぐことはできませんでした。

攻撃者はポート4444を介して外部のVPSへのリバースシェルを生成しました。さらに/etc配下にある40個以上の誰でも読み取り可能な設定ファイルを閲覧し、/tmpに探索スクリプトをドロップし、curlでツールを取得して、カーネル攻撃ベクトルの調査を開始したのです。正規のユーザーセッション内で動作する攻撃者を、従来のファイルアクセス権限だけで食い止めることは到底不可能でした。

攻撃者がデーモンを乗っ取ると、LinuxはそのPIDからのすべてのアクションを正当な処理として扱います。その結果、ファイルシステム全体が水平展開(ラテラルムーブメント)の脅威に晒されることになります。

根本原因の分析:攻撃を受けた際にDACが崩壊する理由

標準的なLinuxセキュリティは任意アクセス制御(DAC: Discretionary Access Control)に依存しており、権限をユーザーID(UID)とグループID(GID)に紐付けています。ワーカーをwww-data(UID 33)として実行すると、カーネルはそのUID 33がディスクやネットワークソケット全体に対して持つすべての権限を付与します。

このモデルには、アーキテクチャ上の重大な欠陥が2つ存在します。

  • アンビエントオーソリティ(環境的権限): Webサービスが必要とするのは、本来/var/www/htmlと単一のログパスへのアクセスだけです。それにもかかわらず、DACは/etc/passwdの読み取り権限、/usr/bin配下の1,500個以上のバイナリの実行権限、さらには生のアウトバウンドTCPソケット作成権限まで無差別に与えてしまいます。
  • バイナリの継承: Webアプリケーションが/bin/shをシェルアウトして呼び出すと、新しく生成された子プロセスは親プロセスが持つ広範な権限をすべて継承します。アプリケーションレベルで設けたガードレールは一瞬で無意味化します。

侵害されたソフトウェアを封じ込めるには、強制アクセス制御(MAC: Mandatory Access Control)が必要です。MACは、どのユーザーアカウントが起動したかに関係なく、特定のバイナリパスが実行できる操作そのものを厳密に制限します。

プロセス分離ソリューションの比較

サービスを厳格に制限する前に、主要な3つの分離レイヤーを評価してみましょう。

1. 標準的なDAC強化(ChrootおよびSystemdサンドボックス化)

ProtectSystem=strictやPrivateTmp=trueといったディレクティブは、初動の防御として堅牢です。しかし、カーネルレベルでの動的なパス単位のフィルタリングやきめ細かなネットワーク制御が不足しています。プロセスがCAP_SYS_CHROOTやrootケイパビリティを保持している場合、chrootジェイルからの脱出は依然として容易です。

2. SELinux(ラベルベースのタイプ強制)

SELinuxは、RHEL、Rocky、Fedoraシステムにおいて優れた性能を発揮します。ポリシーの決定をファイルシステムの拡張属性に紐付けます。実績は十分であるものの、ゼロからカスタムSELinuxポリシーを作成するには特殊なコンパイラや複雑なコンテキストのトラブルシューティングが必要となります。本番環境で問題が発生した際、フラストレーションのあまり即座にpermissiveモードに切り替えてしまうチームも少なくありません。

3. AppArmor(パスベースのMAC)

AppArmorは、セキュリティ境界を/opt/myapp/bin/serverのようなファイルシステムパスに直接結び付けます。構文は平文のプレーンテキストであり、UbuntuやDebianでは標準で組み込まれており、プロファイリングツールが運用パターンをリアルタイムで学習します。変化の速いエンジニアリングチームにとって、セキュリティと保守性のバランスが最も優れた選択肢となります。

最適なアプローチ:カスタムAppArmorプロファイルの構築

AppArmorはバイナリの周囲に封じ込めリング(保護壁)を形成します。攻撃者がアプリケーションメモリ内で生のシェルコードを実行したとしても、宣言されていないファイルの読み取り、ソケットのバインド、サブシェルの呼び出しはすべてシステムコールの境界でカーネルによってブロックされます。

ステップ1:ユーティリティのインストールとカーネルサポートの確認

まず、カーネルの状態を確認します:

sudo aa-status

無効になっているか見つからない場合は、DebianまたはUbuntuでユーザースペースツールチェーンをインストールします:

sudo apt update
sudo apt install apparmor apparmor-utils auditd -y
sudo systemctl enable --now apparmor

ステップ2:aa-genprofによるベースラインプロファイルの生成

/opt/api-service/server.pyで実行されている社内Pythonサービスを例にします。まず、AppArmorのプロファイリングユーティリティを起動します:

sudo aa-genprof /opt/api-service/server.py

そのターミナルは起動したままにしておきます。別のシェルを開き、エンドツーエンドのテストスイートを実行するか、curlやApache Benchを用いて通常のワークロードを流します。ヘルスチェックをトリガーし、ログファイルをローテーションさせ、データベースプールを稼働させ、意図的にアプリケーションエラーを発生させます。AppArmorはカーネルの監査フックと連携し、各システムコールを記録します。

ステップ3:プロファイルの手動構成と調整

AppArmorはプロファイルを/etc/apparmor.d/に保存し、ファイルシステムパスのスラッシュをドットに変換します。ルールを確認・編集するには、/etc/apparmor.d/opt.api-service.server.pyを直接開きます。

以下は堅牢化されたプロファイル構成の例です:

#include <tunables/global>

/opt/api-service/server.py {
  # 標準ベースアブストラクション
  #include <abstractions/base>
  #include <abstractions/python>
  #include <abstractions/nameservice>

  # アプリケーションソースファイル
  /opt/api-service/server.py r,
  /opt/api-service/** r,

  # 追記専用ログファイル
  /var/log/api-service/output.log w,
  /var/log/api-service/*.log a,

  # 読み取り専用設定ファイルアクセス
  /etc/api-service/config.yaml r,

  # 一時作業ファイルの制限
  /tmp/api-service-* rw,

  # シェル、コンパイラ、機密認証情報のブロック
  deny /bin/** x,
  deny /usr/bin/** x,
  deny /etc/shadow* rwklx,
  deny /etc/ssh/** rwklx,
  deny /root/** rwklx,

  # ネットワークアクセス: IPv4およびIPv6ストリームソケットに制限
  network inet stream,
  network inet6 stream,
}

覚えておくべき主要な権限フラグ:

  • r: ディスクからのデータ読み取り
  • w: 既存ファイルの書き込みまたは切り詰め(truncate)
  • a: データの追記のみ(攻撃者によるログの消去を防止)
  • k: ファイルロック
  • x: バイナリの実行
  • deny: 広範なインクルードルールよりも優先される明示的な拒否

ステップ4:厳格な強制モードの前にComplainモードを活用する

新規作成したプロファイルを本番環境でいきなりEnforce(強制)モードにしてはいけません。まずはComplain(警告)モードに切り替えます:

sudo aa-complain /etc/apparmor.d/opt.api-service.server.py

Complainモードでは、AppArmorはシステムコールを中断することなくポリシー違反をログに記録します。48〜72時間ほど実際の本番トラフィックを観察させ、その後にログを確認します:

sudo grep "apparmor=\"ALLOWED\"" /var/log/audit/audit.log
# または systemd-journald を実行しているシステムの場合:
sudo journalctl -fx | grep -i apparmor

監査ストリームに正常なアプリケーションイベントが現れたら、aa-logprofを実行してそれらの例外を評価し、自動的にインポートします:

sudo aa-logprof

ステップ5:サンドボックスの強制とゼロデイ攻撃対策の検証

通常運用中の監査ログノイズが完全にゼロになったら、プロファイルをEnforceモードにロックダウンします:

sudo aa-enforce /etc/apparmor.d/opt.api-service.server.py

続いて、エクスプロイトのシミュレーションを行って隔離状態を検証します。攻撃者が/bin/bashを要求するペイロードを実行したり、/etc/crontabに書き込もうとしたり、curlを実行したりしても、カーネルは即座にPermission denied(EACCES)エラーコードを返して実行を阻止します:

type=AVC msg=audit(1710928421.412:942): apparmor="DENIED" operation="exec" profile="/opt/api-service/server.py" name="/bin/bash" pid=4128 comm="python3" requested_mask="x" denied_mask="x"

本番セキュリティに向けた実践的なアドバイス

プロファイルを書くこと自体は難しくありません。何百ものノードで安定して管理するには、慎重な運用手法が求められます。

  • サービス認証情報の保護: 堅牢なポリシーは、侵害されたプロセスが機密トークンを求めてディレクトリツリーを探索するのを防ぎます。サンドボックスポリシーに加えて、内部データベースやAPI向けに推測困難なシークレットを用意し、デプロイ定義に含める前にクライアント側のシークレット生成ツールで最低32文字以上の文字列を生成してください。
  • プロファイルをバージョン管理下に置く: すべての/etc/apparmor.d/ファイルをAnsibleプレイブックやDockerfileと一緒に追跡・管理します。障害発生後のノード復旧時でも、プロファイルの再読み込みは5秒未満で完了します。
  • ランタイムではなく実行ターゲットをプロファイル化する: Python、Ruby、Node.jsを扱う場合、プロファイルは対象のスクリプトやラッパー(/opt/api-service/run.shや/opt/api-service/server.pyなど)に紐付けてください。ポリシーを/usr/bin/python3に直接適用してしまうと、ホスト全体のシステム保守スクリプトまで制限されてしまいます。
  • CI/CDにプロファイルのリグレッションテストを追加する: CI実行時のテストコンテナは、AppArmorを有効化した状態で起動してください。プルリクエストによってプロファイル未登録のキャッシュディレクトリやデータベースドライバが追加された場合、ステージング環境に到達する前にパイプラインを失敗させることができます。
Share: