Skip to content

relay: refuse to boot if listener ports exceed expected set #81

@ilmoniemi

Description

@ilmoniemi

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

  • An exported sentinel ErrUnexpectedListener exists in internal/relay and is branchable via errors.Is. The wrapped error names the unexpected port(s).
  • A check function in internal/relay accepts (a) the expected-ports set computed from parsed config and (b) the actual-ports set the process is about to bind, and returns ErrUnexpectedListener if 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.go constructs the expected-ports set from parsed flags (:443 and :80 when --domain is set; the port from --insecure-listen otherwise), constructs the actual-ports set from the listeners it is about to start, invokes the check before the first Listen* call, and fail-fasts on error with os.Exit(1) and a structured log line containing at minimum: the offending port(s), the expected-ports set, and an err field carrying the wrapped sentinel.
  • A package-level test in cmd/pyrycode-relay asserts that the binary does not transitively import net/http/pprof (the most common source of :6060 leaks). Style is the architect's call — go list -deps -json parsing in a test, or an equivalent build-tag-gated import check.
  • Unit tests on the check function cover: (a) actual == expected → nil; (b) actual = expected ∪ {6060} → sentinel naming 6060; (c) actual ⊂ expected (fewer listeners than expected) → nil; (d) empty expected, empty actual → nil.

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    security-sensitiveTouches auth, crypto, or internet-exposed input pathssize:sSmall ticket: <100 lines production code

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions