Skip to content
Published on

Operator セキュリティ — 最小権限 RBAC、マルチテナンシー、サプライチェーン

Authors

はじめに — Operator はクラスタで最も危険なワークロードになりうる

Kubernetes クラスタ内で動くほとんどのワークロードは、自身の namespace 内の仕事をします。Web アプリケーションは自分の Pod や Service を持ってトラフィックを処理し、バッチジョブはデータを読み書きして終わります。こうしたワークロードが侵害されても、被害は通常そのワークロードとそのデータに限定されます。

Operator は違います。Operator の本質は、クラスタの状態を代わりに操作する自動化された管理者です。CRD(Custom Resource Definition)で定義したドメインオブジェクトを監視し、それを現実にするために Deployment を作り、Service を作り、Secret を読み、ときには ClusterRole まで作成します。この仕事には強力な権限が必要です。そして多くの Operator はクラスタ全体(cluster-scoped)で動作します。

ここにセキュリティの核心となる命題があります。Operator の ServiceAccount が侵害されると、攻撃者はその Operator が持つすべての権限をそのまま手に入れます。 Operator が全 namespace の Secret を読めるなら、攻撃者も同じことができます。Operator が ClusterRoleBinding を作れるなら、攻撃者は自分自身を cluster-admin に昇格できます。一つのコントローラ Pod で見つかった RCE 脆弱性が、そのままクラスタ全体の掌握につながるのです。

本記事では、Kubebuilder(2026 年時点で Kubernetes 1.36 / Go 1.26、controller-runtime v0.24.x、controller-tools v0.21.x)で Operator を作る際に適用すべきセキュリティ強化(hardening)の全体を扱います。最小権限 RBAC の生成からメトリクスエンドポイントの保護、Webhook の TLS、権限昇格の防止、マルチテナント分離、サプライチェーンセキュリティまで、実際に動くコードとマニフェストを中心に整理しました。

Operator の権限が危険な理由 — 脅威モデルから

セキュリティ設計は常に脅威モデルから出発すべきです。Operator を攻撃者の視点で見ると、次のような攻撃経路が見えてきます。

攻撃経路 1: コントローラ Pod の侵害
  アプリケーション脆弱性(RCE) -> コントローラ Pod のシェル取得
  -> ServiceAccount トークンの窃取(/var/run/secrets/...)
  -> Operator のすべての RBAC 権限を行使
  -> 例: 全 namespace の Secret 読み取り、ClusterRoleBinding 作成

攻撃経路 2: CR を通じた権限昇格
  悪意あるユーザーが CR を作成/変更
  -> Operator がその CR を信頼して RBAC オブジェクトを作成
  -> ユーザーが直接持てなかった権限を迂回して取得

攻撃経路 3: サプライチェーン
  Operator イメージのビルドパイプラインまたはベースイメージの侵害
  -> 悪意あるコードを含むイメージがクラスタにデプロイ
  -> 正常な Operator に見えるがバックドアを含む

攻撃経路 4: メトリクス/Webhook エンドポイント
  認証なしのメトリクスエンドポイント公開 -> 内部情報の漏洩
  TLS 非適用の Webhook -> 中間者攻撃で admission を迂回

この 4 つの経路をそれぞれ塞ぐことが、Operator セキュリティの実務です。中心となる原則は一つに収束します。最小権限(least privilege)。 Operator が実際に必要とする動詞(verb)とリソースにのみ権限を与え、それ以外はすべて拒否することです。

RBAC マーカーで最小権限を生成する

権限はコードのそばに宣言する

Kubebuilder ベースの Operator の最も優れた点の一つは、RBAC をコントローラのコードのすぐそばにマーカー(marker)として宣言することです。controller-tools がこのマーカーを読んで Role/ClusterRole マニフェストを生成します。こうすると「コードは Secret を読むのに RBAC にその権限がなく、ランタイムで落ちる」というよくある事故を減らし、逆に「コードは使わないのに RBAC だけが過度に広い」権限の漏れもコードレビューで捕まえられます。

次は典型的な Reconciler の RBAC マーカーです。

// WidgetReconciler は Widget カスタムリソースを reconcile します。
type WidgetReconciler struct {
	client.Client
	Scheme *runtime.Scheme
}

// +kubebuilder:rbac:groups=apps.example.com,resources=widgets,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=apps.example.com,resources=widgets/status,verbs=get;update;patch
// +kubebuilder:rbac:groups=apps.example.com,resources=widgets/finalizers,verbs=update
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=services,verbs=get;list;watch;create;update;patch;delete
// +kubebuilder:rbac:groups=core,resources=events,verbs=create;patch
func (r *WidgetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
	log := logf.FromContext(ctx)

	var widget appsv1alpha1.Widget
	if err := r.Get(ctx, req.NamespacedName, &widget); err != nil {
		// NotFound は正常 — 削除済みオブジェクトの再キューイングの可能性
		return ctrl.Result{}, client.IgnoreNotFound(err)
	}

	// 望ましい状態(desired state)を計算し、Deployment を保証
	desired := r.buildDeployment(&widget)
	if err := r.ensureDeployment(ctx, desired); err != nil {
		log.Error(err, "deployment の保証に失敗")
		return ctrl.Result{}, err
	}

	return ctrl.Result{}, nil
}

make manifests を実行すると、controller-tools が上のマーカーを読んで、次の ClusterRole を config/rbac/role.yaml に生成します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: widget-manager-role
rules:
  - apiGroups: ["apps.example.com"]
    resources: ["widgets"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: ["apps.example.com"]
    resources: ["widgets/status"]
    verbs: ["get", "update", "patch"]
  - apiGroups: ["apps.example.com"]
    resources: ["widgets/finalizers"]
    verbs: ["update"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["events"]
    verbs: ["create", "patch"]

動詞を絞ることが最小権限の核心

初心者の Operator 開発者が最も犯しやすい間違いは、すべてのリソースに「すべて」の動詞(または get/list/watch/create/update/patch/delete のフルセット)を付与することです。実際には動詞を慎重に選ぶ必要があります。

コントローラが行うこと必要な動詞不必要に広い権限
CR を読んで監視のみget, list, watchcreate, delete
子リソースを生成/更新get, list, watch, create, update, patchdelete(ownerReference に GC を任せれば不要)
状態の報告get, update, patch(status サブリソース)メインリソースの update
イベントの記録create, patchget, list

特に delete 動詞は慎重に扱うべきです。子リソースの整理はほとんどの場合 ownerReference とガベージコレクタに任せられるため、コントローラが直接 delete 権限を持つ必要がない場合が多いです。delete 権限がなければ、侵害されたコントローラがリソースを大量削除するシナリオを根本から遮断できます。

リソース名で権限範囲を絞る

RBAC は resourceNames で特定のオブジェクト名にのみ権限を限定できます。たとえば Operator が自身のリーダー選出(leader election)に使う ConfigMap や Lease 一つだけを変更すればよいなら、すべての ConfigMap に権限を与える代わりにその名前に限定します。ただし controller-tools のマーカーは resourceNames を直接サポートしないため、生成されたマニフェストを kustomize patch で絞るか、別途 Role を手動で管理する方式を使います。

# リーダー選出専用 Role — 名前が明示された Lease 一つだけを変更可能
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: widget-leader-election-role
  namespace: widget-system
rules:
  - apiGroups: ["coordination.k8s.io"]
    resources: ["leases"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
    resourceNames: ["widget-operator-leader"]

Namespace-scoped と Cluster-scoped — Role と ClusterRole の選択

Operator を設計する際に最初に決めるべきセキュリティ事項は、権限の範囲です。Kubernetes RBAC には二組のオブジェクトがあります。

Role + RoleBinding             -> 特定の namespace 内でのみ有効
ClusterRole + ClusterRoleBinding -> クラスタ全体で有効
ClusterRole + RoleBinding      -> ClusterRole 定義を特定 namespace に限定して適用

最後の組み合わせが重要です。権限ルールを ClusterRole で定義しつつ、RoleBinding で結びつければ、その権限は RoleBinding がある namespace にのみ適用されます。これを活用すると、同じ権限定義を再利用しながらも範囲を絞れます。

状況推奨範囲理由
CR が cluster-scoped リソース(例: Namespace、StorageClass)を扱うClusterRole + ClusterRoleBindingnamespace 境界を越える必要がある
Operator が全 namespace の CR を監視ClusterRole + ClusterRoleBindingwatch 対象が全 namespace
Operator が特定 namespace の CR のみ管理(マルチテナンシー)ClusterRole + namespace 別 RoleBinding権限定義の再利用 + 範囲限定
単一 namespace 専用 OperatorRole + RoleBinding最も狭い範囲、最も安全

単一 namespace マネージャーの設定

controller-runtime のマネージャーはキャッシュ範囲を namespace に限定できます。こうすると Operator が他の namespace のオブジェクトを読むこともなく、RBAC もそれに合わせて絞れます。

mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
	Scheme: scheme,
	// キャッシュを特定 namespace に限定 — クラスタ全体の watch をしない
	Cache: cache.Options{
		DefaultNamespaces: map[string]cache.Config{
			"widget-system": {},
			"tenant-a":      {},
		},
	},
	Metrics: server.Options{
		BindAddress: ":8443",
	},
})
if err != nil {
	setupLog.Error(err, "マネージャーの生成に失敗")
	os.Exit(1)
}

namespace に限定すると、ClusterRole の代わりに各 namespace の Role + RoleBinding で権限を与えられます。権限範囲が狭まれば、侵害時の影響範囲(blast radius)も小さくなります。「この Operator は本当にすべての namespace を見る必要があるのか」という問いを、設計段階で必ず投げかけるべきです。

メトリクスエンドポイントの保護 — kube-rbac-proxy 廃止後(2026)

何が変わったか

かつての Kubebuilder のスキャフォールドは、メトリクスエンドポイントを保護するためにサイドカーとして kube-rbac-proxy を起動していました。メトリクスポートの前にプロキシを置き、入ってくるリクエストのトークンを SubjectAccessReview で検証する構造でした。しかしこのサイドカーは、追加のイメージ依存、追加の権限、追加の運用負担を生みました。

2026 年現在、Kubebuilder は kube-rbac-proxy を 廃止 しました。代わりに controller-runtime のメトリクスサーバーが自身で認証/認可を行う WithAuthenticationAndAuthorization フィルタを提供します。メトリクスは HTTPS で提供され、入ってくるリクエストは TokenReview と SubjectAccessReview で検証されます。

import (
	"crypto/tls"

	"sigs.k8s.io/controller-runtime/pkg/metrics/filters"
	metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server"
)

func main() {
	// ... scheme 登録などは省略 ...

	metricsServerOptions := metricsserver.Options{
		BindAddress:   ":8443",
		SecureServing: true,
		// 入ってくるメトリクススクレイプ要求を認証/認可
		// SubjectAccessReview で非リソース URL /metrics へのアクセス権を確認
		FilterProvider: filters.WithAuthenticationAndAuthorization,
		TLSOpts: []func(*tls.Config){
			func(c *tls.Config) {
				c.MinVersion = tls.VersionTLS13
			},
		},
	}

	mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
		Scheme:                 scheme,
		Metrics:                metricsServerOptions,
		HealthProbeBindAddress: ":8081",
		LeaderElection:         true,
		LeaderElectionID:       "widget-operator-leader",
	})
	if err != nil {
		setupLog.Error(err, "マネージャーの生成に失敗")
		os.Exit(1)
	}

	// ... コントローラ登録、mgr.Start(...) ...
}

スクレイパーに権限を付与する

これでメトリクスを取得しようとする側(例: Prometheus の ServiceAccount)は、/metrics 非リソース URL に対する get 権限を持つ必要があります。次の ClusterRole がその権限を定義します。

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: widget-metrics-reader
rules:
  - nonResourceURLs: ["/metrics"]
    verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: widget-metrics-reader-binding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: widget-metrics-reader
subjects:
  - kind: ServiceAccount
    name: prometheus
    namespace: monitoring

この方式の利点は明確です。サイドカーが消えて攻撃面が減り、認証/認可がメインバイナリ内で controller-runtime が検証したコードパスで処理され、TLS 1.3 を強制できます。認証されないリクエストは 401、権限のないリクエストは 403 で拒否されます。

Webhook セキュリティ — TLS と cert-manager

Webhook は API サーバーが直接呼び出す

Operator が ValidatingWebhook や MutatingWebhook を提供すると、API サーバーが admission 要求を HTTPS でコントローラに送ります。この通信は必ず TLS で保護される必要があり、API サーバーは Webhook サーバー証明書を CA で検証します。証明書が失効したり CA バンドルがずれたりすると admission が失敗し、失敗ポリシーによってはオブジェクト作成全体が止まることもあります。

実務では cert-manager で証明書を自動発行/ローテーションします。cert-manager が発行した証明書を Webhook サーバーが使い、CA バンドルは cert-manager の ca-injector が ValidatingWebhookConfiguration に自動注入します。

# cert-manager Certificate — Webhook サーバー TLS 証明書
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: widget-serving-cert
  namespace: widget-system
spec:
  dnsNames:
    - widget-webhook-service.widget-system.svc
    - widget-webhook-service.widget-system.svc.cluster.local
  issuerRef:
    kind: Issuer
    name: widget-selfsigned-issuer
  secretName: widget-webhook-server-cert
  duration: 2160h    # 90 日
  renewBefore: 360h  # 失効 15 日前に更新
# ca-injector が caBundle を自動注入するようアノテーションを付与
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: widget-validating-webhook
  annotations:
    cert-manager.io/inject-ca-from: widget-system/widget-serving-cert
webhooks:
  - name: vwidget.kb.io
    failurePolicy: Fail
    sideEffects: None
    admissionReviewVersions: ["v1"]
    clientConfig:
      service:
        name: widget-webhook-service
        namespace: widget-system
        path: /validate-apps-example-com-v1alpha1-widget
    rules:
      - apiGroups: ["apps.example.com"]
        apiVersions: ["v1alpha1"]
        operations: ["CREATE", "UPDATE"]
        resources: ["widgets"]

検証 Webhook のコードと失敗ポリシー

検証 Webhook は、オブジェクトがセキュリティポリシーに違反していないか確認する強力な統制点です。たとえばユーザーが危険なフィールドの組み合わせを入れられないようにできます。

func (v *WidgetValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
	widget, ok := obj.(*appsv1alpha1.Widget)
	if !ok {
		return nil, fmt.Errorf("Widget 型ではありません")
	}

	// セキュリティポリシー: 特権モードの要求を拒否
	if widget.Spec.Privileged {
		return nil, field.Forbidden(
			field.NewPath("spec", "privileged"),
			"特権モードはこのクラスタでは許可されていません",
		)
	}

	// セキュリティポリシー: 信頼されたレジストリのイメージのみ許可
	if !strings.HasPrefix(widget.Spec.Image, "registry.internal.example.com/") {
		return nil, field.Invalid(
			field.NewPath("spec", "image"),
			widget.Spec.Image,
			"内部レジストリのイメージのみ許可されます",
		)
	}

	return nil, nil
}

failurePolicy はセキュリティの観点から慎重に選ぶべきです。Fail は Webhook が応答できないとリクエストを拒否するため、セキュリティ統制を迂回されませんが、Webhook の可用性が低下するとクラスタ運用が止まることがあります。セキュリティ統制目的の Webhook は Fail を使いつつ、Webhook 自体の高可用性(レプリカ、PDB)も併せて確保すべきです。

CR を通じた権限昇格の防止 — 最も微妙なリスク

問題の構造

Operator が RBAC オブジェクトを作成する場合、微妙だが深刻な権限昇格経路が生まれます。たとえば「テナントごとに ServiceAccount と Role を自動で作る」Operator を考えてみます。ユーザーが CR に望む権限を書くと、Operator がその権限どおりに Role を作ってくれます。

ここで問題が起きます。Kubernetes RBAC には 権限昇格の防止(privilege escalation prevention) ルールがあります。ある主体が RBAC オブジェクトを作成・変更するとき、自身が持たない権限を他の主体に付与することはできません。これを迂回するには rbac.authorization.k8s.io グループの escalate または bind 動詞が必要です。

問題は、Operator の ServiceAccount が強力な権限を持つ場合です。ユーザーが直接作ると権限昇格ルールに引っかかって止められる Role を、Operator を通じて迂回して作れてしまいます。ユーザーは CR を作れさえすればよく、実際の RBAC 作成は強力な Operator が代わりにやってくれるからです。

正常経路(ブロック):
  ユーザー -> 直接 RoleBinding 作成を試行(cluster-admin 付与)
  -> API サーバー: "あなたは cluster-admin を持たないので付与不可" -> 拒否

迂回経路(危険):
  ユーザー -> CR 作成(spec で cluster-admin を要求)
  -> Operator(強力な SA) -> CR を信頼して RoleBinding 作成
  -> ユーザーが迂回路で cluster-admin を取得

防御戦略

このリスクを防ぐ方法は複数の層になります。

第一に、Operator が RBAC を作成する権限そのものを最小化します。本当に RBAC オブジェクトを作る必要があるのか改めて問い、可能であればあらかじめ定義された固定の Role のみをバインドするように設計します。Operator が任意のルールを持つ Role を動的に生成する設計は、できる限り避けます。

第二に、検証 Webhook で CR が要求できる権限をホワイトリストで制限します。CR が危険な動詞(escalate、bind、impersonate)や危険なリソース(clusterrolebindings、すべての secrets)を要求したら、Webhook が拒否します。

var forbiddenVerbs = map[string]bool{
	"escalate":    true,
	"bind":        true,
	"impersonate": true,
}

func (v *TenantRoleValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
	tr, ok := obj.(*appsv1alpha1.TenantRole)
	if !ok {
		return nil, fmt.Errorf("TenantRole 型ではありません")
	}

	for _, rule := range tr.Spec.Rules {
		for _, verb := range rule.Verbs {
			if forbiddenVerbs[verb] {
				return nil, field.Forbidden(
					field.NewPath("spec", "rules"),
					fmt.Sprintf("動詞 %q はテナント Role では許可されていません", verb),
				)
			}
		}
		// cluster-scoped RBAC リソースの要求をブロック
		for _, res := range rule.Resources {
			if res == "clusterroles" || res == "clusterrolebindings" {
				return nil, field.Forbidden(
					field.NewPath("spec", "rules"),
					"cluster-scoped RBAC リソースはテナント Role では許可されていません",
				)
			}
		}
	}
	return nil, nil
}

第三に、集約(aggregated)ClusterRole を活用して動的生成自体をなくします。あらかじめラベルで束ねられる小さな ClusterRole を定義しておき、Operator は新しいルールを作る代わりにこの集約グループに参加させる程度にとどめます。こうすると運用者がレビューした権限の断片だけが組み合わされるため、任意の権限注入を防げます。

第四に、コントローラ ServiceAccount 自体の最小権限を改めて強調します。Operator が rbac.authorization.k8s.io/roles に対して持てる動詞を get/list/watch/create/update/patch に制限し、delete や cluster-scoped RBAC 権限は本当に必要なときだけ付与します。

マルチテナント分離

テナント間の境界をどう引くか

複数のチームが一つのクラスタを共有し、それぞれが Operator の CR を使うマルチテナント環境では、あるテナントが別のテナントのリソースに影響を与えられないように分離する必要があります。分離戦略は大きく二つに分かれます。

戦略 A: 単一 Operator、多テナント(共有 Operator)
  一つの Operator インスタンスがすべてのテナント namespace の CR を処理
  利点: 運用がシンプル、リソース節約
  欠点: Operator 侵害時に全テナントに影響、分離が弱い

戦略 B: テナント別 Operator(専用 Operator)
  テナント namespace ごとに独立した Operator インスタンス
  利点: 強い分離、影響範囲の最小化
  欠点: 運用が複雑、リソースのオーバーヘッド

規制が厳しい環境やテナント間の信頼が低い環境では、戦略 B(テナント別 Operator)が推奨されます。各 Operator はキャッシュと RBAC が自テナント namespace に限定されるため、一つのテナントの Operator が侵害されても他のテナントには影響がありません。

OperatorGroup で範囲を限定(OLM)

Operator Lifecycle Manager(OLM)を使う場合、OperatorGroup が Operator の監視範囲を決めます。OperatorGroup の targetNamespaces を指定すると、Operator はその namespace の CR のみ処理します。

apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  name: tenant-a-operatorgroup
  namespace: tenant-a
spec:
  # この Operator は tenant-a namespace のみ監視
  targetNamespaces:
    - tenant-a

これとあわせて、ネットワークポリシーでテナント間のトラフィックを遮断し、ResourceQuota と LimitRange で一つのテナントがリソースを独占できないようにします。

# テナント namespace へのインバウンドを同じテナントのみに制限
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tenant-a-isolation
  namespace: tenant-a
spec:
  podSelector: {}
  policyTypes: ["Ingress"]
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tenant: tenant-a
# テナント別のリソース上限
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-a-quota
  namespace: tenant-a
spec:
  hard:
    requests.cpu: "20"
    requests.memory: 40Gi
    limits.cpu: "40"
    limits.memory: 80Gi
    count/widgets.apps.example.com: "50"

最後の行に注目です。カスタムリソースの個数も ResourceQuota で制限できるため、あるテナントが CR を無制限に作って Operator を麻痺させるリソース枯渇攻撃を防げます。

サプライチェーンセキュリティ — イメージ署名と SBOM

Operator イメージを信頼できるか

脅威モデルの三つめの経路はサプライチェーンでした。いくら RBAC を絞っても、デプロイされる Operator イメージそのものにバックドアがあれば無意味です。2026 年現在、サプライチェーンセキュリティの標準ツールは sigstore の cosign(署名)、SBOM(Software Bill of Materials、構成明細)、SLSA(サプライチェーン完全性レベル)provenance です。

ビルドパイプラインでイメージに署名します。

# cosign でイメージに署名(キーレス — OIDC アイデンティティを使用)
cosign sign --yes \
  registry.internal.example.com/widget-operator:v1.4.0

# SBOM を生成してイメージに添付
syft registry.internal.example.com/widget-operator:v1.4.0 \
  -o spdx-json > sbom.spdx.json
cosign attach sbom --sbom sbom.spdx.json \
  registry.internal.example.com/widget-operator:v1.4.0

# SBOM 自体にも署名
cosign attest --yes \
  --predicate sbom.spdx.json --type spdxjson \
  registry.internal.example.com/widget-operator:v1.4.0

admission ポリシーで署名検証を強制

イメージに署名するだけでは不十分です。クラスタが 署名されていないイメージを拒否 して初めて意味があります。sigstore policy-controller(または Kyverno、OPA Gatekeeper)を admission 段階に置いて署名を検証します。

# sigstore policy-controller — 信頼されたアイデンティティで署名されたイメージのみ許可
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: require-signed-operator-images
spec:
  images:
    - glob: "registry.internal.example.com/widget-operator*"
  authorities:
    - keyless:
        identities:
          - issuer: https://token.actions.githubusercontent.com
            subject: https://github.com/example/widget-operator/.github/workflows/release.yml@refs/tags/v1.4.0

このポリシーが有効な namespace では、署名検証を通過しないイメージで Pod を作ろうとすると admission が拒否します。SLSA provenance まで検証すれば、「このイメージは信頼されたビルドシステムで、特定のソースコミットから作られた」という事実まで保証できます。

ベースイメージと依存関係

Operator バイナリ自体は静的リンクされた Go バイナリなので、distroless や scratch ベースを使うのがよいです。攻撃面を最小化し、シェルがないため侵害後のラテラルムーブメント(横展開)を困難にします。

# マルチステージビルド — 最終イメージは distroless
FROM golang:1.26 AS builder
WORKDIR /workspace
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -o manager cmd/main.go

FROM gcr.io/distroless/static:nonroot
WORKDIR /
COPY --from=builder /workspace/manager .
USER 65532:65532
ENTRYPOINT ["/manager"]

nonroot ユーザーで実行し、シェルを含まない distroless イメージは、サプライチェーンとランタイムセキュリティの両方に寄与します。

Secret の取り扱い

Secret を扱う Operator の原則

多くの Operator が Secret を読みます。データベースのパスワード、API キー、TLS 証明書などを扱うためです。Secret の取り扱いにはいくつかの鉄則があります。

第一に、Secret をログに残さないこと。デバッグのためにオブジェクト全体をロギングしていて、Secret の値が平文でログに残る事故がよくあります。構造化ロギングで機密フィールドを明示的に除外する必要があります。

// 悪い例: Secret 全体をロギング — 値が露出する
// log.Info("secret を取得", "secret", secret)

// 良い例: メタデータのみロギング、値は絶対に出力しない
log.Info("secret を取得",
	"name", secret.Name,
	"namespace", secret.Namespace,
	"keys", maps.Keys(secret.Data), // キー名のみ、値は除外
)

第二に、Secret に対する RBAC を可能な限り絞ること。Operator がすべての namespace のすべての Secret を読めるようにせず、可能であれば特定の namespace または特定の名前の Secret にのみ権限を限定します。前述の resourceNames を積極的に活用します。

第三に、保存時の暗号化(encryption at rest) を有効にすること。これはクラスタ運用者の責任ですが、Operator 開発者もこの前提を文書化すべきです。etcd に保存される Secret が平文だと、etcd バックアップ一つがすべての Secret の漏洩につながります。KMS provider を通じたエンベロープ暗号化(envelope encryption)を推奨します。

# API サーバーの EncryptionConfiguration — KMS provider で Secret を暗号化
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
  - resources: ["secrets"]
    providers:
      - kms:
          apiVersion: v2
          name: cluster-kms
          endpoint: unix:///var/run/kmsplugin/socket.sock
      - identity: {}

第四に、可能であれば 外部 Secret マネージャー(Vault、クラウド KMS)と連携して Secret をクラスタの外に置き、External Secrets Operator のようなツールで必要なときだけ同期します。Operator が直接、長命の資格情報を持たないようにするのが理想です。

監査(Audit)

何を記録すべきか

Operator がクラスタの状態を変える強力な主体である以上、その行動は監査ログに残るべきです。Kubernetes の監査ポリシー(audit policy)で、Operator の ServiceAccount が行う要求を適切なレベルで記録するように設定します。

# API サーバー audit policy 抜粋 — Operator の書き込み操作を詳細に記録
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
  # Operator SA の RBAC オブジェクト変更は RequestResponse レベルで全記録
  - level: RequestResponse
    users: ["system:serviceaccount:widget-system:widget-controller-manager"]
    verbs: ["create", "update", "patch", "delete"]
    resources:
      - group: "rbac.authorization.k8s.io"
        resources: ["roles", "rolebindings", "clusterroles", "clusterrolebindings"]
  # Secret アクセスは Metadata レベル(値はログに残さない)
  - level: Metadata
    resources:
      - group: ""
        resources: ["secrets"]
  # その他の Operator 書き込み操作は Request レベル
  - level: Request
    users: ["system:serviceaccount:widget-system:widget-controller-manager"]
    verbs: ["create", "update", "patch", "delete"]

特に Operator が RBAC オブジェクトを変更する行為は権限昇格の兆候である可能性があるため、RequestResponse レベルで全体を記録するのがよいです。一方 Secret アクセスは Metadata レベルのみで記録し、値が監査ログに平文で残らないようにします。Operator 自体も意味のあるドメインイベント(例: 「Widget X の reconcile 失敗」)を Kubernetes Event として記録し、運用の可視性を高めます。

reconcile の冪等性とセキュリティの関係

少し違う角度ですが重要な話です。controller-runtime の reconcile は 冪等(idempotent)であり、望ましい状態(desired state)中心 に設計されるべきです。これはセキュリティにもつながります。

reconcile が「現在の状態を見て望ましい状態へ収束する」方式なら、誰かが悪意を持って、あるいは誤ってリソースを改ざんしても、Operator が次の reconcile でそれを元に戻します。つまり reconcile ループ自体が一種のドリフト是正であり、セキュリティ統制になります。逆に reconcile が命令型(イベントが来たら一度実行)で書かれていると、改ざんが是正されずに残り、セキュリティホールになります。

また predicate でイベントをフィルタリングすると不要な reconcile が減り、負荷ベースの攻撃(多数の CR 変更で Operator を麻痺させる試み)への耐性も高められます。

// predicate で spec 変更にのみ反応 — status のみの更新による reconcile 暴走を防止
func (r *WidgetReconciler) SetupWithManager(mgr ctrl.Manager) error {
	return ctrl.NewControllerManagedBy(mgr).
		For(&appsv1alpha1.Widget{}).
		WithEventFilter(predicate.GenerationChangedPredicate{}).
		Owns(&appsv1.Deployment{}).
		Complete(r)
}

finalizer は削除時のクリーンアップ(cleanup)を保証しますが、これもセキュリティに関わります。Operator が外部システムに資格情報やリソースを作成した場合、CR 削除時に finalizer でそれを確実に回収しないと、孤立リソース(orphaned resource)による権限漏れを防げません。

セキュリティチェックリスト

運用に入る前に点検する項目を一目で整理します。

[ RBAC 最小権限 ]
  - すべての RBAC マーカーが実際にコードが使う動詞/リソースのみを付与しているか
  - verbs=* または resources=* ワイルドカードを除去したか
  - delete 権限は本当に必要か(ownerReference で代替できるか)
  - リーダー選出 Lease は resourceNames で限定したか
  - ClusterRole は本当に必要か、Role で十分ではないか

[ 範囲 / マルチテナンシー ]
  - マネージャーのキャッシュを必要な namespace に限定したか
  - テナント分離が必要ならテナント別 Operator を検討したか
  - NetworkPolicy / ResourceQuota / LimitRange を適用したか
  - CR の個数にも quota をかけたか

[ メトリクス / Webhook ]
  - メトリクスエンドポイントに WithAuthenticationAndAuthorization を適用
  - kube-rbac-proxy サイドカーを除去したか(2026)
  - TLS 1.3 の最低バージョンを強制したか
  - Webhook 証明書を cert-manager で自動ローテーションしているか
  - セキュリティ Webhook の failurePolicy=Fail + 高可用性を確保

[ 権限昇格の防止 ]
  - Operator が RBAC を動的生成しないように設計したか
  - CR が要求可能な権限を Webhook でホワイトリスト化したか
  - escalate/bind/impersonate 動詞をブロックしたか

[ サプライチェーン ]
  - イメージを cosign で署名しているか
  - SBOM を生成/添付/署名しているか
  - admission ポリシーで署名検証を強制しているか
  - distroless/nonroot ベースイメージを使っているか

[ Secret / 監査 ]
  - Secret を平文でログに残していないか
  - Secret RBAC を namespace/名前で絞ったか
  - etcd encryption at rest (KMS) を有効にしたか
  - audit policy で Operator の RBAC 変更を RequestResponse で記録しているか

おわりに

Operator はクラスタを運用する自動化の頂点ですが、同時にその強力な権限ゆえに最も魅力的な攻撃対象です。本記事で扱ったセキュリティ強化は、結局一つの原則に収束します。Operator に必要な最小限の権限のみを与え、その権限が悪用されないよう多層で防御せよ。

RBAC マーカーで権限をコードのそばに宣言し、動詞を慎重に絞り、範囲を namespace に限定し、メトリクスと Webhook を認証/TLS で保護し、CR を通じた権限昇格を Webhook で遮断し、イメージを署名して検証し、Secret を慎重に扱い、すべての行動を監査ログに残すこと。これら一つひとつは難しくありませんが、組み合わさって初めて、一度の侵害がクラスタ全体の掌握につながらない防御線になります。

新しい Operator を設計するたびに脅威モデルをまず描き、上のチェックリストを最初から適用することをお勧めします。セキュリティは後から付け足すものではなく、設計の一部であるときに最も堅牢になります。

参考資料