package rotator import ( "context" "crypto/x509" "fmt" "net/http" "net/url" "strings" ) type opnsenseClient struct { baseURL string apiKey string apiSecret string client *http.Client } type activationStatus struct { Status string `json:"status"` Phase string `json:"phase"` Transaction string `json:"transaction"` PreviousRefID string `json:"previous_refid"` CandidateRefID string `json:"candidate_refid"` RefID string `json:"refid"` Deadline int64 `json:"deadline"` Message string `json:"message"` } type addCertificateResponse struct { Result string `json:"result"` UUID string `json:"uuid"` Validations map[string]string `json:"validations"` } type getCertificateResponse struct { Certificate struct { RefID string `json:"refid"` } `json:"cert"` } func (o opnsenseClient) authenticatedRequest(ctx context.Context, method, path string, body any, output any) (*http.Response, error) { endpoint, err := url.JoinPath(o.baseURL, path) if err != nil { return nil, err } client := roundTripperClient(o.client, func(request *http.Request) { request.SetBasicAuth(o.apiKey, o.apiSecret) }) return jsonRequest(ctx, client, method, endpoint, body, nil, output) } func roundTripperClient(source *http.Client, mutate func(*http.Request)) *http.Client { clone := *source clone.Transport = mutatingRoundTripper{next: source.Transport, mutate: mutate} return &clone } type mutatingRoundTripper struct { next http.RoundTripper mutate func(*http.Request) } func (m mutatingRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) { clone := request.Clone(request.Context()) m.mutate(clone) return m.next.RoundTrip(clone) } func (o opnsenseClient) status(ctx context.Context) (activationStatus, *x509.Certificate, error) { var status activationStatus response, err := o.authenticatedRequest(ctx, http.MethodGet, "/api/webguicertificate/service/status", nil, &status) if err != nil { return activationStatus{}, nil, err } if status.Status == "failed" { return activationStatus{}, nil, fmt.Errorf("OPNsense activation status failed: %s", status.Message) } if response.TLS == nil || len(response.TLS.PeerCertificates) == 0 { return activationStatus{}, nil, fmt.Errorf("OPNsense response has no TLS peer certificate") } return status, response.TLS.PeerCertificates[0], nil } func (o opnsenseClient) stage(ctx context.Context, certificate signedCertificate) (string, error) { var added addCertificateResponse description := "Vault-managed OPNsense WebGUI " + strings.ToLower(certificate.leaf.SerialNumber.Text(16)) body := map[string]any{"cert": map[string]any{ "action": "import", "descr": description, "cert_type": "server_cert", "private_key_location": "firewall", "key_type": "prime256v1", "digest": "sha256", "lifetime": "90", "country": "KR", "crt_payload": string(certificate.chain), "prv_payload": string(certificate.key), }} if _, err := o.authenticatedRequest(ctx, http.MethodPost, "/api/trust/cert/add", body, &added); err != nil { return "", err } if added.Result != "saved" || added.UUID == "" { return "", fmt.Errorf("OPNsense rejected certificate: %v", added.Validations) } var stored getCertificateResponse if _, err := o.authenticatedRequest(ctx, http.MethodGet, "/api/trust/cert/get/"+added.UUID, nil, &stored); err != nil { return "", err } if stored.Certificate.RefID == "" { return "", fmt.Errorf("OPNsense stored certificate has no reference") } return stored.Certificate.RefID, nil } func (o opnsenseClient) activate(ctx context.Context, reference string) (activationStatus, error) { var status activationStatus if _, err := o.authenticatedRequest(ctx, http.MethodPost, "/api/webguicertificate/service/activate", map[string]string{"refid": reference}, &status); err != nil { return activationStatus{}, err } if status.Status != "pending" || status.Transaction == "" { return activationStatus{}, fmt.Errorf("OPNsense activation failed: %s", status.Message) } return status, nil } func (o opnsenseClient) confirm(ctx context.Context, transaction string) error { var status activationStatus if _, err := o.authenticatedRequest(ctx, http.MethodPost, "/api/webguicertificate/service/confirm", map[string]string{"transaction": transaction}, &status); err != nil { return err } if status.Status != "confirmed" { return fmt.Errorf("OPNsense confirmation failed: %s", status.Message) } return nil }