fleet-manager: drop self-TLS admin mode — the manager never terminates TLS

Traefik (and the k8s Ingress later) owns UI TLS, so remove the vestigial
self-TLS path: adminTLS(), the certReloader type, the adminCert/adminKey config
+ FLEET_MGR_ADMIN_CERT/_KEY env, and the now-redundant adminBehindProxy flag.
The admin plane always serves plaintext now — two modes: INSECURE (dev, no auth)
or default (passkey behind a trusted TLS proxy). Drops the crypto/tls, fmt, sync
imports and the errString helper.
This commit is contained in:
Audiobuzz
2026-07-05 01:44:23 +00:00
parent 24d67d07db
commit 76c3a6aaa7
+14 -91
View File
@@ -12,25 +12,21 @@
package main package main
import ( import (
"crypto/tls"
"fmt"
"log" "log"
"net/http" "net/http"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/go-webauthn/webauthn/webauthn" "github.com/go-webauthn/webauthn/webauthn"
) )
type config struct { type config struct {
adminAddr string // The admin plane always serves plaintext: passkey auth behind a trusted
adminInsecure bool // TLS-terminating proxy (Traefik / k8s Ingress), or no auth in insecure dev.
adminBehindProxy bool // passkey auth ON, but TLS terminated by a trusted upstream proxy adminAddr string
adminCert string adminInsecure bool
adminKey string
agentAddr string agentAddr string
agentCert string agentCert string
@@ -61,11 +57,8 @@ type config struct {
func loadConfig() config { func loadConfig() config {
return config{ return config{
adminAddr: env("FLEET_MGR_ADMIN_ADDR", "127.0.0.1:9500"), adminAddr: env("FLEET_MGR_ADMIN_ADDR", "127.0.0.1:9500"),
adminInsecure: env("FLEET_MGR_ADMIN_INSECURE", "") == "true", adminInsecure: env("FLEET_MGR_ADMIN_INSECURE", "") == "true",
adminBehindProxy: env("FLEET_MGR_ADMIN_BEHIND_PROXY", "") == "true",
adminCert: env("FLEET_MGR_ADMIN_CERT", ""),
adminKey: env("FLEET_MGR_ADMIN_KEY", ""),
agentAddr: env("FLEET_MGR_AGENT_ADDR", "127.0.0.1:9600"), agentAddr: env("FLEET_MGR_AGENT_ADDR", "127.0.0.1:9600"),
agentCert: env("FLEET_MGR_AGENT_CERT", ""), agentCert: env("FLEET_MGR_AGENT_CERT", ""),
@@ -235,84 +228,18 @@ func main() {
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
} }
switch { if cfg.adminInsecure {
case cfg.adminInsecure:
log.Printf("WARNING: admin plane INSECURE (plaintext, NO passkey auth) on %s — dev/lab only", cfg.adminAddr) log.Printf("WARNING: admin plane INSECURE (plaintext, NO passkey auth) on %s — dev/lab only", cfg.adminAddr)
log.Fatal(srv.ListenAndServe()) log.Fatal(srv.ListenAndServe())
case cfg.adminBehindProxy:
// Passkey auth ON, but a trusted upstream proxy (Traefik / k8s Ingress)
// terminates TLS and forwards plaintext. WebAuthn only needs RP_ORIGIN to
// match the browser's HTTPS origin, not our own transport; session cookies
// stay Secure (the browser sees HTTPS). The listener MUST be reachable ONLY
// via the proxy — bind it private (loopback / a private net), never a
// routable interface.
log.Printf("fleet-manager admin (passkey, behind TLS proxy) plaintext listening on %s (rp=%s) — ensure only the proxy can reach this address", cfg.adminAddr, cfg.rpID)
log.Fatal(srv.ListenAndServe())
default:
tlsCfg, err := adminTLS(cfg)
if err != nil {
log.Fatalf("admin TLS: %v", err)
}
srv.TLSConfig = tlsCfg
log.Printf("fleet-manager admin (HTTPS + passkey) listening on %s (rp=%s)", cfg.adminAddr, cfg.rpID)
log.Fatal(srv.ListenAndServeTLS("", "")) // certs are in TLSConfig
} }
}
// adminTLS builds the admin plane's SERVER-AUTH TLS. Browser identity is a passkey // Secure mode: passkey auth over PLAINTEXT behind a trusted TLS-terminating
// (WebAuthn), not a client cert, so there is no ClientCAs/ClientAuth here — just // proxy (Traefik / k8s Ingress) that owns the cert — the manager never
// our server cert. (manager→broker mTLS is separate; see broker.go.) // terminates TLS. WebAuthn only needs RP_ORIGIN to match the browser's HTTPS
// // origin, not our transport; session cookies stay Secure. The listener MUST be
// The cert is served via a hot-reloading GetCertificate so an out-of-band renewal // bound private (loopback / a private net) so only the proxy reaches it.
// is picked up WITHOUT a restart. The intended deployment is a publicly-trusted log.Printf("fleet-manager admin (passkey, behind TLS proxy) plaintext listening on %s (rp=%s) — ensure only the proxy can reach this address", cfg.adminAddr, cfg.rpID)
// cert obtained by an external ACME client via Let's Encrypt DNS-01 (the UI is on log.Fatal(srv.ListenAndServe())
// the mgmt network, not internet-reachable, so HTTP-01/TLS-ALPN-01 can't validate;
// DNS-01 proves control via a TXT record and supports wildcards). The ACME client
// (lego/certbot + a DNS provider) writes cert+key to FLEET_MGR_ADMIN_CERT/_KEY on
// a shared volume; this reloader notices the mtime bump and swaps the cert live.
func adminTLS(cfg config) (*tls.Config, error) {
if cfg.adminCert == "" || cfg.adminKey == "" {
return nil, errString("FLEET_MGR_ADMIN_CERT and FLEET_MGR_ADMIN_KEY are required (or set FLEET_MGR_ADMIN_INSECURE=true for dev)")
}
rl := &certReloader{certFile: cfg.adminCert, keyFile: cfg.adminKey}
if _, err := rl.GetCertificate(nil); err != nil { // fail fast on a bad/missing cert
return nil, err
}
return &tls.Config{
MinVersion: tls.VersionTLS12,
GetCertificate: rl.GetCertificate,
}, nil
}
// certReloader serves the admin server cert, reloading from disk when the file's
// mtime advances (an ACME renewal), so Let's Encrypt DNS-01 rotations apply live.
type certReloader struct {
certFile, keyFile string
mu sync.Mutex
cached *tls.Certificate
mtime time.Time
}
func (c *certReloader) GetCertificate(*tls.ClientHelloInfo) (*tls.Certificate, error) {
c.mu.Lock()
defer c.mu.Unlock()
fi, statErr := os.Stat(c.certFile)
if statErr == nil && (c.cached == nil || fi.ModTime().After(c.mtime)) {
cert, err := tls.LoadX509KeyPair(c.certFile, c.keyFile)
if err != nil {
if c.cached != nil {
return c.cached, nil // serve the last good cert through a transient mid-rotation error
}
return nil, err
}
c.cached, c.mtime = &cert, fi.ModTime()
}
if c.cached == nil {
return nil, fmt.Errorf("admin cert unavailable: %v", statErr)
}
return c.cached, nil
} }
// ---- tiny helpers --------------------------------------------------------- // ---- tiny helpers ---------------------------------------------------------
@@ -348,7 +275,3 @@ func envInt(k string, def int) int {
} }
return def return def
} }
type errString string
func (e errString) Error() string { return string(e) }