diff --git a/cmd/kat-agent/main.go b/cmd/kat-agent/main.go index 730593b..55e14e8 100644 --- a/cmd/kat-agent/main.go +++ b/cmd/kat-agent/main.go @@ -12,6 +12,7 @@ import ( "git.dws.rip/dubey/kat/internal/config" "git.dws.rip/dubey/kat/internal/leader" + "git.dws.rip/dubey/kat/internal/pki" "git.dws.rip/dubey/kat/internal/store" "github.com/google/uuid" "github.com/spf13/cobra" @@ -43,6 +44,7 @@ const ( clusterUIDKey = "/kat/config/cluster_uid" clusterConfigKey = "/kat/config/cluster_config" // Stores the JSON of pb.ClusterConfigurationSpec defaultNodeName = "kat-node" + leaderCertCN = "leader.kat.cluster.local" // Common Name for leader certificate ) func init() { @@ -69,6 +71,25 @@ func runInit(cmd *cobra.Command, args []string) { // config.SetClusterConfigDefaults(parsedClusterConfig) log.Printf("Successfully parsed and applied defaults to cluster configuration: %s", parsedClusterConfig.Metadata.Name) + // 1.5. Initialize PKI directory and CA if it doesn't exist + pkiDir := pki.GetPKIPathFromClusterConfig(parsedClusterConfig.Spec.BackupPath) + caKeyPath := filepath.Join(pkiDir, "ca.key") + caCertPath := filepath.Join(pkiDir, "ca.crt") + + // Check if CA already exists + _, caKeyErr := os.Stat(caKeyPath) + _, caCertErr := os.Stat(caCertPath) + + if os.IsNotExist(caKeyErr) || os.IsNotExist(caCertErr) { + log.Printf("CA key or certificate not found. Generating new CA in %s", pkiDir) + if err := pki.GenerateCA(pkiDir, caKeyPath, caCertPath); err != nil { + log.Fatalf("Failed to generate CA: %v", err) + } + log.Println("Successfully generated new CA key and certificate") + } else { + log.Println("CA key and certificate already exist, skipping generation") + } + // Prepare etcd embed config // For a single node init, this node is the only peer. // Client URLs and Peer URLs will be based on its own configuration. @@ -137,6 +158,31 @@ func runInit(cmd *cobra.Command, args []string) { } else { log.Printf("Cluster UID already exists in etcd. Skipping storage.") } + + // Generate leader's server certificate for mTLS + leaderKeyPath := filepath.Join(pkiDir, "leader.key") + leaderCSRPath := filepath.Join(pkiDir, "leader.csr") + leaderCertPath := filepath.Join(pkiDir, "leader.crt") + + // Check if leader cert already exists + _, leaderCertErr := os.Stat(leaderCertPath) + if os.IsNotExist(leaderCertErr) { + log.Println("Generating leader server certificate for mTLS") + + // Generate key and CSR for leader + if err := pki.GenerateCertificateRequest(leaderCertCN, leaderKeyPath, leaderCSRPath); err != nil { + log.Printf("Failed to generate leader key and CSR: %v", err) + } else { + // Sign the CSR with our CA + if err := pki.SignCertificateRequest(caKeyPath, caCertPath, leaderCSRPath, leaderCertPath, 365*24*time.Hour); err != nil { + log.Printf("Failed to sign leader CSR: %v", err) + } else { + log.Println("Successfully generated and signed leader server certificate") + } + } + } else { + log.Println("Leader certificate already exists, skipping generation") + } // Store ClusterConfigurationSpec (as JSON) // We store Spec because Metadata might change (e.g. resourceVersion) diff --git a/internal/pki/ca.go b/internal/pki/ca.go index 3f6b2f6..cb1d5f5 100644 --- a/internal/pki/ca.go +++ b/internal/pki/ca.go @@ -107,6 +107,148 @@ func GenerateCA(pkiDir string, keyPath, certPath string) error { return nil } +// GenerateCertificateRequest creates a new key pair and a Certificate Signing Request (CSR). +// It saves the private key and CSR to the specified paths. +func GenerateCertificateRequest(commonName, keyOutPath, csrOutPath string) error { + // Generate RSA key + key, err := rsa.GenerateKey(rand.Reader, DefaultRSAKeySize) + if err != nil { + return fmt.Errorf("failed to generate key: %w", err) + } + + // Create CSR template + template := x509.CertificateRequest{ + Subject: pkix.Name{ + CommonName: commonName, + Organization: []string{"KAT System"}, + }, + SignatureAlgorithm: x509.SHA256WithRSA, + } + + // Create CSR + csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &template, key) + if err != nil { + return fmt.Errorf("failed to create CSR: %w", err) + } + + // Save private key + keyOut, err := os.OpenFile(keyOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600) + if err != nil { + return fmt.Errorf("failed to open key file for writing: %w", err) + } + defer keyOut.Close() + + err = pem.Encode(keyOut, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(key), + }) + if err != nil { + return fmt.Errorf("failed to write key to file: %w", err) + } + + // Save CSR + csrOut, err := os.OpenFile(csrOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open CSR file for writing: %w", err) + } + defer csrOut.Close() + + err = pem.Encode(csrOut, &pem.Block{ + Type: "CERTIFICATE REQUEST", + Bytes: csrBytes, + }) + if err != nil { + return fmt.Errorf("failed to write CSR to file: %w", err) + } + + return nil +} + +// SignCertificateRequest signs a CSR using the CA key and certificate. +// It reads the CSR from csrPath and saves the signed certificate to certOutPath. +func SignCertificateRequest(caKeyPath, caCertPath, csrPath, certOutPath string, duration time.Duration) error { + // Load CA key + caKey, err := LoadCAPrivateKey(caKeyPath) + if err != nil { + return fmt.Errorf("failed to load CA key: %w", err) + } + + // Load CA certificate + caCert, err := LoadCACertificate(caCertPath) + if err != nil { + return fmt.Errorf("failed to load CA certificate: %w", err) + } + + // Read CSR + csrPEM, err := os.ReadFile(csrPath) + if err != nil { + return fmt.Errorf("failed to read CSR file: %w", err) + } + + block, _ := pem.Decode(csrPEM) + if block == nil || block.Type != "CERTIFICATE REQUEST" { + return fmt.Errorf("failed to decode PEM block containing CSR") + } + + csr, err := x509.ParseCertificateRequest(block.Bytes) + if err != nil { + return fmt.Errorf("failed to parse CSR: %w", err) + } + + // Verify CSR signature + if err = csr.CheckSignature(); err != nil { + return fmt.Errorf("CSR signature verification failed: %w", err) + } + + // Create certificate template from CSR + serialNumber, err := generateSerialNumber() + if err != nil { + return fmt.Errorf("failed to generate serial number: %w", err) + } + + notBefore := time.Now() + notAfter := notBefore.Add(duration) + + template := x509.Certificate{ + SerialNumber: serialNumber, + Subject: csr.Subject, + NotBefore: notBefore, + NotAfter: notAfter, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth}, + DNSNames: []string{csr.Subject.CommonName}, // Add the CN as a SAN + } + + // Create certificate + derBytes, err := x509.CreateCertificate( + rand.Reader, + &template, + caCert, + csr.PublicKey, + caKey, + ) + if err != nil { + return fmt.Errorf("failed to create certificate: %w", err) + } + + // Save certificate + certOut, err := os.OpenFile(certOutPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return fmt.Errorf("failed to open certificate file for writing: %w", err) + } + defer certOut.Close() + + err = pem.Encode(certOut, &pem.Block{ + Type: "CERTIFICATE", + Bytes: derBytes, + }) + if err != nil { + return fmt.Errorf("failed to write certificate to file: %w", err) + } + + return nil +} + // GetPKIPathFromClusterConfig determines the PKI directory from the cluster configuration. // If backupPath is provided, it uses the parent directory of backupPath. // Otherwise, it uses the default PKI directory.