Files
rogee 0d29b828bb
Build web service image / build (push) Successful in 48s
feat: harden control-plane deployment
2026-09-12 11:09:42 +08:00

198 lines
6.4 KiB
Go

package controlplane
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/hex"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func TestStoreLocksDataAndKeepsBoundedBackups(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "state.json")
store, err := OpenStore(path, StoreOptions{BackupDir: filepath.Join(directory, "backups"), BackupCount: 2})
if err != nil {
t.Fatal(err)
}
if err := store.Mutate(func(state *PersistedState) error {
state.Nodes["node-1"] = Node{NodeID: "node-1"}
return nil
}); err != nil {
t.Fatal(err)
}
if err := store.Mutate(func(state *PersistedState) error {
state.Nodes["node-1"] = Node{NodeID: "node-1", AgentVersion: "2"}
return nil
}); err != nil {
t.Fatal(err)
}
if err := store.Mutate(func(state *PersistedState) error {
state.Nodes["node-1"] = Node{NodeID: "node-1", AgentVersion: "3"}
return nil
}); err != nil {
t.Fatal(err)
}
if _, err := OpenStore(path, StoreOptions{BackupDir: filepath.Join(directory, "backups"), BackupCount: 2}); err == nil || !strings.Contains(err.Error(), "already in use") {
t.Fatalf("second writer was not rejected: %v", err)
}
if err := store.Close(); err != nil {
t.Fatal(err)
}
entries, err := os.ReadDir(filepath.Join(directory, "backups"))
if err != nil {
t.Fatal(err)
}
if len(entries) != 2 {
t.Fatalf("expected two backups, got %d", len(entries))
}
for _, entry := range entries {
if entry.Type().Perm() != 0o600 {
info, infoErr := entry.Info()
if infoErr != nil {
t.Fatal(infoErr)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("backup permissions = %o", info.Mode().Perm())
}
}
}
second, err := OpenStore(path, StoreOptions{BackupDir: filepath.Join(directory, "backups"), BackupCount: 2})
if err != nil {
t.Fatal(err)
}
defer second.Close()
}
func TestStorePrunesOnlyTerminalRecordsByRetention(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, "state.json")
seed, err := OpenStore(path, StoreOptions{BackupDir: "", BackupCount: -1})
if err != nil {
t.Fatal(err)
}
old := time.Now().UTC().Add(-2 * time.Hour)
if err := seed.Mutate(func(state *PersistedState) error {
state.Tasks["old-terminal"] = Task{TaskID: "old-terminal", Status: TaskSucceeded, UpdatedAt: old}
state.Tasks["old-running"] = Task{TaskID: "old-running", Status: TaskRunning, UpdatedAt: old}
state.Events = append(state.Events, StoredEvent{ReceivedAt: old})
state.Audit = append(state.Audit, AuditEntry{At: old})
return nil
}); err != nil {
t.Fatal(err)
}
if err := seed.Close(); err != nil {
t.Fatal(err)
}
store, err := OpenStore(path, StoreOptions{
BackupDir: filepath.Join(directory, "backups"), BackupCount: -1,
TaskRetention: time.Hour, EventRetention: time.Hour, AuditRetention: time.Hour,
})
if err != nil {
t.Fatal(err)
}
defer store.Close()
if err := store.Mutate(func(state *PersistedState) error {
state.Nodes["node-1"] = Node{NodeID: "node-1"}
return nil
}); err != nil {
t.Fatal(err)
}
snapshot := store.Snapshot()
if _, ok := snapshot.Tasks["old-terminal"]; ok {
t.Fatal("old terminal task was retained")
}
if _, ok := snapshot.Tasks["old-running"]; !ok {
t.Fatal("old running task was pruned")
}
if len(snapshot.Events) != 0 || len(snapshot.Audit) != 0 {
t.Fatalf("old events/audit were retained: events=%d audit=%d", len(snapshot.Events), len(snapshot.Audit))
}
}
func TestTLSConfigSupportsVerifiedNodeClientCertificates(t *testing.T) {
directory := t.TempDir()
certFile, keyFile, caFile := writeTestCertificateMaterial(t, directory)
config := ServerConfig{
TLSCertFile: certFile,
TLSKeyFile: keyFile,
MTLSClientCAFile: caFile,
MTLSRequireNodeCert: true,
MTLSRevokedCertsFile: filepath.Join(directory, "revoked.txt"),
}
if err := os.WriteFile(config.MTLSRevokedCertsFile, []byte("# initially empty\n"), 0o600); err != nil {
t.Fatal(err)
}
tlsConfig, err := newTLSConfig(config)
if err != nil {
t.Fatal(err)
}
if tlsConfig.MinVersion != tls.VersionTLS13 || tlsConfig.ClientAuth != tls.VerifyClientCertIfGiven || len(tlsConfig.Certificates) != 1 {
t.Fatalf("unexpected TLS config: min=%d auth=%d certs=%d", tlsConfig.MinVersion, tlsConfig.ClientAuth, len(tlsConfig.Certificates))
}
rawCertificate := []byte("client-cert")
digest := sha256.Sum256(rawCertificate)
fingerprint := hex.EncodeToString(digest[:])
if err := os.WriteFile(config.MTLSRevokedCertsFile, []byte(fingerprint+"\n"), 0o600); err != nil {
t.Fatal(err)
}
server := &Server{config: config}
request := &http.Request{TLS: &tls.ConnectionState{PeerCertificates: []*x509.Certificate{{Raw: rawCertificate}}}}
if server.clientCertificateAllowed(request) {
t.Fatal("revoked certificate was accepted")
}
}
func TestTLSConfigRejectsIncompleteSecuritySettings(t *testing.T) {
if _, err := newTLSConfig(ServerConfig{TLSCertFile: "cert.pem"}); err == nil {
t.Fatal("incomplete TLS certificate settings were accepted")
}
if _, err := newTLSConfig(ServerConfig{MTLSRequireNodeCert: true}); err == nil {
t.Fatal("mTLS without TLS material was accepted")
}
}
func writeTestCertificateMaterial(t *testing.T, directory string) (string, string, string) {
t.Helper()
key, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatal(err)
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: "wxagent-test"},
NotBefore: time.Now().Add(-time.Minute),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign,
IsCA: true,
BasicConstraintsValid: true,
}
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
if err != nil {
t.Fatal(err)
}
certFile := filepath.Join(directory, "server.pem")
keyFile := filepath.Join(directory, "server-key.pem")
caFile := filepath.Join(directory, "client-ca.pem")
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
for path, data := range map[string][]byte{certFile: certPEM, keyFile: keyPEM, caFile: certPEM} {
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
}
return certFile, keyFile, caFile
}