diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index 5fefa1b..18002cf 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -14,9 +14,12 @@ package main import ( + "crypto/tls" + "crypto/x509" "encoding/json" "flag" "fmt" + "io/ioutil" "net/http" "os" "runtime" @@ -94,11 +97,13 @@ func main() { level.Debug(logger).Log("msg", " matched receiver", "receiver", conf.Name) // TODO: Consider reusing notifiers or just jira clients to reuse connections. - tp := jira.BasicAuthTransport{ - Username: conf.User, - Password: string(conf.Password), + hc, err := createHTTPClient(conf) + if err != nil { + errorHandler(w, http.StatusInternalServerError, err, conf.Name, &data, logger) + return } - client, err := jira.NewClient(tp.Client(), conf.APIURL) + + client, err := jira.NewClient(hc, conf.APIURL) if err != nil { errorHandler(w, http.StatusInternalServerError, err, conf.Name, &data, logger) return @@ -179,3 +184,94 @@ func setupLogger(lvl string, fmt string) (logger log.Logger) { logger = log.With(logger, "ts", log.DefaultTimestampUTC, "caller", log.DefaultCaller) return } + +// createHTTPClient returns a jira.BasicAuthTransport or http Client, depending +// on CAFile/client certificate options +func createHTTPClient(conf *config.ReceiverConfig) (*http.Client, error) { + + // if CAFile, CertFile or KeyFile aren't specified, return BasicAuthTransport client + if len(conf.CAFile) == 0 && len(conf.CertFile) == 0 && len(conf.KeyFile) == 0 { + tp := jira.BasicAuthTransport{ + Username: conf.User, + Password: string(conf.Password), + } + return tp.Client(), nil + } + + tlsConfig, err := newTLSConfig(conf) + if err != nil { + return nil, err + } + + hc := &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } + + return hc, nil +} + +func newTLSConfig(conf *config.ReceiverConfig) (*tls.Config, error) { + tlsConfig := &tls.Config{ + InsecureSkipVerify: conf.InsecureSkipVerify, + Renegotiation: tls.RenegotiateOnceAsClient, + } + + // Read in a CA certificate, if one is specified. + if len(conf.CAFile) > 0 { + b, err := readCAFile(conf.CAFile) + if err != nil { + return nil, err + } + if !updateRootCA(tlsConfig, b) { + return nil, fmt.Errorf("unable to use specified CA certificate %s", conf.CAFile) + } + } + + // Configure TLS with a client certificate, if certificate and key files are specified. + if len(conf.CertFile) > 0 && len(conf.KeyFile) == 0 { + return nil, fmt.Errorf("client certificate file %q specified without client key file", conf.CertFile) + } + + if len(conf.KeyFile) > 0 && len(conf.CertFile) == 0 { + return nil, fmt.Errorf("client key file %q specified without client certificate file", conf.KeyFile) + } + + if len(conf.CertFile) > 0 && len(conf.KeyFile) > 0 { + cert, err := getClientCertificate(conf) + if err != nil { + return nil, err + } + tlsConfig.Certificates = []tls.Certificate{*cert} + } + + return tlsConfig, nil +} + +// readCAFile reads the CA certificate file from disk. +func readCAFile(f string) ([]byte, error) { + data, err := ioutil.ReadFile(f) + if err != nil { + return nil, fmt.Errorf("unable to load specified CA certificate %s: %s", f, err) + } + return data, nil +} + +func updateRootCA(cfg *tls.Config, b []byte) bool { + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(b) { + return false + } + cfg.RootCAs = caCertPool + return true +} + +// getClientCertificate reads the pair of client certificate and key from disk and returns a tls.Certificate. +func getClientCertificate(c *config.ReceiverConfig) (*tls.Certificate, error) { + cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) + if err != nil { + return nil, fmt.Errorf("unable to use specified client certificate (%s) and key (%s): %s", c.CertFile, c.KeyFile, err) + } + return &cert, nil +} diff --git a/examples/jiralert.yml b/examples/jiralert.yml index 1ec7563..62aa54f 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -5,6 +5,9 @@ defaults: api_url: https://jiralert.atlassian.net user: jiralert password: 'JIRAlert' + # Optional client certificate. + # cert_file: /path/to/cert.crt + # key_file: /path/to/cert.key # The type of JIRA issue to create. Required. issue_type: Bug diff --git a/pkg/config/config.go b/pkg/config/config.go index 7359bc7..c214a5b 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -94,9 +94,13 @@ type ReceiverConfig struct { Name string `yaml:"name" json:"name"` // API access fields - APIURL string `yaml:"api_url" json:"api_url"` - User string `yaml:"user" json:"user"` - Password Secret `yaml:"password" json:"password"` + APIURL string `yaml:"api_url" json:"api_url"` + User string `yaml:"user" json:"user"` + Password Secret `yaml:"password" json:"password"` + CAFile string `yaml:"ca_file" json:"ca_file"` + CertFile string `yaml:"cert_file" json:"cert_file"` + KeyFile string `yaml:"key_file" json:"key_file"` + InsecureSkipVerify bool `yaml:"insecure_skip_verify" json:"insecure_skip_verify"` // Required issue fields Project string `yaml:"project" json:"project"` @@ -178,6 +182,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if _, err := url.Parse(rc.APIURL); err != nil { return fmt.Errorf("invalid api_url %q in receiver %q: %s", rc.APIURL, rc.Name, err) } + // Username and password aren't necessarily required if using a client + // certificate for authentication, but they will (usually) be ignored in + // that situation, so leaving them as required. if rc.User == "" { if c.Defaults.User == "" { return fmt.Errorf("missing user in receiver %q", rc.Name) @@ -191,6 +198,26 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { rc.Password = c.Defaults.Password } + // We want to be able to override CAFile/KeyFile/CertFile in each receiver + // even if there is a default value set. Setting it to a blank string will + // cause the existing logic to just fall back to the default, so we use + // "none" to indicate no value set. + if rc.CAFile == "none" { + rc.CAFile = "" + } else if rc.CAFile == "" && c.Defaults.CAFile != "" { + rc.CAFile = c.Defaults.CAFile + } + if rc.CertFile == "none" { + rc.CertFile = "" + } else if rc.CertFile == "" && c.Defaults.CertFile != "" { + rc.CertFile = c.Defaults.CertFile + } + if rc.KeyFile == "none" { + rc.KeyFile = "" + } else if rc.KeyFile == "" && c.Defaults.KeyFile != "" { + rc.KeyFile = c.Defaults.KeyFile + } + // Check required issue fields if rc.Project == "" { if c.Defaults.Project == "" {