admin: terminate TLS in the binary, with a certificate that reloads itself
server-test / test (push) Successful in 33s
server-test / test (push) Successful in 33s
Direct rather than behind Caddy or nginx. This binary already serves TLS for the control plane, so it is reuse rather than new machinery; one process with one config file is most of what makes this thing pleasant to run; and a proxy on the box would invite someone to eventually front the control plane too, which would break SPKI pinning because clients pin that certificate's key. The hard part of TLS is not termination, it is renewal - so the certificate is re-read when the files change. No reload hook to write, and none to quietly stop working months later and be noticed only after the certificate has expired. A torn write (renewal tools write cert and key separately) keeps the previous certificate rather than taking the listener down. Not applied to the control plane, on purpose: clients pin that key, so replacing it should cost an operator a moment's thought and a restart, not happen because a file changed. Two listeners, two different right answers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cd187f9ef5
commit
6afcb131ef
@@ -39,6 +39,7 @@ import (
|
||||
|
||||
"echo-lot.app/server/internal/adminauth"
|
||||
"echo-lot.app/server/internal/canarydns"
|
||||
"echo-lot.app/server/internal/certreload"
|
||||
"echo-lot.app/server/internal/compat"
|
||||
"echo-lot.app/server/internal/config"
|
||||
"echo-lot.app/server/internal/control"
|
||||
@@ -314,7 +315,26 @@ func serve(cfg *config.Config) error {
|
||||
})
|
||||
})
|
||||
adminSrv := &http.Server{Addr: cfg.AdminListen, Handler: admin, ReadHeaderTimeout: 10 * time.Second}
|
||||
go func() { errCh <- fmt.Errorf("admin: %w", adminSrv.ListenAndServe()) }()
|
||||
if cfg.AdminTLSCert != "" {
|
||||
// Terminated here rather than behind a reverse proxy: this binary already serves TLS for
|
||||
// the control plane, so it is reuse rather than new machinery, and one process with one
|
||||
// config file is the property that makes this pleasant to run. A proxy would also invite
|
||||
// someone to later front the control plane too, which would break SPKI pinning.
|
||||
reloader, err := certreload.New(cfg.AdminTLSCert, cfg.AdminTLSKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("admin TLS: %w", err)
|
||||
}
|
||||
adminSrv.TLSConfig = reloader.TLSConfig()
|
||||
if exp := reloader.NotAfter(); !exp.IsZero() {
|
||||
slog.Info("admin UI TLS", "listen", cfg.AdminListen, "cert_expires", exp.Format(time.RFC3339))
|
||||
if time.Until(exp) < 14*24*time.Hour {
|
||||
slog.Warn("admin certificate expires soon", "expires", exp.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
go func() { errCh <- fmt.Errorf("admin: %w", adminSrv.ListenAndServeTLS("", "")) }()
|
||||
} else {
|
||||
go func() { errCh <- fmt.Errorf("admin: %w", adminSrv.ListenAndServe()) }()
|
||||
}
|
||||
|
||||
// UDP data plane — one socket per configured address. Distinct sockets
|
||||
// (not wildcard) also guarantee responses leave from the address the
|
||||
|
||||
Reference in New Issue
Block a user