mirror of
https://github.com/smallstep/certificates.git
synced 2024-11-15 18:12:59 +00:00
240 lines
5.8 KiB
Go
240 lines
5.8 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/certificates/internal/requestid"
|
|
"github.com/smallstep/certificates/templates"
|
|
"github.com/smallstep/certificates/webhook"
|
|
"go.step.sm/linkedca"
|
|
)
|
|
|
|
var ErrWebhookDenied = errors.New("webhook server did not allow request")
|
|
|
|
type WebhookSetter interface {
|
|
SetWebhook(string, any)
|
|
}
|
|
|
|
type WebhookController struct {
|
|
client *http.Client
|
|
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, req, wc.TemplateData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !resp.Allow {
|
|
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, req, wc.TemplateData)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !resp.Allow {
|
|
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:"-"`
|
|
}
|
|
|
|
func (w *Webhook) DoWithContext(ctx context.Context, client *http.Client, 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 {
|
|
transport, ok := client.Transport.(*http.Transport)
|
|
if !ok {
|
|
return nil, errors.New("client transport is not a *http.Transport")
|
|
}
|
|
transport = transport.Clone()
|
|
tlsConfig := transport.TLSClientConfig.Clone()
|
|
tlsConfig.GetClientCertificate = nil
|
|
tlsConfig.Certificates = nil
|
|
transport.TLSClientConfig = tlsConfig
|
|
client = &http.Client{
|
|
Transport: 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
|
|
}
|