ADR 0006: What the operator may read, and why it holds no cluster-wide Secret access

Date: 2026-09-11 Status: accepted

Context

The operator clones customer repositories, so it needs git credentials: a token, or an SSH key, sometimes a private CA bundle. Those live in Kubernetes Secrets in the namespace of the application that uses them.

The default way to write this is a kubebuilder:rbac marker for Secrets, which generates a ClusterRole with get, list, watch on Secrets cluster-wide. That is what the operator had.

It is also the finding that ends an evaluation. A controller with cluster-wide Secret read can read every Secret in the cluster: the core banking system's database password, the payment gateway's signing key, every other team's credentials. Nothing in Nazeel wants those, but the permission does not know that, and a security review does not grade on intent. Worse, list and watch on Secrets means the controller's cache holds them in memory, so a single read primitive anywhere in the process becomes a whole-cluster credential disclosure.

Decision

Two independent limits, so a mistake in either one is not sufficient.

1. Permission is granted per Secret, by name, by the API. When a repository is connected, the API creates a namespaced Role naming exactly that Secret in resourceNames, and binds the operator's service account to it. Disconnecting the repository revokes it. There is no cluster-scoped rule for Secrets at all. The operator can read the Secrets a customer has explicitly pointed it at, and nothing else, and the list is inspectable with kubectl get rolebindings -A.

2. The informer cache only watches labelled Secrets. The manager's cache is configured to select nazeel.sa/git-credentials. Even where permission would allow a read, an unrelated Secret is never fetched and never sits in the operator's memory.

The kubebuilder:rbac marker was removed from the source, not just from the generated YAML. Editing config/rbac/role.yaml alone would have been undone by the next make manifests, and the permission would have come back in a release with nobody noticing.

The operator also holds no Secret write permission outside its own namespace. This came up when distributing the preview signing key to application namespaces: the easy implementation is for the operator to copy the Secret, which needs create on Secrets wherever environments live. That was implemented and then reverted. The API distributes the key instead. Re-granting namespace-wide Secret write to avoid one API call would have undone this decision for a convenience.

Consequences