https://github.com/hops-ops/auth-stack
AuthStack — Crossplane Configuration wrapping Zitadel with durable+operational secrets pattern
https://github.com/hops-ops/auth-stack
Last synced: about 1 month ago
JSON representation
AuthStack — Crossplane Configuration wrapping Zitadel with durable+operational secrets pattern
- Host: GitHub
- URL: https://github.com/hops-ops/auth-stack
- Owner: hops-ops
- Created: 2026-05-14T01:07:25.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-05-22T22:03:43.000Z (2 months ago)
- Last Synced: 2026-05-23T00:07:43.492Z (2 months ago)
- Language: Go Template
- Size: 94.7 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 4
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# auth-stack
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](#auth-group-primitives) below.
## Quick Start
Local-dev (colima cluster, bundled Postgres, no TLS):
```yaml
apiVersion: hops.ops.com.ai/v1alpha1
kind: AuthStack
metadata:
name: auth
namespace: default
spec:
clusterName: colima
domain: auth.localtest.me
externalSecure: false
database:
bundled: true
```
Production (managed Postgres + Gateway API):
```yaml
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-system
```
## Database Modes
Exactly 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. |
## Bootstrap & Status Contract
The `zitadel/zitadel` chart's setup hook creates two machine users on first install:
- `` — JWT machine key in a chart-managed Secret named after the username
- `-pat` — admin PAT
- `` — 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:
```yaml
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 }
```
## Auth-group primitives
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.
### `MachineUser`
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///` 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`.
### `Grant`
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 one `user.zitadel.m.crossplane.io/Grant` MR (the user's role assignment within the project).
- **Cross-Org** (`userOrgId != projectOrgId`): composes a `project.zitadel.m.crossplane.io/Grant` (cross-Org Project Grant authorizing the role set for the user's home Org) plus a `user.zitadel.m.crossplane.io/Grant` with `projectGrantId` set (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`.
## Cross-Stack Integration
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.
## Out of Scope
- 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).
## References
- Spec: `[[specs/auth-stack-zitadel]]`
- Task: `[[tasks/auth-stack]]`
- Upstream chart: `zitadel/zitadel` 9.34.1 (ships Zitadel v4)