diff --git a/cmd/config/notify/help.go b/cmd/config/notify/help.go index a92127819..05f7d9a44 100644 --- a/cmd/config/notify/help.go +++ b/cmd/config/notify/help.go @@ -59,6 +59,18 @@ var ( Optional: true, Type: "sentence", }, + config.HelpKV{ + Key: target.WebhookClientCert, + Description: "client cert for Webhook mTLS auth", + Optional: true, + Type: "string", + }, + config.HelpKV{ + Key: target.WebhookClientKey, + Description: "client cert key for Webhook mTLS auth", + Optional: true, + Type: "string", + }, } HelpAMQP = config.HelpKVS{ diff --git a/cmd/config/notify/legacy.go b/cmd/config/notify/legacy.go index 30f15059f..1c79965f5 100644 --- a/cmd/config/notify/legacy.go +++ b/cmd/config/notify/legacy.go @@ -281,6 +281,14 @@ func SetNotifyWebhook(s config.Config, whName string, cfg target.WebhookArgs) er Key: target.WebhookQueueLimit, Value: strconv.Itoa(int(cfg.QueueLimit)), }, + config.KV{ + Key: target.WebhookClientCert, + Value: cfg.ClientCert, + }, + config.KV{ + Key: target.WebhookClientKey, + Value: cfg.ClientKey, + }, } return nil diff --git a/cmd/config/notify/parse.go b/cmd/config/notify/parse.go index a8953d35a..93f603d49 100644 --- a/cmd/config/notify/parse.go +++ b/cmd/config/notify/parse.go @@ -1428,6 +1428,14 @@ var ( Key: target.WebhookQueueDir, Value: "", }, + config.KV{ + Key: target.WebhookClientCert, + Value: "", + }, + config.KV{ + Key: target.WebhookClientKey, + Value: "", + }, } ) @@ -1471,6 +1479,15 @@ func GetNotifyWebhook(webhookKVS map[string]config.KVS, transport *http.Transpor if k != config.Default { authEnv = authEnv + config.Default + k } + clientCertEnv := target.EnvWebhookClientCert + if k != config.Default { + clientCertEnv = clientCertEnv + config.Default + k + } + + clientKeyEnv := target.EnvWebhookClientKey + if k != config.Default { + clientKeyEnv = clientKeyEnv + config.Default + k + } webhookArgs := target.WebhookArgs{ Enable: enabled, @@ -1479,6 +1496,8 @@ func GetNotifyWebhook(webhookKVS map[string]config.KVS, transport *http.Transpor AuthToken: env.Get(authEnv, kv.Get(target.WebhookAuthToken)), QueueDir: env.Get(queueDirEnv, kv.Get(target.WebhookQueueDir)), QueueLimit: uint64(queueLimit), + ClientCert: env.Get(clientCertEnv, kv.Get(target.WebhookClientCert)), + ClientKey: env.Get(clientKeyEnv, kv.Get(target.WebhookClientKey)), } if err = webhookArgs.Validate(); err != nil { return nil, err diff --git a/docs/bucket/notifications/README.md b/docs/bucket/notifications/README.md index ef0d6274a..1969ed23b 100644 --- a/docs/bucket/notifications/README.md +++ b/docs/bucket/notifications/README.md @@ -1241,6 +1241,8 @@ endpoint* (url) webhook server endpoint e.g. http://localhost:8080/mini auth_token (string) opaque string or JWT authorization token queue_dir (path) staging dir for undelivered messages e.g. '/home/events' queue_limit (number) maximum limit for undelivered messages, defaults to '100000' +client_cert (string) client cert for Webhook mTLS auth +client_key (string) client cert key for Webhook mTLS auth comment (sentence) optionally add a comment to this setting ``` @@ -1256,11 +1258,13 @@ MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN (string) opaque string or JWT authorization MINIO_NOTIFY_WEBHOOK_QUEUE_DIR (path) staging dir for undelivered messages e.g. '/home/events' MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT (number) maximum limit for undelivered messages, defaults to '100000' MINIO_NOTIFY_WEBHOOK_COMMENT (sentence) optionally add a comment to this setting +MINIO_NOTIFY_WEBHOOK_CLIENT_CERT (string) client cert for Webhook mTLS auth +MINIO_NOTIFY_WEBHOOK_CLIENT_KEY (string) client cert key for Webhook mTLS auth ``` ```sh $ mc admin config get myminio/ notify_webhook -notify_webhook:1 queue_limit="0" endpoint="" queue_dir="" +notify_webhook:1 endpoint="" auth_token="" queue_limit="0" queue_dir="" client_cert="" client_key="" ``` Use `mc admin config set` command to update the configuration for the deployment. Here the endpoint is the server listening for webhook notifications. Save the settings and restart the MinIO server for changes to take effect. Note that the endpoint needs to be live and reachable when you restart your MinIO server. diff --git a/pkg/certs/certs.go b/pkg/certs/certs.go index 758782b1b..1888db90f 100644 --- a/pkg/certs/certs.go +++ b/pkg/certs/certs.go @@ -185,6 +185,14 @@ func (c *Certs) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, er return c.cert, nil } +// GetClientCertificate returns the loaded certificate for use by +// the TLSConfig fields GetClientCertificate field in a http.Server. +func (c *Certs) GetClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) { + c.RLock() + defer c.RUnlock() + return c.cert, nil +} + // Stop tells loader to stop watching for changes to the // certificate and key files. func (c *Certs) Stop() { diff --git a/pkg/certs/certs_test.go b/pkg/certs/certs_test.go index 168fbd837..2eeaac44e 100644 --- a/pkg/certs/certs_test.go +++ b/pkg/certs/certs_test.go @@ -93,6 +93,16 @@ func TestValidPairAfterWrite(t *testing.T) { if !reflect.DeepEqual(gcert.Certificate, expectedCert.Certificate) { t.Error("certificate doesn't match expected certificate") } + + rInfo := &tls.CertificateRequestInfo{} + gcert, err = c.GetClientCertificate(rInfo) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(gcert.Certificate, expectedCert.Certificate) { + t.Error("client certificate doesn't match expected certificate") + } } func TestStop(t *testing.T) { diff --git a/pkg/event/target/webhook.go b/pkg/event/target/webhook.go index 5f6e3e672..7cfa62f53 100644 --- a/pkg/event/target/webhook.go +++ b/pkg/event/target/webhook.go @@ -19,6 +19,7 @@ package target import ( "bytes" "context" + "crypto/tls" "encoding/json" "errors" "fmt" @@ -30,6 +31,7 @@ import ( "path/filepath" "time" + "github.com/minio/minio/pkg/certs" "github.com/minio/minio/pkg/event" xnet "github.com/minio/minio/pkg/net" ) @@ -40,12 +42,16 @@ const ( WebhookAuthToken = "auth_token" WebhookQueueDir = "queue_dir" WebhookQueueLimit = "queue_limit" + WebhookClientCert = "client_cert" + WebhookClientKey = "client_key" EnvWebhookEnable = "MINIO_NOTIFY_WEBHOOK_ENABLE" EnvWebhookEndpoint = "MINIO_NOTIFY_WEBHOOK_ENDPOINT" EnvWebhookAuthToken = "MINIO_NOTIFY_WEBHOOK_AUTH_TOKEN" EnvWebhookQueueDir = "MINIO_NOTIFY_WEBHOOK_QUEUE_DIR" EnvWebhookQueueLimit = "MINIO_NOTIFY_WEBHOOK_QUEUE_LIMIT" + EnvWebhookClientCert = "MINIO_NOTIFY_WEBHOOK_CLIENT_CERT" + EnvWebhookClientKey = "MINIO_NOTIFY_WEBHOOK_CLIENT_KEY" ) // WebhookArgs - Webhook target arguments. @@ -56,6 +62,8 @@ type WebhookArgs struct { Transport *http.Transport `json:"-"` QueueDir string `json:"queueDir"` QueueLimit uint64 `json:"queueLimit"` + ClientCert string `json:"clientCert"` + ClientKey string `json:"clientKey"` } // Validate WebhookArgs fields @@ -71,6 +79,9 @@ func (w WebhookArgs) Validate() error { return errors.New("queueDir path should be absolute") } } + if w.ClientCert != "" && w.ClientKey == "" || w.ClientCert == "" && w.ClientKey != "" { + return errors.New("cert and key must be specified as a pair") + } return nil } @@ -209,14 +220,20 @@ func NewWebhookTarget(id string, args WebhookArgs, doneCh <-chan struct{}, logge var store Store target := &WebhookTarget{ - id: event.TargetID{ID: id, Name: "webhook"}, - args: args, - httpClient: &http.Client{ - Transport: transport, - }, + id: event.TargetID{ID: id, Name: "webhook"}, + args: args, loggerOnce: loggerOnce, } + if target.args.ClientCert != "" && target.args.ClientKey != "" { + c, err := certs.New(target.args.ClientCert, target.args.ClientKey, tls.LoadX509KeyPair) + if err != nil { + return target, err + } + transport.TLSClientConfig.GetClientCertificate = c.GetClientCertificate + } + target.httpClient = &http.Client{Transport: transport} + if args.QueueDir != "" { queueDir := filepath.Join(args.QueueDir, storePrefix+"-webhook-"+id) store = NewQueueStore(queueDir, args.QueueLimit)