Commit Graph

15 Commits

Author SHA1 Message Date
471692c283 Automate demo-app AppRole SecretID rotation on a weekly timer
A SecretID cannot be renewed the way a token can -- it must be re-issued by a
privileged caller -- so renew-app-tokens.sh structurally could not cover it.
The original expired on ~2026-07-28 and broke app-get-secret.sh silently until
it was noticed on 2026-08-22. This closes the last credential in the stack
that had no automated lifecycle.

Weekly against a 30d secret_id_ttl gives 4x margin: three consecutive failed
rotations can occur before anything actually breaks, and the first failure
already mails an alert via OnFailure=openbao-alert@%n.service.

Order of operations is the safety property: mint -> verify the new SecretID
actually authenticates -> only then overwrite the file -> only then prune old
accessors. Any failure leaves the previous working credential in place and
exits non-zero. Verified by pointing BAO_ADDR at a dead port: exit 1, file
byte-identical, consumer unaffected. A SecretID that fails its verification
login is destroyed rather than installed.

Accessors are kept one cycle deep (KEEP=2: current + previous) so a consumer
that read the file just before rotation can still log in. Verified across
three consecutive runs: steady state stays at 2, oldest pruned each cycle.

The rotator skips its verification login when secret_id_num_uses would be
consumed by it, which is why the policy grants read on the role config. That
read exposes TTLs and bound policies but NOT the RoleID, which lives at the
separate .../role-id path and stays denied -- verified 403, along with 403 on
the demo secret itself and on other AppRoles.

Token at /etc/openbao-approle-rotate.token matches the /etc/openbao-*.token
glob, so the renew loop picks it up automatically (verified: renewed=10
failed=0) and it cannot lapse the way the backup tokens did.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFtVLA7VVqXL5G2S18c4Jk
2026-08-22 09:33:16 +02:00
ee0b8c818f Alert by email when an OpenBAO maintenance unit fails
A failed oneshot unit is silent by default, which is why the backup failures
went unnoticed for 24 days. All three maintenance units now carry
OnFailure=openbao-alert@%n.service, which mails the failure with systemctl
status and the last 40 journal lines.

Reuses the existing ClaudeAdmin notifier pattern: curl --ssl-reqd to
securesmtp.t-online.de:587, password read from KV secret/data/smtp/healthcheck
via a new root-owned periodic token /etc/openbao-alert-smtp.token bound to
smtp-healthcheck-ro. That path matches /etc/openbao-*.token, so the
token-renew loop picks it up automatically and it cannot lapse the way the
others did (verified: renewed=9 failed=0).

Two failure modes designed around:

- Circular dependency. The SMTP password lives in OpenBAO, but the likeliest
  cause of these units failing is OpenBAO being down or sealed -- so fetching
  the password would fail exactly when the alert matters most. The alert is
  therefore always written to /var/log/openbao-alerts.log (0600) BEFORE any
  network call, and successful fetches refresh a root-only 0600 credential
  cache used as fallback. Verified with BAO_ADDR pointed at a dead port: the
  email still went out, tagged as sent via cache.
- Recursion. The alert unit has no OnFailure of its own and always exits 0;
  a send failure is logged and syslogged rather than raised.

Uses %i, not %I: unit names contain '-', which systemd unescaping would turn
into '/' (openbao-backup.service -> openbao/backup.service).

Verified end to end with a throwaway failing unit (since removed): failure ->
OnFailure -> alert unit -> email delivered.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFtVLA7VVqXL5G2S18c4Jk
2026-08-22 09:22:05 +02:00
c5ff1a3fca Upgrade OpenBAO 2.5.5 -> 2.6.2
Picks up 6 security fixes across 2.6.0/2.6.2, notably GHSA-xp3c-3jw3-4vcr
(LIST bypassing an explicit deny from a wildcard grant).

Checked the 2.6.x breaking changes against this deployment before upgrading:

- Identity-template hardening (+ * / , now rejected in template values):
  no {{ }} templating in any policy, and every pki/ssh role has
  *_template = false. Unaffected.
- Container default user changed root -> openbao (uid 100): the raft volume
  at /openbao/file is already uid 100 and the config bind-mount is
  world-readable :ro, so no chown fixup was needed. Both containers now run
  as uid=100(openbao). Unaffected.
- Cloud KMS + pkcs11 seals become external plugins in 2.7.0: this uses the
  transit seal, which stays built in. Unaffected.
- physical/file deprecated in 2.7.0: storage is raft. Unaffected.
- Pre-v2.5.0 corrupt identity groups purged on unseal: cluster was created
  on 2.5.5, and the `family` group survived. Unaffected.
- Arch-specific tags (2.6.x-arm64) no longer published: compose uses the
  plain multi-arch tag. Unaffected.

Upgrade order was unsealer first (recreate, manually unseal 1-of-1 shamir,
confirm the autounseal transit key is readable), then main, which
auto-unsealed via transit. Cluster IDs preserved on both.

Verified after: all 13 mounts, 4 auth methods and 14 policies intact; KV
read, PKI issue, SSH CA sign and transit encrypt all work through their
scoped tokens; backup token still 403s outside its scope; raft snapshots
succeed on 2.6.2; HTTPS via Traefik serves 2.6.2 with a trusted chain.

Note: sys/seal-status now reports `commit_date` instead of `build_date`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFtVLA7VVqXL5G2S18c4Jk
2026-08-22 09:12:14 +02:00
123e65ee46 Renew root-owned /etc infra tokens too (backups were dead 24 days)
renew-app-tokens.sh only walked ~/.config/openbao/*.token, so the three
root-owned tokens were never renewed and lapsed on 2026-07-29:

  /etc/openbao-backup.token           -> nightly raft snapshots failed (403)
  /etc/openbao-unsealer-backup.token  -> same, unsealer instance
  /etc/openbao-cert-renew.token       -> would have failed silently at <21d

Nightly backups had been failing for 24 days before this was noticed; the
last good snapshot was 2026-07-28. All three tokens have been re-issued as
periodic (30d) and the script now covers both sets.

The unsealer's token belongs to a separate instance with no published port,
so it renews via `docker compose exec` rather than curl -- renewing it
against main returns 403. Note `bao token renew` takes no -self flag; the
bare form is the renew-self call.

Service now runs as root to read /etc, and chowns the log back to lutz.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NFtVLA7VVqXL5G2S18c4Jk
2026-08-22 09:12:14 +02:00
f2919cf50a Add daily app-token renewal (script + systemd timer)
Periodic scoped tokens (~/.config/openbao/*.token) expired because nothing
renewed them within their period. renew-app-tokens.sh renews all of them via
auth/token/renew-self; openbao-token-renew.timer runs it daily (Persistent=true).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 10:20:12 +02:00
63f864b68b gitignore: cover *-credentials.txt (operator login creds)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 10:04:13 +02:00
f12034bab9 apply-policies: fix header always showing [dry-run]
${DRY:+...} expanded on DRY=0 (non-empty). Gate the label on DRY=1 instead;
the apply logic was already correct.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 10:03:12 +02:00
700e4ce15a Add smtp-healthcheck-ro policy (scoped read for ClaudeAdmin SMTP creds)
Read-only access to secret/data/smtp/healthcheck, consumed by the ClaudeAdmin
healthcheck email notifier via a periodic token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-01 10:00:07 +02:00
af31208251 Add prepare-host.sh: one-command SSH host onboarding
Generalize the .26 rollout into a reusable template:
- authorize the host principal on ssh/roles/host (allowlist, merged)
- sign the host certificate (ssh/sign/host)
- emit a self-contained installer (user-CA TrustedUserCAKeys + HostCertificate)
  to artifacts/, additive and lockout-safe
- add @cert-authority for the host to the client's known_hosts
Needs no SSH access to the target (ssh-keyscan). README updated.

Also gitignore .claude/ (local harness settings).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 10:53:32 +02:00
5e5e7f779f Add governance blueprint: policy-as-code, operator/auditor, identity setup
Reviewable governance scaffolding — NOTHING applied to live OpenBAO yet:

- policies/: materialize all existing policies as code (faithfully fetched
  from live) + new `operator` (use engines, no admin) and `auditor`
  (read-only governance visibility, no secret material)
- scripts/apply-policies.sh: idempotent policy-as-code apply, with a
  read-only --dry-run that diffs files vs live (ignores comments)
- scripts/setup-identity.sh: identity-as-code — policy-bound groups
  (g-admins/operators/auditors/personal) + a human entity/alias; DEFAULT
  DRY-RUN, --apply to execute
- GOVERNANCE.md: the layered model, policy catalog, naming, apply order,
  and cross-cutting controls (audit device, root offline, AppRole migration)

Dry-runs verified read-only: apply-policies shows operator/auditor as NEW,
all others unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 21:05:38 +02:00
21929fa899 Add personal-backup playground (KV + transit) and AppRole demo
Two isolated personal-credential backup options to evaluate, both excluded
from the admin policy (root-only break-glass):
- Scenario A: personal/ KV v2 (versioned) + personal-{put,get,list}.sh
- Scenario B: transit-personal/ key personal-backup + secret-{encrypt,decrypt}.sh

Plus an AppRole example of how a system should consume a secret:
- demo-app role/policy (read-only secret/demo-app/*), short-lived tokens
- scripts/app-get-secret.sh: login (role_id+secret_id) -> token -> read

All tokens/credentials live under ~/.config/openbao (outside the repo).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 20:28:32 +02:00
5a111b7a20 Add SSH CA integration: signed user + host certs, ssh-login helper
Turn the ssh/ engine into an SSH CA for cert-based access:

- ssh/ roles: "user" (8h user certs, principal-restricted) and "host"
  (long-lived host certs); mount max-lease-ttl raised for host certs
- scripts/ssh-login.sh: sign a fresh user cert via a scoped ssh/sign/user
  token (API, no bao binary) and connect — no authorized_keys on targets
- ca/openbao-ssh-ca.pub: the SSH CA public key (for TrustedUserCAKeys and
  client @cert-authority trust)
- README: usage, host onboarding, client trust
- gitignore generated per-host artifacts/

First host wired + verified end-to-end: 192.168.0.26 (pifour) — lutz cert
login and host-cert verification both confirmed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:55:01 +02:00
37a97cb0f4 Add OpenBAO-backed git credential helper for gitea pushes
Store the gitea PAT in OpenBAO KV (secret/gitea/push) and fetch it via a
git credential helper instead of keeping it in ~/.git-credentials:

- scripts/git-credential-openbao.sh: helper that reads the cred from the
  OpenBAO API using a scoped, read-only periodic token
- scripts/store-gitea-cred.sh: one-time hidden-input store of the PAT
- README: usage + rotation notes

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:37:39 +02:00
14f46e1082 Add unsealer, internal CA/TLS, auto-unseal, and automated cert+backup
Build out the home-lab OpenBAO deployment beyond the basic node:

- docker-compose: add openbao-unsealer sidecar; main node now transit
  auto-unseals against it (seal config kept in gitignored config/seal.hcl)
- policies/admin.hcl: non-root admin policy; per-engine rules for
  ssh/pki/pki_int/totp/transit
- Internal two-tier CA (pki/ root + pki_int/ intermediate) issues the
  openbao.famfi.home leaf Traefik serves; root CA published under ca/
- scripts/ + systemd/: daily cert renewal and Raft snapshot backups
  (both instances), with scoped tokens stored outside the repo
- README: full runbook (auto-unseal, PKI, renewal, backups, DR/restore)

Secrets (init/unsealer keys, tokens, seal stanza) stay gitignored.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-28 14:28:49 +02:00
95e1b676e2 Initial Commit 2026-06-28 13:41:17 +02:00