The web UI's certificate from Let's Encrypt, through Jarl

tls.acme names the certificate and jarl: the Jarl server and a token that
may only write the challenge record. Steward gets the certificate with
DNS-01 (lego), writing the record through Jarl's API into every view that
has the zone, renews it with a third of its lifetime left and swaps it
in without a restart. A check runs before every order: Jarl, the zone,
the token's permission, a test record, the zone's name servers, public
resolvers, clean-up; each step says what to fix. Staging by default.

With a certificate, each listener serves HTTPS and plain HTTP on one port
(redirecting plain HTTP, except from the server itself).

steward cert / cert check / cert renew, Server -> Certificate in the web
UI, GET /certificate, POST /certificate/check|renew. docs/certificates.md
and docs/jarl.md (the Jarl user and the token's grant).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abrown38andClaude Opus 5.5 committed 2026-10-14 15:46:36 +01:00
1 parent d0ee2c8844
commit 7b3f0f764c
32 files changed
+2544 -27

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
package server
import (
"bufio"
"crypto/tls"
"errors"
"net"
"net/http"
"net/netip"
"strings"
"sync"
"time"
)
// sniffListener serves TLS and plain HTTP on the same port: it looks at a
// connection's first byte and hands the server a *tls.Conn when it is a
// TLS handshake (0x16), the plain connection otherwise. That way stewardd
// can start serving HTTPS when a certificate turns up (one it obtains,
// docs/certificates.md) without a restart, and a plain-HTTP bookmark gets
// a redirect instead of an error.
type sniffListener struct {
net.Listener
cfg *tls.Config
ch chan net.Conn
errc chan error
done chan struct{}
once sync.Once
}
func newSniffListener(ln net.Listener, cfg *tls.Config) *sniffListener {
l := &sniffListener{Listener: ln, cfg: cfg, ch: make(chan net.Conn), errc: make(chan error, 1), done: make(chan struct{})}
go l.loop()
return l
}
func (l *sniffListener) loop() {
for {
c, err := l.Listener.Accept()
if err != nil {
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
time.Sleep(50 * time.Millisecond)
continue
}
select {
case l.errc <- err:
default:
}
return
}
go l.classify(c)
}
}
func (l *sniffListener) classify(c net.Conn) {
c.SetReadDeadline(time.Now().Add(10 * time.Second))
br := bufio.NewReader(c)
b, err := br.Peek(1)
c.SetReadDeadline(time.Time{})
if err != nil {
c.Close()
return
}
var out net.Conn = &peekedConn{Conn: c, r: br}
if b[0] == 0x16 {
out = tls.Server(out, l.cfg)
}
select {
case l.ch <- out:
case <-l.done:
c.Close()
}
}
func (l *sniffListener) Accept() (net.Conn, error) {
select {
case c := <-l.ch:
return c, nil
case err := <-l.errc:
return nil, err
case <-l.done:
return nil, net.ErrClosed
}
}
func (l *sniffListener) Close() error {
l.once.Do(func() { close(l.done) })
return l.Listener.Close()
}
// peekedConn reads through the buffer that holds the peeked byte.
type peekedConn struct {
net.Conn
r *bufio.Reader
}
func (c *peekedConn) Read(p []byte) (int, error) { return c.r.Read(p) }
// webCertificate is the certificate stewardd serves: tls.cert_file's, or
// the one it obtains itself (tls.acme).
func (s *Server) webCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if s.certs != nil {
return s.certs.get(hello)
}
if s.cfg.ACME != nil {
if c := s.cfg.ACME.Certificate(); c != nil {
return c, nil
}
}
return nil, errNoCertificate
}
var errNoCertificate = errors.New("no certificate for HTTPS yet")
// httpsAvailable reports whether stewardd has a certificate to serve.
func (s *Server) httpsAvailable() bool {
return s.certs != nil || s.cfg.ACME != nil && s.cfg.ACME.Certificate() != nil
}
// requireHTTPS sends plain-HTTP requests to HTTPS once there is a
// certificate: browsers (GET and HEAD) get a redirect, anything else an
// error, so tokens and passwords aren't sent in the clear. Requests from
// loopback (tools on this server) and from trusted proxies (which
// terminate TLS themselves) are served as they are.
func (s *Server) requireHTTPS(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.TLS != nil || !s.httpsAvailable() {
next.ServeHTTP(w, r)
return
}
host, _, _ := net.SplitHostPort(r.RemoteAddr)
if a, err := netip.ParseAddr(host); err == nil && (a.Unmap().IsLoopback() || s.trusted(a.Unmap())) {
next.ServeHTTP(w, r)
return
}
target := "https://" + r.Host + r.URL.RequestURI()
if r.Method == http.MethodGet || r.Method == http.MethodHead {
http.Redirect(w, r, target, http.StatusPermanentRedirect)
return
}
msg := "stewardd serves HTTPS on this port; use " + strings.TrimSuffix(target, r.URL.RequestURI())
if r.Header.Get("Authorization") != "" {
msg += ". This request's token crossed the network unencrypted; if that network isn't trusted, rotate the token"
}
writeError(w, http.StatusBadRequest, "https_required", msg)
})
}