Known Limitations¶
A candid list of what bnerd-keycloak-operator does not cover yet.
None of these are silent — where a limitation would otherwise cause
surprising behaviour, the operator either rejects the configuration at
validation or reports it via a condition.
Managed Postgres watch needs the Percona CRD present at operator start¶
As of 0.2.0 the operator adds a live watch on the PerconaPGCluster object
when the Percona PostgreSQL Operator CRD is present at operator start, so a
managed Postgres cluster transitioning to ready is picked up (near-)instantly
rather than at the next requeue. The bounded 30-second requeue is retained as a
fallback. The remaining limitation: if the Percona CRD is installed after
this operator started, the watch is not wired until the operator is
restarted — until then those instances still converge via the 30-second
requeue.
Upgrading to 0.2.0: check default-on NetworkPolicy against existing instances first¶
0.2.0 renders an owned NetworkPolicy on every instance by default. On a
cluster whose CNI actually enforces NetworkPolicy (Cilium, Calico, ...) —
not kindnet, which ignores it entirely — upgrading the operator can silently
break two things on an already-running instance that never had this rule set
before: the admin console, if its ingress controller runs in a namespace
other than the default adminIngressControllerSelector match; and any
outbound call a realm is configured to make — external Identity Provider
federation (SAML/OIDC to Google, Microsoft, a corporate IdP) and an SMTP relay
for realm email are the two common instances, but it's the general case, not a
fixed list — which the default egress rules do not cover. Set
spec.networkPolicy.egressWhitelist to that destination's CIDR(s) if so.
Both are one-look-answerable, checkable before you upgrade — see
Upgrading the operator itself
for the full pre-upgrade checklist.
⚠ Corrected 2026-08-25. This section used to end "do not treat 'the e2e
suite passed' as evidence here: it runs on kindnet, which cannot exercise
either failure mode." That is no longer true and was worth measuring rather than
repeating: kindnet v20240813 (k8s 1.31) does enforce NetworkPolicy —
verified with hack/cni-enforcement-probe.sh, which checks that a cluster can
return both answers (a default-deny blocks, a pod no policy selects is
unaffected, and an explicit allow admits). It passes on kindnet.
The caution still stands in a weaker form: the suite only exercises what its
tests actually exercise, and until the recovery-admin e2e nothing in it made a
pod-to-pod call, so an egress rule could be wrong without any test noticing —
which is exactly what happened. hack/kind-up.sh --cilium brings up a variant
running Cilium, matching the CNI the b'nerd estate actually uses.
Admin console requires an admin Ingress or a port-forward¶
The admin console and Admin REST API are never exposed on the public host — the
public Ingress routes /admin and the bare / to a sink Service that selects
no Pods, so a conformant controller answers 503. This is snippet-free and
works on any Ingress class, so it is no longer a known limitation. The
consequence, by design: when spec.hosts.adminHost is unset there is no
Ingress path to the console at all, and it is reachable only via
kubectl port-forward. To serve it, set spec.hosts.adminHost (must differ
from host) — that creates a separate, internal-only admin Ingress (default
class nginx-internal) meant to stay off the public internet.
Gateway API mode: the operator never creates the Gateway¶
In spec.ingress.mode: GatewayAPI the operator renders HTTPRoutes only. It
never creates a Gateway, a GatewayClass or a ReferenceGrant — the Gateway
is platform infrastructure with its own lifecycle, usually shared by many
workloads, and an operator creating one per instance would fight whoever owns
the cluster's edge. You provide the Gateway, and its listener's
allowedRoutes.namespaces must admit the instance's namespace; otherwise the
instance reports RoutesAccepted=False / NotAllowedByListeners.
TLS in this mode terminates at the Gateway listener, so clusterIssuer does
not apply and the operator manages no certificates.
adminExposure: Public is unrestricted and unverified¶
The default spec.hosts.adminExposure: Public places no constraint on where
the admin host is served: it may share the public Gateway or ingress class, and
adminParentRef / adminClass may be omitted entirely. The operator cannot
verify how exposed that entry point actually is — it does not know whether a
Gateway's LoadBalancer is internet-facing, and it does not resolve the admin
hostname. An instance serving its admin console on the public internet looks
exactly like one serving it on a private network.
Use adminExposure: Internal to have the operator enforce a separate Gateway
(or ingress class), or set it estate-wide on a KeycloakProfile. Before
v0.3.2 that isolation was unconditional; see the v0.3.2 upgrade notes.
Gateway API mode: TLS is the listener's, so a plain :80 listener serves cleartext¶
In GatewayAPI mode the operator emits no TLS configuration and no HTTP→HTTPS
redirect — the certificate lives on the Gateway listener (design D4). Attaching
an instance to a plain :80 listener therefore serves the Keycloak login form,
and accepts the password, in cleartext, while the CR reports Ready=True and
RoutesAccepted=True. The operator cannot detect this: it is a property of the
listener you attached to. Attach only to an HTTPS listener.
A parentRef without sectionName is the common way to end up there
without meaning to: the route then attaches to every listener that admits it
and matches its hostname, including a hostname-less :80 one, and Gateway API
resolves hostname specificity before path, so the route out-ranks the
platform's HTTP→HTTPS redirect.
Since v0.3.2 the operator rejects the case it can detect: under
adminExposure: Public, once parentRef.sectionName is set, an
adminParentRef that is missing or unpinned is refused at apply time — that is
where the unblocked admin route would be served in cleartext.
Two gaps remain yours, because the operator has no read on Gateway
objects (least privilege, v0.3.0 security review) and therefore cannot see
your listeners at all:
- The public
parentRefitself. Unpinned, the app (not the admin console) can be served on a plaintext listener. Nothing checks this; the operator uses your pin as evidence, it cannot supply one. - An unpinned admin route on an unpinned public Gateway. If that Gateway is
in fact multi-listener, both routes can land on
:80. The operator reads the missing public pin as "single-listener, by your choice" — which is the assumption that keeps the simple case simple.
Gateway API mode: a co-tenant on a from: All listener can hijack the host¶
The public route keeps the admin console off the public host with a deny rule,
but Gateway API resolves conflicting routes by match specificity, regardless
of namespace. On a listener with allowedRoutes.namespaces.from: All, anyone
able to create an HTTPRoute in any admitted namespace can attach a more
specific match for your hostname (an exact /admin/master/console/, say),
out-specify the deny rule and serve the admin console from a namespace you do
not control. The operator cannot prevent this — it does not own the Gateway and
does not resolve the conflict. Use allowedRoutes.namespaces.from: Selector and
label only the namespaces that should serve the hostname. See the
Serving guide.
Gateway API mode is verified on Envoy Gateway only¶
The operator emits only spec-conformant gateway.networking.k8s.io/v1 objects,
so other conformant implementations are expected to work — but what b'nerd has
verified is Envoy Gateway v1.9.0 / Gateway API v1.6.1, on kind and on a
Gardener shoot (Cilium, Kubernetes 1.35) with managed Percona PostgreSQL and a
Let's Encrypt (staging) certificate issued through cert-manager's Gateway
solver. Traefik, Cilium's own implementation and others are untested here.
The HTTPRoute watch needs an operator restart if Gateway API arrives later¶
The operator probes for Gateway API once at startup, and that decides both
whether it watches HTTPRoutes and whether they are in its informer cache —
neither can change while the process runs. Install the CRDs on a running
operator and the GatewayAPIUnavailable condition clears by itself within
~5 minutes, but route status changes will not wake a reconcile until you
kubectl rollout restart the controller-manager. Same shape as the Percona
CRD limitation above.
Ready is not gated on RoutesAccepted¶
An instance in GatewayAPI mode reports Ready=True as soon as its StatefulSet
is healthy, even when RoutesAccepted is False — the workload and the path
to it are reported separately. This is parity with Ingress mode, where the
operator never knew whether an Ingress controller had accepted the Ingress
either. Alert on RoutesAccepted, not only on Ready, if you serve through a
Gateway.
Not covered in v0.1¶
- SMTP configuration is realm-scoped — Keycloak has no server-level SMTP
settings: outbound-mail config (host, port, from, auth, STARTTLS/SSL) lives
in each realm's
smtpServermap, not in server env. There are noKC_SMTP_*environment variables — aspec.enventry for them is ignored — so there is deliberately nospec.smtpfield. Configure SMTP in the realm: via the Admin Console (Realm Settings → Email) or by including ansmtpServerblock in the realm JSON you import throughspec.realmImport.secretName. See the SMTP example in the KeycloakInstance guide. - Backup/restore CRD — Postgres backups follow whatever backup policy your Percona PG Operator installation defines; the operator does not orchestrate Keycloak-specific backups.
- Realm/client lifecycle management —
spec.realmImportcovers a one-time import at startup; ongoing realm/client/role management is done through the Keycloak admin API or console, not through this CRD. - HorizontalPodAutoscaler — not rendered by the operator; apply your own
alongside the
KeycloakInstanceif your platform requires it. (As of 0.2.0 a NetworkPolicy is rendered by default — see Network isolation — this bullet now covers HPA only.) - Cache (JGroups/Infinispan) transport encryption is opt-in, not default.
spec.cacheMtlsdefaults tofalsein 0.2.0; set it totrueto enable mutual TLS on intra-cluster cache traffic. A future release may flip the default. - NetworkPolicy enforcement depends on the CNI (Cilium, Calico, ... enforce
it;
kindnetdoes not) — see "Upgrading to 0.2.0" above for what that means for an existing instance on upgrade.
See the Changelog for what shipped in each release.