Conversation
config_vip.go carried `// +build no-compile`, which Go has rejected as a non-alphanumeric build constraint since 1.25. `go build` skipped the file and passed, so nobody noticed; `go vet` and `go test` instead failed to build the whole config package, which meant none of its tests could run - not on the build node and not in CI. This is gofmt's own output: it rewrites the legacy line into the //go:build form and settles on `ignore`, which excludes the file exactly as `no-compile` did. Neither tag is ever enabled, so the behaviour is unchanged. Refs #1421 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Seki Xu <seki.xu@bigstack.co>
The cluster certificate carried only the VIP, but AMQP clients address each
control node directly - RabbitMqServers() iterates cubesys.control.addrs - so
enabling RabbitMQ TLS on top of /var/www/certs would fail the handshake against
every node. Confirmed on a live HA cluster: the SAN held IP:10.32.10.140 while
the control group was 10.32.10.141-143, two disjoint sets.
Assemble the SAN from the control group instead, covering every node's IP and
hostname alongside the existing localhost / cube-controller / 127.0.0.1 / VIP
entries, plus the controller name itself, which was missing too.
Drop empty values rather than emitting a bare "IP:". cubesys.control.addrs is
absent altogether from settings.txt on a non-HA node, and strings.Split("", ",")
yields [""] rather than an empty slice; openssl rejects the whole extension with
"invalid null value", which would leave a fresh non-HA install unable to sign a
certificate at all.
Refs #1421
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Seki Xu <seki.xu@bigstack.co>
genSelfSignCerts() only runs while creating a new cluster, and only when the certificate is absent; CONFIG_MIGRATE then carries /var/www/certs across upgrades untouched. Widening the SAN therefore reaches new installs only. Deleting the certificate and committing does not help either: the commit path skips generation once the node has joined, and health_httpd_repair rsyncs the old certificate back from a peer. cubectl config cluster gencerts signs into a staging directory, reads the result back against buildCertSANs() to confirm the control group actually landed in the SAN, backs the live certificate up outside /var/www/certs - health_httpd_repair treats any file count other than three as damage - and only then swaps it in, syncs it to every control node and restarts the consumers that read it off disk. It also works on a node whose certificate directory is missing, which is exactly where someone lands after trying to force a re-sign by deleting it: there is simply nothing to back up, and the directory is recreated before the swap. The nova copy is dropped rather than overwritten, because config_nova.cpp only refreshes /var/lib/nova/certs/server.pem when its own copy is missing. A certificate this cluster did not issue is left alone unless --force is given: the FQDN procedure installs the customer's own chain in the same location, and replacing it would swap out the TLS identity of the whole site. --dry-run prints what would happen and touches nothing, so the command can be inspected on a shared cluster before it is run for real. Ceph is reconfigured with the two dashboard and two config-key commands rather than through ceph_dashboard_init(), which also creates a radosgw user, wires up SAML2 SSO and runs terraform. Rancher and k3s carry their own platform PKI and are left to the runbook. Refs #1421 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Seki Xu <seki.xu@bigstack.co>
…control distributeCerts() pushed /var/www/certs to --role=control. A compute node holds that directory too -- it is rsynced there at join time regardless of role -- so a re-sign left every compute node holding a certificate the cluster had stopped presenting, with nothing in the command's output to say so. Today that mostly goes unnoticed: nova copies server.pem but only the control group's novnc proxy reads it back. It stops going unnoticed with #1427, where the AMQP listener runs TLS and every nova and neutron agent on a compute node verifies the broker against this directory. The failure mode there is agents dropping off their queues, which points nowhere near a certificate command run some time earlier. Dropping --role from the rsync and from the stale-copy removal reaches all nodes; cubectl node rsync/exec default to ROLE_ALL. The restart stays on the control group because certConsumerUnits are control-only services, and nothing on a compute node needs one: py-amqp builds its SSL context per connection and reads ca_certs off disk each time, so a reconnecting client picks up the new file by itself. Refs #1427 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Seki Xu <seki.xu@bigstack.co>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1421.
The problem
The cluster's self-signed certificate named only the VIP, while several clients address each
control node directly.
nginxlistens on<node-ip>:9999and keystone's httpd on<node-ip>:5443— node IPs, not the VIP — and those clients do verify the hostname. AMQP isin the same shape:
RabbitMqServers()builds the HAtransport_urlby walkingcubesys.control.addrs, one entry per node. The SAN and the set of connection targets weredisjoint.
Confirmed on a live HA cluster before the change:
This was not only a future problem.
nginxlistens on<node-ip>:9999and keystone'shttpd on
<node-ip>:5443— node IPs, not the VIP. Probing all three nodes on:443,:9999and:5443returnedhostname mismatchon all nine. Nobody had noticed becausenothing addresses those endpoints by node IP today.
What this does
buildCertSANs()assembles the SAN from the control group: every node IP and hostname,plus the controller name (which was missing too), alongside the existing
localhost/cube-controller/127.0.0.1/ VIP entries. Empty values are droppedrather than emitted as a bare
IP:—cubesys.control.addrsis absent altogether fromsettings.txton a non-HA node andstrings.Split("", ",")yields[""], which OpenSSLrejects with
invalid null value. Without that guard a fresh non-HA install would fail tosign a certificate at all.
cubectl config cluster gencertsgives an existing cluster a way to pick this up.genSelfSignCerts()only runs while creating a new cluster and only when the certificateis absent, and
CONFIG_MIGRATE(cluster, "/var/www/certs")carries the old one acrossupgrades untouched — so without this, the wider SAN would reach new installs only.
Deleting the certificate and committing does not work either: the commit path skips
generation once the node has joined, and
health_httpd_repairrsyncs the old certificateback from a peer.
The re-signed certificate now reaches every node, not just the control group.
distributeCerts()pushed/var/www/certswith--role=control, but a compute node holdsthat directory too — it is rsynced there at join time regardless of role — so a re-sign left
every compute node trusting a certificate the cluster had stopped presenting, with nothing in
the command's output to say so. Today that mostly goes unnoticed, because nova copies
server.pembut only the control group's novnc proxy reads it back. It stops going unnoticedwith #1427, where the AMQP listener runs TLS and every nova and neutron agent on a compute node
verifies the broker against this directory; the failure there is agents dropping off their
queues, which points nowhere near a certificate command run some time earlier. Dropping
--rolefrom the rsync and from the stale-copy removal reaches all nodes (cubectl node rsync/execdefault toROLE_ALL). The restart stays on the control group, becausecertConsumerUnitsare control-only services and nothing on a compute node needs one: py-amqpbuilds its SSL context per connection and reads
ca_certsoff disk each time, so areconnecting client picks the new file up by itself.
The command signs into a staging directory, reads the result back against
buildCertSANs()itself to confirm the control group landed in the SAN, backs the livecertificate up outside
/var/www/certs(health_httpd_repairtreats any file countother than three as damage), and only then swaps it in, syncs to every control node,
drops nova's copy, restarts the consumers and reconfigures ceph.
Three behaviours worth calling out:
--dry-runtouches nothing — verified on a live cluster: mtimes unchanged, all threefiles byte-identical, no staging left behind. It exists so the command can be inspected
on a shared cluster first.
--forceis given. TheFQDN procedure installs the customer's own chain in this exact location.
config_nova.cpponly refreshes/var/lib/nova/certs/server.pemwhen its own copy is missing, so a regeneratedcertificate would otherwise never reach it.
Ceph is reconfigured with the two dashboard and two
config-keycommands rather thanthrough
ceph_dashboard_init(), which also creates a radosgw user, wires up SAML2 SSO andruns terraform. Rancher and k3s carry their own platform PKI and are left to the runbook.
The
config_vip.gocommit is a prerequisite, not a drive-by: its// +build no-compilehas been an invalid build constraint since Go 1.25, which made
go vetandgo testfailto build the whole
configpackage — none of its tests could run, on the build node or inCI. The fix is gofmt's own output.
Verification
Measured on the sky 3cc HA lab, then fully rolled back.
gencerts18 self-signed, as expected)active;ceph HEALTH_OK; dashboard, mgr restful and on-disk certificate all the same/var/lib/cube-certs-backup/Before/after with one offline probe on the old (backed-up) and new certificates:
10.32.10.140(VIP)10.32.10.141/142/143sky,sky141cube-controllerThe VIP and
cube-controllerstill match, so the consumers that reach the cluster throughthe VIP are unaffected.
Unit tests:
TestBuildCertSANscovers HA, non-HA with the keys absent, empty members,de-duplication, and a non-control node;
TestGenSelfSignCertssigns end to end against thereal openssl. Every commit builds, vets and tests clean on its own.
Known gaps
rsyncor the restarts fail, the certificate has already been swapped in on the master— the nodes are then inconsistent. There is a backup and its path is logged, but there is
no automatic rollback. Deliberate: an automatic rollback can fail too, and would make the
resulting state harder to read.
0644, as it was before. Tightening it needs an audit of which uideach of the consumers runs as; worth a separate ticket.
equivalent to before the change and the risk is concentrated in whether it signs at all.
🤖 Generated with Claude Code