ostension/internal/rotator/opnsense.go

137 lines
4.5 KiB
Go

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
}