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]>
113 lines
5.1 KiB
YAML
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
|