Skip to content

relay: TLS via Let's Encrypt autocert for production --domain mode #9

@ilmoniemi

Description

@ilmoniemi

User Story

As the relay operator, I want the binary to terminate TLS itself via Let's Encrypt (golang.org/x/crypto/acme/autocert) when --domain is set, so that production deployment is one command (pyrycode-relay --domain relay.example.com) without a separate reverse proxy.

Context

The scaffold's cmd/pyrycode-relay/main.go currently refuses to start without --insecure-listen ("autocert TLS path not yet implemented"). This ticket fills that in. See pyrycode/pyrycode/docs/protocol-mobile.md § TLS.

Acceptance Criteria

  • In cmd/pyrycode-relay/main.go, when --domain is set (and --insecure-listen is empty):
    • Use autocert.Manager with HostPolicy: autocert.HostWhitelist(*domain) (only the configured domain accepted; any other Host header → certificate request rejected).
    • Cache directory: *certCache flag (default ~/.pyrycode-relay/certs). Create it if missing with 0700 permissions.
    • Bind two listeners:
      • :443 for HTTPS via manager.TLSConfig().
      • :80 for the ACME http-01 challenge via manager.HTTPHandler(nil). The nil argument means non-challenge HTTP requests get a 404 (we do NOT redirect to HTTPS — we want explicit-failure semantics for misconfigured clients).
    • Use &http.Server{} with the same timeouts as the existing insecure path (ReadHeaderTimeout 10s, ReadTimeout 60s, WriteTimeout 60s, IdleTimeout 120s).
    • Connections to a Host header other than *domain get 421 Misdirected Request per the spec.
  • Add golang.org/x/crypto/acme/autocert to go.mod.
  • Update README.md:
    • Move --domain example to the primary "Run" section (was previously after --insecure-listen).
    • Add note: certificates issued on first request to the domain; first request may take ~10–20s.
  • Tests in cmd/pyrycode-relay/main_test.go (or internal/relay/tls_test.go):
    • Cache-dir creation test: missing dir → created with 0700; existing dir → no-op.
    • HostPolicy test: incoming request with wrong Host → autocert rejects (synthetic via manager.HostPolicy).
    • Cannot integration-test real ACME issuance; that's verified manually on first deploy.

Technical Notes

  • autocert requires the relay to be reachable on port 80 from the public internet for the http-01 challenge. Document this in README.
  • Cache dir at 0700 is critical — TLS keys live there. Test verifies permissions.
  • --cert-cache flag default: $HOME/.pyrycode-relay/certs. Resolved via os.UserHomeDir() at startup.
  • Out of scope:
    • Wildcard certs (would require dns-01 challenge, much more complex).
    • Multiple-domain support (autocert.HostWhitelist accepts multiple, but UX implications and ops story aren't worth solving until a second domain is needed).
    • Cert renewal monitoring / metrics — autocert handles renewal silently; first-pass deploy doesn't need observability for it.
    • Falling back to a self-signed cert in dev (use --insecure-listen for dev, that's its purpose).

Size Estimate

S — ~80 LOC + ~50 LOC tests + go.mod update.

Depends on

  • None (independent of routing logic).

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