From 7c0a652e5ed4896f2b73b8fda98ba763660aa13d Mon Sep 17 00:00:00 2001 From: bendwyer <17102207+bendwyer@users.noreply.github.com> Date: Mon, 29 Jun 2026 19:33:27 +0000 Subject: [PATCH] fix(tailscale-exit-nodes): lock AWS firewall after instance is running Lightsail rejects PutInstancePublicPorts while an instance is still in the pending transition state (InvalidResourceState). The provision saga locked the firewall immediately after create, before the instance had settled. Move the lock to after the readiness poll, which already waits for the instance to reach running, so the port change targets a settled instance. Vultr is unaffected (its firewall group is applied at create time). Co-Authored-By: Claude Opus 4.8 (1M context) --- images/tailscale-exit-nodes/src/saga.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/images/tailscale-exit-nodes/src/saga.ts b/images/tailscale-exit-nodes/src/saga.ts index ffd04b5..a07d3d3 100644 --- a/images/tailscale-exit-nodes/src/saga.ts +++ b/images/tailscale-exit-nodes/src/saga.ts @@ -142,7 +142,7 @@ const defaultSleep = (ms: number): Promise => /** * Run the full provisioning saga: mint a key, create the instance, record its - * handles, lock the firewall (AWS), poll for readiness, then mark active. Any + * handles, poll for readiness, lock the firewall (AWS), then mark active. Any * failure is recorded on the row as `failed` (the reaper reconciles it) and * swallowed — the saga runs detached, so there is no caller to throw to. */ @@ -163,10 +163,6 @@ export async function provisionSaga( // it even if a later step fails. await db.updateInstanceRefs(params.deploymentId, refs.instanceRef, refs.firewallRef); - if (params.provider === 'aws') { - await lockLightsailFirewall(creds, refs.instanceRef, params.providerRegion); - } - let publicIp: string | null = null; for (let attempt = 1; attempt <= pollAttempts; attempt++) { publicIp = await checkInstanceReady(creds, params, refs.instanceRef); @@ -177,6 +173,13 @@ export async function provisionSaga( throw new Error(`instance ${refs.instanceRef} did not become ready in time`); } + // AWS only: lock the public firewall to UDP 41641. This MUST run after the + // instance is `running` — Lightsail rejects PutInstancePublicPorts while the + // instance is still in transition (pending). Vultr locks down at create time. + if (params.provider === 'aws') { + await lockLightsailFirewall(creds, refs.instanceRef, params.providerRegion); + } + await db.markActive(params.deploymentId, publicIp); console.log( `provision ${params.deploymentId} active provider=${params.provider} ip=${publicIp}`,