User Story
As an operator deploying pyrycode-relay, I want the relay to refuse to start if it would bind to any port outside an explicit expected-ports set, so that a leaked debug listener (stray net/http/pprof on :6060, an env-overridden debug port, an accidentally-enabled metrics endpoint) fails loudly at boot rather than quietly exposing an unauthenticated surface on the internet.
Context
The relay's intended listeners are the public TLS/plaintext port (443 / --insecure-listen value) and, when autocert is on, the ACME HTTP-01 challenge port (80). Anything else bound by the process is suspect: pprof endpoints, debug interfaces, metrics exporters, etc.
This ticket adds a boot-time check that compares the set of ports the process is about to bind against an expected set derived from the parsed flags, and aborts if there is a surplus listener. The pattern mirrors ErrCacheDirInsecure (#9): a single sentinel in internal/relay, a single decision point in cmd/pyrycode-relay/main.go, fail-fast on mismatch.
Acceptance Criteria
Technical Notes
- The relay currently uses
http.Server.ListenAndServe rather than direct net.Listen. The simplest implementation derives the actual-ports set from the Addr fields of the http.Server values just before they start (no net.Listen wrapper or listener registry needed). An explicit registry helper (every binding call appends to a slice) is also acceptable but is not required; both approaches are within scope.
- If autocert is on, the ACME HTTP-01 challenge listener on
:80 must be in the expected set; the test for case (a) should include the autocert-on flag combination.
- Out of scope: matching on interface (e.g.
127.0.0.1 vs 0.0.0.0). The check is port-only; the threat model is "a listener on this port is exposed."
Size Estimate
S
Split from #42.
User Story
As an operator deploying pyrycode-relay, I want the relay to refuse to start if it would bind to any port outside an explicit expected-ports set, so that a leaked debug listener (stray
net/http/pprofon:6060, an env-overridden debug port, an accidentally-enabled metrics endpoint) fails loudly at boot rather than quietly exposing an unauthenticated surface on the internet.Context
The relay's intended listeners are the public TLS/plaintext port (443 /
--insecure-listenvalue) and, when autocert is on, the ACME HTTP-01 challenge port (80). Anything else bound by the process is suspect: pprof endpoints, debug interfaces, metrics exporters, etc.This ticket adds a boot-time check that compares the set of ports the process is about to bind against an expected set derived from the parsed flags, and aborts if there is a surplus listener. The pattern mirrors
ErrCacheDirInsecure(#9): a single sentinel ininternal/relay, a single decision point incmd/pyrycode-relay/main.go, fail-fast on mismatch.Acceptance Criteria
ErrUnexpectedListenerexists ininternal/relayand is branchable viaerrors.Is. The wrapped error names the unexpected port(s).internal/relayaccepts (a) the expected-ports set computed from parsed config and (b) the actual-ports set the process is about to bind, and returnsErrUnexpectedListenerif any port in actual is absent from expected, nil otherwise. The check is asymmetric: missing ports (fewer listeners than expected) are not an error.cmd/pyrycode-relay/main.goconstructs the expected-ports set from parsed flags (:443and:80when--domainis set; the port from--insecure-listenotherwise), constructs the actual-ports set from the listeners it is about to start, invokes the check before the firstListen*call, and fail-fasts on error withos.Exit(1)and a structured log line containing at minimum: the offending port(s), the expected-ports set, and anerrfield carrying the wrapped sentinel.cmd/pyrycode-relayasserts that the binary does not transitively importnet/http/pprof(the most common source of:6060leaks). Style is the architect's call —go list -deps -jsonparsing in a test, or an equivalent build-tag-gated import check.Technical Notes
http.Server.ListenAndServerather than directnet.Listen. The simplest implementation derives the actual-ports set from theAddrfields of thehttp.Servervalues just before they start (nonet.Listenwrapper or listener registry needed). An explicit registry helper (every binding call appends to a slice) is also acceptable but is not required; both approaches are within scope.:80must be in the expected set; the test for case (a) should include the autocert-on flag combination.127.0.0.1vs0.0.0.0). The check is port-only; the threat model is "a listener on this port is exposed."Size Estimate
S
Split from #42.