Skip to content

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 parentRef itself. 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 smtpServer map, not in server env. There are no KC_SMTP_* environment variables — a spec.env entry for them is ignored — so there is deliberately no spec.smtp field. Configure SMTP in the realm: via the Admin Console (Realm Settings → Email) or by including an smtpServer block in the realm JSON you import through spec.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 managementspec.realmImport covers 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 KeycloakInstance if 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.cacheMtls defaults to false in 0.2.0; set it to true to 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; kindnet does 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.