diff --git a/cmd/promtool/main.go b/cmd/promtool/main.go index b630b61665..656d5ec125 100644 --- a/cmd/promtool/main.go +++ b/cmd/promtool/main.go @@ -108,13 +108,18 @@ func checkConfig(t cli.Term, filename string) ([]string, error) { return nil, fmt.Errorf("error checking bearer token file %q: %s", scfg.BearerTokenFile, err) } - if scfg.ClientCert != nil { - if err := check(scfg.ClientCert.Cert); err != nil { - return nil, fmt.Errorf("error checking client cert file %q: %s", scfg.ClientCert.Cert, err) - } - if err := check(scfg.ClientCert.Key); err != nil { - return nil, fmt.Errorf("error checking client key file %q: %s", scfg.ClientCert.Key, err) - } + if err := check(scfg.TLSConfig.CertFile); err != nil { + return nil, fmt.Errorf("error checking client cert file %q: %s", scfg.TLSConfig.CertFile, err) + } + if err := check(scfg.TLSConfig.KeyFile); err != nil { + return nil, fmt.Errorf("error checking client key file %q: %s", scfg.TLSConfig.KeyFile, err) + } + + if len(scfg.TLSConfig.CertFile) > 0 && len(scfg.TLSConfig.KeyFile) == 0 { + return nil, fmt.Errorf("client cert file %s specified without client key file", scfg.TLSConfig.CertFile) + } + if len(scfg.TLSConfig.KeyFile) > 0 && len(scfg.TLSConfig.CertFile) == 0 { + return nil, fmt.Errorf("client key file %s specified without client cert file", scfg.TLSConfig.KeyFile) } } diff --git a/config/config.go b/config/config.go index 775de45d47..51b2588ff0 100644 --- a/config/config.go +++ b/config/config.go @@ -187,10 +187,15 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, scfg := range cfg.ScrapeConfigs { scfg.BearerTokenFile = join(scfg.BearerTokenFile) + scfg.TLSConfig.CAFile = join(scfg.TLSConfig.CAFile) + scfg.TLSConfig.CertFile = join(scfg.TLSConfig.CertFile) + scfg.TLSConfig.KeyFile = join(scfg.TLSConfig.KeyFile) - if scfg.ClientCert != nil { - scfg.ClientCert.Cert = join(scfg.ClientCert.Cert) - scfg.ClientCert.Key = join(scfg.ClientCert.Key) + for _, kcfg := range scfg.KubernetesSDConfigs { + kcfg.BearerTokenFile = join(kcfg.BearerTokenFile) + kcfg.TLSConfig.CAFile = join(kcfg.TLSConfig.CAFile) + kcfg.TLSConfig.CertFile = join(kcfg.TLSConfig.CertFile) + kcfg.TLSConfig.KeyFile = join(kcfg.TLSConfig.KeyFile) } } } @@ -293,6 +298,18 @@ func (c *GlobalConfig) isZero() bool { c.EvaluationInterval == 0 } +// TLSConfig configures the options for TLS connections. +type TLSConfig struct { + // The CA cert to use for the targets. + CAFile string `yaml:"ca_file,omitempty"` + // The client cert file for the targets. + CertFile string `yaml:"cert_file,omitempty"` + // The client key file for the targets. + KeyFile string `yaml:"key_file,omitempty"` + // Disable target certificate validation. + InsecureSkipVerify bool `yaml:"insecure_skip_verify"` +} + // ScrapeConfig configures a scraping unit for Prometheus. type ScrapeConfig struct { // The job name to which the job label is set by default. @@ -315,12 +332,10 @@ type ScrapeConfig struct { BearerToken string `yaml:"bearer_token,omitempty"` // The bearer token file for the targets. BearerTokenFile string `yaml:"bearer_token_file,omitempty"` - // The ca cert to use for the targets. - CACert string `yaml:"ca_cert,omitempty"` - // The client cert authentication credentials for the targets. - ClientCert *ClientCert `yaml:"client_cert,omitempty"` // HTTP proxy server to use to connect to the targets. ProxyURL URL `yaml:"proxy_url,omitempty"` + // Inlined TLSConfig. + TLSConfig TLSConfig `yaml:"tls_config,omitempty"` // List of labeled target groups for this job. TargetGroups []*TargetGroup `yaml:"target_groups,omitempty"` @@ -605,18 +620,15 @@ type MarathonSDConfig struct { // KubernetesSDConfig is the configuration for Kubernetes service discovery. type KubernetesSDConfig struct { - Masters []URL `yaml:"masters"` - KubeletPort int `yaml:"kubelet_port,omitempty"` - InCluster bool `yaml:"in_cluster,omitempty"` - BearerTokenFile string `yaml:"bearer_token_file,omitempty"` - Username string `yaml:"username,omitempty"` - Password string `yaml:"password,omitempty"` - Insecure bool `yaml:"insecure,omitempty"` - CertFile string `yaml:"cert_file,omitempty"` - KeyFile string `yaml:"key_file,omitempty"` - CAFile string `yaml:"ca_file,omitempty"` - RetryInterval Duration `yaml:"retry_interval,omitempty"` - RequestTimeout Duration `yaml:"request_timeout,omitempty"` + Masters []URL `yaml:"masters"` + KubeletPort int `yaml:"kubelet_port,omitempty"` + InCluster bool `yaml:"in_cluster,omitempty"` + BearerTokenFile string `yaml:"bearer_token_file,omitempty"` + Username string `yaml:"username,omitempty"` + Password string `yaml:"password,omitempty"` + RetryInterval Duration `yaml:"retry_interval,omitempty"` + RequestTimeout Duration `yaml:"request_timeout,omitempty"` + TLSConfig TLSConfig `yaml:"tls_config,omitempty"` // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` diff --git a/config/config_test.go b/config/config_test.go index 0447c0ff93..8e7ad834db 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -180,10 +180,11 @@ var expectedConf = &Config{ MetricsPath: "/metrics", Scheme: "http", - ClientCert: &ClientCert{ - Cert: "testdata/valid_cert_file", - Key: "testdata/valid_key_file", + TLSConfig: TLSConfig{ + CertFile: "testdata/valid_cert_file", + KeyFile: "testdata/valid_key_file", }, + BearerToken: "avalidtoken", }, { diff --git a/config/testdata/conf.good.yml b/config/testdata/conf.good.yml index ae20b69e6b..13493cc876 100644 --- a/config/testdata/conf.good.yml +++ b/config/testdata/conf.good.yml @@ -94,9 +94,9 @@ scrape_configs: - job_name: service-z - client_cert: - cert: valid_cert_file - key: valid_key_file + tls_config: + cert_file: valid_cert_file + key_file: valid_key_file bearer_token: avalidtoken diff --git a/documentation/examples/prometheus-kubernetes.yml b/documentation/examples/prometheus-kubernetes.yml index 6a0b7cbd4e..f3459e92b4 100644 --- a/documentation/examples/prometheus-kubernetes.yml +++ b/documentation/examples/prometheus-kubernetes.yml @@ -8,7 +8,9 @@ global: scrape_configs: - job_name: 'kubernetes' - ca_cert: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + tls_config: + ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt + bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token kubernetes_sd_configs: diff --git a/retrieval/discovery/kubernetes/discovery.go b/retrieval/discovery/kubernetes/discovery.go index 19e336ceda..a495895fe5 100644 --- a/retrieval/discovery/kubernetes/discovery.go +++ b/retrieval/discovery/kubernetes/discovery.go @@ -14,8 +14,6 @@ package kubernetes import ( - "crypto/tls" - "crypto/x509" "encoding/json" "fmt" "io/ioutil" @@ -574,7 +572,7 @@ func (kd *Discovery) updateServiceEndpoints(endpoints *Endpoints, eventType Even func newKubernetesHTTPClient(conf *config.KubernetesSDConfig) (*http.Client, error) { bearerTokenFile := conf.BearerTokenFile - caFile := conf.CAFile + caFile := conf.TLSConfig.CAFile if conf.InCluster { if len(bearerTokenFile) == 0 { bearerTokenFile = serviceAccountToken @@ -582,46 +580,31 @@ func newKubernetesHTTPClient(conf *config.KubernetesSDConfig) (*http.Client, err if len(caFile) == 0 { // With recent versions, the CA certificate is provided as a token // but we need to handle older versions too. In this case, don't - // set the CAFile & the configuration will have to use Insecure. + // set the CAFile & the configuration will have to use InsecureSkipVerify. if _, err := os.Stat(serviceAccountCACert); err == nil { caFile = serviceAccountCACert } } } - tlsConfig := &tls.Config{InsecureSkipVerify: conf.Insecure} - - // Load client cert if specified. - if len(conf.CertFile) > 0 && len(conf.KeyFile) > 0 { - cert, err := tls.LoadX509KeyPair(conf.CertFile, conf.KeyFile) - if err != nil { - return nil, err - } - tlsConfig.Certificates = []tls.Certificate{cert} + tlsOpts := httputil.TLSOptions{ + InsecureSkipVerify: conf.TLSConfig.InsecureSkipVerify, + CAFile: caFile, + CertFile: conf.TLSConfig.CertFile, + KeyFile: conf.TLSConfig.KeyFile, + } + tlsConfig, err := httputil.NewTLSConfig(tlsOpts) + if err != nil { + return nil, err } - caCertPool := x509.NewCertPool() - if len(caFile) > 0 { - // Load CA cert. - caCert, err := ioutil.ReadFile(caFile) - if err != nil { - return nil, err - } - caCertPool.AppendCertsFromPEM(caCert) - } - tlsConfig.RootCAs = caCertPool - - tlsConfig.BuildNameToCertificate() - - tr := &http.Transport{ + var rt http.RoundTripper = &http.Transport{ Dial: func(netw, addr string) (c net.Conn, err error) { c, err = net.DialTimeout(netw, addr, time.Duration(conf.RequestTimeout)) return }, + TLSClientConfig: tlsConfig, } - tr.TLSClientConfig = tlsConfig - var rt http.RoundTripper - rt = tr bearerToken, err := ioutil.ReadFile(bearerTokenFile) if err != nil { diff --git a/retrieval/target.go b/retrieval/target.go index 4a89b144db..e9f6bdd2b4 100644 --- a/retrieval/target.go +++ b/retrieval/target.go @@ -14,8 +14,6 @@ package retrieval import ( - "crypto/tls" - "crypto/x509" "errors" "fmt" "io" @@ -249,33 +247,21 @@ func (t *Target) Update(cfg *config.ScrapeConfig, baseLabels, metaLabels model.L } func newHTTPClient(cfg *config.ScrapeConfig) (*http.Client, error) { - tlsConfig := &tls.Config{} - - // If a CA cert is provided then let's read it in so we can validate the - // scrape target's certificate properly. - if len(cfg.CACert) > 0 { - caCertPool := x509.NewCertPool() - // Load CA cert. - caCert, err := ioutil.ReadFile(cfg.CACert) - if err != nil { - return nil, fmt.Errorf("unable to use specified CA cert %s: %s", cfg.CACert, err) - } - caCertPool.AppendCertsFromPEM(caCert) - tlsConfig.RootCAs = caCertPool - } - - // If a client cert & key is provided then configure TLS config accordingly. - if cfg.ClientCert != nil && len(cfg.ClientCert.Cert) > 0 && len(cfg.ClientCert.Key) > 0 { - cert, err := tls.LoadX509KeyPair(cfg.ClientCert.Cert, cfg.ClientCert.Key) - if err != nil { - return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", cfg.ClientCert.Cert, cfg.ClientCert.Key, err) - } - tlsConfig.Certificates = []tls.Certificate{cert} - } - tlsConfig.BuildNameToCertificate() - - // Get a default roundtripper with the scrape timeout. rt := httputil.NewDeadlineRoundTripper(time.Duration(cfg.ScrapeTimeout), cfg.ProxyURL.URL) + + tlsOpts := httputil.TLSOptions{ + InsecureSkipVerify: cfg.TLSConfig.InsecureSkipVerify, + CAFile: cfg.TLSConfig.CAFile, + } + if len(cfg.TLSConfig.CertFile) > 0 && len(cfg.TLSConfig.KeyFile) > 0 { + tlsOpts.CertFile = cfg.TLSConfig.CertFile + tlsOpts.KeyFile = cfg.TLSConfig.KeyFile + } + tlsConfig, err := httputil.NewTLSConfig(tlsOpts) + if err != nil { + return nil, err + } + // Get a default roundtripper with the scrape timeout. tr := rt.(*http.Transport) // Set the TLS config from above tr.TLSClientConfig = tlsConfig diff --git a/retrieval/target_test.go b/retrieval/target_test.go index 4b7518343a..9b731632e5 100644 --- a/retrieval/target_test.go +++ b/retrieval/target_test.go @@ -563,7 +563,9 @@ func TestNewHTTPCACert(t *testing.T) { cfg := &config.ScrapeConfig{ ScrapeTimeout: config.Duration(1 * time.Second), - CACert: "testdata/ca.cer", + TLSConfig: config.TLSConfig{ + CAFile: "testdata/ca.cer", + }, } c, err := newHTTPClient(cfg) if err != nil { @@ -594,10 +596,10 @@ func TestNewHTTPClientCert(t *testing.T) { cfg := &config.ScrapeConfig{ ScrapeTimeout: config.Duration(1 * time.Second), - CACert: "testdata/ca.cer", - ClientCert: &config.ClientCert{ - Cert: "testdata/client.cer", - Key: "testdata/client.key", + TLSConfig: config.TLSConfig{ + CAFile: "testdata/ca.cer", + CertFile: "testdata/client.cer", + KeyFile: "testdata/client.key", }, } c, err := newHTTPClient(cfg) diff --git a/util/httputil/client.go b/util/httputil/client.go index e8c3a43fb7..c05b0494d1 100644 --- a/util/httputil/client.go +++ b/util/httputil/client.go @@ -14,6 +14,10 @@ package httputil import ( + "crypto/tls" + "crypto/x509" + "fmt" + "io/ioutil" "net" "net/http" "net/url" @@ -108,3 +112,39 @@ func cloneRequest(r *http.Request) *http.Request { } return r2 } + +type TLSOptions struct { + InsecureSkipVerify bool + CAFile string + CertFile string + KeyFile string +} + +func NewTLSConfig(opts TLSOptions) (*tls.Config, error) { + tlsConfig := &tls.Config{InsecureSkipVerify: opts.InsecureSkipVerify} + + // If a CA cert is provided then let's read it in so we can validate the + // scrape target's certificate properly. + if len(opts.CAFile) > 0 { + caCertPool := x509.NewCertPool() + // Load CA cert. + caCert, err := ioutil.ReadFile(opts.CAFile) + if err != nil { + return nil, fmt.Errorf("unable to use specified CA cert %s: %s", opts.CAFile, err) + } + caCertPool.AppendCertsFromPEM(caCert) + tlsConfig.RootCAs = caCertPool + } + + // If a client cert & key is provided then configure TLS config accordingly. + if len(opts.CertFile) > 0 && len(opts.KeyFile) > 0 { + cert, err := tls.LoadX509KeyPair(opts.CertFile, opts.KeyFile) + if err != nil { + return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %s", opts.CertFile, opts.KeyFile, err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + tlsConfig.BuildNameToCertificate() + + return tlsConfig, nil +}