Installs Zitadel into a Kubernetes cluster as the platform identity provider, and hosts a focused set of auth.hops.ops.com.ai primitive XRDs (HumanUser, MachineUser, Grant, IDP) that compose against the installed Zitadel.
The stack XRD (AuthStack) wraps the upstream zitadel/zitadel Helm chart — handling the namespace, database wiring, Gateway API routing, and re-projecting chart-managed bootstrap secrets (admin PAT + login-client PAT) into XR status for downstream consumers.
The primitive XRDs let operators declaratively manage the identity model inside a running Zitadel — see Auth-group primitives below.
Local-dev (colima cluster, bundled Postgres, no TLS):
apiVersion: hops.ops.com.ai/v1alpha1
kind: AuthStack
metadata:
name: auth
namespace: default
spec:
clusterName: colima
domain: auth.localtest.me
externalSecure: false
database:
bundled: trueProduction (managed Postgres + Gateway API):
apiVersion: hops.ops.com.ai/v1alpha1
kind: AuthStack
metadata:
name: auth
namespace: platform
spec:
clusterName: prod
domain: auth.example.com
firstInstance:
org: hops-ops
database:
external:
dsnSecretRef:
name: zitadel-db
key: dsn
gateway:
enabled: true
parentRef:
name: platform-gateway
namespace: istio-systemExactly one of:
| Mode | When | Notes |
|---|---|---|
external |
Production with managed Postgres (RDS, CNPG cluster, etc.) | Provide a Secret containing the DSN; chart reads ZITADEL_DATABASE_POSTGRES_DSN. |
bundled |
Local dev only | Sets postgresql.enabled=true so the chart uses its bundled Bitnami subchart. Not for production. |
psqlStack |
Future | Reserved for the platform PSQLStack integration; pending the PSQLDatabase XRD. The composition currently rejects this mode. |
The zitadel/zitadel chart's setup hook creates two machine users on first install:
<iamAdmin.username>— JWT machine key in a chart-managed Secret named after the username<iamAdmin.username>-pat— admin PAT<loginClient.username>— login-client PAT
This stack does not author its own init Job. It re-projects those Secret references into XR status so downstream consumers (the future Zitadel Crossplane provider, ad-hoc admin tooling) have a stable place to read the credentials:
status:
oidc:
issuerURL: https://auth.example.com
discoveryURL: https://auth.example.com/.well-known/openid-configuration
bootstrap:
iamAdminPatSecretRef: { name: iam-admin-pat, namespace: zitadel, key: pat }
iamAdminKeySecretRef: { name: iam-admin, namespace: zitadel, key: key }
loginClientPatSecretRef: { name: login-client, namespace: zitadel, key: pat }Per [[specs/identity-architecture]], the auth-group primitive XRDs that have substantive composition value-add — MachineUser, Grant — live in this repo alongside AuthStack under the auth.hops.ops.com.ai group.
Status:
| Kind | Plural | Composes | Status |
|---|---|---|---|
MachineUser |
machineusers |
MachineUser + opt-in AccessToken + opt-in AWS SM Secret + ESO PushSecret (provider-kubernetes Object) |
✓ |
Grant |
grants |
user.zitadel.../Grant (same-Org) or project.zitadel.../Grant + user.zitadel.../Grant with projectGrantId (cross-Org) |
✓ |
Single-resource wrappers we deliberately didn't make: HumanUser, IDP, OrganizationSsoConfig (and the previously-attempted Organization, Project). Operators apply raw Zitadel / OpenPanel MRs directly for those.
Declarative Zitadel machine identity for CI runners, Crossplane providers, cross-cluster syncs — anything that needs a long-lived credential to call a SaaS API.
The XRD's value-add is bundling. The MachineUser MR alone is one Zitadel resource. With spec.pat.enabled: true it adds an AccessToken MR (the PAT). With spec.pat.pushToAwsSm: true it adds an AWS Secrets Manager Secret MR + an ESO PushSecret Kubernetes Object — pushing the control-plane connection secret's access_token into AWS SM at the canonical path push/<cluster>/<tenant>/<name> per [[reference_aws_sm_push_tag_convention]]. Four resources, one declarative flag.
PAT generation is opt-in by default — pat.enabled: false means no long-lived token is minted. Adoption of an existing Zitadel machine user uses spec.machineUserId (propagates as crossplane.io/external-name on the underlying MR).
See examples/machineusers/{minimal,with-pat,with-pat-push}.yaml.
First-class membership relationship that ties a Zitadel User to a Project + Roles. Polymorphic dispatch — caller writes userId + userOrgId + projectId + projectOrgId + roles and the composition picks the right Zitadel mechanism:
- Same-Org (
userOrgId == projectOrgId): composes oneuser.zitadel.m.crossplane.io/GrantMR (the user's role assignment within the project). - Cross-Org (
userOrgId != projectOrgId): composes aproject.zitadel.m.crossplane.io/Grant(cross-Org Project Grant authorizing the role set for the user's home Org) plus auser.zitadel.m.crossplane.io/GrantwithprojectGrantIdset (the user's role assignment, pulling roles from the granted set). Multi-iter: user/Grant emits once project/Grant is observed.
See examples/grants/{same-org,cross-org}.yaml.
The intent is for consumer stacks (gitops/ArgoCD, observe/Grafana, the-website) to wire to AuthStack's status surface rather than configuring OIDC manually. Today, those consumers still need a Zitadel OIDC application created out-of-band (via the Zitadel UI/API) and a client ID/secret provided to them. Once the Zitadel Crossplane provider lands, consumer stacks can declaratively create OIDC applications by referencing status.bootstrap.iamAdminPatSecretRef.
See [[specs/auth-stack-zitadel]] for the design and open questions.
- Per-app OIDC client creation (lives with the Zitadel API or the future Zitadel Crossplane provider).
- Istio
RequestAuthentication/AuthorizationPolicy(per-app concern, may land later). - Authentik decommission (per-consumer migration tracked separately).
- Spec:
[[specs/auth-stack-zitadel]] - Task:
[[tasks/auth-stack]] - Upstream chart:
zitadel/zitadel9.34.1 (ships Zitadel v4)