Skip to content

Settle what a cloud apply would tell us, without performing one - #48

Merged
sethbergman merged 3 commits into
mainfrom
fix/leader-tls-servername-and-preflight
Aug 28, 2026
Merged

Settle what a cloud apply would tell us, without performing one#48
sethbergman merged 3 commits into
mainfrom
fix/leader-tls-servername-and-preflight

Conversation

@sethbergman

Copy link
Copy Markdown
Owner

You asked me to be sure there was nothing left to figure out before
spending. There was. Nobody could have joined the cluster.

The bug

retry_join's leader_tls_servername is the one name a follower
verifies the leader against — whichever node holds leadership — so every
node's certificate has to carry it.

Both cloud templates ask for vault.<cluster>.internal
The PKI role issues CN <host>.vault.internal, SANs <host>, localhost

vault.<cluster>.internal appears in exactly two files in the repository —
the two cloud-init templates — and nothing issues a certificate for it.
Every Raft join would have failed TLS verification. The symptom is the one
this repo keeps producing: every node healthy, alone, cluster never
forms.

Fixed by deriving vault_pki_cluster_servername from the cluster name and
including it in every certificate's SANs.

The part that matters more

Finding that by reading was luck. tests/preflight-static is the part
meant to outlast it — 22 assertions, no credentials, nothing created,
seconds to run:

  • every value a template reads is one Terraform passes, and vice versa
  • the rendered cloud-init parses and is shellchecked — CI's shellcheck
    step covers scripts/ and the harnesses, so the two scripts that boot
    every node were linted nowhere
  • every auto_join key is one go-discover accepts, in a combination it
    accepts, against key sets recorded from the provider sources; and the tag
    AWS filters on is one compute.tf actually sets
  • the name the templates verify the leader against is a name the PKI role
    issues, and the CA path they read is where Ansible writes it

Mutation-checked against both historical bugs: reverting the SAN fix
turns one assertion red; reintroducing Azure's tag_name/resource_group
mix turns another red. Neither is visible to terraform validate,
terraform test against mocked providers, or any other suite here.

Also checked, and clean

Not everything I suspected was real, and the negatives are worth recording:

  • AWS auto_join keys verified against go-discover's aws_discover.go
    region/tag_key/tag_value are correct, and the unset addr_type
    defaults to private_v4, which matches private subnets
  • leader_ca_cert_file agrees with vault_tls_dir once resolved
  • Every template variable is supplied and read on both profiles

Scope

This settles configuration, not behaviour. Neither profile has been
applied and this does not change that. What it changes is that the apply,
when you pay for it, is spent on questions only a real API can answer —
not on a TLS name that was never going to work.

🤖 Generated with Claude Code

sethbergman and others added 3 commits August 28, 2026 05:21
Nobody could have joined the cluster.

retry_join's leader_tls_servername is the one name a follower verifies the
leader against, whichever node holds leadership, so every node's
certificate has to carry it. Both cloud templates asked for
vault.<cluster>.internal. The PKI role issued <host>.vault.internal with
SANs <host>,localhost. That name appeared in exactly two files in the
repository and nothing issued a certificate for it, so every join would
have failed TLS verification -- and the symptom is a cluster that never
forms while each node reports healthy on its own.

The PKI role now derives vault_pki_cluster_servername from the cluster
name and includes it in every certificate's SANs.

Finding it by reading was luck. tests/preflight-static is the part meant
to outlast that: it checks the agreements no single layer can see, and it
needs no credentials and creates nothing.

  - every value a template reads is one Terraform passes, and every value
    Terraform passes is one a template reads
  - the rendered cloud-init parses, and is shellchecked -- CI's shellcheck
    step covers scripts/ and the harnesses, so these two scripts, which
    boot every node, were linted nowhere
  - every auto_join key is one go-discover accepts, in a combination it
    accepts, against key sets recorded from the provider sources; and the
    tag AWS filters on is one compute.tf actually sets
  - the name the templates verify the leader against is a name the PKI
    role issues, and the CA path they read is where Ansible writes it

Both historical bugs are caught by it: reverting the SAN fix turns one
assertion red, and reintroducing Azure's tag/scale-set mix turns another
red. Neither was visible to terraform validate, terraform test against
mocked providers, or any other suite here.

This settles configuration, not behaviour. Neither profile has been
applied and this does not change that -- it means the apply, when it
happens, is spent on questions only a real API can answer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tive

The new suite failed the lint job it was written to extend. A comment
line beginning with the word shellcheck is parsed as a directive, not as
prose, so "# shellcheck step covers scripts/..." became SC1072/SC1073:
expected '=' after the directive key.

Reworded so the word does not open a line, and the reason is recorded in
place -- it is not obvious, and the next person writing a comment about
linting will hit it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first version of this fix would have been worse than the bug.

It gave every certificate a SAN of vault.<cluster>.internal, but
bootstrap-pki.sh sets the role's allowed_domains to vault_pki_domain with
allow_subdomains, and vault.vault-reference.internal is not under
vault.internal. Vault would have refused every issuance -- so instead of
failing only the Raft joins, nothing would have had a certificate at all.

The shared name is now <cluster>.<pki_domain>, which is a subdomain of
the domain the role allows, and both cloud templates follow it.

Adds the assertion that would have caught this: the cluster servername
has to sit inside vault_pki_domain, because a name every certificate must
carry is worth nothing if the role refuses to issue it. Reintroducing the
broken form turns it red, along with the agreement check.

Also silences SC2016 over render(), where the single quotes are the point
-- sed has to receive ${x} literally rather than the empty expansion the
shell would hand it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sethbergman
sethbergman force-pushed the fix/leader-tls-servername-and-preflight branch from 2ee76ce to 424f5de Compare August 28, 2026 10:21
@sethbergman
sethbergman merged commit 385bde0 into main Aug 28, 2026
26 checks passed
@sethbergman
sethbergman deleted the fix/leader-tls-servername-and-preflight branch August 28, 2026 10:42
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.

1 participant