Skip to content

feat(linode): orchestrator-side cache wg-pubkey discovery — Phase B (FJB-99)#112

Merged
hstern merged 1 commit into
mainfrom
feat/fjb-99-phase-b-pubkey-discovery
May 29, 2026
Merged

feat(linode): orchestrator-side cache wg-pubkey discovery — Phase B (FJB-99)#112
hstern merged 1 commit into
mainfrom
feat/fjb-99-phase-b-pubkey-discovery

Conversation

@hstern

@hstern hstern commented May 29, 2026

Copy link
Copy Markdown
Owner

Summary

Completes the FJB-99 bootstrap loop started in Phase A. The cache publishes its WG pubkey to S3 as public-read; the orchestrator polls via plain HTTPS GET (no S3 SDK — WG pubkeys are designed to be world-readable), reads the cache's public IPv4 from the Linode API, and feeds both into wgboot.Config as runtime overrides for transport.wg.peer.{public_key,endpoint}.

  • cache cloud-init: the aws s3 cp now sets --acl public-read.
  • managedCache.WaitForWGPubkey: polls <bucket-endpoint>/<bucket>/wg-pubkey.txt with 5s → 30s exponential backoff. Returns the trimmed pubkey on first 200 OK.
  • managedCache.PublicEndpoint: GetInstance(linodeID), picks the public (non-RFC1918) IPv4, appends operator-configured listen port (or 51820).
  • Linode.{WaitForCacheWGPubkey,CachePublicEndpoint}: duck-typed accessors for cmd/fj-bellows.
  • wgboot.Config: new CachePubkey + CacheEndpoint overrides. planBoot reads override first, falls back to config peer, errors with a clear message pointing at the bootstrap loop if neither is set.
  • config.WGPeer: public_key + endpoint are now optional at config-load (the loop fills them at runtime).
  • cmd/fj-bellows.discoverCachePeer: runs the bootstrap loop ONLY when static config knobs are empty — keeps daemon startup fast under back-compat static config and preserves the FJB-91 e2e harness's persistent-cache path.

Why HTTPS GET, not signed S3

WG public keys are intentionally public; the threat model isn't worse than what's already on the wire during a handshake. Skipping the S3 SDK keeps the dep surface lean.

Not in this PR (Phase C)

  • E2E harness reactivation against the ephemeral cache (drop persistent-cache preflight + reactivate worker readiness checks).
  • Drop the static transport.wg.peer.{public_key,endpoint} config knobs entirely.

Test plan

  • go build ./...
  • go test ./... (incl. TestWaitForWGPubkey_*, TestPublicEndpoint_PicksPublicIPv4, TestBoot_RejectsMissingPeerWithoutOverrides)
  • golangci-lint run (0 issues)
  • bash test/e2e-linode/run-local.sh --transport=cache-gateway → ALL OK (live Linode; static-peer back-compat path verified — Phase C exercises the dynamic bootstrap path)

🤖 Generated with Claude Code

…FJB-99)

Completes the bootstrap loop FJB-99 Phase A started: the cache nanode
generates its WG keypair at first boot and publishes the pubkey to S3
as public-read; the orchestrator polls the bucket via plain HTTPS GET
(no S3 SDK — WG public keys are designed to be world-readable) and
reads the cache's public IPv4 from the Linode API to build the WG
peer endpoint. Both feed into wgboot.Config as runtime overrides for
transport.wg.peer.{public_key,endpoint}.

Why HTTPS GET over signed S3: WG pubkeys are intentionally public;
the threat model isn't worse than what's already on the wire during
a handshake. Skipping the S3 SDK keeps the dependency surface lean.

What lands:
- cache cloud-init: the aws s3 cp now sets --acl public-read so the
  orchestrator can fetch the pubkey via plain HTTPS.
- managedCache.WaitForWGPubkey: polls <bucket-endpoint>/<bucket>/wg-
  pubkey.txt with 5s initial delay, exponential backoff to 30s, ctx-
  cancellable. Returns the trimmed pubkey on first 200 OK with a
  non-empty body.
- managedCache.PublicEndpoint: GetInstance on the cache linodeID,
  picks the public (non-RFC1918) IPv4, appends the operator-configured
  listen port or defaults to 51820.
- Linode duck-typed accessors WaitForCacheWGPubkey + CachePublic-
  Endpoint surface both through cmd/fj-bellows.
- wgboot.Config: new optional CachePubkey + CacheEndpoint fields.
  planBoot reads from override first, falls back to config peer
  knobs, errors out at boot if neither is set — clear error message
  points operators at the bootstrap loop.
- config.WGPeer.validate: public_key + endpoint are now optional at
  config-load time (the bootstrap loop populates them at runtime).
- cmd/fj-bellows.discoverCachePeer: runs the bootstrap loop ONLY when
  static config knobs are empty. Daemon startup stays fast under
  back-compat static config — matters for the FJB-91 e2e harness
  that uses the persistent test cache.

Tests cover the bucket-poll happy path, timeout surface, public-IPv4
selection, and wgboot's new no-peer rejection. Verified end-to-end
against live Linode via test/e2e-linode/run-local.sh --transport=
cache-gateway (FJB-91 stack-up scope still passes; the new code path
is exercised but no-op since the harness sets static peer config).

Out of scope (FJB-99 Phase C):
- e2e harness reactivation against the ephemeral cache (drops
  persistent-cache preflight, reactivates worker readiness checks).
- Drop the static transport.wg.peer.{public_key,endpoint} config
  knobs entirely.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hstern hstern enabled auto-merge (squash) May 29, 2026 00:06
@hstern hstern merged commit 5f78d50 into main May 29, 2026
7 checks passed
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