feat(nebula): CA rotation, the certificate audit, and pb-nebula v0.3's host flags - #5
Merged
Merged
Conversation
…s host flags pb-nebula v0.3.0 grew a rotation trigger and twelve fields; the platform put two routes in front of them. This catches the CLI up to both. STONE NEBULA CA-ROTATE. Three steps, because Nebula verification is mutual and hosts pull their config on their own schedule -- one write carrying both the new trust bundle and the new certificate splits the mesh until propagation finishes. The long help says so at each step rather than only at the parent, because the step someone is about to run is the one they are reading. It is a route because nebula_ca.updateRule is operator-only: permitting the trigger through an update rule means a deny-list over the certificate, the private key and the rest of the CA material, on the record holding the trust anchor for the whole mesh. Same reason as `stone nats account-keys`. STONE NEBULA CERT-AUDIT exists for a different reason -- deciding whether a host certificate still matches its network means parsing a Nebula certificate, which no client can do. The route answers in ids; this resolves them to hostnames, and falls back to the bare id rather than dropping a row, because a host we cannot name is still a host that needs re-signing. No bulk re-issue verb, deliberately. Re-signing moves a fingerprint and the revocation blocklist matches on fingerprints, so a sweep rewrites every peer config in the mesh. The help says to go one at a time. SEVEN NEW HOST FLAGS: --is-relay, --unsafe-networks, --unsafe-routes, --preferred-ranges, --mtu, --tun-device, --renew. is_relay joins the ls columns. ROUTEONLYFIELDS is the new half of the drift guard. The existing tests catch a flag for a field that does not exist -- which is how `nebula-ca --rotate-keys` was found. They cannot catch a flag for a field that DOES exist and that a tenant may not write: that one 404s at the rule layer, on a command that looks like every other update. `nebula_ca.rotate` is exactly that shape now, and the nats_accounts signing-key triggers have been in that position all along with only a prose note guarding them. Verified the guard fails when given a --rotate flag before removing the probe. libraryOwnedFields gains pb-nebula's additions. Worth noting it behaves better than the pb-nats case documented beside it: pb-nebula MIGRATES missing fields, so a database created before v0.3.0 acquires them on the next start rather than never. The floor is v0.3.0, and below it every flag here is a silent no-op -- which the vendored schema cannot catch, because it is a copy of a file that never declared these fields either. ALSO FIXED: every error the CLI printed ended in "(0)". PocketBase sends the status as `status`; PBError only read `code`, which nothing populates. The distinction that matters most -- 400 from a validator versus 404 from an update rule -- was invisible. Both are read now, with the HTTP status as a fallback. Verified against a live platform on pb-nebula v0.3.2: a full prepare -> commit -> finish through the CLI with the out-of-order refusals coming back intact, the audit flagging both hosts after a network widened and clearing per host on --renew, and the new flags round-tripping into the generated config. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
Catches the CLI up to pb-nebula v0.3 and the two routes the platform put in front of it (stone-age-io/platform#10).
stone nebula— two operations that are not record writesca-rotateis a route becausenebula_ca.updateRuleis operator-only. Permitting the trigger through an update rule would mean a deny-list over the certificate, the private key, the incoming and outgoing CA material and the expiry — a deny-list that silently opens the moment anyone adds a field, on the record holding the trust anchor for the tenant's whole mesh. Same reasonstone nats account-keysexists.Three steps, and the wait between them is the feature. Nebula verification is mutual — each peer checks the other against its own local CA pool, with no chain and no fallback — and hosts pull their config whenever they like. So one write carrying both the new trust bundle and the new certificate splits the mesh: a host that has fetched presents a new-CA certificate to one that has not, and the handshake fails in both directions until propagation finishes. The long help repeats this per step rather than only on the parent, because the step someone is about to run is the one they are reading.
cert-auditis a route for a different reason. Deciding whether a host certificate still matches its network means parsing a Nebula certificate, which no client can do. It answers in ids;cmd/nebula.goresolves those to hostnames, falling back to the bare id rather than dropping the row — a host we cannot name is still a host that needs re-signing.There is deliberately no bulk re-issue verb. Re-signing moves a certificate's fingerprint, and a fingerprint is what the revocation blocklist matches, so a sweep rewrites every peer config in the mesh. The help says to go one at a time.
Seven new
nebula-hostflags--is-relay,--unsafe-networks,--unsafe-routes,--preferred-ranges,--mtu,--tun-device,--renew.is_relayjoins thelscolumns.--unsafe-networksand--unsafe-routesare two halves of one feature on different hosts: the first is signed into the gateway's certificate and authorizes it to route that subnet, the second goes on each host that wants to reach it. Neither derives the other, and setting one alone moves no traffic — the help says so.routeOnlyFields, the half of the drift guard that was missingThe existing tests catch a flag for a field that does not exist. That is how
nebula-ca --rotate-keyswas found — a flag against a field with no home anywhere, reporting success on every run because PocketBase discards writes to fields a collection lacks.They cannot catch the adjacent case: a flag for a field that does exist and that a tenant may not write. That one 404s at the rule layer, on a command that looks exactly like every other update.
nebula_ca.rotateis precisely that shape now that it is real, and thenats_accountssigning-key triggers have been in that position all along with only a prose note guarding them.routeOnlyFieldsnames those fields with the reason and the command to use instead. I confirmed it fails when handed a--rotateflag before removing the probe.libraryOwnedFieldsgains pb-nebula's additions. Worth noting it behaves better than the pb-nats case documented beside it: pb-nebula migrates missing fields (addMissingFields), so a database created before v0.3.0 acquires them on the next start rather than never.Version floor
pb-nebula v0.3.0; the platform pins v0.3.2. Against v0.2.0 the two routes 404 and all seven flags name fields the collection does not have — so the write is discarded and the command reports success. That is exactly the failure the drift guard exists to make loud, and it is the one case it cannot catch, because the vendored schema is a copy of a file that never declared these fields either. Written down in CLAUDE.md, README and SKILLS rather than left to be rediscovered.
Also fixed
Every error the CLI has ever printed ended in
(0). PocketBase sends the status asstatus;PBErroronly readcode, which nothing populates. So the number in every error message was zero, and the distinction that matters most — a 400 from a validator versus a 404 from an update rule — was invisible:Both names are read now, with the HTTP status as a fallback.
Verification
Against a live platform on pb-nebula v0.3.2:
prepare → commit → finishthrough the CLI, with the server's refusals arriving intact — out-of-order steps, a secondprepare, and the named-host interlock onfinish.cert-auditflagging both hosts after a network was widened, clearing per host on--renew, and reporting nothing once every host was re-signed. Also confirmed it never lists an inactive host: re-signing one would publish a new fingerprint while the old certificate stayed valid and unblocklisted.Invalid preferred range: 172.16.0.5/24 has host bits set, did you mean 172.16.0.0/24?).gofmt,go vet,go test ./...clean.🤖 Generated with Claude Code