The reverser ships able to read every object in the cluster, because a WatchRule may name
any type — including a type installed after the operator. That default is honest, not
required. This page shows how to run it with the read access it actually uses, which on a
management cluster is the difference between "mirrors two CRDs" and "can read every
credential we own".
| ClusterRole | Contents | Where it comes from |
|---|---|---|
<release>-manager-role |
Exactly what the binary calls: its own CRs, namespaces, customresourcedefinitions, apiservices, and secrets with get, create, update |
Generated from the kubebuilder markers into config/rbac/role.yaml. Never edit by hand. |
<release>-watch-any or <release>-watch-selected |
The types a WatchRule may read, get/list/watch only |
The chart's rbac.watchTypes; for kustomize, config/rbac/watch-any-role.yaml. |
Two more ClusterRoles exist only when asked for, for a folder that is both written and applied
(bi-directional.md): <release>-applier-flux (chart value
applierAccess.flux; for kustomize,
config/rbac/flux-applier-role.yaml) lets a
FluxClaim suspend and resume its Kustomization, and <release>-applier-argocd
(applierAccess.argocd;
config/rbac/argocd-applier-role.yaml) lets an
ArgoCDClaim start its Application's syncs.
They are separate because RBAC is additive. A wildcard read folded into the manager role
would grant cluster-wide Secret list/watch no matter how narrow the Secret rule beside
it is, and no chart value could take it back. Split out, the wildcard can be replaced.
rbac:
create: true
watchTypes:
mode: any # any | selected
selected: []mode: any (default) grants cluster-wide read on every resource. A WatchRule can name
any type, including one installed after the operator, and it will just work. The price is
that the reverser can read every Secret in the cluster — every git credential, every
token — even though it never asks for one it was not pointed at.
mode: selected grants read on exactly the types you list, and nothing else:
rbac:
watchTypes:
mode: selected
selected:
- apiGroups: [""] # core group
resources: ["configmaps"]
- apiGroups: ["apps"]
resources: ["deployments"]Verbs are always get, list, watch (the applier grants above are separate): the reverser
never writes to a watched type, so a verbs: key is rejected rather than letting you grant one
by accident. So is mode: selected with an empty list, which would leave the operator able to
watch nothing, and an unknown key under rbac, which is how you think you narrowed access when you did not.
Those rules live in the chart's
values.schema.json, which Helm enforces on
template, lint, install and upgrade, naming the offending path:
Error: values don't meet the specifications of the schema(s) in the following chart(s):
- at '/rbac/watchTypes/selected/0': additional properties 'verbs' not allowed
Do not restate namespaces, customresourcedefinitions or apiservices: the manager
role already carries them. A WatchRule naming a type you did not grant will fail to watch
it, so the list must cover every type your rules name.
The operator needs no Namespace access at all in a source cluster. The manager role's
namespaces get/list/watch covers the operator's own cluster, where Selector grants (in
GitProvider.spec.allowedGitBranches, GitBranch.spec.allowedGitTargets and the KubeProvider's
grant lists) match control-cluster labels.
This used to be different: GitTarget.spec.allowedSourceNamespaces could carry a label selector
evaluated against Namespaces in the source cluster, so a remote provider's identity needed
namespaces get/list/watch there. That field is gone, and with it the cross-cluster
Namespace read. What a source credential may read is now the whole of the source-side bound, so
grant it exactly the namespaces and types the rules name — a sourceNamespace: "*" item asks for a
cluster-wide list/watch of its types, and gets a clean 403 if the credential cannot serve it.
A source credential only reads, with one opt-in exception. A KubeProvider whose
spec.commitRequests is Reconcile serves the
CommitRequests created on its
cluster, in the namespaces it lends its GitTargets, and writes their status back there. Its
credential then needs, in the configbutler.ai group and in each of those namespaces, get, list
and watch on commitrequests, and update on commitrequests/status.
ReconcileAndDeleteFinished also deletes finished requests, so it needs patch and delete on
commitrequests too. Nothing else on a source cluster is written, the CRD included. The manager
role carries the same rules for the cluster the operator runs in.
Whoever may create a CommitRequest in a namespace may save their own commit window on any
GitTarget whose spec.allowedCommitRequests admits it: by sourceNamespaces on a source cluster,
by consumerNamespaces on the cluster GitOps Reverser runs in. That grant, not the provider's
switch, is the GitTarget owner's consent.
It holds no Secret informer: Secrets are excluded from the manager's cache
(cmd/main.go, Cache.DisableFor), so every read is a direct get of a
Secret a GitProvider, GitReceiver, KubeProvider or GitTarget names. Nothing in the operator
calls list or watch on Secrets. It creates and updates only the Secrets it owns (the generated
signing key).
That is why the manager role asks for get, create, update and nothing more.
Set mode: selected and list your types. The chart renders the ClusterRole and its binding
for you — there is no hand-written role to maintain, and no way to drift from the
ServiceAccount the operator actually runs as.
Result: GitProvider Ready, ClusterWatchRule Ready with its streams, commits flowing, and
the reverser cannot enumerate Secrets — no list, no watch, on any namespace.
Be precise about what is left, because RBAC is additive and the manager role is still bound:
Verb on secrets |
Still granted? | Why |
|---|---|---|
list, watch |
No | The operator never enumerates Secrets. Removed from the manager role, and selected adds no wildcard to put them back. |
get |
Yes, cluster-scoped | It reads the git-creds, receiver-token, kubeconfig and age-key Secrets a GitProvider, GitReceiver, KubeProvider or GitTarget names, each in that object's own namespace — and those objects may live in any namespace. |
create, update |
Yes, cluster-scoped | It generates the signing key and, with generateWhenMissing, the age key. |
So a selected install can still read a Secret whose name and namespace it already
knows. It cannot discover one. That is a real reduction — enumeration is what turns a
mirroring operator into a credential-harvesting one — but it is not zero Secret access, and
this page will not pretend otherwise.
Scoping get/create/update down to the namespaces that hold a GitProvider, GitReceiver,
KubeProvider or GitTarget needs a namespaced Role the chart does not yet render. Until then the escape hatch is
rbac.create: false plus your own role set. See
future/least-privilege-remaining-work.md.
Discovery answers what the API server serves, not what this ServiceAccount may read.
An ordinary apiserver serves apiregistration.k8s.io whether or not your ClusterRole names
apiservices, so a narrowed role sails past the "is it served?" check and then gets a 403
on the informer's first LIST.
For the two API-surface trigger informers (customresourcedefinitions, apiservices)
the operator treats a 403 as authoritative: it stops that informer, logs once, and falls
back to refreshing its API-resource catalog on the periodic tick. It does not retry the
denial forever, which would bury real errors under benign noise. Grant the permission later
and the next catalog refresh re-arms the informer — no restart.
These two are conveniences: they only make the catalog notice a new CRD or aggregated API sooner than the periodic refresh would. Everything else keeps working without them, which is why failing closed and quiet is the right answer to "you may not read this".
A denied watched type is different: that is the job you asked for, not a convenience, so
the operator keeps retrying it rather than quietly dropping it. Its watch stream errors and
the manager retries with backoff. If you narrow the role, make sure every type your
WatchRules name is granted, and check the operator logs after the change.