Skip to content

fix(openbao): issue the listener certificate from a CA and trust the CA - #145

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/879-openbao-ca-issuer
Nov 12, 2026
Merged

github-actions[bot] merged 2 commits into
mainfrom
fix/879-openbao-ca-issuer

Conversation

@znerol2

@znerol2 znerol2 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor

What this changes

OpenBao's listener certificate came from a self-signed issuer, so the certificate was its own trust anchor. Every reissue, on renewal or when the names change, changed what clients trusted. ESO went InvalidProviderConfig, and the in-pod bao CLI failed with "certificate signed by unknown authority", until the pods restarted and the stores were refreshed by hand.

The certificate now comes from a CA that lives with OpenBao in its namespace, and every client trusts the CA:

  • CA. A self-signed root openbao-ca (10 years, renewed a year early, rotationPolicy: Never so a renewed root keeps its key) and a CA Issuer that signs the openbao leaf. The leaf Certificate and Secret keep their names.
  • Clients. The ESO and object-storage stores take the CA's own certificate. Inside the pods, BAO_CACERT and leader_ca_cert_file point at ca.crt.
  • Issuance proof. After applying, the play waits for the Certificate's current generation, the CA Issuer's annotations on the Secret, and the leaf verifying against the current CA. A stale self-signed Secret is refused.
  • Listener reload. After each issuance, the cert-manager play waits until each running pod holds the new files, sends SIGHUP, and compares the certificate the pod serves, read over the pod's cluster address, with the new leaf. A renewal then takes effect without a restart, and an existing env migrates without stranding a pod.
  • Fail closed. The openbao role stops on an unreadable bao status. It no longer treats a failed read as "not initialized".
  • Missing CA. The store plays stop with a pointer to 320-cert-manager.yml, unless external trust is set explicitly (eso_openbao_external_trust, object_storage_eso_openbao_external_trust).

The role READMEs describe the new behaviour and variables. The operator procedure (migration, routine renewal, CA renewal) is a new umbrella runbook, docs/runbooks/openbao-listener-certificate.md, in a companion PR.

Tests

  • New playbook tests: test-openbao-cert-issuance.yml (migration, renewal, a server that ignores the signal, stale and old-generation issuances, late and missing projection), test-openbao-ca-bundle.yml (the real ESO and object-storage tasks, reading the caBundle each store gets), test-openbao-init-status.yml, and test-openbao-tls-secret-wait.yml. A new unit test covers the static wiring. All run in CI.
  • 598 unit tests pass, and all 15 Ansible test playbooks pass with CI's fakes. 14 mutations, each reverting one consumer or check, all fail a test.
  • An adversarial review ran two rounds. Round 1 found a migration deadlock, stale-issuance waits, a test gap and a renewal-docs gap, all fixed. Round 2's three findings were all in code that round 1's fixes had added, and all are fixed; per our stop rule, no third round ran. The live run below was the final check.

Live run (sandbox env, three Raft pods)

  1. 320-cert-manager.yml: CA created, leaf reissued and proven, each pod reloaded, and every pod's own CLI kept working. Until step 2, the ESO store was InvalidProviderConfig, as expected.
  2. vertical-orchestration/100-eso.yml: the store was Valid again and every ExternalSecret was synced.
  3. vertical-security/100-openbao.yml: new pod spec applied. Nothing ran init.
  4. vertical-security/103-openbao-restart-pod.yml for each pod, standbys first and the leader last, unsealed from the automation file: every pod on BAO_CACERT=.../ca.crt, unsealed, at the same Raft index.
  5. A forced leaf renewal, then 320-cert-manager.yml again: every pod served the renewed leaf (fingerprint compared over the pod address), with no restart. The CLI worked, the store stayed Valid without the ESO play, and a forced ExternalSecret sync succeeded.

Object storage is off on that env, so the object-storage play didn't run.

Known limit and follow-ups

OpenBao 2.5.2 has no tls_auto_reload, so a renewed leaf reaches the servers when 320-cert-manager.yml next runs, within the renewal window. dmfdeploy/dmfdeploy#902 upgrades OpenBao to 2.7.x, which reloads the certificate by itself. dmfdeploy/dmfdeploy#903 pins platform images by digest.

The companion dmf-env PR updates the TLS error text in bin/unseal-openbao.sh.

Closes dmfdeploy/dmfdeploy#879

OpenBao's listener certificate came from a self-signed issuer, so the
certificate was its own trust anchor. Every reissue changed what ESO, the
object-storage stores and the in-pod CLI trusted, and they failed TLS until
someone restarted the pods and refreshed the stores by hand.

Issue the certificate from a CA that lives with OpenBao in its namespace
(a self-signed root that keeps its key on renewal, and a CA Issuer backed by
it), and point every client at the CA:

- ESO and the object-storage stores take the CA's own certificate. A missing
  CA stops those plays with a pointer to the cert-manager play, unless
  external trust is set explicitly.
- The pods trust ca.crt (BAO_CACERT, leader_ca_cert_file).
- The issuance waits prove the new issuance: current generation, the CA
  Issuer's annotations, and the leaf verifying against the current CA.
- After each issuance, the cert-manager play reloads every running pod's
  listener (SIGHUP) and checks over the pod address that it serves the new
  leaf, so a renewal reaches the servers without a restart and an existing
  env migrates without a stranded pod.
- The openbao role stops on an unreadable status instead of treating it as
  not initialized.

The role READMEs describe the new behaviour and variables. The operator
procedure is the umbrella runbook docs/runbooks/openbao-listener-certificate.md.

Closes dmfdeploy/dmfdeploy#879

Signed-off-by: znerol2 <6800371+znerol2@users.noreply.github.com>
The playbook tests run one after another against fakes. With the OpenBao
certificate tests added, the job runs past its 15-minute limit and is
cancelled before it finishes. Allow 30 minutes.

Refs dmfdeploy/dmfdeploy#879

Signed-off-by: znerol2 <6800371+znerol2@users.noreply.github.com>

@lkirc lkirc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes: the migration/recovery instructions rely on a runbook that is unavailable at the linked URL.

  • docs/openbao-bootstrap-security-model.md:480 — The main-branch link currently returns 404; its companion dmfdeploy/dmfdeploy#904 is still open. Merge/publish that runbook before this PR, or include/link to a currently available complete procedure, so operators can safely perform the ordered migration and recovery.

@znerol2

znerol2 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Agreed: the runbook has to be on main first. dmfdeploy/dmfdeploy#904 now merges before this PR (its own review finding is fixed and it's off hold). Once it merges, the link at docs/openbao-bootstrap-security-model.md:480 resolves, and I'll re-request review here. No change to this head.

@znerol2

znerol2 commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

dmfdeploy/dmfdeploy#904 has merged, so the runbook link in this PR now resolves (HTTP 200 on main). The head is unchanged at fadf731, and all checks pass. Ready for another look.

@znerol2
znerol2 requested a review from lkirc November 12, 2026 20:43

@lkirc lkirc left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved. The companion runbook that prompted the earlier request now resolves on main, and the current head coherently moves clients to the CA trust anchor while proving issuance and listener reload. Required checks and the precise ntfy OK token are present.

@github-actions
github-actions Bot merged commit 774c294 into main Nov 12, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

OpenBao's self-signed leaf is its own trust anchor, so every renewal breaks ESO and the in-pod CLI

2 participants