Kubernetes APIサーバーを狙う攻撃者の手口と、その盲点を突く監査ログ分析による鉄壁の防御策
おい、諸君。君たちが日々運用しているKubernetesクラスター、その心臓部とも言えるAPIサーバーに、一体どれほどの注意を払っているだろうか? 多くのエンジニアは、アプリケーションのデプロイやスケーリングにばかり気を取られがちだが、そのAPIサーバーこそが、サイバー攻撃者たちが最も血眼になって狙う「牙城」なのだ。
私はこれまで、数々の不正アクセスインシデントの現場に立ち会ってきた。その中で、Kubernetes APIサーバーを悪用した攻撃がどれほど巧妙で、そしてどれほど壊滅的な被害をもたらすかを身をもって知っている。今日の話は、教科書的なセキュリティガイドラインの受け売りではない。現場の泥臭いインシデントハンドリングから見えてきた、攻撃者の思考、そしてそれを打ち破るための具体的な防御策について、君たちに伝授しよう。
攻撃者はAPIサーバーの「どこ」を狙うのか?
攻撃者がKubernetes APIサーバーを狙う目的は、大きく分けて以下の3つだ。
1. 情報窃取: クラスター内のPod、Service、Secretなどのリソース情報を盗み出し、さらなる攻撃の足がかりにする。
2. 不正操作: 権限を奪取したり、悪意のあるPodをデプロイしたりして、クラスターの乗っ取りや、他のシステムへの攻撃の踏み台にする。
3. サービス妨害(DoS): APIサーバーに過負荷をかけ、クラスター全体の機能を麻痺させる。
特に、1と2は巧妙化しており、気づかれないうちに進行するケースが多い。例えば、以下のような攻撃シナリオが考えられる。
攻撃シナリオ例:機密情報への不正アクセスと権限昇格
攻撃者は、まず何らかの方法でKubernetesクラスターへの限定的なアクセス権(例えば、脆弱なServiceAccountや、外部からアクセス可能なAPIエンドポイントの脆弱性など)を獲得する。そこから、APIサーバーの監査ログを細かく監視し、管理者がどのような操作を行っているか、どのような権限を持つServiceAccountが存在するかを学習する。
そして、攻撃者は以下の手順で進行する。
1. 情報収集:
kubectl get pods --all-namespacesで全てのPod情報を取得。kubectl get secrets --all-namespacesで全てのSecret情報を取得。特に、kube-system名前空間にあるSecretは、クラスターの運用に関わる重要な情報を含んでいることが多い。kubectl get serviceaccounts --all-namespacesで利用可能なServiceAccountとその権限を確認。
2. 脆弱なServiceAccountの発見:
- 特定のアプリケーションが使用しているServiceAccountに、必要以上の権限が付与されている場合、それを発見する。例えば、
cluster-admin権限のような強力なRoleBindingが誤って紐づけられているServiceAccountなどだ。
3. 不正なリソース作成/変更:
- 発見した強力な権限を持つServiceAccountを悪用し、例えば、外部からアクセス可能なIngressを新規作成して、本来アクセスできないはずの内部Serviceにルーティングしたり、悪意のあるコンテナイメージを持つPodをデプロイしたりする。
- あるいは、既存のPodの設定を書き換え、特権(privileged)モードで実行されるように変更し、ホストOSへのアクセスを試みる。
この攻撃の恐ろしい点は、一連の操作が正規のAPIリクエストとして実行されるため、ログだけを追っていても、一見すると正規の操作に見えてしまうことだ。特に、自動化されたデプロイパイプラインや、CI/CDツールからのリクエストに紛れ込ませられると、発見はさらに困難になる。
攻撃者の盲点を突く:監査ログ分析による異常検知
ここで、我々が頼るべきは「監査ログ」だ。Kubernetes APIサーバーは、すべてのリクエストとレスポンスを詳細に記録する監査ログ機能を持っている。このログを適切に収集・分析し、異常なアクティビティを検知する仕組みを構築することが、攻撃者の裏をかくための鍵となる。
監査ログの「何」を監視すべきか?
監視すべき項目は多岐にわたるが、特に以下の点に注目したい。
- 異常なAPIエンドポイントへのアクセス: 通常はアクセスされないはずのエンドポイント(例:
/debug/pprofなど)へのアクセス。 - 権限の変更: Role、ClusterRole、RoleBinding、ClusterRoleBinding の作成、変更、削除。特に、
cluster-admin権限が付与されるような操作は厳重に監視すべきだ。 - 機密情報(Secret)へのアクセス・操作: Secretの作成、取得、削除。特に、
kube-system名前空間や、重要なアプリケーションが使用するSecretへのアクセス。 - Podの特権モードでのデプロイ:
securityContext.privileged: trueを指定したPodの作成。 - ネットワーク設定の変更: Ingress、Service、NetworkPolicy の作成・変更。特に、外部公開されるIngressの追加は注意が必要だ。
- ServiceAccountの変更・作成: 新規ServiceAccountの作成や、既存のServiceAccountに付与される権限の変更。
- 大量のリソース作成/削除: 短時間に大量のPodやDeploymentが作成・削除される場合。
監査ログ基盤の構築:FluentdとElasticsearch/OpenSearch、Kibana/OpenSearch Dashboardsの活用
監査ログを効率的に収集・分析するには、一般的にFluentd(またはFluent Bit)をログコレクターとして、ElasticsearchやOpenSearchをログストレージとして、KibanaやOpenSearch Dashboardsを可視化・分析ツールとして組み合わせるのが定番だ。
1. 監査ログの有効化と出力設定
まず、Kubernetes APIサーバーの起動オプションで監査ログを有効にする必要がある。これは、APIサーバーを起動するコンポーネント(kube-apiserver)の設定ファイルや、Kubernetesクラスターのデプロイ方法(kubeadm, kops, クラウドマネージドサービスなど)によって異なる。
例として、kube-apiserver の起動オプションに以下のようなものを追加する。(これはあくまで例であり、実際の環境に合わせて調整が必要だ)
# kube-apiserver の起動オプション例(一部抜粋)
command:
- kube-apiserver
- --audit-log-path=/var/log/kubernetes/audit.log # 監査ログの出力パス
- --audit-policy-file=/etc/kubernetes/audit-policy.yaml # 監査ポリシーファイル
- --audit-log-maxsize=100 # ログファイルの最大サイズ (MB)
- --audit-log-maxbackup=10 # バックアップするログファイルの数
- --audit-log-maxage=10 # ログファイルの保持期間 (日)
# ... その他のオプション ...
次に、どのようなイベントを記録するかを定義する監査ポリシーファイル (audit-policy.yaml) を作成する。
audit-policy.yaml の例:
# audit-policy.yaml
apiVersion: apiserver.k8s.io/v1
kind: Policy
# RequestReceived: リクエスト受信時、ResponseStarted: レスポンス開始時、ResponseComplete: レスポンス完了時、LocalAudit: クラスター内でのみ記録
# Verbosity: 0: イベントのみ, 1: イベントとメタデータ, 2: イベント、メタデータ、ボディ
rules:
# 重要なリソース操作(作成、更新、削除)は詳細に記録
- level: RequestResponse
resources:
- group: "" # core API group
resources: ["secrets", "configmaps", "serviceaccounts", "namespaces", "pods", "services", "ingresses", "persistentvolumes", "persistentvolumeclaims"]
verbs: ["create", "update", "patch", "delete"]
omitStages: ["UnsavedRequestHeaders"] # ヘッダー情報は省略
# 権限関連の変更は詳細に記録
- level: RequestResponse
resources:
- group: rbac.authorization.k8s.io
resources: ["roles", "clusterroles", "rolebindings", "clusterrolebindings"]
verbs: ["create", "update", "patch", "delete"]
omitStages: ["UnsavedRequestHeaders"]
# 特定のユーザーやServiceAccountからの操作は詳細に記録(例: adminユーザーやkube-system内のServiceAccount)
# - level: RequestResponse
# users: ["system:admin", "user:some-user@example.com"]
# verbs: ["*"]
# omitStages: ["UnsavedRequestHeaders"]
# 特定のNamespace内の操作を詳細に記録
# - level: RequestResponse
# namespaces: ["kube-system", "monitoring"]
# verbs: ["*"]
# omitStages: ["UnsavedRequestHeaders"]
# 全てのAPIリクエストを簡易的に記録(デバッグ用、本番では注意)
# - level: Metadata
# verbs: ["*"]
# omitStages: ["UnsavedRequestHeaders"]
# 上記ルールにマッチしないものは記録しない(デフォルト)
【重要】 監査ポリシーは、パフォーマンスとストレージ容量に影響するため、必要最小限の情報を記録するようにチューニングすることが重要です。RequestResponse レベルでの記録は、リクエストボディとレスポンスボディまで保存するため、情報量が多くなります。まずは Request レベルや RequestResponse のうち、対象リソースとVerbを限定することから始めましょう。
2. Fluentd/Fluent Bitによるログ収集
次に、APIサーバーから出力される監査ログをFluentdやFluent Bitを使って収集し、Elasticsearch/OpenSearchに転送します。
fluentd.conf (一部抜粋):
# /etc/fluentd/fluent.conf または /etc/fluent/fluent.conf
# 監査ログファイルの入力プラグイン
<source>
@type tail
path /var/log/kubernetes/audit.log # APIサーバーの監査ログパスと一致させる
pos_file /var/log/fluentd-audit.pos # ログの進行状況を記録するファイル
tag kubernetes.audit # タグ名
<parse>
@type json # 監査ログはJSON形式で出力される
</parse>
</source>
# Elasticsearch/OpenSearchへの出力プラグイン
<match kubernetes.audit>
@type elasticsearch # または opensearch
host "your-elasticsearch-host" # Elasticsearch/OpenSearch のホスト名またはIPアドレス
port 9200
logstash_format true # Logstash互換のフォーマットで出力
logstash_prefix "kubernetes-audit" # インデックス名のプレフィックス
include_timestamp true
flush_interval 5s # 5秒ごとにバッチ送信
# 認証情報の設定 (必要に応じて)
# user "elastic"
# password "your_password"
</match>
Fluent Bit の設定例 (fluent-bit.conf):
# fluent-bit.conf
[INPUT]
Name tail
Path /var/log/kubernetes/audit.log # APIサーバーの監査ログパス
Tag kubernetes.audit
Format json
Interval_Sec 1
DB /var/log/flb_audit.db # 状態保存用DBファイル
[OUTPUT]
Name es # または opensearch
Match kubernetes.audit
Host your-elasticsearch-host # Elasticsearch/OpenSearch のホスト名またはIPアドレス
Port 9200
Logstash_Format On # Logstash互換フォーマット
Logstash_Prefix kubernetes-audit
Logstash_Dateformat %Y-%m-%d
# 認証情報の設定 (必要に応じて)
# HTTP_User elastic
# HTTP_Passwd your_password
【補足】 クラウド環境(AWS EKS, GCP GKE, Azure AKSなど)では、マネージドなログ収集サービス(CloudWatch Logs, Cloud Logging, Azure Monitor Logs)や、専用のエージェント(Fluent Bit DaemonSetなど)が用意されていることが多いです。これらのサービスを活用することで、ログ基盤の構築・運用負荷を大幅に軽減できます。
3. Kibana/OpenSearch Dashboardsでの可視化と異常検知
Elasticsearch/OpenSearchにログが蓄積されたら、KibanaやOpenSearch Dashboardsを使って可視化し、異常検知ルールを設定します。
異常検知ルールの設定例
Kibana/OpenSearch Dashboardsの「Alerting」機能(またはWatcher for Elasticsearch)を使って、以下のようなルールを設定します。
ルール1: cluster-admin 権限の不正な付与検知
- 条件: 監査ログで、
rbac.authorization.k8s.io/v1のclusterrolebindingsリソースに対して、createまたはpatchverb が実行され、かつclusterroleがcluster-adminであるイベント。 - アクション: Slack、メール、PagerDutyなどにアラートを送信。
- 設定例(Kibana Alerting):
- Rule Type: Threshold
- Index pattern:
kubernetes-audit-*(Elasticsearch/OpenSearchのインデックスパターン) - Timestamp field:
@timestamp - Group by: (なし、または
user.nameなど) - Conditions:
verbiscreateORverbispatch- AND
requestURIcontains/apis/rbac.authorization.k8s.io/v1/clusterrolebindings - AND
requestBody.roleRef.nameiscluster-admin(※ログの構造により、このフィールド名は調整が必要) - Run this action when: For each document
- Frequency: 10 minutes (アラートの頻度)
ルール2:特権モード (privileged: true) でのPod作成検知
- 条件: 監査ログで、
""(core API group) のpodsリソースに対して、createverb が実行され、かつrequestBody.spec.containers[].securityContext.privilegedがtrueであるイベント。 - アクション: Slack、メールなどにアラートを送信。
- 設定例(Kibana Alerting):
- Index pattern:
kubernetes-audit-* - Conditions:
verbiscreate- AND
requestURIstarts with/api/v1/namespaces/.*/pods - AND
requestBody.spec.containerscontains a document wheresecurityContext.privilegedistrue(※ログの構造により、この条件は複雑になる場合があります。Elasticsearch Query DSLで表現する必要があるかもしれません)
PHP/Pythonによるログ分析サンプル(概念実証)
ここでは、監査ログを直接分析するのではなく、APIサーバーの監査ログをHTTPリクエストとして受け取り、リアルタイムで処理するような簡易的なWebアプリケーションの概念実証(PoC)を示します。これは、Kubernetes APIサーバーの監査ログをwebhookとして設定し、それをこのPHPスクリプトが受け取るシナリオを想定しています。
PHPによる簡易監査ログ受信・分析スクリプト (audit_listener.php):
<?php
// ログレベルを定義
define('LOG_LEVEL_INFO', 'INFO');
define('LOG_LEVEL_WARN', 'WARN');
define('LOG_LEVEL_ERROR', 'ERROR');
// ログ出力関数
function write_log($message, $level = LOG_LEVEL_INFO) {
$timestamp = date('Y-m-d H:i:s');
// 実際には、ファイルやデータベース、または別のログ集約システムに送信
error_log("[$timestamp] [$level] $message\n");
}
// 危険な操作のキーワード
$dangerous_verbs = ['create', 'update', 'patch', 'delete'];
$privileged_keywords = ['privileged: true', '"privileged":true']; // Podの特権モードなど
// リクエストボディの取得
$json_input = file_get_contents('php://input');
$event = json_decode($json_input, true);
if (!$event) {
write_log('Invalid JSON received.', LOG_LEVEL_ERROR);
http_response_code(400); // Bad Request
exit;
}
// 監査イベントの解析
$stage = $event['stage'] ?? 'Unknown'; // イベントの段階 (RequestReceived, ResponseCompleteなど)
$verb = $event['verb'] ?? 'Unknown'; // 実行されたHTTPメソッド (GET, POST, DELETEなど)
$request_uri = $event['requestURI'] ?? '';
$user_info = $event['user'] ?? [];
$user_name = $user_info['username'] ?? 'N/A';
$user_groups = implode(',', $user_info['groups'] ?? []);
$namespace = $event['namespace'] ?? '';
$resource = $event['objectRef']['resource'] ?? '';
write_log("Received audit event: Verb={$verb}, URI={$request_uri}, User={$user_name}, Namespace={$namespace}, Resource={$resource}, Stage={$stage}");
// 監視対象のイベントかどうかを判定
if ($stage === 'ResponseComplete' && in_array(strtolower($verb), $dangerous_verbs)) {
// --- 異常検知ロジック ---
// 1. cluster-admin 権限の不正な付与検知
if (strpos($request_uri, '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings') !== false &&
($verb === 'create' || $verb === 'patch')) {
// リクエストボディからロール名を確認(実際のログ構造に合わせて調整が必要)
if (isset($event['requestObject']['roleRef']['name']) && $event['requestObject']['roleRef']['name'] === 'cluster-admin') {
write_log("ALERT: Potential unauthorized cluster-admin role binding created by user {$user_name}!", LOG_LEVEL_ERROR);
// ここでアラート通知処理 (Slack, Emailなど) を実行
send_alert("ALERT: Potential unauthorized cluster-admin role binding created by user {$user_name} on URI: {$request_uri}");
}
}
// 2. 特権モードでのPod作成検知
if (strpos($request_uri, '/api/v1/namespaces/') !== false && strpos($request_uri, '/pods') !== false && $verb === 'create') {
// リクエストボディから Pod の securityContext.privileged を確認
// 実際のログ構造に合わせて $event['requestObject']['spec']['containers'] を辿る必要がある
if (isset($event['requestObject']['spec']['containers'])) {
foreach ($event['requestObject']['spec']['containers'] as $container) {
if (isset($container['securityContext']['privileged']) && $container['securityContext']['privileged'] === true) {
write_log("ALERT: Pod created in privileged mode by user {$user_name} in namespace {$namespace}!", LOG_LEVEL_ERROR);
send_alert("ALERT: Pod created in privileged mode by user {$user_name} in namespace {$namespace} on URI: {$request_uri}");
break; // 一つ見つかったらループを抜ける
}
}
}
}
// 3. 機密情報 (Secret) への不正アクセス検知 (GETリクエストなどを監視する場合)
// if (strpos($request_uri, '/api/v1/namespaces/') !== false && strpos($request_uri, '/secrets/') !== false && $verb === 'get') {
// // 特定のnamespaceやsecret名を除外するロジックを追加
// if (!in_array($namespace, ['kube-system', 'monitoring'])) { // 例: kube-systemやmonitoring namespaceは除外
// write_log("ALERT: Potential unauthorized access to secret in namespace {$namespace} by user {$user_name}!", LOG_LEVEL_WARN);
// send_alert("ALERT: Potential unauthorized access to secret in namespace {$namespace} by user {$user_name} on URI: {$request_uri}");
// }
// }
// その他の監視ルールを追加...
} else {
// 通常のGETリクエストなどはここでは処理しない(必要に応じて追加)
}
// HTTPレスポンスを返す(Kubernetes APIサーバーへ)
http_response_code(200); // OK
echo json_encode(['status' => 'success']);
/**
* アラート通知関数(例:Slack Webhook)
* @param string $message
*/
function send_alert(string $message) {
$slack_webhook_url = 'YOUR_SLACK_WEBHOOK_URL'; // ここに実際のSlack Webhook URLを設定
$data = json_encode(['text' => $message]);
$ch = curl_init($slack_webhook_url);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "POST");
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
'Content-Type: application/json',
'Content-Length: ' . strlen($data)
));
$result = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($http_code !== 200) {
write_log("Failed to send alert to Slack. HTTP Code: {$http_code}, Response: {$result}", LOG_LEVEL_ERROR);
} else {
write_log("Alert sent to Slack successfully.", LOG_LEVEL_INFO);
}
}
?>
【解説】
- このPHPスクリプトは、Kubernetes APIサーバーから監査ログのWebhookを受け取るためのシンプルな例です。
file_get_contents('php://input')でリクエストボディ(JSON形式の監査イベント)を取得します。json_decode()でPHPの連想配列に変換します。- イベントの
stage,verb,requestURI,user,namespace,resourceなどを抽出し、監視対象のパターンに合致するかどうかを判定します。 cluster-admin権限の付与や、特権モードでのPod作成といった、特に危険度の高い操作を検知するロジックを記述しています。- 検知した場合は、
write_log()関数でサーバーのログに出力し、send_alert()関数でSlackなどの外部サービスに通知します。 send_alert()関数は、Slack Webhook URL を使ってメッセージを送信する例です。実際には、メール送信、Microsoft Teamsへの通知、PagerDutyへの連携など、運用体制に合わせた通知方法を実装してください。- 【重要】 このPHPスクリプトを実際に動作させるには、Kubernetes APIサーバーの監査ログ設定でWebhookを有効にし、このスクリプトが動作しているWebサーバーのURLを指定する必要があります。また、KubernetesクラスターからこのWebhook URLへアクセス可能である必要があります。
- 【注意】 このスクリプトはあくまで概念実証(PoC)です。本番環境で利用するには、エラーハンドリングの強化、セキュリティ対策(入力値のサニタイズ、認証など)、スケーラビリティの考慮が必要です。
攻撃を防ぐための「あとがき」
今日の話は、Kubernetes APIサーバーの監査ログ分析に焦点を当てたが、これはサイバーセキュリティの一側面に過ぎない。しかし、この「ログ」という泥臭い作業こそが、巧妙化する攻撃からシステムを守るための最も確実な方法の一つだと、私は断言する。
攻撃者は常に我々の裏をかこうと進化し続けている。しかし、彼らが利用する「ログ」という痕跡を見逃さないこと、そしてその痕跡から異常をいち早く検知する仕組みを持つこと。これこそが、堅牢なシステムを構築するための基本であり、我々エンジニアが常に意識すべき「守りの姿勢」なのだ。
今回紹介したコードや設定は、あくまで出発点だ。君たちの環境に合わせて、さらに洗練させ、より強力な防御策を構築していってほしい。分からないことがあれば、いつでも私に聞くがいい。我々が共に、サイバー空間の平和を守っていこう。
コメント