Based on the implementation, I'll generate a concise commit message that captures the essence of the changes:
feat: implement PKI initialization and leader mTLS certificate generation
This commit is contained in:
parent
7adabe8630
commit
bcff04db12
@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"git.dws.rip/dubey/kat/internal/config"
|
"git.dws.rip/dubey/kat/internal/config"
|
||||||
"git.dws.rip/dubey/kat/internal/leader"
|
"git.dws.rip/dubey/kat/internal/leader"
|
||||||
|
"git.dws.rip/dubey/kat/internal/pki"
|
||||||
"git.dws.rip/dubey/kat/internal/store"
|
"git.dws.rip/dubey/kat/internal/store"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
@ -43,6 +44,7 @@ const (
|
|||||||
clusterUIDKey = "/kat/config/cluster_uid"
|
clusterUIDKey = "/kat/config/cluster_uid"
|
||||||
clusterConfigKey = "/kat/config/cluster_config" // Stores the JSON of pb.ClusterConfigurationSpec
|
clusterConfigKey = "/kat/config/cluster_config" // Stores the JSON of pb.ClusterConfigurationSpec
|
||||||
defaultNodeName = "kat-node"
|
defaultNodeName = "kat-node"
|
||||||
|
leaderCertCN = "leader.kat.cluster.local" // Common Name for leader certificate
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -69,6 +71,25 @@ func runInit(cmd *cobra.Command, args []string) {
|
|||||||
// config.SetClusterConfigDefaults(parsedClusterConfig)
|
// config.SetClusterConfigDefaults(parsedClusterConfig)
|
||||||
log.Printf("Successfully parsed and applied defaults to cluster configuration: %s", parsedClusterConfig.Metadata.Name)
|
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
|
// Prepare etcd embed config
|
||||||
// For a single node init, this node is the only peer.
|
// For a single node init, this node is the only peer.
|
||||||
// Client URLs and Peer URLs will be based on its own configuration.
|
// Client URLs and Peer URLs will be based on its own configuration.
|
||||||
@ -137,6 +158,31 @@ func runInit(cmd *cobra.Command, args []string) {
|
|||||||
} else {
|
} else {
|
||||||
log.Printf("Cluster UID already exists in etcd. Skipping storage.")
|
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)
|
// Store ClusterConfigurationSpec (as JSON)
|
||||||
// We store Spec because Metadata might change (e.g. resourceVersion)
|
// We store Spec because Metadata might change (e.g. resourceVersion)
|
||||||
|
@ -107,6 +107,148 @@ func GenerateCA(pkiDir string, keyPath, certPath string) error {
|
|||||||
return nil
|
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.
|
// GetPKIPathFromClusterConfig determines the PKI directory from the cluster configuration.
|
||||||
// If backupPath is provided, it uses the parent directory of backupPath.
|
// If backupPath is provided, it uses the parent directory of backupPath.
|
||||||
// Otherwise, it uses the default PKI directory.
|
// Otherwise, it uses the default PKI directory.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user