Add Vault certificate rotation operator
This commit is contained in:
parent
c685229437
commit
8009b5fd9c
1 changed files with 1013 additions and 0 deletions
137
internal/rotator/opnsense.go
Normal file
137
internal/rotator/opnsense.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
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
|
||||
}
|
||||
Loading…
Reference in a new issue