forked from wrenn/wrenn
Add mTLS to CP→agent channel
- Internal ECDSA P-256 CA (WRENN_CA_CERT/WRENN_CA_KEY env vars); when absent the system falls back to plain HTTP so dev mode works without certificates - Host leaf cert (7-day TTL, IP SAN) issued at registration and renewed on every JWT refresh; fingerprint + expiry stored in DB (cert_expires_at column replaces the removed mtls_enabled flag) - CP ephemeral client cert (24-hour TTL) via CPCertStore with atomic hot-swap; background goroutine renews it every 12 hours without restarting the server - Host agent uses tls.Listen + httpServer.Serve so GetCertificate callback is respected (ListenAndServeTLS always reads cert from disk) - Sandbox reverse proxy now uses pool.Transport() so it shares the same TLS config as the Connect RPC clients instead of http.DefaultTransport - Credentials file renamed host-credentials.json with cert_pem/key_pem/ ca_cert_pem fields; duplicate register/refresh response structs collapsed to authResponse
This commit is contained in:
@ -15,6 +15,7 @@ import (
|
||||
|
||||
"git.omukk.dev/wrenn/sandbox/internal/api"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/audit"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/auth"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/auth/oauth"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/config"
|
||||
"git.omukk.dev/wrenn/sandbox/internal/db"
|
||||
@ -68,8 +69,52 @@ func main() {
|
||||
}
|
||||
slog.Info("connected to redis")
|
||||
|
||||
// mTLS: parse internal CA and build a TLS-capable host client pool.
|
||||
// When CA env vars are absent the pool falls back to plain HTTP (dev mode).
|
||||
var ca *auth.CA
|
||||
if cfg.CACert != "" && cfg.CAKey != "" {
|
||||
var err error
|
||||
ca, err = auth.ParseCA(cfg.CACert, cfg.CAKey)
|
||||
if err != nil {
|
||||
slog.Error("failed to parse mTLS CA from environment", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
slog.Info("mTLS enabled: CA loaded")
|
||||
} else {
|
||||
slog.Warn("mTLS disabled: WRENN_CA_CERT/WRENN_CA_KEY not set — host agent connections are unencrypted")
|
||||
}
|
||||
|
||||
// Host client pool — manages Connect RPC clients to host agents.
|
||||
hostPool := lifecycle.NewHostClientPool()
|
||||
var hostPool *lifecycle.HostClientPool
|
||||
if ca != nil {
|
||||
cpCertStore, err := auth.NewCPCertStore(ca)
|
||||
if err != nil {
|
||||
slog.Error("failed to issue CP client certificate", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
// Renew the CP client certificate periodically so it never expires
|
||||
// while the control plane is running (TTL = 24h, renewal = every 12h).
|
||||
go func() {
|
||||
ticker := time.NewTicker(auth.CPCertRenewInterval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := cpCertStore.Refresh(); err != nil {
|
||||
slog.Error("failed to renew CP client certificate", "error", err)
|
||||
} else {
|
||||
slog.Info("CP client certificate renewed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
hostPool = lifecycle.NewHostClientPoolTLS(auth.CPClientTLSConfig(ca, cpCertStore))
|
||||
slog.Info("host client pool: mTLS enabled")
|
||||
} else {
|
||||
hostPool = lifecycle.NewHostClientPool()
|
||||
}
|
||||
|
||||
// Scheduler — picks a host for each new sandbox (round-robin for now).
|
||||
hostScheduler := scheduler.NewRoundRobinScheduler(queries)
|
||||
@ -88,7 +133,7 @@ func main() {
|
||||
}
|
||||
|
||||
// API server.
|
||||
srv := api.New(queries, hostPool, hostScheduler, pool, rdb, []byte(cfg.JWTSecret), oauthRegistry, cfg.OAuthRedirectURL)
|
||||
srv := api.New(queries, hostPool, hostScheduler, pool, rdb, []byte(cfg.JWTSecret), oauthRegistry, cfg.OAuthRedirectURL, ca)
|
||||
|
||||
// Start template build workers (2 concurrent).
|
||||
stopBuildWorkers := srv.BuildSvc.StartWorkers(ctx, 2)
|
||||
|
||||
Reference in New Issue
Block a user