diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index 37cb913..fad0b3c 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -1598,6 +1598,26 @@ **adding v6 IP SANs needs its own ruling**. `tests/phase-05` **14 cases** (was 9); gauntlet **ALL GREEN (85) on vcloud**; repo-lint 0 fail / 611 files. +- **IPv6 IP SANs RULED + BUILT 2026-07-29 for the Octavia controller cert.** Question as + presented: R7's work left the SAN derived by `.split()[0]` -- the provider v4 leg only -- + while ruling 3 commit 2 had made every API VIP dual-family, so the amphora control-plane cert + carried no IPv6 IP SAN; flagged as a gap needing its own ruling. **Operator answer, exact + utterance: "Yes, add the v6 IP sans."** Recorded as a **D-109 RULING NOTE (2026-07-29)**; + OPS under GA-R3 (a generator change extending an already-ruled principle to a surface D-109 + governs), no new D-number -- next-free stays 138. + The generator now emits `IP.1` = this DC's provider **v4** leg and `IP.2` = its provider + **v6** leg, both derived from the same per-DC overlay under R7's `$DC` selector. Verified: + dc0 -> `10.12.4.57` + `2602:f3e2:f02:11::57`; dc1 -> `10.12.64.57` + `2602:f3e2:f03:11::57`; + and a v4-only control emits NO v6 SAN, so the generator stays correct on a tree where the + dual-stack ADD has not landed. **Admin and internal legs stay EXCLUDED, exactly as before** -- + DOCFIX-067's design has always been provider-leg-only and widening it was not what was asked. + **OBSERVATION RECORDED, NOT ACTED ON:** amphorae reach the controller on `o-hm0`'s + charm-generated `fc00::/64` ULA (`controller_ip_port_list`), NOT on any VIP leg, so neither + SAN matches THAT path. Measured upstream, the CONTROLLER verifies the amphora by UUID + (`assert_hostname = self.uuid`); the reverse direction was NOT established from source. This + ruling makes the SAN set consistent with the VIP it has always been derived from -- whether + that is the right derivation is a separate question to settle by inspection at the Octavia + step of Stage 5. - **THREE PLATFORM BEHAVIOURS GRADUATED to `references/platform-traps.md`** at session close, having been recorded only in this status document (which is consolidated over time, so a durable trap does not belong here alone): MAAS auto-reserves `::1`-`::ffff:ffff` on EVERY diff --git a/docs/changelog-20260728-vip-arity-gate.md b/docs/changelog-20260728-vip-arity-gate.md index cbe393f..c9ca63b 100644 --- a/docs/changelog-20260728-vip-arity-gate.md +++ b/docs/changelog-20260728-vip-arity-gate.md @@ -586,3 +586,31 @@ **Evidence.** `tests/phase-05` **14 cases ALL PASS** (was 9); gauntlet **ALL GREEN (85) on vcloud**; repo-lint 0 fail / 611 files. + +## 20. IPv6 IP SANs on the Octavia controller cert -- RULED and BUILT + +**Ruling (GA-R5).** Question as presented: R7's work left the SAN derived by `.split()[0]` +-- the provider v4 leg only -- while ruling 3 commit 2 had made every API VIP dual-family, +so the amphora control-plane cert carried no IPv6 IP SAN. Flagged as a gap needing its own +ruling. **Operator answer, exact utterance: "Yes, add the v6 IP sans."** Recorded as a +D-109 RULING NOTE (2026-07-29); OPS under GA-R3, no new D-number. + +**What.** The generator emits `IP.1` = this DC's provider v4 leg and `IP.2` = its provider +v6 leg, both derived from the same per-DC overlay under R7's `$DC` selector. + +**Verified:** dc0 -> `10.12.4.57` + `2602:f3e2:f02:11::57`; dc1 -> `10.12.64.57` + +`2602:f3e2:f03:11::57`; a v4-only control emits NO v6 SAN, so the generator stays correct +before the dual-stack ADD. Admin and internal legs stay excluded, as they always were. + +**Two repo guards fired on the way and both were right.** `repo-lint` L5 rejected a +heading that LED with `D-109` (it reads as a second definition of the decision) -- retitled +to the repo's own note form, `RULING NOTE -- D-109:`. That is the third time this +trap has bitten in this branch's history. And L10 rejected the commit for touching a +design-decisions Status line without CURRENT-STATE in the same change (GA-R1 C1). + +**OBSERVATION LOGGED, NOT ACTED ON:** amphorae reach the controller on `o-hm0`'s +charm-generated `fc00::/64` ULA (`controller_ip_port_list`), not on any VIP leg, so neither +SAN matches THAT path. Measured upstream, the controller verifies the amphora by UUID; the +reverse direction was not established from source. This ruling makes the SAN set consistent +with the VIP it has always been derived from -- whether that is the right derivation is a +separate question, to settle by inspection at the Octavia step. diff --git a/docs/design-decisions.md b/docs/design-decisions.md index 92c02ab..736e7cd 100644 --- a/docs/design-decisions.md +++ b/docs/design-decisions.md @@ -2712,6 +2712,40 @@ --- +## RULING NOTE 2026-07-29 -- D-109: the Octavia controller cert gains IPv6 IP SANs + +**Status: RULED 2026-07-29.** OPS under GA-R3 -- a generator change extending an +already-ruled principle to a surface D-109 already governs; doubt resolves DOWN, so no +new D-number is assigned. Recorded here because D-109 is the Octavia-PKI authority after +its 2026-07-27 amendment (R7). + +**Question as presented.** The R7 work of 2026-07-29 left the controller cert's IP SAN +derived by `.split()[0]` -- the PROVIDER v4 leg only -- while ruling 3 commit 2 had made +every API VIP dual-family. So the amphora control-plane cert carries no IPv6 IP SAN even +though the VIP it is derived from now has three v6 legs. This was flagged as a gap rather +than a break (it is a separate trust domain from the API TLS that Vault issues with IPv6 +SANs under D-109's original text), and was put to the operator as needing its own ruling. + +**Operator answer, exact utterance: "Yes, add the v6 IP sans."** + +**Scope, stated precisely so a later reader does not over-read it.** The generator now +emits `IP.1` = this DC's provider **v4** VIP leg and `IP.2` = its provider **v6** leg, +both DERIVED from the same per-DC overlay under the `$DC` selector R7 introduced. The +admin and internal legs stay EXCLUDED, exactly as they were before this ruling -- the +DOCFIX-067 design has always been provider-leg-only, and widening it was not what was +asked. A v4-only (three-address) VIP still yields `IP.1` alone and no v6 SAN, so the +generator remains correct on a tree where the dual-stack ADD has not landed. + +**OBSERVATION RECORDED, NOT ACTED ON.** The address amphorae actually use to reach the +controller is `o-hm0`'s charm-generated `fc00::/64` ULA (`controller_ip_port_list`), NOT +any VIP leg -- so neither `IP.1` nor `IP.2` matches that path. Whether the amphora +verifies the controller cert against an IP at all is not asserted here: measured upstream, +the CONTROLLER verifies the amphora by UUID (`assert_hostname = self.uuid`), and the +reverse direction was not established from source. This ruling therefore makes the SAN set +consistent with the VIP it has always been derived from; whether that is the right +derivation is a separate question, and the place to settle it is by inspection at the +Octavia step of Stage 5. + ## D-109 -- AMENDMENT (2026-07-27): per-DC cryptographic independence EXTENDS to the Octavia control-plane PKI **Status:** RULED 2026-07-27 (operator, GA-R5). Question as presented, verbatim: "R7 -- diff --git a/runbooks/phase-01-bundle-deploy.md b/runbooks/phase-01-bundle-deploy.md index 9d64fdb..422291b 100644 --- a/runbooks/phase-01-bundle-deploy.md +++ b/runbooks/phase-01-bundle-deploy.md @@ -391,10 +391,11 @@ # IP-ONLY endpoints per B5), so nothing resolves them today. Revisit with D-106's # hostname work, not here. # - # ALSO LOGGED: `.split()[0]` takes only the PROVIDER v4 leg, so this cert gains no IPv6 - # IP SAN even though the VIP is dual-family since 2026-07-29. That is a gap, not a - # break -- this is the amphora control-plane trust domain, separate from API TLS, which - # Vault issues with IPv6 SANs under D-109. Adding v6 IP SANs needs its own ruling. + # (The "no IPv6 IP SAN" gap this block used to log was RULED and CLOSED 2026-07-29 -- + # operator: "Yes, add the v6 IP sans" -- see the D-109 ruling note. IP.2 is emitted + # below. Still true and still worth knowing: amphorae reach the controller on o-hm0's + # charm-generated fc00::/64 ULA, not on any VIP leg, so neither SAN matches THAT path; + # settle by inspection at the Octavia step rather than assuming.) # # DOCFIX-067: the SAN IP is DERIVED from the bundle (provider leg of octavia's # vip triple), never a literal -- the old baked 10.12.4.233 went stale when R14 @@ -425,7 +426,19 @@ echo "measured octavia provider VIP: ${OCTAVIA_VIP:-} (DC=$DC, expected prefix ${VIP_PREFIX:-}.)" [ -n "$VIP_PREFIX" ] || { echo "FAIL: could not derive this DC's provider prefix from $VIP_OVERLAY -- stop"; exit 1; } echo "$OCTAVIA_VIP" | grep -qE "^$(printf '%s' "$VIP_PREFIX" | sed 's/\./\\./g')\.[0-9]{1,3}$" || { echo "FAIL: implausible VIP -- stop"; exit 1; } + # D-109 RULING NOTE (2026-07-29, operator: "Yes, add the v6 IP sans"): the cert gains + # this DC's provider v6 leg alongside its v4 one. Both are DERIVED from the same + # per-DC overlay; the admin and internal legs stay excluded exactly as before, because + # DOCFIX-067's design has always been provider-leg-only. A v4-only (3-address) vip + # yields IP.1 alone and no v6 SAN, so this stays correct on a pre-dual-stack tree. + OCTAVIA_VIP6=$(python3 -c "import yaml,sys,ipaddress;v=yaml.safe_load(open(sys.argv[1]))['applications']['octavia']['options']['vip'].split();print(next((a for a in v if ipaddress.ip_address(a).version==6),''))" "$VIP_OVERLAY" 2>/dev/null || true) printf 'IP.1 = %s\n' "$OCTAVIA_VIP" >> controller.cnf + if [ -n "${OCTAVIA_VIP6:-}" ]; then + echo "measured octavia provider v6 VIP: $OCTAVIA_VIP6" + printf 'IP.2 = %s\n' "$OCTAVIA_VIP6" >> controller.cnf + else + echo "NOTE: octavia vip is v4-only (3 addresses) -- no IPv6 IP SAN emitted (expected before the dual-stack ADD)" + fi openssl req -new -sha256 -key controller.key -config controller.cnf -out controller.csr openssl x509 -req -sha256 -in controller.csr \ -CA ../controller-ca/controller-ca.cert.pem \