Security¶
projection is a small operator with a large blast radius, because a Projection CR can reference any Kind the apiserver knows about. This page explains the trade-offs and how to tighten them for production.
RBAC scope¶
The operator ships with a ClusterRole granting it */* — every verb on every resource in every API group. The controller-source marker:
…is what generates that ClusterRole in config/rbac/. The reason it's that broad: a Projection can point at any Kind (including CRDs the operator doesn't know about at build time), so any narrower default would ship broken for the long tail of use cases.
The trade-off is real: a misconfigured or malicious Projection can cause the controller to read any Secret in the cluster and write it to a different namespace. Anyone who can create Projection CRs in any namespace can effectively exfiltrate data across namespaces they otherwise couldn't access directly.
Source projectability policy¶
The primary user-facing defense is the source projectability policy, documented in detail in Concepts § 7. The defaults:
--source-mode=allowlist(default). Sources must carry the annotationprojection.be0x74a.io/projectable: "true"to be mirrored. AProjectionpointing at an unannotated source getsSourceResolved=False reason=SourceNotProjectablein status.- Source owner veto: annotation value
"false"is always honored regardless of mode. Post-hoc veto garbage-collects the existing destination.
This shifts the trust model from "anyone with Projection-create rights reads everything" to "source owners decide what's projectable." Clusters that want the historic wide-open behavior can set --source-mode=permissive explicitly.
Note this is a policy control, not an isolation boundary (the controller still has cluster-wide read RBAC). Pair it with admission policy (Kyverno, OPA) constraining who can add the projectable=true annotation for defense-in-depth.
Hardening recommendations¶
1. Narrow the controller's RBAC to the Kinds you actually mirror¶
If you only ever project ConfigMap and Secret, replace the stock ClusterRole with a narrowed one. Example:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: projection-manager
rules:
- apiGroups: [""]
resources: ["configmaps", "secrets"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["projection.be0x74a.io"]
resources: ["projections"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
- apiGroups: ["projection.be0x74a.io"]
resources: ["projections/status"]
verbs: ["get", "update", "patch"]
- apiGroups: ["projection.be0x74a.io"]
resources: ["projections/finalizers"]
verbs: ["update"]
A Projection referencing any other Kind will then fail with SourceResolutionFailed or SourceFetchFailed, which is the correct outcome — it's cluster policy, not silent behavior.
The stock chart accepts an override for the controller ClusterRole; see the chart values.yaml rbac.clusterRoleRules field.
2. Restrict who can create Projection CRs in which namespaces¶
Controlling who can mirror what is as important as the controller's RBAC. Two common patterns:
Kubernetes RBAC — the simplest fix. Only grant create on projections.projection.be0x74a.io to the namespaces / service accounts that actually need it:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: projection-author
namespace: platform
rules:
- apiGroups: ["projection.be0x74a.io"]
resources: ["projections"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
Bind it only to the platform team's namespaces/SAs. Everyone else cannot create Projections.
Admission policies — Kyverno or OPA Gatekeeper for fine-grained rules:
- Deny Projections whose
spec.source.namespaceis not in an allowlist. - Deny Projections whose
spec.source.kindisSecretunless the creator is in a specific group. - Require
overlay.labels.tenantto match the Projection's own namespace.
These rules run at admission time, so they fail kubectl apply, not at reconcile.
3. NetworkPolicy¶
The controller only talks to the apiserver. Restrict its egress to exactly that:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: projection-controller-egress
namespace: projection-system
spec:
podSelector:
matchLabels:
control-plane: controller-manager
policyTypes: [Egress]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
component: kube-apiserver
ports:
- protocol: TCP
port: 6443
# DNS
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
Adjust selectors for your cluster. The chart ships an optional NetworkPolicy under config/network-policy/ you can enable.
Image supply chain¶
Release images are pushed to ghcr.io/be0x74a/projection and cosign-signed with GitHub's OIDC keyless workflow. Verify before pulling:
cosign verify ghcr.io/be0x74a/projection:v0.1.0-alpha \
--certificate-identity-regexp "https://github.com/be0x74a/projection/.github/workflows/.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
The Helm chart is published to oci://ghcr.io/be0x74a/charts/projection and signed with the same workflow:
cosign verify oci://ghcr.io/be0x74a/charts/projection:0.1.0-alpha \
--certificate-identity-regexp "https://github.com/be0x74a/projection/.github/workflows/.*" \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
Running images are distroless, multi-arch (amd64, arm64), non-root, with readOnlyRootFilesystem: true in the supplied Deployment.
Audit trail¶
Every destination write is observable from three places:
- Kubernetes Events on the
Projection(Projected,Updated,DestinationConflict, ...). See Observability. - Status conditions on the
Projection—lastTransitionTimetells you when each state changed. - Cluster audit logs capture every
Create/Update/Deletethe controller does on destination objects, with the controller's service account as the subject.
Together these are enough to answer "who created this object, when, and on whose behalf?" without any extra tooling.
Reporting vulnerabilities¶
Privately via GitHub Security Advisories. See SECURITY.md in the repo for the process.