Skip to content

ci(06): verify kubectl-native CD — WG handshake + SA-auth + server-side dry-run - #1

Merged
Afgan0r merged 7 commits into
masterfrom
ci/verify-phase6-cd
Jun 13, 2026
Merged

ci(06): verify kubectl-native CD — WG handshake + SA-auth + server-side dry-run#1
Afgan0r merged 7 commits into
masterfrom
ci/verify-phase6-cd

Conversation

@Afgan0r

@Afgan0r Afgan0r commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Deferred Phase 6 verification: prove the kubectl-native CD pipeline from a real runner.

Exercises (non-mutating)

  • validate — manifest/script shape + validate-staging.py
  • dry-run — WireGuard handshake from runner -> ci-deployer SA-token auth -> kubectl apply --dry-run=server of all k8s/staging/*.yaml (excl. operator-managed 01-ci-rbac.yaml)

Change

  • Pass WG_LOCAL_IP=10.8.0.3/32 to the WG step (both jobs) so CI uses a dedicated WG peer, distinct from the operator laptop 10.8.0.2.

Operator prereqs (done out-of-band)

  • 01-ci-rbac.yaml applied -> ci-deployer SA + token + Role/Binding
  • Dedicated CI [Peer] at 10.8.0.3/32 on staging VPS
  • staging env secrets set: WG_, K8S_TOKEN, K8S_CA_CERT, GHCR_

Real deploy job runs only on master push / dispatch — not on this PR.

Generated with Claude Code

…8.0.2 collision)

The operator laptop already occupies 10.8.0.2 on the WG network; the CI runner
must use a distinct peer IP so both tunnels can coexist. Pass WG_LOCAL_IP to the
wg-tunnel-up.sh step in both dry-run and deploy jobs. Pairs with a dedicated
[Peer] at 10.8.0.3/32 added on the staging VPS WG server.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 44aed3d6c2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

WG_PRIVATE_KEY: ${{ secrets.WG_PRIVATE_KEY }}
WG_PEER_PUBLIC_KEY: ${{ secrets.WG_PEER_PUBLIC_KEY }}
WG_ENDPOINT: ${{ secrets.WG_ENDPOINT }}
WG_LOCAL_IP: 10.8.0.3/32

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the CI peer IP consistent with the bootstrap runbook

With this workflow now assigning the runner 10.8.0.3/32, a fresh or rotated staging setup that follows the checked-in runbook still configures the VPS peer with AllowedIPs = 10.8.0.2/32 (docs/operator-bootstrap.md:154-168). WireGuard treats AllowedIPs as the tunnel source IPs a peer may send from, so that documented server config will drop traffic from this runner even though the job is using the right keys; update the runbook/server-side peer IP at the same time or keep the workflow on the documented address.

Useful? React with 👍 / 👎.

…substitution

First live CI run exposed `fopen: No such file or directory` at `wg set
private-key <(...)`: sudo closes inherited FDs (closefrom=3), so the
process-substitution /dev/fd/NN path vanishes in the wg process. /dev/stdin
(FD 0) is preserved by sudo, keeping the key off disk while fixing the handshake
step. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… just wait

Second live run: interface showed no `transfer:` line — zero bytes sent, so the
handshake was never initiated. WireGuard is lazy: it only sends a handshake
when an outbound packet is destined for the peer. The wait loop polled
latest-handshakes without generating any traffic, so it always timed out.

Fix: set `persistent-keepalive 25` on the peer and poke the API server
(/dev/tcp/10.8.0.1:6443) at the top of the wait loop to force immediate
handshake initiation. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… none)

Third live run: handshake completed but the API reachability check timed out.
Root cause: `wg set allowed-ips` only sets WireGuard cryptokey routing, not a
kernel route, and the /32 local address adds no subnet route — so 10.8.0.1 had
no route via wg0 (the handshake still succeeded because keepalive/handshake go
endpoint-to-endpoint over the public net, bypassing the tunnel IP). Add an
explicit `ip route replace <allowed-ip> dev wg0` after link-up. Found by the
dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e files)

Fourth live run: `xargs kubectl apply ... -f` placed all filenames after a
single -f, so kubectl took only the first and rejected the rest ("Unexpected
args"). Prefix each path with `-f ` via sed so every manifest gets its own flag.
Same fix in both the dry-run and deploy jobs. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Fifth live run: the dry-run apply failed with "unable to read
certificate-authority /tmp/tmp.XXX: no such file or directory". set-cluster
stored the CA *path*, but the temp CA file is rm'd by the script's EXIT trap;
the in-script `auth whoami` passed only because the trap had not fired yet.
Add --embed-certs=true so the CA is inlined into the kubeconfig and later
steps work after cleanup. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ped)

Sixth live run: every workload dry-ran cleanly, but 00-namespace.yaml was
Forbidden — the namespace-scoped ci-deployer Role cannot get/create the
cluster-scoped Namespace (by design: "CI never creates the namespace"). The
glob already excluded 01-ci-rbac.yaml; add 00-namespace.yaml to the same
operator-managed exclusion in both jobs. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@Afgan0r
Afgan0r merged commit c95daf4 into master Jun 13, 2026
3 checks passed
Afgan0r pushed a commit that referenced this pull request Jun 13, 2026
…substitution

First live CI run exposed `fopen: No such file or directory` at `wg set
private-key <(...)`: sudo closes inherited FDs (closefrom=3), so the
process-substitution /dev/fd/NN path vanishes in the wg process. /dev/stdin
(FD 0) is preserved by sudo, keeping the key off disk while fixing the handshake
step. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Afgan0r pushed a commit that referenced this pull request Jun 13, 2026
… just wait

Second live run: interface showed no `transfer:` line — zero bytes sent, so the
handshake was never initiated. WireGuard is lazy: it only sends a handshake
when an outbound packet is destined for the peer. The wait loop polled
latest-handshakes without generating any traffic, so it always timed out.

Fix: set `persistent-keepalive 25` on the peer and poke the API server
(/dev/tcp/10.8.0.1:6443) at the top of the wait loop to force immediate
handshake initiation. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Afgan0r pushed a commit that referenced this pull request Jun 13, 2026
… none)

Third live run: handshake completed but the API reachability check timed out.
Root cause: `wg set allowed-ips` only sets WireGuard cryptokey routing, not a
kernel route, and the /32 local address adds no subnet route — so 10.8.0.1 had
no route via wg0 (the handshake still succeeded because keepalive/handshake go
endpoint-to-endpoint over the public net, bypassing the tunnel IP). Add an
explicit `ip route replace <allowed-ip> dev wg0` after link-up. Found by the
dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Afgan0r pushed a commit that referenced this pull request Jun 13, 2026
…e files)

Fourth live run: `xargs kubectl apply ... -f` placed all filenames after a
single -f, so kubectl took only the first and rejected the rest ("Unexpected
args"). Prefix each path with `-f ` via sed so every manifest gets its own flag.
Same fix in both the dry-run and deploy jobs. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Afgan0r pushed a commit that referenced this pull request Jun 13, 2026
Fifth live run: the dry-run apply failed with "unable to read
certificate-authority /tmp/tmp.XXX: no such file or directory". set-cluster
stored the CA *path*, but the temp CA file is rm'd by the script's EXIT trap;
the in-script `auth whoami` passed only because the trap had not fired yet.
Add --embed-certs=true so the CA is inlined into the kubeconfig and later
steps work after cleanup. Found by the dry-run job in PR #1.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Afgan0r added a commit that referenced this pull request Jun 13, 2026
The deferred live-CI human-verification item was executed end-to-end on
2026-06-13: PR #1 dry-run green (WG handshake + ci-deployer SA auth + server-side
apply) and master-push deploy green (render+apply secrets, apply manifests,
rollout of 5 workloads). Six latent script/workflow bugs surfaced by the first
live runs were fixed (private-key /dev/stdin, handshake init, kernel route,
per-file -f, kubeconfig --embed-certs, exclude 00-namespace). Updates
06-VERIFICATION.md (status human_needed → verified, Live CI Verification section)
and STATE.md (current position, pending todos, blockers resolved).

[skip ci] — docs-only; does not need to re-trigger the deploy workflow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Afgan0r added a commit that referenced this pull request Jun 15, 2026
Activate the merged errors-only Sentry SDK on staging by pinning each app to a
post-Sentry-merge image:
- server-2:        3866f6b → e31b129 (sentry #19 + parity #18/#21)
- replay-parser-2: 5e09d33c → 72116db (sentry #1)
- replays-fetcher: 7e5ca97 → 205b4cd (sentry #1 + shared-clients #2)

replays-fetcher: 7e5ca97 (F1 conditional-writes baseline) is an ancestor of
205b4cd, so the F1 fix is preserved. PARITY-COORDINATION.md is archived.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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