mirror of
https://github.com/outbackdingo/certificates.git
synced 2026-01-27 18:18:30 +00:00
This commit replaces the client in provisioners and webhooks with an interface. Then it implements the interface using the new poolhttp package. This package implements the HTTPClient interface but it is backed by a sync.Pool, this improves memory, allowing the GC to clean more memory. It also removes the timer in the keystore to avoid having extra goroutines if a provisioner goes away. This commit avoids creating the templates func multiple times, reducing some memory in the heap.
258 lines
6.3 KiB
Go
258 lines
6.3 KiB
Go
package provisioner
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"net/http"
|
|
"text/template"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/smallstep/linkedca"
|
|
|
|
"github.com/smallstep/certificates/authority/poolhttp"
|
|
"github.com/smallstep/certificates/internal/httptransport"
|
|
"github.com/smallstep/certificates/middleware/requestid"
|
|
"github.com/smallstep/certificates/templates"
|
|
"github.com/smallstep/certificates/webhook"
|
|
)
|
|
|
|
var ErrWebhookDenied = errors.New("webhook server did not allow request")
|
|
|
|
type WebhookSetter interface {
|
|
SetWebhook(string, any)
|
|
}
|
|
|
|
type WebhookController struct {
|
|
client HTTPClient
|
|
wrapTransport httptransport.Wrapper
|
|
webhooks []*Webhook
|
|
certType linkedca.Webhook_CertType
|
|
options []webhook.RequestBodyOption
|
|
TemplateData WebhookSetter
|
|
}
|
|
|
|
// Enrich fetches data from remote servers and adds returned data to the
|
|
// templateData
|
|
func (wc *WebhookController) Enrich(ctx context.Context, req *webhook.RequestBody) error {
|
|
if wc == nil {
|
|
return nil
|
|
}
|
|
|
|
// Apply extra options in the webhook controller
|
|
for _, fn := range wc.options {
|
|
if err := fn(req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, wh := range wc.webhooks {
|
|
if wh.Kind != linkedca.Webhook_ENRICHING.String() {
|
|
continue
|
|
}
|
|
if !wc.isCertTypeOK(wh) {
|
|
continue
|
|
}
|
|
|
|
whCtx, cancel := context.WithTimeout(ctx, time.Second*10)
|
|
defer cancel() //nolint:gocritic // every request canceled with its own timeout
|
|
|
|
resp, err := wh.DoWithContext(whCtx, wc.client, wc.wrapTransport, req, wc.TemplateData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !resp.Allow {
|
|
if resp.Error != nil {
|
|
return resp.Error
|
|
}
|
|
return ErrWebhookDenied
|
|
}
|
|
wc.TemplateData.SetWebhook(wh.Name, resp.Data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Authorize checks that all remote servers allow the request
|
|
func (wc *WebhookController) Authorize(ctx context.Context, req *webhook.RequestBody) error {
|
|
if wc == nil {
|
|
return nil
|
|
}
|
|
|
|
// Apply extra options in the webhook controller
|
|
for _, fn := range wc.options {
|
|
if err := fn(req); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, wh := range wc.webhooks {
|
|
if wh.Kind != linkedca.Webhook_AUTHORIZING.String() {
|
|
continue
|
|
}
|
|
if !wc.isCertTypeOK(wh) {
|
|
continue
|
|
}
|
|
|
|
whCtx, cancel := context.WithTimeout(ctx, time.Second*10)
|
|
defer cancel() //nolint:gocritic // every request canceled with its own timeout
|
|
|
|
resp, err := wh.DoWithContext(whCtx, wc.client, wc.wrapTransport, req, wc.TemplateData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !resp.Allow {
|
|
if resp.Error != nil {
|
|
return resp.Error
|
|
}
|
|
return ErrWebhookDenied
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (wc *WebhookController) isCertTypeOK(wh *Webhook) bool {
|
|
if wc.certType == linkedca.Webhook_ALL {
|
|
return true
|
|
}
|
|
if wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == "" {
|
|
return true
|
|
}
|
|
return wc.certType.String() == wh.CertType
|
|
}
|
|
|
|
type Webhook struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
URL string `json:"url"`
|
|
Kind string `json:"kind"`
|
|
DisableTLSClientAuth bool `json:"disableTLSClientAuth,omitempty"`
|
|
CertType string `json:"certType"`
|
|
Secret string `json:"-"`
|
|
BearerToken string `json:"-"`
|
|
BasicAuth struct {
|
|
Username string
|
|
Password string
|
|
} `json:"-"`
|
|
}
|
|
|
|
// TransportWrapper wraps the set of functions mapping [http.Transport] references to
|
|
// [http.RoundTripper].
|
|
type TransportWrapper = httptransport.Wrapper
|
|
|
|
func (w *Webhook) DoWithContext(ctx context.Context, client HTTPClient, tw TransportWrapper, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) {
|
|
tmpl, err := template.New("url").Funcs(templates.StepFuncMap()).Parse(w.URL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buf := &bytes.Buffer{}
|
|
if err := tmpl.Execute(buf, data); err != nil {
|
|
return nil, err
|
|
}
|
|
url := buf.String()
|
|
|
|
/*
|
|
Sending the token to the webhook server is a security risk. A K8sSA
|
|
token can be reused multiple times. The webhook can misuse it to get
|
|
fake certificates. A webhook can misuse any other token to get its own
|
|
certificate before responding.
|
|
switch tmpl := data.(type) {
|
|
case x509util.TemplateData:
|
|
reqBody.Token = tmpl[x509util.TokenKey]
|
|
case sshutil.TemplateData:
|
|
reqBody.Token = tmpl[sshutil.TokenKey]
|
|
}
|
|
*/
|
|
|
|
reqBody.Timestamp = time.Now()
|
|
|
|
reqBytes, err := json.Marshal(reqBody)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
retries := 1
|
|
retry:
|
|
|
|
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(reqBytes))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if requestID, ok := requestid.FromContext(ctx); ok {
|
|
req.Header.Set("X-Request-Id", requestID)
|
|
}
|
|
|
|
secret, err := base64.StdEncoding.DecodeString(w.Secret)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
h := hmac.New(sha256.New, secret)
|
|
h.Write(reqBytes)
|
|
sig := h.Sum(nil)
|
|
req.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig))
|
|
req.Header.Set("X-Smallstep-Webhook-ID", w.ID)
|
|
|
|
if w.BearerToken != "" {
|
|
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.BearerToken))
|
|
} else if w.BasicAuth.Username != "" || w.BasicAuth.Password != "" {
|
|
req.SetBasicAuth(w.BasicAuth.Username, w.BasicAuth.Password)
|
|
}
|
|
|
|
if w.DisableTLSClientAuth {
|
|
var transport *http.Transport
|
|
if ct, ok := client.(poolhttp.Transporter); ok {
|
|
transport = ct.Transport()
|
|
} else {
|
|
transport = httptransport.New()
|
|
}
|
|
|
|
if transport.TLSClientConfig != nil {
|
|
transport.TLSClientConfig.GetClientCertificate = nil
|
|
transport.TLSClientConfig.Certificates = nil
|
|
}
|
|
|
|
client = &http.Client{
|
|
Transport: tw(transport),
|
|
}
|
|
}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
if errors.Is(err, context.DeadlineExceeded) {
|
|
return nil, err
|
|
} else if retries > 0 {
|
|
retries--
|
|
time.Sleep(time.Second)
|
|
goto retry
|
|
}
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
log.Printf("Failed to close body of response from %s", w.URL)
|
|
}
|
|
}()
|
|
if resp.StatusCode >= 500 && retries > 0 {
|
|
retries--
|
|
time.Sleep(time.Second)
|
|
goto retry
|
|
}
|
|
if resp.StatusCode >= 400 {
|
|
return nil, fmt.Errorf("Webhook server responded with %d", resp.StatusCode)
|
|
}
|
|
|
|
respBody := &webhook.ResponseBody{}
|
|
if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return respBody, nil
|
|
}
|