Skip to content

Secrets: on the box, and in OpenBao

What must not be in the config repository (WireGuard keys, passwords and tokens for dynamic DNS, BGP, certificates, Wi-Fi, single sign-on, the backup passphrase) is a secret: a file under /var/lib/stronghold/secrets, readable by root only (internal/secrets). The config only names them.

Shared and the box's own

The two boxes of a pair share every secret but the box's own: the pairing key (ha/pair-key: it proves the peer, and is set on each box), the DNS update key Kea and Knot use on this box (ddns-tsig), the credentials for OpenBao (openbao/*), and the SSH key each box pushes its config to git with (git/*). The pair's standby takes the active node's shared secrets over the sync network (ha.md): its set is the pair's, as its config is, so a shared secret only on the standby goes, and an event names it. (When sharing was first widened, the git key wasn't on the box's own list yet: gw1's went that way during a rolling update, and it had to make a new one.) Until October 2026 only wireguard/* was shared, so a peer that took over had no BGP or dynamic DNS passwords, certificate tokens, Wi-Fi passwords or backup passphrase.

OpenBao

# stronghold.yaml
secrets:
  openbao:
    address: https://bao.example.com:8200
    mount: secret              # a KV version 2 engine (default "secret")
    path: stronghold/home      # where under it (default "stronghold"); a pair's two boxes the same
    role-id: 7a9d0e41-...      # log in with this AppRole; empty: with a token
    namespace: ""              # Vault Enterprise / OpenBao namespaces
    ca-certificate: |          # optional: the CA its certificate is checked against
      -----BEGIN CERTIFICATE-----

The AppRole's secret ID (or the token) is the box's own secret, set on each box: stronghold secrets secret-id (or token), POST /v1/secrets/credentials, the System tab's Secrets card. Not config, so a pair's non-primary box sets its own too. OpenBao (MPL-2.0) is in Fedora; HashiCorp Vault itself is BSL and isn't, but speaks the same API.

  • Each shared secret is <mount>/data/<path>/<name>, its value in the field value (wireguard/wg0.key, bgp/isp, ...). A policy for the box needs create, read, update and delete on <mount>/data/<path>/*, and list, read and delete on <mount>/metadata/<path>/* and list on <mount>/metadata/<path>.
  • The files stay the working copy. A gateway must boot and route with its secret server down, so the box always reads its files; OpenBao is what they are kept in line with:
  • Writes go to OpenBao first: a new or changed secret is written there, then to the file; if OpenBao doesn't take it, nothing changes and the change is refused (a value only on the box would be overwritten by OpenBao's at the next sync).
  • Every minute (and on stronghold secrets sync) the box syncs: what OpenBao holds is taken (it is the truth: set or rotate a secret there, and the boxes take it); what was there at the last sync and is gone was deleted there, so it is removed here; what OpenBao never had (secrets made before it was set up, or a pair's) is given to it. When a sync changed secrets, the config is applied again, since they are in generated files (WireGuard's keys, FRR's config).
  • When OpenBao is away the box works on with its files; the sync says why it failed (stronghold secrets, GET /v1/secrets), and catches up when it is back.
  • A pair keeps its secrets at the same path: both boxes sync with it, and the pair's own secret sync goes on as well (they agree).
  • Logging in: an AppRole (auth/approle/login, again when its token expires) or a token.

  • Reverting a change (commit-with-confirm) reverts the config, not the secrets set with it: they are stored before the apply, as they always were.

In the lab

OpenBao 2.6 (Fedora's) on stronghold-ipa, https://ipa1.ipa.lab.home.arpa:8200 (its certificate from FreeIPA's CA by certmonger; raft storage), KV v2 at secret, an AppRole stronghold whose policy covers secret/data/stronghold/* and its metadata only, a secret ID for each of gw1 and gw2. The pair at stronghold/lab-pair:

  • gw1 gave it its three shared secrets (the VPN's keys and the tailnet's auth key); gw2 found them there. Nothing of the boxes' own (pairing key, DNS update key, git key, the secret IDs) went.
  • A secret made in OpenBao was on both boxes within seconds; changed there, both took the new value; deleted there, both removed it within a minute.
  • OpenBao stopped: the boxes went on routing, stronghold secrets said why the sync failed, and a change to a shared secret was refused with nothing written. Started and unsealed: both synced at once, writes went through again.

Not yet

  • OpenBao on the box itself, as a function in its container: for a box that is the secret server of others. Its own secrets gain nothing from it (the files are as safe).
  • SPIFFE/SPIRE for workload identity: SPIRE isn't in Fedora. Packaging it (Go, Apache-2.0, many vendored modules) comes first.
  • PKI from the same OpenBao for the certificates (certificates.md has its PKI issuer already, with its own token).