Files
harmony/examples/harmony_sso/README.md
Sylvain Tremblay 4eee06a1f2
All checks were successful
Run Check Script / check (pull_request) Successful in 2m37s
feat(example-harmony-sso): example aligned to AGENTS.md minimalism bar
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>
2026-05-29 15:44:27 -04:00

4.6 KiB

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/hosts entries (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-dev grants CRUD on secret/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)