Skip to content

Releases: hops-ops/auth-stack

v1.3.0

21 May 04:46

Choose a tag to compare

What's changed in v1.3.0

  • feat: add Grant primitive XRD with polymorphic same-Org / cross-Org dispatch (#9) (by @patrickleet)

    Second auth-group primitive that survives the "is it just a single-MR
    wrapper?" test. Composes 1 or 2 underlying Zitadel MRs depending on
    whether the user's home Org matches the project's enclosing Org.

    Same-Org (spec.userOrgId == spec.projectOrgId): one MR

    • user.zitadel.m.crossplane.io/Grant — the user's role assignment
      within the project (no projectGrantId)

    Cross-Org (spec.userOrgId != spec.projectOrgId): two MRs

    • project.zitadel.m.crossplane.io/Grant — the cross-Org Project
      Grant authorizing the role set for the user's home Org
    • user.zitadel.m.crossplane.io/Grant — the user's role assignment
      with projectGrantId pointing at the Project Grant, pulling roles
      from the granted set

    Multi-iter convergence: in cross-Org mode the user/Grant emits only
    once the project/Grant's atProvider.id is observed (standard composition
    gating per feedback_crossplane_composition_gates).

    Schema takes flat IDs (userId + userOrgId + projectId + projectOrgId +
    roles[]), matching the established convention from Tenant + MachineUser
    — operator copies UUIDs from the relevant XR statuses or Zitadel UI.

    Two examples (same-org + cross-org); 4 KCL CompositionTests covering
    same-org, cross-org iter 1, multi-role, managementPolicies propagation.
    Multi-iter convergence verified via observed-resources fixture.

    21/21 KCL tests pass (13 AuthStack + 4 MachineUser + 4 Grant); all 8
    examples render via make render:all.

    Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com

See full diff: v1.2.0...v1.3.0

v1.2.0

21 May 04:46

Choose a tag to compare

What's changed in v1.2.0

  • feat: add MachineUser primitive XRD with opt-in PAT + AWS SM push pipeline (#7) (by @patrickleet)

    The first auth-group primitive that survives the "is it just a single-MR
    wrapper?" test. Composes 1-4 underlying resources depending on opt-ins:

    1. Always: Zitadel MachineUser MR (machineusers.user.zitadel.m.crossplane.io)
    2. When spec.pat.enabled (default false): + Zitadel AccessToken MR (the PAT;
      connection secret on control-plane K8s under key access_token)
    3. When spec.pat.pushToAwsSm (default false; requires pat.enabled): + AWS
      Secrets Manager Secret MR + ESO PushSecret (via provider-kubernetes
      Object) that pushes the control-plane K8s Secret's access_token into
      AWS SM at the canonical path push/// per
      reference_aws_sm_push_tag_convention. Consumer clusters pull via ESO
      ExternalSecret.

    Composition gating per feedback_crossplane_composition_gates: AccessToken
    emits once MachineUser.atProvider.id observed; AWS SM Secret + PushSecret
    emit once AccessToken.atProvider.id observed. Standard multi-iteration
    Crossplane convergence.

    PAT generation is opt-in by default — operators explicitly acknowledge
    minting a long-lived bearer token. Adoption via spec.machineUserId
    propagates as crossplane.io/external-name on the underlying MR.

    upbound.yaml gains provider-upjet-zitadel >=v0.1.1 and
    provider-aws-secretsmanager >=v2.5.0 deps (both required by the
    composition; aws-secretsmanager only matters when pushToAwsSm is on).

    3 examples (minimal, with-pat, with-pat-push) — all render via
    up composition render. Multi-iter convergence verified via
    --observed-resources fixtures.

    4 KCL CompositionTests covering: minimal-machineuser-only, pat-enabled-
    iter1-still-only-machineuser (gating verification), adopt-existing-
    machineuser-via-id, field-overrides. All 17 tests (13 AuthStack + 4
    MachineUser) pass under up test run.

    Makefile EXAMPLES + CI workflows updated for the new examples.
    README "Auth-group primitives" table updated: MachineUser ✓, Grant
    TO WRITE (next); HumanUser / IDP / OrganizationSsoConfig dropped as
    thin wrappers — operators apply raw provider MRs.

    Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com

See full diff: v1.1.0...v1.2.0

v1.1.0

20 May 18:04

Choose a tag to compare

What's changed in v1.1.0

  • feat(deps): update crossplane-contrib/function-auto-ready docker tag to v0.6.4 (#1) (by @renovate[bot])

    Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>

See full diff: v1.0.0...v1.1.0

v1.0.0

20 May 01:25

Choose a tag to compare

What's changed in v1.0.0

  • initial: auth-stack with durable+operational secrets pattern (by @patrickleet)

    AuthStack XRD wraps the Zitadel Helm chart with a typed XRD surface.
    Implements [[specs/authstack-reconciler]] — durable values (masterkey,
    admin-password) project from AWS SM via ExternalSecrets, operational
    PATs (iam-admin, login-client) re-mintable via Zitadel API by an
    in-cluster reconciler CronJob.

    Composition pieces:

    • ExternalSecret + PushSecret pairs for the two operational PATs
    • ExternalSecret for masterkey + admin-password
    • Reconciler CronJob + namespaced RBAC
    • Chart values disable FirstInstance PAT sidecars when reconciler is on
    • Zitadel init wired with Admin.ExistingDatabase pinned to the configured
      database so VerifyDatabase short-circuits (CNPG app user lacks CREATEDB)
  • fix(state-init): pin reconciler image default to v0.0.1 (by @patrickleet)

    The hops-ops/authstack-reconciler repo uses vnext-tag for releases and
    doesn't publish a :latest tag. Pin to the first published version;
    bump in lockstep with new reconciler releases.

  • fix(state-init): bump reconciler image default to v0.0.3 (multi-arch) (by @patrickleet)

  • feat(authstack): add iamAdminMachineKey durable secret (by @patrickleet)

    The reconciler's primary credential is the iam-admin JWT machine key —
    not a PAT. The chart's setup-job's MachineKey sidecar writes the K8s
    Secret `iam-admin` on first install, but it's purely transient — lost
    on AuthStack delete, never recreated on subsequent installs (Zitadel
    detects existing instance data and skips FirstInstance).

    This adds the missing piece of the durability pattern from
    [[specs/authstack-reconciler]]:

    • XRD: new `spec.externalSecrets.iamAdminMachineKey.secretPath`.
    • State-init: surface as `state.externalSecrets.iamAdminMachineKey`
      with K8s Secret name fixed at `iam-admin` (matches chart).
    • 154-external-secret-iam-admin-key: project from AWS SM to K8s.
    • 162-push-secret-iam-admin-key: capture K8s to AWS SM on first
      install.
    • 200-helm-release-zitadel: set FirstInstance.Org.Machine.MachineKey
      (ExpirationDate + Type=1) so the chart actually GENERATES the
      machine key on first install. Previously we only set Pat.
    • 250-reconciler-cronjob: RBAC + env wire-up so the reconciler reads
      `iam-admin` as MACHINE_KEY_SECRET.

    Lifecycle (matches the PATs):
    First install → chart writes K8s → PushSecret → AWS SM populated
    AuthStack delete → AWS SM keeps it
    Re-install → ESO projects back → reconciler authenticates

  • feat(authstack): embed PSQLCluster; drop reconciler (by @patrickleet)

    Replaces the prior reconciler + capture-restore design with an embedded
    PSQLCluster pattern. AuthStack composes a PSQLCluster XR (XR-composing-
    XR) with targetNamespace matching the install ns, so CNPG resources land
    where Zitadel pods env-var-mount the app Secret natively. Delete cascades
    atomically (DB + ns + Secrets); reapply runs FirstInstance from scratch
    against a fresh DB, so the reconciler's re-mint path is no longer needed.

    XRD changes:

    • Add spec.database.embedded mirroring PSQLCluster's surface (storage,
      postgresql, app, ha, monitoring, sslMode, cnpg passthrough). Defaults
      to a 2Gi single-node Postgres. Mutually exclusive with psqlClusterRef
      and external.
    • Remove spec.reconciler subschema.
    • Remove spec.externalSecrets.{iamAdminMachineKey,iamAdminPat,
      loginClientPat} subschemas. ExternalSecrets retained only for the
      durable inputs: masterkey + admin-password.

    Composition changes:

    • New 120-embedded-psql-cluster.yaml.gotmpl renders the composed
      PSQLCluster XR with name -pg.
    • 100-namespace.yaml.gotmpl: restore full * ns ownership.
    • 200-helm-release-zitadel.yaml.gotmpl: unify DB wiring across embedded
      and psqlClusterRef modes; chart FirstInstance sidecars left enabled
      (they own the operational Secret lifecycle).
    • Delete reconciler CronJob + RBAC template (250-) and three
      iam-admin*/login-client ExternalSecret + matching PushSecret templates
      (152-, 153-, 154-, 160-, 161-, 162-).

    Tests rewritten: dropped reconciler-pattern tests, added
    embedded-composes-psql-cluster-xr test.

    Verified end-to-end on aws/hops/pat-local: cold start reaches Ready in
    ~90s; AuthStack delete + reapply cascades cleanly and re-bootstraps in
    ~90s.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

  • feat(authstack): collapse externalSecrets to a single secretPath (by @patrickleet)

    BREAKING CHANGE: Both durable values (masterkey + admin-password) now live as JSON
    properties under one AWS SM secret instead of two. The XRD's
    externalSecrets.{masterkey,adminPassword}.secretPath pair is
    replaced by a single externalSecrets.secretPath; both ExternalSecret
    renders reference that one path with different property: selectors:

    externalSecrets:
    enabled: true
    secretPath: /zitadel # AWS SM blob
    # { "masterkey": "...",
    # "admin-password": "..." }

    The collapse drops the redundant directory-name=property-name shape
    in the SOPS plaintext layout (masterkey/masterkey,
    admin-password/password → flat files under one dir). Operationally
    the two values are seeded together by hops auth bootstrap and rotate
    together, so grouping them as one secret is the natural fit.

    BREAKING: existing manifests using
    externalSecrets.{masterkey,adminPassword}.secretPath need to migrate
    to externalSecrets.secretPath. Only consumer in-tree (pat-local) is
    updated in the hops parent repo.

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

  • fix(authstack): Usage forces Zitadel helm release to drain before PSQLCluster (by @patrickleet)

    The chart's helm uninstall pre-delete hooks (init/setup/cleanup jobs)
    need DB access. If Crossplane tears down the embedded PSQLCluster XR
    (and its composed CNPG Cluster + app Secret) in parallel with the Helm
    release, the chart's jobs hang in CreateContainerConfigError ("secret
    '-pg-app' not found") and helm uninstall stalls indefinitely
    — the release ends up stuck in pending-upgrade and provider-helm
    can't make progress.

    Add a Usage:
    by: Release/-zitadel
    of: PSQLCluster/-pg

    so the embedded PSQLCluster is gated on the Helm release's deletion
    completing first. Mirrors the existing delete-zitadel-before-namespace
    Usage (both protect their of: from the Helm release).

    Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

  • fix(authstack): render login.gateway.httpRoute when spec.gateway.enabled (by @patrickleet)

    Zitadel v4 chart ships a separate Next.js login UI Deployment+Service
    serving /ui/v2/login. Composition previously only enabled
    gateway.httpRoute (main API service); /ui/v2/login fell through to the
    main service which returned gRPC code:5 NotFound.

    Mirror $gw.parentRef + $hostnames into login.gateway.httpRoute alongside
    the existing gateway.httpRoute/grpcRoute wiring. Chart-default paths
    (/ui/v2/login) handle the route to the login UI service.

    Verified live on pat-local: pat-local-zitadel-login HTTPRoute renders
    with hostname auth.ops.com.ai; curl /ui/v2/login → HTTP/2 200 with the
    login UI; full / → /ui/console/ flow returns 200.

  • feat(authstack): publish iam-admin PAT to AWS SM via PushSecret (by @patrickleet)

    Replaces the in-composition Zitadel ProviderConfig + credentials Secret
    with an ESO PushSecret that publishes the chart-managed iam-admin PAT
    to AWS Secrets Manager. Downstream consumers (any cluster, any
    namespace, any GitOps repo) now author their own ExternalSecret +
    ProviderConfig referencing the AWS SM path — AuthStack no longer
    composes a PC.

    Why: in-composition PC creation hit three trade-offs stacked together —
    (1) provider-kubernetes SA RBAC has no permission on zitadel.m.crossplane.io
    PCs by default, (2) Crossplane v2's CustomToManagedResource conversion
    ignores metadata.name on directly-composed CRD resources and derives
    <xr-name>-<composition-resource-name>, breaking the user-tunable
    spec.providerConfig.name, (3) function-auto-ready can't synthesize a
    Ready condition on a PC CRD without an annotation hack. The
    publisher/consumer split sidesteps all three: AuthStack publishes once,
    consumers own their own PC lifecycle.

    XRD shape:

    • drop spec.providerConfig.{enabled, scope} (early-alpha field, no
      consumers in the wild yet)
    • add spec.externalSecrets.pushCredentials.{enabled, path, accessTokenProperty} — defaults to top-level
      externalSecrets.enabled; path defaults to
      push/<clusterName>/zitadel-credentials
    • reshape status.providerConfig to {awsSecretsManagerPath, accessTokenProperty} (consumer wiring contract)

    Composition:

    • new 300-credentials-pushsecret.yaml.gotmpl: composes a
      kubernetes.m.crossplane.io Object wrapping an
      external-secrets.io/v1alpha1 PushSecret on the target cluster
    • 11 AWS SM tags on every pushed secret for hops secrets list
      routing and programmatic discovery:
      hops.ops.com.ai/managed = true
      hops.ops.com.ai/secret = true
      hops.ops.com.ai/managed-by = pushsecret
      hops.ops.com.ai/cluster =
      hops.ops.com.ai/namespace =
      hops.ops.com.ai/kind = AuthStack
      hops.ops.com.ai/name =
      hops.ops.com.ai/apiVersion = hops.ops.com.ai/v1alpha1
      hops.ops.com.ai/<kind.lower> = (K8s-selector parity)

      managed-by = external-secrets (ESO auto-add)

    Consumer reference: `examples/consumer-providercon...

Read more