198 lines
6.4 KiB
Go
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
|
|
}
|