【実務・中級編】 Admission Controllerを用いたPodセキュリティ標準(PSS)の強制 – インフラ・ネットワーク & クラウドセキュリティ防御ガイド

クラウドネイティブの防衛線:KubernetesにおけるPodセキュリティ標準(PSS)の強制と要塞化

おい、最近のコンテナ設計のトレンドに乗っかって、何でもかんでも latest タグのイメージをデプロイして満足してないだろうな?

「うちはマネージドKubernetesを使っているからインフラは安全だ」なんてお花畑な考えを持っているなら、今日ですぐにその幻想を捨ててほしい。数々のインシデント現場を見てきた私から言わせれば、クラウド基盤自体の安全性と、その上で動くワークロード(Pod)の安全性はまったく別物だ。

特に、開発チームの利便性を優先するあまり、セキュリティのガードレールを外し、「とりあえず動く」状態のまま本番環境に放り込まれているクラスターをよく見かける。だが、攻撃者はそんな甘い隙を絶対に逃さない。今回は、Kubernetesの要塞化において最も効果的でありながら、現場で骨が折れる 「Admission Controllerを用いたPodセキュリティ標準(PSS)の強制」 について、攻撃者の視点と実務的な防御策を交えて徹底的に解説しよう。

—

なぜ「特権コンテナ」と「ホストパス」が命取りになるのか

まずは、攻撃者がKubernetesクラスターに侵入した際に何を狙うか、その典型的なシナリオを話しておこう。

アプリケーション層の脆弱性(例えば、不適切な入力検証や古いライブラリのRCEなど)を突いて、Webアプリケーションのコンテナ内に初期侵入を果たす。ここまではよくある侵入経路だ。しかし、ここからがKubernetes特有の悪夢のはじまりとなる。

もし、そのPodが以下のような危険な設定で作られていたとしたらどうなるか?

1. 特権コンテナ(privileged: true)の実行
コンテナ側からホストマシンのカーネル空間へ直接アクセスできるようになる。これは実質的に「ホストのroot権限を奪ったも同然」の状態だ。コンテナの脱出(Container Escape)など朝飯前で、クラウドのメタデータサービスを踏み台にしてAWSのIAMロールを強奪し、インフラ全体を乗っ取られる。

2. ホストのディレクトリマウント(hostPath の悪用)
ホストの /var/run/docker.sock や /etc などのクリティカルなパスがコンテナ内にマウントされている場合、攻撃者はホスト上で動いている他のコンテナを自由にいじったり、SSHのauthorized_keysを書き換えて永続的なバックドアを仕掛けたりできる。

「いやいや、うちの開発者はそんな危ないYAML書かないよ」と性善説にすがるのは、セキュリティチーフとしては失格だ。人間のミスや、外部から持ち込まれたサードパーティ製Helmチャートの隠し設定など、ヒューマンエラーは必ず起きる。だからこそ、人間を信じるな、仕組み(ポリシー)で縛れ。これがインフラ要塞化の鉄則だ。

—

Pod Security Admission (PSA) による強制の仕組み

Kubernetes v1.25以降、従来のPodSecurityPolicy(PSP)が完全に廃止され、標準のAdmission Controllerである Pod Security Admission (PSA) がデフォルトで有効化されている。

PSAでは、Kubernetes公式が定義する3つの「Podセキュリティ標準(PSS)」レベルを名前空間(Namespace)ごとに適用できる。

  • Privileged: 制限なし。既存のワークロード移行用であり、本番では論外。
  • Baseline: 既知の特権昇格を防ぐ最小限の制限(ホストのネットワークやPID名前空間の共有禁止など)。
  • Restricted: ハードニングの最高峰。ルートでの実行禁止、読み取り専用ルートファイルシステムの強制など、徹底的な制限。

これを「監査(Audit)」「警告(Warn)」「強制(Enforce)」の3つのモードで挙動を制御できる。もちろん、本番環境で守るべきは Enforce(違反したPodの作成を即座に拒否する) 一択だ。

—

【実践】セキュアな名前空間とワークロードの設定サンプル

では、実際にどのようにつまらない設定ミスや攻撃を防ぐのか、具体的なマニフェストを見ていこう。

ここでは、最も厳格な restricted レベルを強制する名前空間を作り、その中で安全に動作するPython製マイクロサービスのデプロイメント定義を例にする。

1. 名前空間レベルでのPSA強制設定

まず、名前空間を作成し、ラベルを用いて restricted プロファイル(バージョンは最新)を enforce モードで適用する。これにより、この名前空間内では基準を満たさないPodのデプロイが物理的に不可能になる。

# 1_namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: secure-production-app
  labels:
    # Pod Security Standardのレベルを「restricted(最も厳格)」に指定
    pod-security.kubernetes.io/enforce: "restricted"
    # 違反があった場合にデプロイを即座にブロック(拒否)する
    pod-security.kubernetes.io/enforce-version: "latest"
    
    # 開発者への警告メッセージを出すモード(任意)
    pod-security.kubernetes.io/warn: "restricted"
    pod-security.kubernetes.io/warn-version: "latest"

2. PSS(Restricted)に完全準拠したデプロイメント定義

次に、上記で作成した名前空間にデプロイするアプリケーションのマニフェストだ。
restricted レベルをクリアするためには、以下の要件を満たす必要がある。

  • ルートユーザー(UID 0)での実行禁止
  • 特権昇格の明示的な禁止(allowPrivilegeEscalation: false)
  • デフォルトのセキュリティコンテキストの適用(Seccompプロファイルの指定など)
  • ルートファイルシステムの読み取り専用化(推奨)
# 2_deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-api-service
  namespace: secure-production-app
  labels:
    app: api-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: api-service
  template:
    metadata:
      labels:
        app: api-service
    spec:
      # ポッド全体のセキュリティコンテキスト(Restrictedの必須要件)
      securityContext:
        runAsNonRoot: true          # ルートユーザーでの実行を強制的に禁止
        runAsUser: 10001            # 非特権UIDを指定
        runAsGroup: 10001           # 非特権GIDを指定
        fsGroup: 10001              # ボリュームのマウント時パーミッション用
        seccompProfile:
          type: RuntimeDefault      # ランタイムデフォルトのシステムコール制限を適用
      
      containers:
        - name: api
          image: python:3.11-slim
          command: ["python", "-m", "http.server", "8080"]
          
          # コンテナ単位のセキュリティコンテキスト
          securityContext:
            allowPrivilegeEscalation: false # 特権昇格を完全にブロック
            readOnlyRootFilesystem: true    # ルートファイルシステムを読み取り専用にする
            capabilities:
              drop:
                - ALL               # すべてのLinuxケーパビリティ(特権)を一旦剥奪
          
          ports:
            - containerPort: 8080
              
          resources:
            limits:
              cpu: "500m"
              memory: "512Mi"
            requests:
              cpu: "100m"
              memory: "128Mi"
              
          # 読み取り専用ルートFSのため、一時的な書き込み領域をメモリ上にマウント
          volumeMounts:
            - mountPath: /tmp
              name: tmp-dir
              
      volumes:
        - name: tmp-dir
          emptyDir: {}

このマニフェストを अप्लाई(apply)すれば、仮に開発者がうっかり privileged: true や runAsRoot: true を記述したコンテナを混ぜようとしても、KubernetesのAPIサーバーが次のようなエラーを吐いて跳ね返してくれる。

pods "secure-api-service-xxx" is forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false ...

このエラーログを見た瞬間、君のクラスターはサイバー攻撃者にとって「非常にコストの高い、旨味のない標的」に変わるのだ。

—

現場の裏話:既存アプリの移行で躓かないための現実的なアプローチ

「理論は分かったが、既存の古いレガシーアプリを動かしているから、いきなり restricted なんて適用したら全部落ちるよ!」という悲鳴が聞こえてきそうだ。その通り、現場のシステムは教科書通りには動かない。

もし大規模なクラスターでいきなり Enforce をかけてシステムを全滅させたら、私でも冷や汗をかく。だからこそ、現場では以下のステップを踏むべきだ。

1. まずは Audit モードと Warn モードで現状把握
名前空間のラベルを audit: restricted、warn: restricted に設定し、既存のPodたちがどれだけポリシーに違反しているかを監査ログ(Audit Log)で洗い出す。どのアプリケーションがどのルールに引っかかっているのかを可視化するのが先決だ。
2. 例外処理(Exemptions)の活用
どうしてもコードの改修が間に合わないサードパーティ製ミドルウェア(例えば、どうしてもroot権限を要求する古いDBなど)がある場合は、クラスタースコープの設定(PodSecurity リソース)で特定の名前空間やユーザー、プレフィックスを例外処理(Exemptions)として一時的に除外する。ただし、これには明確な期限を設けることだ。
3. 段階的な引き上げ
最初は baseline からスタートし、コードの改修が進み次第、徐々に restricted へ引き上げていく。

セキュリティは一日にして成らず。だが、放置すればインシデントは一瞬でやってくる。今日、この瞬間から、君の手元のクラスターの名前空間を見直し、不要な特権の剥奪を進めてほしい。それがプロのエンジニアの仕事だ。

コメント

タイトルとURLをコピーしました