お疲れ様。今日もKubernetesクラスターの運用とデプロイ、本当にお疲れ様。
だが、本題に入る前に少しだけ手を止めて、私からの質問に答えてほしい。
「今運用しているそのクラスター、Kubernetes Dashboard(ダッシュボード)をインターネットからアクセスできる状態にしていないか?」
「いや、ベーシック認証を入れているから大丈夫」「開発環境だし、URLが複雑だから見つからないはず」……もし君の頭の中に一瞬でもそんな甘えがよぎったとしたら、今すぐそのダッシュボードの公開設定を削除してほしい。
私はこれまでに、安易に公開されたKubernetes Dashboardを踏み台にされ、コンテナはおろか、AWSやGCPといったクラウドインフラのルート権限(管理者権限)まで一瞬で奪い去られた凄惨なインシデントを何度も見てきた。攻撃者にとって、無防備なKubernetes Dashboardは「クラスターの全権を握る特権ターミナル」がWeb UIとして目の前に差し出されているのと同じなんだ。
今回は、なぜKubernetes Dashboardの外部公開がこれほどまでに危険なのか、攻撃者がどのような手法(PoC)で君たちのシステムを完全掌握するのかを暴き、その上で実務において100%安全に運用するための「最小特権YAML」と「OAuth2-Proxy + Ingressによる多層防御設定」を伝授する。
セキュリティの教科書には載っていない、現場の泥臭い知見を詰め込んだ。しっかりついてきてほしい。
—
1. 攻撃者はどう狙うか?Kubernetes Dashboardが「最高のご馳走」である理由
攻撃者は、Googleハッキング(Dorking)や、Shodan、Censysといったポートスキャンサービスを使って、インターネット上に露出しているKubernetes Dashboardを24時間365日自動で探し回っている。
ターゲットが見つかった瞬間、彼らが狙うのは以下の3つのシナリオだ。
シナリオA:認証のバイパスとデフォルト特権の奪取(過去のCVE)
過去に発見された脆弱性(例えば CVE-2018-18264 など)では、DashboardのAPIを介して、認証を迂回してサービスアカウントのトークンを読み取ることが可能だった。
また、Dashboardのデプロイ時に、利便性のためにデフォルトのサービスアカウント(kubernetes-dashboard)に対して、過剰な権限(cluster-admin など)を付与してしまうという、運用上の致命的なミスが後を絶たない。
シナリオB:サービスアカウント・トークンを悪用したAPIサーバーへの直接攻撃
攻撃者がDashboardの画面にアクセスできた場合、画面上の「Skip」ボタンが有効化されていると、Dashboardが稼働しているPod自身のサービスアカウント権限でクラスター内部を操作できてしまう。
ここで、攻撃者がDashboard経由で実行する一般的な悪意あるアクション(PoC)の流れを見てみよう。
# 1. 攻撃者はDashboard経由で、特権(Privileged)コンテナを起動するPodのYAMLを流し込む。
# 2. そのPodの仕様(Spec)で、ホスト(Node)のルートディレクトリ(/)をマウントする。
# 3. 起動したPodのコンテナ内から、ホストの /etc/shadow や、Docker/Containerdのソケット、
# さらにはクラウドのメタデータAPI(IMDSv2など)にアクセスする。
シナリオC:コンテナ脱出とクラウドプロバイダーの完全掌握
特にAWS(EKS)やGCP(GKE)で恐ろしいのが、コンテナ内からのクラウド資格情報の窃取だ。
Dashboardを介して不正なPodを起動した攻撃者は、コンテナ内からリンクローカルアドレス(169.254.169.254)にアクセスし、Node(EC2など)に付与されているIAMインスタンスプロファイルの認証情報を引っこ抜く。これにより、攻撃者のローカルPCからクラスターの外側にあるS3バケットやRDS、最悪の場合は組織全体のAWSリソースを自由に操作できるようになってしまう。
これが、「たかがダッシュボードの公開」が「企業全体のインフラ壊滅」に直結するメカニズムだ。
—
2. アンチパターン:やってはいけない「その場しのぎ」の対策
「危険なのはわかった。じゃあ、これで対策しよう」と、以下のような「生ぬるい対策」で済ませようとしてはいけない。
- NodePortでポートを空け、適当なBasic認証をかける
- Basic認証はブルートフォース(総当たり)に弱く、通信が暗号化(HTTPS)されていない場合、認証情報がネットワーク上で平文で盗聴される。
- IP制限(Security GroupやNginxのAllowリスト)だけで守る
- 開発者のリモートワーク移行などでIPリストのメンテナンスが形骸化し、結局「一時的に
0.0.0.0/0を許可」したまま放置されるのがオチだ。 - 複雑なURL(サブドメイン)にして隠す
- いわゆる「隠蔽によるセキュリティ(Security through obscurity)」だ。DNSのパブリックログ(Certificate Transparency: 証明書透明性)を監視している攻撃者には、新しいサブドメインの発行など一瞬で検知される。
—
3. 鉄壁の防衛アーキテクチャ:どう設計すべきか?
私たちが目指すべきゴールはシンプルだ。
「Kubernetes Dashboardは、いかなる理由があっても、パブリックインターネットに直接公開してはならない」
これを実現するためのベストプラクティスは、以下の2つに絞られる。
1. 【推奨】Port-Forward経由アクセス:インターネット側への公開ルートを一切作らず、開発者がローカルから kubectl port-forward を使って安全なトンネル越しにアクセスする。
2. 【組織運用向け】OAuth2-Proxy + Ingress + OIDC認証:どうしてもチームでダッシュボードを共有する必要がある場合、IDプロバイダー(Google Workspace、GitHub、Oktaなど)による多要素認証(MFA)を事前に強制し、認証されたユーザーのみをリバースプロキシ経由でダッシュボードに通す。
今回は、この2つのアプローチを完全に実装するための、具体的かつセキュアな設定ファイルを共有する。
—
4. 【実践】コピペで動くセキュア実装&設定テンプレート
対策1:Dashboard自身の権限を「最小特権(Read-Only)」にする
多くのエンジニアが犯す最大の過ちは、Dashboardにクラスター全体の管理者権限を与えてしまうことだ。まずは、Dashboardが使用するサービスアカウントの権限を、クラスターの変更ができない「閲覧専用(Read-Only)」に制限する。
以下のYAMLを適用し、不必要な書き込み権限を剥奪しよう。
# dashboard-readonly-rbac.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kubernetes-dashboard-readonly
rules:
# 読み取り(Get, List, Watch)アクションのみを許可し、Create, Update, Deleteは一切許可しない
- apiGroups: [""]
resources: ["pods", "services", "deployments", "replicasets", "statefulsets", "ingresses", "configmaps", "secrets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["apps"]
resources: ["deployments", "daemonsets", "statefulsets", "replicasets"]
verbs: ["get", "list", "watch"]
- apiGroups: ["batch"]
resources: ["jobs", "cronjobs"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: kubernetes-dashboard-readonly-binding
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kubernetes-dashboard
roleRef:
kind: ClusterRole
name: kubernetes-dashboard-readonly # 作成したRead-Onlyロールを紐付け
apiGroup: rbac.authorization.k8s.io
これで、万が一ダッシュボードのUIが突破されても、攻撃者がDashboardの権限を使ってリソースを改ざんしたり、悪意あるPodを新規起動したりすることはできなくなる。
—
対策2:oauth2-proxy と Nginx Ingress による多層防御
どうしてもダッシュボードをURL経由でアクセスさせたい場合、ダッシュボードの手前に「認証の関所」を設ける。
ここでは、広く使われている ingress-nginx と oauth2-proxy を組み合わせ、「GitHubの二段階認証をパスした組織メンバーだけがダッシュボードにアクセスできる」環境を構築する。
ステップ1:OAuth2-Proxyの設定(Deployment & Service)
まずは、認証プロキシとなる oauth2-proxy をデプロイする。
# oauth2-proxy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth2-proxy
namespace: kubernetes-dashboard
spec:
replicas: 1
selector:
matchLabels:
app: oauth2-proxy
template:
metadata:
labels:
app: oauth2-proxy
spec:
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.4.0
ports:
- containerPort: 4180
protocol: TCP
env:
# 認証プロバイダーとしてGitHubを使用(GoogleやOkta等も指定可能)
- name: OAUTH2_PROXY_PROVIDER
value: "github"
- name: OAUTH2_PROXY_CLIENT_ID
value: "YOUR_GITHUB_CLIENT_ID" # 事前にGitHubで作成したOAuth AppのClient ID
- name: OAUTH2_PROXY_CLIENT_SECRET
value: "YOUR_GITHUB_CLIENT_SECRET" # GitHub OAuth AppのClient Secret
# 組織(Organization)やチームでアクセス制限をかける(セキュリティ上極めて重要)
- name: OAUTH2_PROXY_GITHUB_ORG
value: "your-secure-github-org"
# クッキーの暗号化キー(openssl rand -base64 32 などで生成したランダムな文字列)
- name: OAUTH2_PROXY_COOKIE_SECRET
value: "S3cr3tC00k1eSh0uldB3Str0ngAndV3ryS3cur3=="
- name: OAUTH2_PROXY_EMAIL_DOMAINS
value: "*"
- name: OAUTH2_PROXY_HTTP_ADDRESS
value: "0.0.0.0:4180"
---
apiVersion: v1
kind: Service
metadata:
name: oauth2-proxy
namespace: kubernetes-dashboard
spec:
ports:
- port: 4180
protocol: TCP
targetPort: 4180
selector:
app: oauth2-proxy
ステップ2:Ingressでの認証連携(Nginx Ingress Annotations)
次に、Kubernetes Dashboardへのトラフィックを捌くIngressを作成する。
ここで、Nginx Ingress Controllerの auth-signin と auth-url アノテーションを利用し、「未認証のアクセスはすべて oauth2-proxy にリダイレクトする」というルールを強制する。
# dashboard-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: kubernetes-dashboard-ingress
namespace: kubernetes-dashboard
annotations:
kubernetes.io/ingress.class: nginx
nginx.ingress.kubernetes.io/ssl-redirect: "true"
# 【最重要】アクセス制御のトリガー:すべてのリクエストをoauth2-proxyで認証チェックする
nginx.ingress.kubernetes.io/auth-url: "https://dashboard.example.com/oauth2/auth"
# 認証されていない場合、OIDCのログイン画面へリダイレクト
nginx.ingress.kubernetes.io/auth-signin: "https://dashboard.example.com/oauth2/start?rd=$escaped_request_uri"
# Dashboardへの通信をHTTPS(TLS)にするためのバックエンドプロトコル指定
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
tls:
- hosts:
- dashboard.example.com
secretName: dashboard-tls-cert # 事前に取得したTLS証明書のSecret
rules:
- host: dashboard.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: kubernetes-dashboard
port:
number: 443
# oauth2-proxy自体のエンドポイントも同じドメイン配下にマッピングする
- path: /oauth2
pathType: Prefix
backend:
service:
name: oauth2-proxy
port:
number: 4180
この構成を導入することで、攻撃者が https://dashboard.example.com/ にアクセスしたとしても、Dashboardのコードが1行も実行される前に、Nginx IngressレベルでGitHub(または設定したOIDC)の認証画面に強制リダイレクトされる。
認証を突破できない攻撃者からは、Dashboardの脆弱性を突くためのAPIリクエストすら送信できないため、脆弱性(ゼロデイを含む)に対する極めて強固なシールドとなる。
—
5. 運用チームのルールに組み込むべき「最終防衛ライン」
技術的な対策(YAMLの適用)を施したら、最後にチームの運用プロセスとして以下の運用ルールを「絶対厳守」として合意してほしい。
1. Dashboardの「Skip」ボタンは起動引数で無効化する
DashboardのDeploymentの引数(args)に --enable-skip-login=false を明示的に設定し、トークンなしでのログインを完全に禁止すること。
2. 踏み台(Bastion)経由、または社内VPNを必須とする
上記で紹介したOAuth2-Proxyでの認証に加え、そもそも該当のドメイン(dashboard.example.com)への名前解決や通信自体を、社内VPN(WireGuardやOpenVPNなど)またはAWS Client VPNの内部セグメントからのみ許可するように、インフラ(DNS・WAF・セキュリティグループ)レベルでネットワークの境界線を絞り込むこと。
3. 定期的なアセットスキャン
ShodanのAPIなどを利用し、自社の所有するIP帯において Kubernetes Dashboard や k8s といったキーワードに引っかかるポートが外部公開されていないかを定期的に自動スキャンする仕組みを導入すること。
—
現場のリーダーである君へ
便利さとセキュリティは、往々にしてトレードオフの関係にある。しかし、Kubernetesクラスターにおいては、「利便性のためにセキュリティを犠牲にした結果の代償」が重すぎる。
「ちょっとデバッグしたいから、30分だけNodePortを開けておこう」
その「30分」は、自動化された攻撃ボットが君のクラスターを発見し、バックドアを仕込み、クラウドの認証情報を盗み出すのに十分すぎる時間だ。
今回紹介したセキュアな設計を標準テンプレートとしてチームに共有し、「安全ではない公開」がプルリクエスト(PR)の段階で自動で弾かれるような仕組み(OPA/GatekeeperやKyvernoなどのポリシーエンジンによる統制)へと昇華させていってほしい。
何か疑問があれば、いつでもSlackで声をかけてくれ。安全なコンテナライフを!
コメント