Skip to content

RBAC and security

Reactor runs with a dedicated ServiceAccount holding exactly the verbs it needs. Which verbs those are depends on what you turn on, and each one is argued for where the feature that needs it is documented:

PermissionGranted whenWhere it is explained
get on Secretsactions.allowedDestinations is non-emptyNotifications and HTTP
list on HorizontalPodAutoscalerssafety.detectHPA is setArbitration
get and patch on Nodes, cluster-scopedrbac.allowNodeActions is setKubernetes actions
create on TokenReviews and SubjectAccessReviewsmetrics.enabled is setMetrics and alerts
create on events.k8s.io EventsalwaysEvents and status
write verbs on workload kindswithheld entirely under safety.dryRunSuspend and dry run

rbac.clusterWide: false restricts the manager to its own namespace; an action targeting another namespace then reports the target as unreachable rather than failing at the API server. The threat model for everything that leaves the cluster is in SECURITY.md.

The controller pod satisfies the restricted Pod Security Standard with no exemptions — it sets runAsNonRoot, seccompProfile: RuntimeDefault, drops all capabilities, and runs with allowPrivilegeEscalation: false and a read-only root filesystem. You can label its namespace accordingly without any trial and error:

Terminal window
kubectl label namespace reactor-system \
pod-security.kubernetes.io/enforce=restricted

Note that the operator patches other workloads’ Deployments. If a target namespace enforces or warns on restricted and the target’s own pod spec doesn’t comply, the API server returns an admission warning on the otherwise successful patch. That warning describes the target, not Reactor; it is logged under the target-warning logger at debug level so it can’t be mistaken for a failed action.

Off by default. Enabling it denies all inbound traffic and applies networkPolicy.egress, which starts unrestricted so that turning the policy on cannot silently break polling. Narrow it to the three things the operator actually talks to — DNS, the API server, and your console:

networkPolicy:
enabled: true
egress:
- to:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: kube-system }
ports:
- { protocol: UDP, port: 53 }
- { protocol: TCP, port: 53 }
- to:
- ipBlock: { cidr: <your API server IP>/32 }
ports:
- { protocol: TCP, port: 6443 }
- to:
- ipBlock: { cidr: <your UniFi console IP>/32 }
ports:
- { protocol: TCP, port: 443 }

Denying all inbound is right until you turn the webhook fast path on, at which point the console does need to reach the receiver. The chart does not widen networkPolicy.ingress for you — a policy that opens a port you did not write there would be a bad surprise — so add the rule yourself. Without it, deliveries are dropped and Reactor quietly falls back to the poll interval, which is the one failure here that looks like nothing at all:

networkPolicy:
ingress:
- from:
- ipBlock: { cidr: <your UniFi console IP>/32 }
ports:
- { protocol: TCP, port: 9090 }

The same applies to metrics.enabled: Prometheus has to reach the metrics port, and the chart does not widen networkPolicy.ingress for that either.

networkPolicy:
ingress:
- from:
- namespaceSelector:
matchLabels: { kubernetes.io/metadata.name: monitoring }
ports:
- { protocol: TCP, port: 8443 }