You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Ruling 12 of #5 lists eight things external verification should establish where practical, and #5's success criteria require that an external port scan reaches only the intentionally public services. The PRD's open question asks something narrower: what the minimum external security smoke test is before a deployment can be declared healthy. #35 builds the suite; this decision sets the floor that suite must clear before anyone says the relay is deployed.
The floor matters because the bundle's own validation is an inside-the-host check. deploy/compose/README.md tells the operator to run curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness" — loopback, plaintext, on the machine being tested. That proves the relay process is up and proves nothing about what the internet can reach, which is exactly the gap Ruling 12 exists to close.
What "public" should mean is readable from the bundle. Under compose.caddy.yml the relay's own port is removed with ports: !reset [] and Caddy publishes 80 and 443; Caddyfile declares a single site for {$BUZZ_DOMAIN} whose only route is reverse_proxy relay:3000, so TLS terminates at Caddy and nothing else should answer. The relay's health port 8080 and metrics port 9102 are configured but never published, and compose.dev.yml would publish 5432, 6379, 9000, 9001, 8082 and 9090 if it were ever enabled on the VPS.
This is kept separate from the companion ADR on CI/CD deployment gates. That one decides what blocks the pipeline and is implemented by #37, which is blocked on the access question in #13. This one defines the health bar itself, is implemented by #35, and can be decided and used now — including by hand against the destroyable VM before any pipeline exists.
Decision drivers
Ruling 12: the system must be checked from the perspective of an untrusted internet client, not only an authorised one
Ruling 12 also asks that Buzz be running the expected deployment version, which matters because .env.example ships BUZZ_IMAGE=ghcr.io/block/buzz:main, a floating tag
Port scanning against a provider's network may be constrained by that provider's acceptable-use policy
Considered options
Floor A — reachability and TLS only: the public endpoint answers over TLS, a plaintext attempt fails, and the relay reports healthy through that endpoint
Floor B — Floor A plus the negative assertions: an off-host scan showing that 5432, 6379, 9000, 9001, 8082, 9090 and the relay's 8080/9102 are not reachable, and that no other unexpected port answers
Expected rejection: the bundle's existing loopback check as the health bar — curl -fsS "http://127.0.0.1:.../_liveness" from deploy/compose/README.md. It is a useful functional check and a worthless security one: it runs on the host, over plaintext loopback, and would pass identically whether the firewall were correct or absent.
Left blank deliberately.launchpad/AGENTS.md rule 1 for agents: draft everything, approve nothing.
Consequences
Good: a named floor turns "deployed" into a claim with evidence behind it, and gives #35 a subset to build first and #22 something to satisfy before the relay is handed to the cohort. It also lets the rest of Ruling 12's list be recorded as deferred rather than quietly dropped, which #35's definition of done already requires.
Bad: any floor is also a ceiling in practice — checks outside it tend not to get written. Setting it too low certifies a host that is only superficially safe; setting it high enough to be meaningful means the check must run from off-host, so declaring health depends on a second machine being available. The negative assertions are the expensive part: proving a port is closed requires scanning rather than reading configuration, which takes longer and can be affected by provider-side filtering that makes a misconfigured host look correctly closed.
Security implications
The purpose of this decision is to make the negative claims testable. Positive checks — the relay answers, TLS works — fail loudly and are hard to get wrong; the claims that actually carry the security posture are the ones about what is not reachable, and those fail silently by simply never being made. That is why an off-host scan rather than a configuration read is the load-bearing part, and why #35 requires proof that the suite fails against a deliberately broken host: a negative check that has only ever passed may not be a check at all. Two cautions attach to it. Scanning must be scoped to the cohort's own host and should be confirmed against the provider's acceptable-use policy before it runs from CI. And the smoke test's output describes the host's exposure, so it must not be published anywhere the public repository would carry it, per launchpad/AGENTS.md section 8.
Supersedes
none
Filed by an AI agent (Claude Opus 5) on behalf of @tucktuck101, as an ADR raised from #5's open questions per launchpad/AGENTS.md section 4. Verified by reading the repository at launchpad-issue-pr-scaffold: the loopback validation command and production notes in deploy/compose/README.md, the !reset and 80/443 publishing in compose.caddy.yml, the reverse_proxy relay:3000 line in Caddyfile, the unpublished BUZZ_HEALTH_PORT/BUZZ_METRICS_PORT and published relay port in compose.yml, the ports in compose.dev.yml, the floating BUZZ_IMAGE=ghcr.io/block/buzz:main default in .env.example, and the text of #5, #22, #23, #26 and #35. Not verified: any of these ports' actual reachability on a deployed host, the cohort VPS provider's acceptable-use policy on port scanning, and whether a public endpoint or DNS name exists yet — nothing was scanned or executed.
Context and problem statement
Ruling 12 of #5 lists eight things external verification should establish where practical, and #5's success criteria require that an external port scan reaches only the intentionally public services. The PRD's open question asks something narrower: what the minimum external security smoke test is before a deployment can be declared healthy. #35 builds the suite; this decision sets the floor that suite must clear before anyone says the relay is deployed.
The floor matters because the bundle's own validation is an inside-the-host check.
deploy/compose/README.mdtells the operator to runcurl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness"— loopback, plaintext, on the machine being tested. That proves the relay process is up and proves nothing about what the internet can reach, which is exactly the gap Ruling 12 exists to close.What "public" should mean is readable from the bundle. Under
compose.caddy.ymlthe relay's own port is removed withports: !reset []and Caddy publishes80and443;Caddyfiledeclares a single site for{$BUZZ_DOMAIN}whose only route isreverse_proxy relay:3000, so TLS terminates at Caddy and nothing else should answer. The relay's health port8080and metrics port9102are configured but never published, andcompose.dev.ymlwould publish5432,6379,9000,9001,8082and9090if it were ever enabled on the VPS.This is kept separate from the companion ADR on CI/CD deployment gates. That one decides what blocks the pipeline and is implemented by #37, which is blocked on the access question in #13. This one defines the health bar itself, is implemented by #35, and can be decided and used now — including by hand against the destroyable VM before any pipeline exists.
Decision drivers
.env.exampleshipsBUZZ_IMAGE=ghcr.io/block/buzz:main, a floating tagConsidered options
5432,6379,9000,9001,8082,9090and the relay's8080/9102are not reachable, and that no other unexpected port answersBUZZ_IMAGE, and remote privileged access matching the policy decided in adr: administrative access model after bootstrap #26curl -fsS "http://127.0.0.1:.../_liveness"fromdeploy/compose/README.md. It is a useful functional check and a worthless security one: it runs on the host, over plaintext loopback, and would pass identically whether the firewall were correct or absent.Decision outcome
Left blank deliberately.
launchpad/AGENTS.mdrule 1 for agents: draft everything, approve nothing.Consequences
Good: a named floor turns "deployed" into a claim with evidence behind it, and gives #35 a subset to build first and #22 something to satisfy before the relay is handed to the cohort. It also lets the rest of Ruling 12's list be recorded as deferred rather than quietly dropped, which #35's definition of done already requires.
Bad: any floor is also a ceiling in practice — checks outside it tend not to get written. Setting it too low certifies a host that is only superficially safe; setting it high enough to be meaningful means the check must run from off-host, so declaring health depends on a second machine being available. The negative assertions are the expensive part: proving a port is closed requires scanning rather than reading configuration, which takes longer and can be affected by provider-side filtering that makes a misconfigured host look correctly closed.
Security implications
The purpose of this decision is to make the negative claims testable. Positive checks — the relay answers, TLS works — fail loudly and are hard to get wrong; the claims that actually carry the security posture are the ones about what is not reachable, and those fail silently by simply never being made. That is why an off-host scan rather than a configuration read is the load-bearing part, and why #35 requires proof that the suite fails against a deliberately broken host: a negative check that has only ever passed may not be a check at all. Two cautions attach to it. Scanning must be scoped to the cohort's own host and should be confirmed against the provider's acceptable-use policy before it runs from CI. And the smoke test's output describes the host's exposure, so it must not be published anywhere the public repository would carry it, per
launchpad/AGENTS.mdsection 8.Supersedes
none
Filed by an AI agent (Claude Opus 5) on behalf of @tucktuck101, as an ADR raised from #5's open questions per
launchpad/AGENTS.mdsection 4. Verified by reading the repository atlaunchpad-issue-pr-scaffold: the loopback validation command and production notes indeploy/compose/README.md, the!resetand80/443publishing incompose.caddy.yml, thereverse_proxy relay:3000line inCaddyfile, the unpublishedBUZZ_HEALTH_PORT/BUZZ_METRICS_PORTand published relay port incompose.yml, the ports incompose.dev.yml, the floatingBUZZ_IMAGE=ghcr.io/block/buzz:maindefault in.env.example, and the text of #5, #22, #23, #26 and #35. Not verified: any of these ports' actual reachability on a deployed host, the cohort VPS provider's acceptable-use policy on port scanning, and whether a public endpoint or DNS name exists yet — nothing was scanned or executed.