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>
59 lines
2.0 KiB
Go
59 lines
2.0 KiB
Go
package server
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"io"
|
|
"math/big"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// TestHTTPSAndPlainOnOnePort serves a certificate file: the same port
|
|
// answers HTTPS, and plain HTTP from loopback.
|
|
func TestHTTPSAndPlainOnOnePort(t *testing.T) {
|
|
dir := t.TempDir()
|
|
key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "dhcp.test"}, DNSNames: []string{"dhcp.test"},
|
|
NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour)}
|
|
der, _ := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
|
|
kd, _ := x509.MarshalPKCS8PrivateKey(key)
|
|
os.WriteFile(filepath.Join(dir, "c.pem"), pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}), 0o600)
|
|
os.WriteFile(filepath.Join(dir, "k.pem"), pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: kd}), 0o600)
|
|
ln, _ := net.Listen("tcp", "127.0.0.1:0")
|
|
addr := ln.Addr().String()
|
|
ln.Close()
|
|
e := newEnv(t)
|
|
e.srv.cfg.Listen, e.srv.cfg.TLSCertFile, e.srv.cfg.TLSKeyFile = []string{addr}, filepath.Join(dir, "c.pem"), filepath.Join(dir, "k.pem")
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
ready := make(chan struct{})
|
|
go e.srv.Serve(ctx, func() { close(ready) })
|
|
<-ready
|
|
pool := x509.NewCertPool()
|
|
pool.AddCert(func() *x509.Certificate { c, _ := x509.ParseCertificate(der); return c }())
|
|
hc := &http.Client{Timeout: 5 * time.Second, Transport: &http.Transport{TLSClientConfig: &tls.Config{RootCAs: pool, ServerName: "dhcp.test"}}}
|
|
for _, url := range []string{"https://" + addr + "/healthz", "http://" + addr + "/healthz"} {
|
|
resp, err := hc.Get(url)
|
|
if err != nil {
|
|
t.Fatalf("%s: %v", url, err)
|
|
}
|
|
b, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != 200 || !strings.Contains(string(b), `"ok":true`) {
|
|
t.Errorf("%s: %d %s", url, resp.StatusCode, b)
|
|
}
|
|
}
|
|
}
|