Operands in Other Namespaces¶
The default install assumes every Bind9Instance runs in the operator's own
namespace, bindy-system. Running instances (the operands) anywhere else
needs two extra grants per operand namespace. Neither is created automatically,
neither bindy bootstrap operator nor kubectl apply -f deploy/operator/rbac/
makes them, and each one fails in a different place when it is missing.
When this applies¶
You need this page for every namespace, other than the operator's, that holds a
Bind9Instance. That includes instances you create yourself, instances a
Bind9Cluster creates in its own namespace, and the managed Bind9Cluster a
ClusterBind9Provider creates in each namespace that has an instance
referencing it.
It applies in both operator modes:
- Cluster-wide (
BINDY_WATCH_NAMESPACESunset, the default): thebindy-roleClusterRole already lets the operator see the namespace, but it is read-only on Secrets. - Namespace-scoped (
BINDY_WATCH_NAMESPACESset): the operand namespace must also be inBINDY_WATCH_NAMESPACESand carry the per-namespacebindy-roleRole fromdeploy/operator/rbac/namespaced/(see Environment Variables). That Role is read-only on Secrets too, so the two grants below are still required.
The examples use team-dns as the operand namespace and assume the operator
runs as ServiceAccount bindy in bindy-system. Substitute your own names.
The two grants¶
| Grant | Who is granted | What for | Missing it breaks |
|---|---|---|---|
Role + RoleBinding bindy-secrets-writer in the operand namespace |
operator SA system:serviceaccount:bindy-system:bindy |
create, update, patch, delete on secrets: creating, rotating and deleting each instance's <instance>-rndc-key Secret |
Bind9Instance reconcile: no BIND9 pods are created |
ClusterRole bindcar-tokenreview bound to the operand namespace's bind9 SA |
system:serviceaccount:team-dns:bind9 |
create on tokenreviews.authentication.k8s.io: the bindcar sidecar validating the operator's bearer token |
every bindcar API call: zones and records never reach BIND9 |
Why they are needed¶
Secrets (B-5 hardening). The operator's ClusterRole grants only get,
list and watch on Secrets cluster-wide. The mutating verbs live in the
namespaced bindy-secrets-writer Role, which the shipped manifests
(deploy/operator/rbac/secrets-role.yaml, secrets-rolebinding.yaml) and
bindy bootstrap operator create in the operator namespace only. This is
deliberate: a compromised operator cannot write Secrets in namespaces you have
not opted in, such as kube-system. The operator writes each instance's RNDC
key Secret in the instance's own namespace, so every operand namespace has to
be opted in explicitly.
TokenReview (bindcar Mode B). Every BIND9 pod runs the bindcar API sidecar
(container api) as the bind9 ServiceAccount of its own namespace. The
operator authenticates to bindcar with a bindcar-audience ServiceAccount
token, and bindcar validates it by creating a TokenReview. The shipped
bindcar-tokenreview ClusterRoleBinding names only
system:serviceaccount:bindy-system:bind9, so a sidecar in team-dns is
refused when it tries to create the TokenReview.
The sidecar's allow-list does not change per namespace: BIND_ALLOWED_SERVICE_ACCOUNTS
always names the operator SA (system:serviceaccount:bindy-system:bindy), which
the operator sets on every operand Deployment.
Granting a namespace¶
Option 1: the template (recommended)¶
deploy/operator/rbac/operand-namespace/rbac.yaml holds both grants for one
namespace, with REPLACE_NAMESPACE as the placeholder:
NS=team-dns
sed "s/REPLACE_NAMESPACE/$NS/g" deploy/operator/rbac/operand-namespace/rbac.yaml \
| kubectl apply -f -
It is the following YAML. The Role and RoleBinding mirror
deploy/operator/rbac/secrets-role.yaml and secrets-rolebinding.yaml with only
the namespace changed:
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: bindy-secrets-writer
namespace: team-dns
labels:
app.kubernetes.io/name: bindy
app.kubernetes.io/component: rbac
rules:
- apiGroups: [""]
resources: ["secrets"]
verbs: ["create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: bindy-secrets-writer
namespace: team-dns
labels:
app.kubernetes.io/name: bindy
app.kubernetes.io/component: rbac
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: bindy-secrets-writer
subjects:
- kind: ServiceAccount
name: bindy # the operator SA
namespace: bindy-system # the operator namespace
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: bindcar-tokenreview-team-dns
labels:
app.kubernetes.io/name: bindy
app.kubernetes.io/part-of: bindy
app.kubernetes.io/component: rbac
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: bindcar-tokenreview # shipped in tokenreview-clusterrole.yaml
subjects:
- kind: ServiceAccount
name: bind9 # the operand SA bindcar runs as
namespace: team-dns
The template binds the existing bindcar-tokenreview ClusterRole with a
separate ClusterRoleBinding per namespace rather than editing the shared
bindcar-tokenreview binding. subjects is replaced as a whole on apply, so
re-applying deploy/operator/rbac/tokenreview-clusterrolebinding.yaml or
re-running bindy bootstrap operator resets the shared binding to its single
bindy-system subject and silently drops any namespace you added to it. A
per-namespace binding survives both, and is one object to delete when the
namespace goes away. This is the right shape for GitOps too: commit one rendered
copy of the template per operand namespace.
Option 2: add a subject to the shared binding¶
If you prefer a single bindcar-tokenreview ClusterRoleBinding, create the
bindy-secrets-writer Role and RoleBinding as above, then append a subject with
a JSON patch:
NS=team-dns
kubectl patch clusterrolebinding bindcar-tokenreview --type=json -p \
"[{\"op\":\"add\",\"path\":\"/subjects/-\",\"value\":{\"kind\":\"ServiceAccount\",\"name\":\"bind9\",\"namespace\":\"$NS\"}}]"
Declaratively, the binding then lists one subject per operand namespace:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: bindcar-tokenreview
labels:
app.kubernetes.io/name: bindy
app.kubernetes.io/part-of: bindy
app.kubernetes.io/component: rbac
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: bindcar-tokenreview
subjects:
- kind: ServiceAccount
name: bind9
namespace: bindy-system # the operator namespace, from the default install
- kind: ServiceAccount
name: bind9
namespace: team-dns # one entry per operand namespace
Re-applying the shipped binding drops patched subjects
With this option, own the full subject list in one place (your GitOps
repository) and never re-apply deploy/operator/rbac/tokenreview-clusterrolebinding.yaml
or re-run bindy bootstrap operator without re-adding the operand
namespaces afterwards.
Verifying¶
Run these before creating the first instance in the namespace. Both must print
yes:
NS=team-dns
# 1. The operator may write Secrets in the operand namespace.
kubectl auth can-i create secrets -n "$NS" \
--as=system:serviceaccount:bindy-system:bindy
# 2. The operand namespace's bind9 SA may create TokenReviews.
kubectl auth can-i create tokenreviews.authentication.k8s.io \
--as=system:serviceaccount:"$NS":bind9
Check update and delete on secrets the same way if you have customised the
Role; RNDC key rotation uses update and instance deletion uses delete.
The second check works before the bind9 ServiceAccount exists: the operator
creates that ServiceAccount itself, as the first step of reconciling the first
instance in the namespace.
Symptoms when a grant is missing¶
Missing bindy-secrets-writer: no BIND9 pods¶
The operator creates the bind9 ServiceAccount, then fails creating the RNDC
key Secret, before any ConfigMap, Deployment or Service exists. The instance
reports:
kubectl get bind9instance -n team-dns <name> \
-o jsonpath='{.status.conditions[?(@.type=="Ready")]}'
status:Falsereason:NotReadymessage: starts withFailed to create resources: ApiError: secrets is forbidden:and continuesUser "system:serviceaccount:bindy-system:bindy" cannot create resource "secrets" in API group "" in the namespace "team-dns": Forbidden
The operator log carries the same text after
Failed to create/update resources for team-dns/<name>:, and the reconcile is
retried with backoff until the grant is added. No restart is needed: the next
retry succeeds once the Role and RoleBinding exist.
Missing TokenReview subject: pods run, zones never load¶
The BIND9 pods start and answer queries, but every bindcar call from the
operator is refused, so zones and records never reach BIND9 and DNSZones do
not become Ready.
- Operator log:
HTTP API request failedwithstatus=401 Unauthorizedanderror={"error":"Unauthorized"}; zone errors containHTTP 401 Unauthorized: {"error":"Unauthorized"}. A 401 is not retried as a transient error. - bindcar log (the real cause; bindcar deliberately returns a generic
Unauthorizedto the caller):
shows TokenReview API call failed: followed by
tokenreviews.authentication.k8s.io is forbidden: User "system:serviceaccount:team-dns:bind9" cannot create resource "tokenreviews" in API group "authentication.k8s.io" at the cluster scope,
and Token validation failed: Failed to validate token with Kubernetes API: ....
Adding the subject or binding fixes it without restarting anything: bindcar creates a TokenReview per request, and the operator's next reconcile succeeds.
For the other causes of a bindcar 401 (audience mismatch, wrong allow-list), see Common Issues.
Removing a namespace¶
Once a namespace no longer hosts operands, remove its grants so the operator and
the namespace's bind9 SA keep no access they do not use.
- Delete the namespace's
Bind9Instances (and anyBind9Cluster) first, while the grant still exists. The instance finalizer deletes the<instance>-rndc-keySecret; withoutdeleteon secrets that step only logs a warning, and the Secret is left to Kubernetes garbage collection through its owner reference. - Remove the grants.
With the template (option 1):
NS=team-dns
sed "s/REPLACE_NAMESPACE/$NS/g" deploy/operator/rbac/operand-namespace/rbac.yaml \
| kubectl delete -f -
With a subject on the shared binding (option 2), find the subject's index and
remove it. The test op makes the patch fail instead of removing the wrong
entry if the list changed in between:
NS=team-dns
IDX=$(kubectl get clusterrolebinding bindcar-tokenreview \
-o jsonpath='{range .subjects[*]}{.namespace}{"\n"}{end}' | grep -nx "$NS" | cut -d: -f1)
IDX=$((IDX - 1))
kubectl patch clusterrolebinding bindcar-tokenreview --type=json -p \
"[{\"op\":\"test\",\"path\":\"/subjects/$IDX/namespace\",\"value\":\"$NS\"},{\"op\":\"remove\",\"path\":\"/subjects/$IDX\"}]"
kubectl delete rolebinding,role bindy-secrets-writer -n "$NS"
Deleting the namespace itself removes the namespaced Role and RoleBinding, but
not a cluster-scoped binding or subject. A leftover subject means a
namespace later recreated with the same name gets TokenReview access for its
bind9 SA without anyone granting it, so always remove the cluster-scoped half
explicitly.
In namespace-scoped mode, also drop the namespace from BINDY_WATCH_NAMESPACES
and delete its bindy-role Role and RoleBinding, changing both together (see
Environment Variables).
See also¶
- RBAC: the operator's ClusterRole and the
bindcar-tokenreviewClusterRole - Environment Variables:
BINDY_WATCH_NAMESPACES - Multi-Tenancy Guide: tenancy models that put instances in team namespaces
deploy/operator/rbac/operand-namespace/: the template used above