All checks were successful
Run Check Script / check (pull_request) Successful in 2m37s
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>
114 lines
4.6 KiB
Markdown
114 lines
4.6 KiB
Markdown
# 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
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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`):
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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)
|
|
```
|