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