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:
@@ -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 {
|
||||||
|
// The admin plane always serves plaintext: passkey auth behind a trusted
|
||||||
|
// TLS-terminating proxy (Traefik / k8s Ingress), or no auth in insecure dev.
|
||||||
adminAddr string
|
adminAddr string
|
||||||
adminInsecure bool
|
adminInsecure bool
|
||||||
adminBehindProxy bool // passkey auth ON, but TLS terminated by a trusted upstream proxy
|
|
||||||
adminCert string
|
|
||||||
adminKey string
|
|
||||||
|
|
||||||
agentAddr string
|
agentAddr string
|
||||||
agentCert string
|
agentCert string
|
||||||
@@ -63,9 +59,6 @@ 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:
|
// Secure mode: passkey auth over PLAINTEXT behind a trusted TLS-terminating
|
||||||
// Passkey auth ON, but a trusted upstream proxy (Traefik / k8s Ingress)
|
// proxy (Traefik / k8s Ingress) that owns the cert — the manager never
|
||||||
// terminates TLS and forwards plaintext. WebAuthn only needs RP_ORIGIN to
|
// terminates TLS. WebAuthn only needs RP_ORIGIN to match the browser's HTTPS
|
||||||
// match the browser's HTTPS origin, not our own transport; session cookies
|
// origin, not our transport; session cookies stay Secure. The listener MUST be
|
||||||
// stay Secure (the browser sees HTTPS). The listener MUST be reachable ONLY
|
// bound private (loopback / a private net) so only the proxy reaches it.
|
||||||
// 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.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())
|
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
|
|
||||||
// (WebAuthn), not a client cert, so there is no ClientCAs/ClientAuth here — just
|
|
||||||
// our server cert. (manager→broker mTLS is separate; see broker.go.)
|
|
||||||
//
|
|
||||||
// The cert is served via a hot-reloading GetCertificate so an out-of-band renewal
|
|
||||||
// is picked up WITHOUT a restart. The intended deployment is a publicly-trusted
|
|
||||||
// cert obtained by an external ACME client via Let's Encrypt DNS-01 (the UI is on
|
|
||||||
// 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) }
|
|
||||||
|
|||||||
Reference in New Issue
Block a user