Ports examples/harmony_sso/ onto current master, trimmed to the new bar: - main.rs: drop multi-paragraph docstrings, WHAT-narration, ASCII section dividers; factor the duplicated OpenbaoSetupScore literal into openbao_setup(jwt: Option<OpenbaoJwtAuth>); add SAFETY note on the second unsafe set_var block - README.md: drop doc-rotted --demo / --sso-demo sections (flags don't exist), drop duplicated HMAC/identity Notes section, tighten audit prose - harmony_sso_plan.md unchanged (Lessons Learned are real institutional knowledge not derivable from code) Same end-to-end runtime behaviour as the validated integration branch. PR 4 of 4 splitting feat/unified-config-and-secrets. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Harmony SSO Example
Deploys Zitadel (identity provider) and OpenBao (secrets management) on a local k3d cluster, then round-trips two harmony_config structs (Secret + Standard class) through OpenBao via SSO-authenticated access.
Prerequisites
- Docker running
- Ports 8080 and 8200 free
/etc/hostsentries (or a local DNS resolver):127.0.0.1 sso.harmony.local 127.0.0.1 bao.harmony.local
Usage
# Deploy everything (OpenBao + Zitadel) and round-trip the example configs
cargo run -p example-harmony-sso
# OpenBao only (faster, skip Zitadel)
cargo run -p example-harmony-sso -- --skip-zitadel
# Teardown
cargo run -p example-harmony-sso -- --cleanup
The example sets HARMONY_SECRET_STORE=file and HARMONY_SECRET_NAMESPACE=harmony-sso-example at startup so the global harmony_secret manager uses the local-file backend (~/.local/share/harmony/secrets/). Override either in your shell to change behaviour.
What gets deployed
| Component | Namespace | Access |
|---|---|---|
| OpenBao (standalone, file storage) | openbao |
http://bao.harmony.local:8200 |
| Zitadel (with CNPG PostgreSQL) | zitadel |
http://sso.harmony.local:8080 |
OpenBao configuration
- Auth methods: userpass, JWT
- Secrets engine: KV v2 at
secret/ - Policy:
harmony-devgrants CRUD onsecret/data/harmony/* - Userpass credentials:
harmony/harmony-dev-password - JWT auth: Zitadel as OIDC provider, role
harmony-developer - Unseal keys:
~/.local/share/harmony/openbao/unseal-keys.json - Audit device: file audit at
/openbao/audit/audit.log(see below)
Audit log: who changed what
OpenBao's KV v2 version history shows when a value changed and which version, but never who — the caller identity isn't in KV metadata. It lives in the audit log.
A file audit device is enabled at /openbao/audit/audit.log (auditStorage PVC), logging every request/response with the authenticated identity. SSO mints the OpenBao token from the Zitadel id_token with user_claim=email, so each operation is attributed to the user.
Applying audit config changes: the device loads at OpenBao startup, and the helm chart's StatefulSet uses updateStrategy: OnDelete (a Vault/OpenBao restart needs a manual unseal — auto-rolling is intentionally disabled). So helm upgrade updates the server config but does not recreate the pod; kubectl rollout restart reports success but does nothing here. Delete the pod yourself and re-run to unseal:
kubectl delete pod openbao-0 -n openbao
cargo run -p example-harmony-sso # unseals the fresh pod
Reading the audit needs the root token (the harmony-dev policy can't touch sys/audit):
ROOT=$(jq -r .root_token ~/.local/share/harmony/openbao/unseal-keys.json)
# Confirm device is on:
kubectl exec -n openbao openbao-0 -- sh -c "export VAULT_TOKEN=$ROOT && bao audit list"
# Path Type Description
# file/ file n/a
(bao audit list with no token returns 403 permission denied — that means "you didn't authenticate", not "audit is off".)
See who performed each operation. Paths and values are HMAC-hashed by default, so filter on the identity fields:
kubectl exec -n openbao openbao-0 -- cat /openbao/audit/audit.log | \
jq -c 'select(.type == "response" and .auth.display_name != null)
| {time, op: .request.operation,
who: .auth.display_name, role: .auth.metadata.role,
entity: .auth.entity_id}' | tail -n 15
SSO-authenticated operations are attributed to the user:
{"time":"...","op":"update","who":"jwt-admin@zitadel.sso.harmony.local","role":"harmony-developer","entity":"81ddf1d7-..."}
The jwt- prefix is the JWT auth-mount name; the rest is the user_claim=email value from the id_token. Setup/unseal steps run with the root token and show as "who":"root". For raw paths/values, tune the audit device (log_raw, hmac_accessor) — out of scope here.
Architecture
Developer CLI
|
|-- harmony_config::ConfigClient
| |-- EnvSource (HARMONY_CONFIG_* env vars)
| |-- StoreSource<OpenbaoSecretStore>
| |-- Token auth (OPENBAO_TOKEN)
| |-- Cached token validation
| |-- Zitadel OIDC device flow (RFC 8628)
| |-- Userpass fallback
|
v
k3d cluster (harmony-example)
|-- OpenBao (KV v2 secrets engine)
| |-- JWT auth -> validates Zitadel id_tokens
| |-- userpass auth -> dev credentials
|
|-- Zitadel (OpenID Connect IdP)
|-- Device authorization grant
|-- Federated login (Google, GitHub, Entra ID)