Files
universe/infra/k8s/iam/provision-job.yaml
zeekayandHanzo Dev d86f10847a feat(iam): Zoo's own app graph — 5 clients, one issuer, proven end to end
Zoo had exactly one working OAuth client (zoo-cloud) and no declaration of it
anywhere. This ports the Lux shape into this repo: the brand-neutral mechanism
stays in hanzoai/iam (internal/provision), Zoo owns its DATA here, and
`make iam-provision` is the only supported way that data reaches the IdP.

ISSUER. id.zoo.network serves iss=https://zoolabs.id — host and issuer really do
disagree, and it is resolved in favour of zoolabs.id: IAM_ISSUER_MAP already
maps all three Zoo hosts to it, discovery agrees from either host, every sibling
brand's issuer is its <brand>.id apex (hanzo.id / lux.id / pars.id) with
id.<brand>.network as the alias, and zoo.id has no DNS at all so it cannot be
the issuer. The remaining defect is the DISCOVERY DOCUMENT (OIDC Discovery §4.3
wants issuer == fetch prefix), which is a frontend/ingress fix, not a data one.
Reasoning is in the file header, not just here.

Five clients, each declared only on evidence measured against the live surface:
zoo-cloud, zoo-wallet, zoo-dao, zoo-explore, zoo-id. Hosts that carry no OAuth
wiring, hosts served by shared Hanzo backends, and hosts with no DNS are listed
too, so each omission is a recorded decision.

Two defects fixed rather than described:

  cert. The upsert copies req.Cert verbatim on CREATE, so an omitted cert means
  an EMPTY cert, and a client with no cert cannot mint an id_token — the login
  dies AFTER a successful code exchange. Four Zoo clients were in exactly that
  state. Every app now names cert-zoo (convention cert-<org>: lux-cloud has
  cert-lux, hanzo-cloud has cert-hanzo); for zoo-cloud it is a no-op.

  cloud.zoo.network is no longer declared. It has an ingress but NO DNS record
  at 1.1.1.1 or 8.8.8.8, so no browser can reach it; a redirect URI for a host
  that cannot exist is the drift this document exists to prevent. Lux keeps
  cloud.lux.network because that host does resolve.

Image pin moved forward to sha-f9813f6: sha-4bec5ef predates the `cert` field
and the strict parser rejects the document outright. f9813f6 is a strict
descendant of the public-client fixes and of 4b0f4e93, and is the newest sha-
tag in GHCR that resolves to a known commit.

PROVEN, not asserted:
  - converge run twice; the five records are byte-identical apart from
    updatedAt, and no client secret was issued (all five stay public/PKCE)
  - real login as [email protected] -> authorization code -> PKCE token exchange with
    NO client secret, on all five clients
  - id_token minted for each: kid=cert-zoo, iss=https://zoolabs.id,
    aud=<the client>, and the signature verifies against the published JWKS
  - the dropped redirect now correctly 400s: invalid redirect_uri

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-26 19:02:15 -07:00

113 lines
5.1 KiB
YAML

# Applies Zoo's IAM app graph (provision.yaml) to the live IdP.
#
# ONE source of truth: the ConfigMap this Job reads is generated FROM
# provision.yaml by `make iam-provision`, never hand-written, so the document
# in git is always what converged.
#
# It runs the OSS mechanism (`iam2 provision`, hanzoai/iam internal/provision)
# against Zoo's data. The image is pinned — a Job that floats on :latest is not
# a deterministic deployment.
#
# WHY THIS TAG. Two requirements, both measured, not assumed:
# * `sha-bb7eff5` is the FLOOR: that commit ("feat(bootstrap): let a public
# (PKCE) client be registered at all") is where the `public` field appears.
# Anything older cannot register a browser SPA at all.
# * `sha-7badd9d` ("fix(bootstrap): a public client must STAY public across
# reconciles") is why the floor is NOT good enough. Before it, an upsert
# that merely omitted `public` minted a fresh secret and silently turned a
# working public client confidential again — every browser login then died
# on `invalid_client` with nothing in this document changed to explain it.
# Zoo has exactly one login that works today (zoo.cloud); converging with a
# pre-fix image is how you lose it.
# * `sha-4bec5ef` is not good enough EITHER, and that is measured, not
# guessed: this document names a signing cert per app, `iam2 provision`
# parses strictly, and 4bec5ef predates the `cert` field —
# `provision: parse: [126:36] unknown field "cert"`, exit 1, nothing
# written. The field arrives in 4b0f4e93 "feat(provision): apps can name a
# signing cert and their own callback".
# sha-f9813f6 is the pin. It is a strict descendant of all three, verified:
# git merge-base --is-ancestor bb7eff5 f9813f6e0 -> yes
# git merge-base --is-ancestor 7badd9d f9813f6e0 -> yes
# git merge-base --is-ancestor 4b0f4e93 f9813f6e0 -> yes
# and it is the NEWEST sha- tag in ghcr.io/hanzoai/iam that resolves to a known
# commit at all. None of the 82 sha- tags is reachable from hanzoai/iam
# origin/main today — that history was rewritten, and the public-client fixes
# now sit on main as 483aadb7 and 74747364 — so "pin the newest main sha" has
# nothing to pin; the newest tag that both EXISTS and provably carries every
# feature this document uses is the honest maximum.
#
# The tag is sha-<7>, not v<X.Y.Z>, because sha- is commit-addressable: it names
# the exact source that produced the artifact. The v1.33.x line does publish
# (v1.33.16 is what deploy hanzo/iam runs; v1.33.17 is the newest in GHCR) but
# it lags main and none of those tags exist in the git remote, so a v-pin here
# cannot be traced back to a commit.
#
# Checking the tag yourself: GHCR answers 404 for a manifest unless the request
# sends a matching Accept. Ask for the OCI/Docker manifest and index types
# explicitly — `Accept: */*` returns 404 for a tag that is plainly there, which
# reads as "deleted" and is not.
#
# Runs in ns hanzo on hanzo-k8s because that is where the IAM that answers
# zoolabs.id lives, and where the service token it authenticates with is stored.
# Re-running is a no-op: the upsert keys on <org>-<app> and preserves each
# client secret IAM already issued.
apiVersion: batch/v1
kind: Job
metadata:
name: zoo-iam-provision
namespace: hanzo
labels:
app.kubernetes.io/name: zoo-iam-provision
app.kubernetes.io/part-of: zoo
spec:
# Keep the pod briefly so `kubectl logs job/zoo-iam-provision` shows what
# converged; the run report IS the audit record.
ttlSecondsAfterFinished: 600
backoffLimit: 2
template:
metadata:
labels:
app.kubernetes.io/name: zoo-iam-provision
spec:
restartPolicy: Never
imagePullSecrets:
- name: ghcr-secret
containers:
- name: provision
image: ghcr.io/hanzoai/iam:sha-f9813f6
args:
- provision
- --config=/etc/iam-provision/provision.yaml
# The IAM SERVICE, not a public hostname. A public host resolves
# through DNS/CDN and may reach a different instance than the one
# the token endpoint reads — writing Zoo's clients to a store the
# login never consults is silent and total: every converge reports
# success while every login keeps failing.
- --url=http://iam.hanzo.svc
env:
# The same unified service token the admin upsert endpoints
# validate. Never inlined — it is a credential.
- name: IAM_SERVICE_TOKEN
valueFrom:
secretKeyRef:
name: iam-service-token
key: IAM_SERVICE_TOKEN
volumeMounts:
- name: doc
mountPath: /etc/iam-provision
readOnly: true
resources:
requests: { cpu: 10m, memory: 32Mi }
limits: { cpu: 200m, memory: 128Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 65532
capabilities:
drop: ["ALL"]
volumes:
- name: doc
configMap:
name: zoo-iam-provision