feat: harden control-plane deployment
Build web service image / build (push) Successful in 48s

This commit is contained in:
2026-09-12 11:09:42 +08:00
parent 13c31fc902
commit 0d29b828bb
22 changed files with 1213 additions and 71 deletions
+79 -9
View File
@@ -5,6 +5,7 @@ import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"crypto/tls"
"encoding/base64"
"encoding/hex"
"encoding/json"
@@ -26,13 +27,24 @@ import (
)
type ServerConfig struct {
ListenAddr string
DataFile string
NodeTokens map[string]string
WebUsers map[string]string
LeaseTTL time.Duration
HeartbeatTimeout time.Duration
SessionTTL time.Duration
ListenAddr string
DataFile string
NodeTokens map[string]string
WebUsers map[string]string
LeaseTTL time.Duration
HeartbeatTimeout time.Duration
SessionTTL time.Duration
TLSCertFile string
TLSKeyFile string
MTLSClientCAFile string
MTLSRequireNodeCert bool
MTLSRevokedCertsFile string
BackupDir string
BackupCount int
BackupInterval time.Duration
TaskRetention time.Duration
EventRetention time.Duration
AuditRetention time.Duration
}
func DefaultServerConfig() ServerConfig {
@@ -44,12 +56,18 @@ func DefaultServerConfig() ServerConfig {
LeaseTTL: 30 * time.Second,
HeartbeatTimeout: 45 * time.Second,
SessionTTL: 8 * time.Hour,
BackupCount: 7,
BackupInterval: 5 * time.Minute,
TaskRetention: 30 * 24 * time.Hour,
EventRetention: 30 * 24 * time.Hour,
AuditRetention: 90 * 24 * time.Hour,
}
}
type Server struct {
config ServerConfig
store *Store
tlsConfig *tls.Config
nodeTokenHashes map[string][32]byte
userPasswords map[string][32]byte
sessionMu sync.Mutex
@@ -77,6 +95,24 @@ func NewServer(config ServerConfig) (*Server, error) {
if config.DataFile == "" {
config.DataFile = defaults.DataFile
}
if config.BackupDir == "" {
config.BackupDir = config.DataFile + ".backups"
}
if config.BackupCount == 0 {
config.BackupCount = defaults.BackupCount
}
if config.BackupInterval == 0 {
config.BackupInterval = defaults.BackupInterval
}
if config.TaskRetention == 0 {
config.TaskRetention = defaults.TaskRetention
}
if config.EventRetention == 0 {
config.EventRetention = defaults.EventRetention
}
if config.AuditRetention == 0 {
config.AuditRetention = defaults.AuditRetention
}
if config.LeaseTTL <= 0 {
config.LeaseTTL = defaults.LeaseTTL
}
@@ -92,13 +128,24 @@ func NewServer(config ServerConfig) (*Server, error) {
if config.WebUsers == nil {
config.WebUsers = map[string]string{}
}
store, err := OpenStore(config.DataFile)
if config.BackupCount < 0 || config.BackupInterval < 0 || config.TaskRetention < 0 || config.EventRetention < 0 || config.AuditRetention < 0 {
return nil, errors.New("backup count and retention settings must be non-negative")
}
tlsConfig, err := newTLSConfig(config)
if err != nil {
return nil, err
}
store, err := OpenStore(config.DataFile, StoreOptions{
BackupDir: config.BackupDir, BackupCount: config.BackupCount, BackupInterval: config.BackupInterval,
TaskRetention: config.TaskRetention, EventRetention: config.EventRetention, AuditRetention: config.AuditRetention,
})
if err != nil {
return nil, err
}
s := &Server{
config: config,
store: store,
tlsConfig: tlsConfig,
nodeTokenHashes: map[string][32]byte{},
userPasswords: map[string][32]byte{},
sessions: map[string]session{},
@@ -126,13 +173,23 @@ func (s *Server) ListenAndServe(ctx context.Context) error {
defer cancel()
_ = server.Shutdown(shutdownCtx)
}()
err := server.ListenAndServe()
defer func() { _ = s.Close() }()
var err error
if s.tlsConfig != nil {
server.TLSConfig = s.tlsConfig
err = server.ListenAndServeTLS("", "")
} else {
err = server.ListenAndServe()
}
if errors.Is(err, http.ErrServerClosed) {
return nil
}
return err
}
// Close releases the active/passive store lock.
func (s *Server) Close() error { return s.store.Close() }
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
correlationID := r.Header.Get("X-Correlation-Id")
if !validIdentifier(correlationID, 128) {
@@ -158,6 +215,8 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case r.URL.Path == "/healthz" && r.Method == http.MethodGet:
writeJSON(w, http.StatusOK, map[string]any{"status": "ok", "protocol_version": ProtocolVersion, "correlation_id": correlationID})
return
case r.URL.Path == "/readyz" && r.Method == http.MethodGet:
err = s.ready(w, correlationID)
case r.URL.Path == "/v1/auth/login" && r.Method == http.MethodPost:
err = s.login(w, r, correlationID)
case r.URL.Path == "/v1/nodes" && r.Method == http.MethodGet:
@@ -182,6 +241,14 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
func (s *Server) ready(w http.ResponseWriter, correlationID string) error {
if err := s.store.Read(func(PersistedState) error { return nil }); err != nil {
return requestError{status: http.StatusServiceUnavailable, code: "NotReady", message: "The control plane store is not ready."}
}
writeJSON(w, http.StatusOK, map[string]any{"status": "ready", "protocol_version": ProtocolVersion, "correlation_id": correlationID})
return nil
}
func (s *Server) login(w http.ResponseWriter, r *http.Request, correlationID string) error {
var request struct {
Username string `json:"username"`
@@ -1071,6 +1138,9 @@ func (s *Server) auditRoute(w http.ResponseWriter, r *http.Request, _ string) er
}
func (s *Server) authenticateNode(r *http.Request) (string, error) {
if !s.clientCertificateAllowed(r) {
return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Node certificate authentication failed."}
}
token := bearerToken(r)
if token == "" {
return "", requestError{status: http.StatusUnauthorized, code: "Unauthorized", message: "Node authentication is required."}