Files
2026-07-11 16:20:21 +09:30

136 lines
4.5 KiB
Go

// host.go — the mTLS HOST plane: enrolled hosts get their per-boot SSH host key
// signed by the fleet SSH CA.
//
// This is the THIRD listener, distinct from the two in main.go:
//
// HOST (mTLS, boot-LAN reachable) -- fleet-CA host client cert:
// POST /ssh/hostcert {publicKey, principals, keyID} -> {cert}
//
// It is the automatic, self-authenticating counterpart of enrolment: a host
// proves it is enrolled by presenting its TPM identity as the mTLS client cert
// (verified to the fleet CA), and the broker has step-ca sign an SSH HOST cert —
// no human approval, no CA secret on the host. Admins then trust every fleet host
// via one `@cert-authority` line instead of per-boot TOFU (F-02 / POA&M P-17).
// Keep step-ca itself off the boot LAN; the broker is the single front door.
package main
import (
"crypto/tls"
"crypto/x509"
"encoding/json"
"log"
"net/http"
"os"
"time"
)
type sshHostCertRequest struct {
PublicKey string `json:"publicKey"`
Principals []string `json:"principals"`
KeyID string `json:"keyID"`
}
type sshHostCertResponse struct {
Cert string `json:"cert,omitempty"`
Error string `json:"error,omitempty"`
}
func (b *broker) serveHost() {
if b.cfg.hostAddr == "" {
log.Printf("host plane disabled (set BROKER_HOST_ADDR + BROKER_HOST_CERT/KEY to enable SSH host-cert issuance)")
return
}
mux := http.NewServeMux()
mux.HandleFunc("POST /ssh/hostcert", b.handleSSHHostCert)
mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) })
srv := &http.Server{
Addr: b.cfg.hostAddr,
Handler: mux,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 10 * time.Second,
WriteTimeout: 40 * time.Second, // signing blocks on step-ca
IdleTimeout: 30 * time.Second,
MaxHeaderBytes: 16 << 10,
}
if b.cfg.hostInsecure {
log.Printf("WARNING: BROKER_HOST_INSECURE=true — SSH host plane is PLAINTEXT with NO client-cert auth. DEV ONLY.")
log.Fatalf("host listener: %v", srv.ListenAndServe())
return
}
if b.cfg.hostCert == "" || b.cfg.hostKey == "" {
log.Fatalf("host mTLS requires BROKER_HOST_CERT and BROKER_HOST_KEY (or BROKER_HOST_INSECURE=true for dev)")
}
clientCAPath := b.cfg.hostClientCA
if clientCAPath == "" {
clientCAPath = b.cfg.caRoot // default: the fleet root that signs host identities
}
cert, err := tls.LoadX509KeyPair(b.cfg.hostCert, b.cfg.hostKey)
if err != nil {
log.Fatalf("host server cert: %v", err)
}
caPEM, err := os.ReadFile(clientCAPath)
if err != nil {
log.Fatalf("host client CA: %v", err)
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(caPEM) {
log.Fatalf("host client CA %s contains no certificates", clientCAPath)
}
srv.TLSConfig = &tls.Config{
Certificates: []tls.Certificate{cert},
ClientAuth: tls.RequireAndVerifyClientCert, // must be an enrolled fleet host
ClientCAs: pool,
MinVersion: tls.VersionTLS12,
}
log.Printf("host plane up: %s (mTLS, fleet-CA host client certs) ssh-signer=%s", b.cfg.hostAddr, b.sshSigner.Kind())
log.Fatalf("host listener: %v", srv.ListenAndServeTLS("", ""))
}
func (b *broker) handleSSHHostCert(w http.ResponseWriter, r *http.Request) {
// RequireAndVerifyClientCert already guaranteed the peer chains to the fleet
// CA; the presented identity is who we're issuing for.
peerCN := hostPeerCN(r)
r.Body = http.MaxBytesReader(w, r.Body, 16<<10)
var req sshHostCertRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeSSHErr(w, http.StatusBadRequest, "invalid JSON body")
return
}
if req.KeyID == "" {
req.KeyID = peerCN
}
if len(req.Principals) == 0 {
writeSSHErr(w, http.StatusBadRequest, "no principals requested")
return
}
ctx, cancel := ctxWithTimeout(35 * time.Second)
defer cancel()
certLine, err := b.sshSigner.SignHost(ctx, req.PublicKey, req.KeyID, req.Principals)
if err != nil {
log.Printf("audit ssh-hostcert-FAILED cn=%q principals=%v err=%v", peerCN, req.Principals, err)
writeSSHErr(w, http.StatusBadGateway, "signing failed: "+err.Error())
return
}
log.Printf("audit SSH-HOSTCERT cn=%q keyid=%q principals=%v", peerCN, req.KeyID, req.Principals)
writeJSON(w, http.StatusOK, sshHostCertResponse{Cert: certLine})
}
func hostPeerCN(r *http.Request) string {
if r.TLS != nil && len(r.TLS.PeerCertificates) > 0 {
if cn := r.TLS.PeerCertificates[0].Subject.CommonName; cn != "" {
return cn
}
return "unknown-host"
}
return "dev-insecure"
}
func writeSSHErr(w http.ResponseWriter, code int, msg string) {
writeJSON(w, code, sshHostCertResponse{Error: msg})
}