acme: answer HTTP-01 from the server itself, on port 80
HTTP-01 always arrives on port 80 - the CA chooses the port, not the operator - so it never collides with an admin UI on 443. The conflict only exists for TLS-ALPN-01, which is the challenge type that does use 443. Given that, the server keeps a permanent listener on 80 that answers challenges from a webroot and redirects everything else to the admin UI. Same arrangement as the webroot plugins for Apache and nginx, and better than letting the ACME client bind 80 per renewal: nothing binds and unbinds, so a renewal cannot fail because the port was briefly busy, and the client needs only write access to a directory instead of the privilege to bind a low port. Port 80 also gets a use it would want anyway. The ACME client stays an external program. lego is also a Go library, but importing it would put a large dependency tree into a server that deliberately has none, and the CLI does the same job from a timer. Tokens are validated by *shape* before any filesystem call, so traversal never reaches the disk - a stronger guarantee than sanitising a path and trusting the sanitiser. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
6afcb131ef
commit
5d7f59a66a
@@ -37,6 +37,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"echo-lot.app/server/internal/acmehttp"
|
||||
"echo-lot.app/server/internal/adminauth"
|
||||
"echo-lot.app/server/internal/canarydns"
|
||||
"echo-lot.app/server/internal/certreload"
|
||||
@@ -336,6 +337,27 @@ func serve(cfg *config.Config) error {
|
||||
go func() { errCh <- fmt.Errorf("admin: %w", adminSrv.ListenAndServe()) }()
|
||||
}
|
||||
|
||||
// ACME HTTP-01 responder. Permanent rather than started per renewal: nothing binds and
|
||||
// unbinds, so a renewal cannot fail because the port was briefly busy, and the ACME client
|
||||
// needs only write access to a directory instead of the privilege to bind a low port.
|
||||
if cfg.ACMEHTTPListen != "" {
|
||||
webroot := cfg.ACMEWebroot
|
||||
if webroot == "" {
|
||||
webroot = filepath.Join(cfg.StateDir, "acme")
|
||||
}
|
||||
if err := acmehttp.EnsureWebroot(webroot); err != nil {
|
||||
return fmt.Errorf("acme webroot: %w", err)
|
||||
}
|
||||
acmeSrv := &http.Server{
|
||||
Addr: cfg.ACMEHTTPListen,
|
||||
Handler: acmehttp.Handler(webroot, cfg.AdminBaseURL),
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
slog.Info("acme http-01 responder", "listen", cfg.ACMEHTTPListen, "webroot", webroot,
|
||||
"redirects_to", cfg.AdminBaseURL)
|
||||
go func() { errCh <- fmt.Errorf("acme-http: %w", acmeSrv.ListenAndServe()) }()
|
||||
}
|
||||
|
||||
// UDP data plane — one socket per configured address. Distinct sockets
|
||||
// (not wildcard) also guarantee responses leave from the address the
|
||||
// request arrived on, which stun-5780 will rely on.
|
||||
|
||||
Reference in New Issue
Block a user