ostension/internal/rotator/rotate.go

146 lines
5.3 KiB
Go
Raw Permalink Normal View History

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:
}
}
}