Add Vault certificate rotation operator
This commit is contained in:
parent
c685229437
commit
8009b5fd9c
1 changed files with 1013 additions and 0 deletions
146
internal/rotator/rotate.go
Normal file
146
internal/rotator/rotate.go
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
package rotator
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
func rotate(ctx context.Context, configuration config, logger *slog.Logger) error {
|
||||
vaultHTTP, err := tlsClient([]string{configuration.vaultCAPath}, configuration.vaultServerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
primaryHTTP, err := tlsClient([]string{configuration.opnsenseManagedCAPath}, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bootstrapHTTP, err := tlsClient([]string{configuration.opnsenseBootstrapCAPath}, configuration.opnsenseBootstrapName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
apiKey, err := readTrimmed(configuration.opnsenseAPIKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read OPNsense API key: %w", err)
|
||||
}
|
||||
apiSecret, err := readTrimmed(configuration.opnsenseAPISecretPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read OPNsense API secret: %w", err)
|
||||
}
|
||||
|
||||
primary := opnsenseClient{baseURL: configuration.opnsenseURL, apiKey: apiKey, apiSecret: apiSecret, client: primaryHTTP}
|
||||
current := primary
|
||||
status, certificate, primaryError := primary.status(ctx)
|
||||
if primaryError == nil {
|
||||
if status.Status == "pending" {
|
||||
return fmt.Errorf("OPNsense has pending transaction %s", status.Transaction)
|
||||
}
|
||||
if certificateComplies(certificate, configuration.opnsenseIPSANs) && time.Until(certificate.NotAfter) > configuration.renewBefore {
|
||||
logger.Info("certificate remains valid", "not_after", certificate.NotAfter.UTC().Format(time.RFC3339))
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if !configuration.bootstrapMode {
|
||||
return fmt.Errorf("connect to OPNsense with managed trust: %w", primaryError)
|
||||
}
|
||||
current = opnsenseClient{baseURL: configuration.opnsenseURL, apiKey: apiKey, apiSecret: apiSecret, client: bootstrapHTTP}
|
||||
status, _, err = current.status(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect to OPNsense with managed and bootstrap trust: managed=%v bootstrap=%w", primaryError, err)
|
||||
}
|
||||
if status.Status == "pending" {
|
||||
return fmt.Errorf("OPNsense has pending transaction %s", status.Transaction)
|
||||
}
|
||||
logger.Info("using bootstrap OPNsense trust")
|
||||
}
|
||||
|
||||
jwt, err := readTrimmed(configuration.serviceAccountTokenPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read service account token: %w", err)
|
||||
}
|
||||
vault := vaultClient{baseURL: configuration.vaultURL, client: vaultHTTP}
|
||||
token, err := vault.login(ctx, configuration.vaultAuthRole, jwt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
privateKey, request, err := generateRequest(configuration.opnsenseIPSANs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signed, err := vault.sign(ctx, configuration.vaultPKISignPath, token, configuration.opnsenseManagedCAPath, configuration.opnsenseIPSANs, configuration.certificateTTL, privateKey, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reference, err := current.stage(ctx, signed)
|
||||
if err != nil {
|
||||
return fmt.Errorf("stage certificate: %w", err)
|
||||
}
|
||||
logger.Info("certificate staged", "not_after", signed.leaf.NotAfter.UTC().Format(time.RFC3339))
|
||||
|
||||
activation, err := current.activate(ctx, reference)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("certificate activation pending", "deadline", time.Unix(activation.Deadline, 0).UTC().Format(time.RFC3339))
|
||||
|
||||
current.client.CloseIdleConnections()
|
||||
primary.client.CloseIdleConnections()
|
||||
if err := waitForCandidate(ctx, primary, reference, activation.Transaction, signed.leaf.Raw); err != nil {
|
||||
return err
|
||||
}
|
||||
logger.Info("certificate activation confirmed", "not_after", signed.leaf.NotAfter.UTC().Format(time.RFC3339))
|
||||
return nil
|
||||
}
|
||||
|
||||
func certificateComplies(certificate *x509.Certificate, addresses []net.IP) bool {
|
||||
return len(certificate.DNSNames) == 0 && hasExactIPSANs(certificate, addresses)
|
||||
}
|
||||
|
||||
type activationClient interface {
|
||||
status(context.Context) (activationStatus, *x509.Certificate, error)
|
||||
confirm(context.Context, string) error
|
||||
}
|
||||
|
||||
func waitForCandidate(ctx context.Context, client activationClient, reference, transaction string, expectedLeaf []byte) error {
|
||||
return waitForCandidateWithInterval(ctx, client, reference, transaction, expectedLeaf, 2*time.Second)
|
||||
}
|
||||
|
||||
func waitForCandidateWithInterval(ctx context.Context, client activationClient, reference, transaction string, expectedLeaf []byte, interval time.Duration) error {
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
var lastError error
|
||||
for {
|
||||
status, certificate, err := client.status(ctx)
|
||||
if err != nil {
|
||||
lastError = err
|
||||
} else if status.Status == "idle" || status.Status == "expired" {
|
||||
return fmt.Errorf("certificate activation ended before confirmation: %s", status.Status)
|
||||
} else if status.Transaction != transaction || status.CandidateRefID != reference {
|
||||
return fmt.Errorf("certificate activation transaction changed")
|
||||
} else if bytes.Equal(certificate.Raw, expectedLeaf) && status.Status == "pending" && status.Phase == "activated" {
|
||||
if err := client.confirm(ctx, transaction); err != nil {
|
||||
return err
|
||||
}
|
||||
confirmed, confirmedCertificate, err := client.status(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if confirmed.Status != "idle" || confirmed.RefID != reference || !bytes.Equal(confirmedCertificate.Raw, expectedLeaf) {
|
||||
return fmt.Errorf("OPNsense did not retain confirmed certificate")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if lastError != nil {
|
||||
return fmt.Errorf("wait for candidate certificate: %w: last status error: %v", ctx.Err(), lastError)
|
||||
}
|
||||
return fmt.Errorf("wait for candidate certificate: %w", ctx.Err())
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue