146 lines
5.3 KiB
Go
146 lines
5.3 KiB
Go
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:
|
|
}
|
|
}
|
|
}
|