Repository: prometheus/common
Pull request #538 (2023-11-30 00:41:10)

Use common interface to fetch secrets in HTTP client config
Part 1 for secret provider support within `common`. See: https://groups.google.com/g/prometheus-developers/c/WKOej_pnhXg

This change consolidates fetching secrets across all authorization types. Authorization types which previously had different structs per secret type now have a single type.

The unification helps with adding secret manager support which part 2 will include. There are two ways we could do this.
1. With a secret "reference" (or "handle"), e.g. a name to a secret which is configured somewhere else. This PR implements this approach: https://github.com/prometheus/common/pull/572
2. Inline. This would not be backwards-compatible. The only way I could think of serialization to work with the type defined outside of this package (secret provider configs would be part of the main prometheus repository since they're plugins), is to use a generic secret config type.


Patch:
diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml
index a7a40c1..b36f71c 100644
--- a/.github/workflows/golangci-lint.yml
+++ b/.github/workflows/golangci-lint.yml
@@ -33,6 +33,6 @@ jobs:
         run: sudo apt-get update && sudo apt-get -y install libsnmp-dev
         if: github.repository == 'prometheus/snmp_exporter'
       - name: Lint
-        uses: golangci/golangci-lint-action@3cfe3a4abbb849e10058ce4af15d205b6da42804 # v4.0.0
+        uses: golangci/golangci-lint-action@9d1e0624a798bb64f6c3cea93db47765312263dc # v5.1.0
         with:
           version: v1.56.2
diff --git a/Makefile.common b/Makefile.common
index 0acfb9d..0e9ace2 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -55,7 +55,7 @@ ifneq ($(shell command -v gotestsum 2> /dev/null),)
 endif
 endif
 
-PROMU_VERSION ?= 0.15.0
+PROMU_VERSION ?= 0.17.0
 PROMU_URL     := https://github.com/prometheus/promu/releases/download/v$(PROMU_VERSION)/promu-$(PROMU_VERSION).$(GO_BUILD_PLATFORM).tar.gz
 
 SKIP_GOLANGCI_LINT :=
diff --git a/config/config.go b/config/config.go
index 3da4854..7588da5 100644
--- a/config/config.go
+++ b/config/config.go
@@ -60,9 +60,9 @@ func (s Secret) MarshalJSON() ([]byte, error) {
 	return json.Marshal(secretToken)
 }
 
-type Header map[string][]Secret
+type ProxyHeader map[string][]Secret
 
-func (h *Header) HTTPHeader() http.Header {
+func (h *ProxyHeader) HTTPHeader() http.Header {
 	if h == nil || *h == nil {
 		return nil
 	}
diff --git a/config/headers.go b/config/headers.go
new file mode 100644
index 0000000..7c3149e
--- /dev/null
+++ b/config/headers.go
@@ -0,0 +1,134 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This package no longer handles safe yaml parsing. In order to
+// ensure correct yaml unmarshalling, use "yaml.UnmarshalStrict()".
+
+package config
+
+import (
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+)
+
+// reservedHeaders that change the connection, are set by Prometheus, or can
+// be changed otherwise.
+var reservedHeaders = map[string]struct{}{
+	"Authorization":                       {},
+	"Host":                                {},
+	"Content-Encoding":                    {},
+	"Content-Length":                      {},
+	"Content-Type":                        {},
+	"User-Agent":                          {},
+	"Connection":                          {},
+	"Keep-Alive":                          {},
+	"Proxy-Authenticate":                  {},
+	"Proxy-Authorization":                 {},
+	"Www-Authenticate":                    {},
+	"Accept-Encoding":                     {},
+	"X-Prometheus-Remote-Write-Version":   {},
+	"X-Prometheus-Remote-Read-Version":    {},
+	"X-Prometheus-Scrape-Timeout-Seconds": {},
+
+	// Added by SigV4.
+	"X-Amz-Date":           {},
+	"X-Amz-Security-Token": {},
+	"X-Amz-Content-Sha256": {},
+}
+
+// Headers represents the configuration for HTTP headers.
+type Headers struct {
+	Headers map[string]Header `yaml:",inline" json:",inline"`
+	dir     string
+}
+
+// Headers represents the configuration for HTTP headers.
+type Header struct {
+	Values  []string `yaml:"values,omitempty" json:"values,omitempty"`
+	Secrets []Secret `yaml:"secrets,omitempty" json:"secrets,omitempty"`
+	Files   []string `yaml:"files,omitempty" json:"files,omitempty"`
+}
+
+// SetDirectory records the directory to make headers file relative to the
+// configuration file.
+func (h *Headers) SetDirectory(dir string) {
+	if h == nil {
+		return
+	}
+	h.dir = dir
+}
+
+// Validate validates the Headers config.
+func (h *Headers) Validate() error {
+	for n, header := range h.Headers {
+		if _, ok := reservedHeaders[http.CanonicalHeaderKey(n)]; ok {
+			return fmt.Errorf("setting header %q is not allowed", http.CanonicalHeaderKey(n))
+		}
+		for _, v := range header.Files {
+			f := JoinDir(h.dir, v)
+			_, err := os.ReadFile(f)
+			if err != nil {
+				return fmt.Errorf("unable to read header %q from file %s: %w", http.CanonicalHeaderKey(n), f, err)
+			}
+		}
+	}
+	return nil
+}
+
+// NewHeadersRoundTripper returns a RoundTripper that sets HTTP headers on
+// requests as configured.
+func NewHeadersRoundTripper(config *Headers, next http.RoundTripper) http.RoundTripper {
+	if len(config.Headers) == 0 {
+		return next
+	}
+	return &headersRoundTripper{
+		config: config,
+		next:   next,
+	}
+}
+
+type headersRoundTripper struct {
+	next   http.RoundTripper
+	config *Headers
+}
+
+// RoundTrip implements http.RoundTripper.
+func (rt *headersRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	req = cloneRequest(req)
+	for n, h := range rt.config.Headers {
+		for _, v := range h.Values {
+			req.Header.Add(n, v)
+		}
+		for _, v := range h.Secrets {
+			req.Header.Add(n, string(v))
+		}
+		for _, v := range h.Files {
+			f := JoinDir(rt.config.dir, v)
+			b, err := os.ReadFile(f)
+			if err != nil {
+				return nil, fmt.Errorf("unable to read headers file %s: %w", f, err)
+			}
+			req.Header.Add(n, strings.TrimSpace(string(b)))
+		}
+	}
+	return rt.next.RoundTrip(req)
+}
+
+// CloseIdleConnections implements closeIdler.
+func (rt *headersRoundTripper) CloseIdleConnections() {
+	if ci, ok := rt.next.(closeIdler); ok {
+		ci.CloseIdleConnections()
+	}
+}
diff --git a/config/http_config.go b/config/http_config.go
index 2044181..e217886 100644
--- a/config/http_config.go
+++ b/config/http_config.go
@@ -29,6 +29,7 @@ import (
 	"strings"
 	"sync"
 	"time"
+	"unsafe"
 
 	conntrack "github.com/mwitkow/go-conntrack"
 	"golang.org/x/net/http/httpproxy"
@@ -311,6 +312,9 @@ type HTTPClientConfig struct {
 	EnableHTTP2 bool `yaml:"enable_http2" json:"enable_http2"`
 	// Proxy configuration.
 	ProxyConfig `yaml:",inline"`
+	// HTTPHeaders specify headers to inject in the requests. Those headers
+	// could be marshalled back to the users.
+	HTTPHeaders *Headers `yaml:"http_headers" json:"http_headers"`
 }
 
 // SetDirectory joins any relative file paths with dir.
@@ -322,6 +326,7 @@ func (c *HTTPClientConfig) SetDirectory(dir string) {
 	c.BasicAuth.SetDirectory(dir)
 	c.Authorization.SetDirectory(dir)
 	c.OAuth2.SetDirectory(dir)
+	c.HTTPHeaders.SetDirectory(dir)
 	c.BearerTokenFile = JoinDir(dir, c.BearerTokenFile)
 }
 
@@ -388,6 +393,11 @@ func (c *HTTPClientConfig) Validate() error {
 	if err := c.ProxyConfig.Validate(); err != nil {
 		return err
 	}
+	if c.HTTPHeaders != nil {
+		if err := c.HTTPHeaders.Validate(); err != nil {
+			return err
+		}
+	}
 	return nil
 }
 
@@ -551,27 +561,27 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
 
 		// If a authorization_credentials is provided, create a round tripper that will set the
 		// Authorization header correctly on each request.
-		if cfg.Authorization != nil && len(cfg.Authorization.CredentialsFile) > 0 {
-			rt = NewAuthorizationCredentialsFileRoundTripper(cfg.Authorization.Type, cfg.Authorization.CredentialsFile, rt)
-		} else if cfg.Authorization != nil {
-			rt = NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, cfg.Authorization.Credentials, rt)
+		if cfg.Authorization != nil {
+			rt = NewAuthorizationCredentialsRoundTripper(cfg.Authorization.Type, secretFrom(cfg.Authorization.Credentials, cfg.Authorization.CredentialsFile), rt)
 		}
 		// Backwards compatibility, be nice with importers who would not have
 		// called Validate().
-		if len(cfg.BearerToken) > 0 {
-			rt = NewAuthorizationCredentialsRoundTripper("Bearer", cfg.BearerToken, rt)
-		} else if len(cfg.BearerTokenFile) > 0 {
-			rt = NewAuthorizationCredentialsFileRoundTripper("Bearer", cfg.BearerTokenFile, rt)
+		if len(cfg.BearerToken) > 0 || len(cfg.BearerTokenFile) > 0 {
+			rt = NewAuthorizationCredentialsRoundTripper("Bearer", secretFrom(cfg.BearerToken, cfg.BearerTokenFile), rt)
 		}
 
 		if cfg.BasicAuth != nil {
-			rt = NewBasicAuthRoundTripper(cfg.BasicAuth.Username, cfg.BasicAuth.Password, cfg.BasicAuth.UsernameFile, cfg.BasicAuth.PasswordFile, rt)
+			rt = NewBasicAuthRoundTripper(secretFrom(Secret(cfg.BasicAuth.Username), cfg.BasicAuth.UsernameFile), secretFrom(cfg.BasicAuth.Password, cfg.BasicAuth.PasswordFile), rt)
 		}
 
 		if cfg.OAuth2 != nil {
 			rt = NewOAuth2RoundTripper(cfg.OAuth2, rt, &opts)
 		}
 
+		if cfg.HTTPHeaders != nil {
+			rt = NewHeadersRoundTripper(cfg.HTTPHeaders, rt)
+		}
+
 		if opts.userAgent != "" {
 			rt = NewUserAgentRoundTripper(opts.userAgent, rt)
 		}
@@ -589,59 +599,93 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT
 		return nil, err
 	}
 
-	if len(cfg.TLSConfig.CAFile) == 0 {
+	tlsSettings := cfg.TLSConfig.roundTripperSettings()
+	if tlsSettings.CA == nil || tlsSettings.CA.immutable() {
 		// No need for a RoundTripper that reloads the CA file automatically.
 		return newRT(tlsConfig)
 	}
-	return NewTLSRoundTripper(tlsConfig, cfg.TLSConfig.roundTripperSettings(), newRT)
+	return NewTLSRoundTripper(tlsConfig, tlsSettings, newRT)
 }
 
-type authorizationCredentialsRoundTripper struct {
-	authType        string
-	authCredentials Secret
-	rt              http.RoundTripper
+type secret interface {
+	fetch() (string, error)
+	description() string
+	immutable() bool
 }
 
-// NewAuthorizationCredentialsRoundTripper adds the provided credentials to a
-// request unless the authorization header has already been set.
-func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials Secret, rt http.RoundTripper) http.RoundTripper {
-	return &authorizationCredentialsRoundTripper{authType, authCredentials, rt}
+type inlineSecret struct {
+	text string
 }
 
-func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	if len(req.Header.Get("Authorization")) == 0 {
-		req = cloneRequest(req)
-		req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, string(rt.authCredentials)))
+func (s *inlineSecret) fetch() (string, error) {
+	return s.text, nil
+}
+
+func (s *inlineSecret) description() string {
+	return "inline"
+}
+
+func (s *inlineSecret) immutable() bool {
+	return true
+}
+
+type fileSecret struct {
+	file string
+}
+
+func (s *fileSecret) fetch() (string, error) {
+	fileBytes, err := os.ReadFile(s.file)
+	if err != nil {
+		return "", fmt.Errorf("unable to read file %s: %w", s.file, err)
 	}
-	return rt.rt.RoundTrip(req)
+	return strings.TrimSpace(string(fileBytes)), nil
 }
 
-func (rt *authorizationCredentialsRoundTripper) CloseIdleConnections() {
-	if ci, ok := rt.rt.(closeIdler); ok {
-		ci.CloseIdleConnections()
+func (s *fileSecret) description() string {
+	return fmt.Sprintf("file %s", s.file)
+}
+
+func (s *fileSecret) immutable() bool {
+	return false
+}
+
+func secretFrom(text Secret, file string) secret {
+	if text != "" {
+		return &inlineSecret{
+			text: string(text),
+		}
+	}
+	if file != "" {
+		return &fileSecret{
+			file: file,
+		}
 	}
+	return nil
 }
 
-type authorizationCredentialsFileRoundTripper struct {
-	authType            string
-	authCredentialsFile string
-	rt                  http.RoundTripper
+type authorizationCredentialsRoundTripper struct {
+	authType        string
+	authCredentials secret
+	rt              http.RoundTripper
 }
 
-// NewAuthorizationCredentialsFileRoundTripper adds the authorization
-// credentials read from the provided file to a request unless the authorization
-// header has already been set. This file is read for every request.
-func NewAuthorizationCredentialsFileRoundTripper(authType, authCredentialsFile string, rt http.RoundTripper) http.RoundTripper {
-	return &authorizationCredentialsFileRoundTripper{authType, authCredentialsFile, rt}
+// NewAuthorizationCredentialsRoundTripper adds the authorization credentials
+// read from the provided secret to a request unless the authorization header
+// has already been set.
+func NewAuthorizationCredentialsRoundTripper(authType string, authCredentials secret, rt http.RoundTripper) http.RoundTripper {
+	return &authorizationCredentialsRoundTripper{authType, authCredentials, rt}
 }
 
-func (rt *authorizationCredentialsFileRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+func (rt *authorizationCredentialsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 	if len(req.Header.Get("Authorization")) == 0 {
-		b, err := os.ReadFile(rt.authCredentialsFile)
-		if err != nil {
-			return nil, fmt.Errorf("unable to read authorization credentials file %s: %w", rt.authCredentialsFile, err)
+		var authCredentials string
+		if rt.authCredentials != nil {
+			var err error
+			authCredentials, err = rt.authCredentials.fetch()
+			if err != nil {
+				return nil, fmt.Errorf("unable to read authorization credentials: %w", err)
+			}
 		}
-		authCredentials := strings.TrimSpace(string(b))
 
 		req = cloneRequest(req)
 		req.Header.Set("Authorization", fmt.Sprintf("%s %s", rt.authType, authCredentials))
@@ -650,49 +694,43 @@ func (rt *authorizationCredentialsFileRoundTripper) RoundTrip(req *http.Request)
 	return rt.rt.RoundTrip(req)
 }
 
-func (rt *authorizationCredentialsFileRoundTripper) CloseIdleConnections() {
+func (rt *authorizationCredentialsRoundTripper) CloseIdleConnections() {
 	if ci, ok := rt.rt.(closeIdler); ok {
 		ci.CloseIdleConnections()
 	}
 }
 
 type basicAuthRoundTripper struct {
-	username     string
-	password     Secret
-	usernameFile string
-	passwordFile string
-	rt           http.RoundTripper
+	username secret
+	password secret
+	rt       http.RoundTripper
 }
 
 // NewBasicAuthRoundTripper will apply a BASIC auth authorization header to a request unless it has
 // already been set.
-func NewBasicAuthRoundTripper(username string, password Secret, usernameFile, passwordFile string, rt http.RoundTripper) http.RoundTripper {
-	return &basicAuthRoundTripper{username, password, usernameFile, passwordFile, rt}
+func NewBasicAuthRoundTripper(username secret, password secret, rt http.RoundTripper) http.RoundTripper {
+	return &basicAuthRoundTripper{username, password, rt}
 }
 
 func (rt *basicAuthRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	var username string
-	var password string
 	if len(req.Header.Get("Authorization")) != 0 {
 		return rt.rt.RoundTrip(req)
 	}
-	if rt.usernameFile != "" {
-		usernameBytes, err := os.ReadFile(rt.usernameFile)
+	var username string
+	var password string
+	if rt.username != nil {
+		var err error
+		username, err = rt.username.fetch()
 		if err != nil {
-			return nil, fmt.Errorf("unable to read basic auth username file %s: %w", rt.usernameFile, err)
+			return nil, fmt.Errorf("unable to read basic auth username: %w", err)
 		}
-		username = strings.TrimSpace(string(usernameBytes))
-	} else {
-		username = rt.username
 	}
-	if rt.passwordFile != "" {
-		passwordBytes, err := os.ReadFile(rt.passwordFile)
+	if rt.password != nil {
+		var err error
+		password, err = rt.password.fetch()
 		if err != nil {
-			return nil, fmt.Errorf("unable to read basic auth password file %s: %w", rt.passwordFile, err)
+			return nil, fmt.Errorf("unable to read basic auth password: %w", err)
 		}
-		password = strings.TrimSpace(string(passwordBytes))
-	} else {
-		password = string(rt.password)
 	}
 	req = cloneRequest(req)
 	req.SetBasicAuth(username, password)
@@ -706,20 +744,22 @@ func (rt *basicAuthRoundTripper) CloseIdleConnections() {
 }
 
 type oauth2RoundTripper struct {
-	config *OAuth2
-	rt     http.RoundTripper
-	next   http.RoundTripper
-	secret string
-	mtx    sync.RWMutex
-	opts   *httpClientOptions
-	client *http.Client
+	config       *OAuth2
+	clientSecret secret
+	rt           http.RoundTripper
+	next         http.RoundTripper
+	secret       string
+	mtx          sync.RWMutex
+	opts         *httpClientOptions
+	client       *http.Client
 }
 
 func NewOAuth2RoundTripper(config *OAuth2, next http.RoundTripper, opts *httpClientOptions) http.RoundTripper {
 	return &oauth2RoundTripper{
-		config: config,
-		next:   next,
-		opts:   opts,
+		config:       config,
+		clientSecret: secretFrom(config.ClientSecret, config.ClientSecretFile),
+		next:         next,
+		opts:         opts,
 	}
 }
 
@@ -729,19 +769,16 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
 		changed bool
 	)
 
-	if rt.config.ClientSecretFile != "" {
-		data, err := os.ReadFile(rt.config.ClientSecretFile)
+	if rt.clientSecret != nil {
+		var err error
+		secret, err = rt.clientSecret.fetch()
 		if err != nil {
-			return nil, fmt.Errorf("unable to read oauth2 client secret file %s: %w", rt.config.ClientSecretFile, err)
+			return nil, fmt.Errorf("unable to read oauth2 client secret: %w", err)
 		}
-		secret = strings.TrimSpace(string(data))
-		rt.mtx.RLock()
-		changed = secret != rt.secret
-		rt.mtx.RUnlock()
-	} else {
-		// Either an inline secret or nothing (use an empty string) was provided.
-		secret = string(rt.config.ClientSecret)
 	}
+	rt.mtx.RLock()
+	changed = secret != rt.secret
+	rt.mtx.RUnlock()
 
 	if changed || rt.rt == nil {
 		config := &clientcredentials.Config{
@@ -772,10 +809,11 @@ func (rt *oauth2RoundTripper) RoundTrip(req *http.Request) (*http.Response, erro
 		}
 
 		var t http.RoundTripper
-		if len(rt.config.TLSConfig.CAFile) == 0 {
+		tlsSettings := rt.config.TLSConfig.roundTripperSettings()
+		if tlsSettings.CA == nil || tlsSettings.CA.immutable() {
 			t, _ = tlsTransport(tlsConfig)
 		} else {
-			t, err = NewTLSRoundTripper(tlsConfig, rt.config.TLSConfig.roundTripperSettings(), tlsTransport)
+			t, err = NewTLSRoundTripper(tlsConfig, tlsSettings, tlsTransport)
 			if err != nil {
 				return nil, err
 			}
@@ -860,17 +898,14 @@ func NewTLSConfig(cfg *TLSConfig) (*tls.Config, error) {
 
 	// 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.CA) > 0 {
-		if !updateRootCA(tlsConfig, []byte(cfg.CA)) {
-			return nil, fmt.Errorf("unable to use inline CA cert")
-		}
-	} else if len(cfg.CAFile) > 0 {
-		b, err := readCAFile(cfg.CAFile)
+	caSecret := secretFrom(Secret(cfg.CA), cfg.CAFile)
+	if caSecret != nil {
+		ca, err := caSecret.fetch()
 		if err != nil {
-			return nil, err
+			return nil, fmt.Errorf("unable to read CA cert: %w", err)
 		}
-		if !updateRootCA(tlsConfig, b) {
-			return nil, fmt.Errorf("unable to use specified CA cert %s", cfg.CAFile)
+		if !updateRootCA(tlsConfig, []byte(ca)) {
+			return nil, fmt.Errorf("unable to use specified CA cert %s", caSecret.description())
 		}
 	}
 
@@ -966,57 +1001,45 @@ func (c *TLSConfig) usingClientKey() bool {
 
 func (c *TLSConfig) roundTripperSettings() TLSRoundTripperSettings {
 	return TLSRoundTripperSettings{
-		CA:       c.CA,
-		CAFile:   c.CAFile,
-		Cert:     c.Cert,
-		CertFile: c.CertFile,
-		Key:      string(c.Key),
-		KeyFile:  c.KeyFile,
+		CA:   secretFrom(Secret(c.CA), c.CAFile),
+		Cert: secretFrom(Secret(c.Cert), c.CertFile),
+		Key:  secretFrom(c.Key, c.KeyFile),
 	}
 }
 
 // getClientCertificate reads the pair of client cert and key from disk and returns a tls.Certificate.
 func (c *TLSConfig) getClientCertificate(_ *tls.CertificateRequestInfo) (*tls.Certificate, error) {
 	var (
-		certData, keyData []byte
+		certData, keyData string
 		err               error
 	)
 
-	if c.CertFile != "" {
-		certData, err = os.ReadFile(c.CertFile)
+	certSecret := secretFrom(Secret(c.Cert), c.CertFile)
+	if certSecret != nil {
+		certData, err = certSecret.fetch()
 		if err != nil {
-			return nil, fmt.Errorf("unable to read specified client cert (%s): %w", c.CertFile, err)
+			return nil, fmt.Errorf("unable to read specified client cert: %w", err)
 		}
-	} else {
-		certData = []byte(c.Cert)
 	}
 
-	if c.KeyFile != "" {
-		keyData, err = os.ReadFile(c.KeyFile)
+	keySecret := secretFrom(Secret(c.Key), c.KeyFile)
+	if keySecret != nil {
+		keyData, err = keySecret.fetch()
 		if err != nil {
-			return nil, fmt.Errorf("unable to read specified client key (%s): %w", c.KeyFile, err)
+			return nil, fmt.Errorf("unable to read specified client key: %w", err)
 		}
-	} else {
-		keyData = []byte(c.Key)
 	}
 
-	cert, err := tls.X509KeyPair(certData, keyData)
+	certStr := unsafe.Slice(unsafe.StringData(certData), len(certData))
+	keyStr := unsafe.Slice(unsafe.StringData(keyData), len(keyData))
+	cert, err := tls.X509KeyPair(certStr, keyStr)
 	if err != nil {
-		return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %w", c.CertFile, c.KeyFile, err)
+		return nil, fmt.Errorf("unable to use specified client cert (%s) & key (%s): %w", certSecret.description(), keySecret.description(), err)
 	}
 
 	return &cert, nil
 }
 
-// readCAFile reads the CA cert file from disk.
-func readCAFile(f string) ([]byte, error) {
-	data, err := os.ReadFile(f)
-	if err != nil {
-		return nil, fmt.Errorf("unable to load specified CA cert %s: %w", f, err)
-	}
-	return data, nil
-}
-
 // updateRootCA parses the given byte slice as a series of PEM encoded certificates and updates tls.Config.RootCAs.
 func updateRootCA(cfg *tls.Config, b []byte) bool {
 	caCertPool := x509.NewCertPool()
@@ -1044,9 +1067,9 @@ type tlsRoundTripper struct {
 }
 
 type TLSRoundTripperSettings struct {
-	CA, CAFile     string
-	Cert, CertFile string
-	Key, KeyFile   string
+	CA   secret
+	Cert secret
+	Key  secret
 }
 
 func NewTLSRoundTripper(
@@ -1074,37 +1097,30 @@ func NewTLSRoundTripper(
 }
 
 func (t *tlsRoundTripper) getTLSDataWithHash() ([]byte, []byte, []byte, []byte, error) {
-	var (
-		caBytes, certBytes, keyBytes []byte
-
-		err error
-	)
+	var caBytes, certBytes, keyBytes []byte
 
-	if t.settings.CAFile != "" {
-		caBytes, err = os.ReadFile(t.settings.CAFile)
+	if t.settings.CA != nil {
+		ca, err := t.settings.CA.fetch()
 		if err != nil {
-			return nil, nil, nil, nil, err
+			return nil, nil, nil, nil, fmt.Errorf("unable to read CA cert: %w", err)
 		}
-	} else if t.settings.CA != "" {
-		caBytes = []byte(t.settings.CA)
+		caBytes = []byte(ca)
 	}
 
-	if t.settings.CertFile != "" {
-		certBytes, err = os.ReadFile(t.settings.CertFile)
+	if t.settings.Cert != nil {
+		cert, err := t.settings.Cert.fetch()
 		if err != nil {
-			return nil, nil, nil, nil, err
+			return nil, nil, nil, nil, fmt.Errorf("unable to read client cert: %w", err)
 		}
-	} else if t.settings.Cert != "" {
-		certBytes = []byte(t.settings.Cert)
+		certBytes = []byte(cert)
 	}
 
-	if t.settings.KeyFile != "" {
-		keyBytes, err = os.ReadFile(t.settings.KeyFile)
+	if t.settings.Key != nil {
+		key, err := t.settings.Key.fetch()
 		if err != nil {
-			return nil, nil, nil, nil, err
+			return nil, nil, nil, nil, fmt.Errorf("unable to read client key: %w", err)
 		}
-	} else if t.settings.Key != "" {
-		keyBytes = []byte(t.settings.Key)
+		keyBytes = []byte(key)
 	}
 
 	var caHash, certHash, keyHash [32]byte
@@ -1145,7 +1161,7 @@ func (t *tlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
 	// using GetClientCertificate.
 	tlsConfig := t.tlsConfig.Clone()
 	if !updateRootCA(tlsConfig, caData) {
-		return nil, fmt.Errorf("unable to use specified CA cert %s", t.settings.CAFile)
+		return nil, fmt.Errorf("unable to use specified CA cert %s", t.settings.CA.description())
 	}
 	rt, err = t.newRT(tlsConfig)
 	if err != nil {
@@ -1236,7 +1252,7 @@ type ProxyConfig struct {
 	// proxies during CONNECT requests. Assume that at least _some_ of
 	// these headers are going to contain secrets and use Secret as the
 	// value type instead of string.
-	ProxyConnectHeader Header `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
+	ProxyConnectHeader ProxyHeader `yaml:"proxy_connect_header,omitempty" json:"proxy_connect_header,omitempty"`
 
 	proxyFunc func(*http.Request) (*url.URL, error)
 }
diff --git a/go.mod b/go.mod
index d27efad..4937716 100644
--- a/go.mod
+++ b/go.mod
@@ -8,10 +8,10 @@ require (
 	github.com/google/go-cmp v0.6.0
 	github.com/julienschmidt/httprouter v1.3.0
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f
-	github.com/prometheus/client_model v0.6.0
-	golang.org/x/net v0.22.0
-	golang.org/x/oauth2 v0.18.0
-	google.golang.org/protobuf v1.33.0
+	github.com/prometheus/client_model v0.6.1
+	golang.org/x/net v0.24.0
+	golang.org/x/oauth2 v0.19.0
+	google.golang.org/protobuf v1.34.0
 	gopkg.in/yaml.v2 v2.4.0
 )
 
@@ -20,15 +20,13 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/go-logfmt/logfmt v0.5.1 // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
 	github.com/prometheus/client_golang v1.19.0 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
 	github.com/rogpeppe/go-internal v1.10.0 // indirect
 	github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
 	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
 )
 
diff --git a/go.sum b/go.sum
index df79787..60ffee1 100644
--- a/go.sum
+++ b/go.sum
@@ -12,11 +12,6 @@ github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU=
 github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0=
 github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA=
 github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
@@ -34,8 +29,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
 github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
-github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
-github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@@ -45,27 +40,16 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc=
 github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
-golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
+golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
+google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
diff --git a/sigv4/go.mod b/sigv4/go.mod
index 1411264..1b8ff1f 100644
--- a/sigv4/go.mod
+++ b/sigv4/go.mod
@@ -5,7 +5,7 @@ go 1.20
 replace github.com/prometheus/common => ../
 
 require (
-	github.com/aws/aws-sdk-go v1.51.11
+	github.com/aws/aws-sdk-go v1.51.32
 	github.com/prometheus/client_golang v1.19.0
 	github.com/prometheus/common v0.48.0
 	github.com/stretchr/testify v1.9.0
@@ -16,19 +16,17 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/golang/protobuf v1.5.3 // indirect
 	github.com/jmespath/go-jmespath v0.4.0 // indirect
 	github.com/jpillora/backoff v1.0.0 // indirect
 	github.com/kr/text v0.2.0 // indirect
 	github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect
 	github.com/pmezard/go-difflib v1.0.0 // indirect
-	github.com/prometheus/client_model v0.6.0 // indirect
+	github.com/prometheus/client_model v0.6.1 // indirect
 	github.com/prometheus/procfs v0.12.0 // indirect
-	golang.org/x/net v0.22.0 // indirect
-	golang.org/x/oauth2 v0.18.0 // indirect
-	golang.org/x/sys v0.18.0 // indirect
+	golang.org/x/net v0.24.0 // indirect
+	golang.org/x/oauth2 v0.19.0 // indirect
+	golang.org/x/sys v0.19.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
-	google.golang.org/appengine v1.6.7 // indirect
-	google.golang.org/protobuf v1.33.0 // indirect
+	google.golang.org/protobuf v1.34.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/sigv4/go.sum b/sigv4/go.sum
index 0ad7a43..ebdbf74 100644
--- a/sigv4/go.sum
+++ b/sigv4/go.sum
@@ -1,5 +1,5 @@
-github.com/aws/aws-sdk-go v1.51.11 h1:El5VypsMIz7sFwAAj/j06JX9UGs4KAbAIEaZ57bNY4s=
-github.com/aws/aws-sdk-go v1.51.11/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go v1.51.32 h1:A6mPui7QP4mwmovyzgtdedbRbNur1Iu0/El7hBWNHms=
+github.com/aws/aws-sdk-go v1.51.32/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
@@ -8,11 +8,6 @@ github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ3
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
-github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
-github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
 github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@@ -29,35 +24,24 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
 github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
 github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
-github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
-github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
+github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
+github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
 github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
 github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
-golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
-golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
-golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
-golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
-golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
+golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
+golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
+golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
+golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
+golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
 golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
-google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
-google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
-google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
-google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4=
+google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=


Test patch:
diff --git a/config/config_test.go b/config/config_test.go
index af499d1..9486ba1 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -83,11 +83,11 @@ func TestJSONMarshalSecret(t *testing.T) {
 
 func TestHeaderHTTPHeader(t *testing.T) {
 	testcases := map[string]struct {
-		header   Header
+		header   ProxyHeader
 		expected http.Header
 	}{
 		"basic": {
-			header: Header{
+			header: ProxyHeader{
 				"single": []Secret{"v1"},
 				"multi":  []Secret{"v1", "v2"},
 				"empty":  []Secret{},
@@ -119,32 +119,32 @@ func TestHeaderHTTPHeader(t *testing.T) {
 func TestHeaderYamlUnmarshal(t *testing.T) {
 	testcases := map[string]struct {
 		input    string
-		expected Header
+		expected ProxyHeader
 	}{
 		"void": {
 			input: ``,
 		},
 		"simple": {
 			input:    "single:\n- a\n",
-			expected: Header{"single": []Secret{"a"}},
+			expected: ProxyHeader{"single": []Secret{"a"}},
 		},
 		"multi": {
 			input:    "multi:\n- a\n- b\n",
-			expected: Header{"multi": []Secret{"a", "b"}},
+			expected: ProxyHeader{"multi": []Secret{"a", "b"}},
 		},
 		"empty": {
 			input:    "{}",
-			expected: Header{},
+			expected: ProxyHeader{},
 		},
 		"empty value": {
 			input:    "empty:\n",
-			expected: Header{"empty": nil},
+			expected: ProxyHeader{"empty": nil},
 		},
 	}
 
 	for name, tc := range testcases {
 		t.Run(name, func(t *testing.T) {
-			var actual Header
+			var actual ProxyHeader
 			err := yaml.Unmarshal([]byte(tc.input), &actual)
 			if err != nil {
 				t.Fatalf("error unmarshaling %s: %s", tc.input, err)
@@ -158,7 +158,7 @@ func TestHeaderYamlUnmarshal(t *testing.T) {
 
 func TestHeaderYamlMarshal(t *testing.T) {
 	testcases := map[string]struct {
-		input    Header
+		input    ProxyHeader
 		expected []byte
 	}{
 		"void": {
@@ -166,15 +166,15 @@ func TestHeaderYamlMarshal(t *testing.T) {
 			expected: []byte("{}\n"),
 		},
 		"simple": {
-			input:    Header{"single": []Secret{"a"}},
+			input:    ProxyHeader{"single": []Secret{"a"}},
 			expected: []byte("single:\n- <secret>\n"),
 		},
 		"multi": {
-			input:    Header{"multi": []Secret{"a", "b"}},
+			input:    ProxyHeader{"multi": []Secret{"a", "b"}},
 			expected: []byte("multi:\n- <secret>\n- <secret>\n"),
 		},
 		"empty": {
-			input:    Header{"empty": nil},
+			input:    ProxyHeader{"empty": nil},
 			expected: []byte("empty: []\n"),
 		},
 	}
@@ -195,32 +195,32 @@ func TestHeaderYamlMarshal(t *testing.T) {
 func TestHeaderJsonUnmarshal(t *testing.T) {
 	testcases := map[string]struct {
 		input    string
-		expected Header
+		expected ProxyHeader
 	}{
 		"void": {
 			input: `null`,
 		},
 		"simple": {
 			input:    `{"single": ["a"]}`,
-			expected: Header{"single": []Secret{"a"}},
+			expected: ProxyHeader{"single": []Secret{"a"}},
 		},
 		"multi": {
 			input:    `{"multi": ["a", "b"]}`,
-			expected: Header{"multi": []Secret{"a", "b"}},
+			expected: ProxyHeader{"multi": []Secret{"a", "b"}},
 		},
 		"empty": {
 			input:    `{}`,
-			expected: Header{},
+			expected: ProxyHeader{},
 		},
 		"empty value": {
 			input:    `{"empty":null}`,
-			expected: Header{"empty": nil},
+			expected: ProxyHeader{"empty": nil},
 		},
 	}
 
 	for name, tc := range testcases {
 		t.Run(name, func(t *testing.T) {
-			var actual Header
+			var actual ProxyHeader
 			err := json.Unmarshal([]byte(tc.input), &actual)
 			if err != nil {
 				t.Fatalf("error unmarshaling %s: %s", tc.input, err)
@@ -234,7 +234,7 @@ func TestHeaderJsonUnmarshal(t *testing.T) {
 
 func TestHeaderJsonMarshal(t *testing.T) {
 	testcases := map[string]struct {
-		input    Header
+		input    ProxyHeader
 		expected []byte
 	}{
 		"void": {
@@ -242,15 +242,15 @@ func TestHeaderJsonMarshal(t *testing.T) {
 			expected: []byte("null"),
 		},
 		"simple": {
-			input:    Header{"single": []Secret{"a"}},
+			input:    ProxyHeader{"single": []Secret{"a"}},
 			expected: []byte("{\"single\":[\"\\u003csecret\\u003e\"]}"),
 		},
 		"multi": {
-			input:    Header{"multi": []Secret{"a", "b"}},
+			input:    ProxyHeader{"multi": []Secret{"a", "b"}},
 			expected: []byte("{\"multi\":[\"\\u003csecret\\u003e\",\"\\u003csecret\\u003e\"]}"),
 		},
 		"empty": {
-			input:    Header{"empty": nil},
+			input:    ProxyHeader{"empty": nil},
 			expected: []byte(`{"empty":null}`),
 		},
 	}
diff --git a/config/headers_test.go b/config/headers_test.go
new file mode 100644
index 0000000..39c6f9f
--- /dev/null
+++ b/config/headers_test.go
@@ -0,0 +1,31 @@
+// Copyright 2024 The Prometheus Authors
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// This package no longer handles safe yaml parsing. In order to
+// ensure correct yaml unmarshalling, use "yaml.UnmarshalStrict()".
+
+package config
+
+import (
+	"net/http"
+	"testing"
+)
+
+func TestReservedHeaders(t *testing.T) {
+	for k := range reservedHeaders {
+		l := http.CanonicalHeaderKey(k)
+		if k != l {
+			t.Errorf("reservedHeaders keys should be lowercase: got %q, expected %q", k, http.CanonicalHeaderKey(k))
+		}
+	}
+}
diff --git a/config/http_config_test.go b/config/http_config_test.go
index cd13a18..085ecb7 100644
--- a/config/http_config_test.go
+++ b/config/http_config_test.go
@@ -131,6 +131,10 @@ var invalidHTTPClientConfigs = []struct {
 		httpClientConfigFile: "testdata/http.conf.no-proxy-without-proxy-url.bad.yaml",
 		errMsg:               "if no_proxy is configured, proxy_url must also be configured",
 	},
+	{
+		httpClientConfigFile: "testdata/http.conf.headers-reserved.bad.yaml",
+		errMsg:               `setting header "User-Agent" is not allowed`,
+	},
 }
 
 func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httptest.Server, error) {
@@ -602,7 +606,7 @@ func TestNewClientFromInvalidConfig(t *testing.T) {
 					InsecureSkipVerify: true,
 				},
 			},
-			errorMsg: fmt.Sprintf("unable to load specified CA cert %s:", MissingCA),
+			errorMsg: fmt.Sprintf("unable to read CA cert: unable to read file %s", MissingCA),
 		},
 		{
 			clientConfig: HTTPClientConfig{
@@ -611,7 +615,7 @@ func TestNewClientFromInvalidConfig(t *testing.T) {
 					InsecureSkipVerify: true,
 				},
 			},
-			errorMsg: fmt.Sprintf("unable to use specified CA cert %s", InvalidCA),
+			errorMsg: fmt.Sprintf("unable to use specified CA cert file %s", InvalidCA),
 		},
 	}
 
@@ -702,7 +706,7 @@ func TestMissingBearerAuthFile(t *testing.T) {
 		t.Fatal("No error is returned here")
 	}
 
-	if !strings.Contains(err.Error(), "unable to read authorization credentials file missing/bearer.token: open missing/bearer.token: no such file or directory") {
+	if !strings.Contains(err.Error(), "unable to read authorization credentials: unable to read file missing/bearer.token: open missing/bearer.token: no such file or directory") {
 		t.Fatal("wrong error message being returned")
 	}
 }
@@ -721,7 +725,7 @@ func TestBearerAuthRoundTripper(t *testing.T) {
 	}, nil, nil)
 
 	// Normal flow.
-	bearerAuthRoundTripper := NewAuthorizationCredentialsRoundTripper("Bearer", BearerToken, fakeRoundTripper)
+	bearerAuthRoundTripper := NewAuthorizationCredentialsRoundTripper("Bearer", &inlineSecret{text: BearerToken}, fakeRoundTripper)
 	request, _ := http.NewRequest("GET", "/hitchhiker", nil)
 	request.Header.Set("User-Agent", "Douglas Adams mind")
 	_, err := bearerAuthRoundTripper.RoundTrip(request)
@@ -730,7 +734,7 @@ func TestBearerAuthRoundTripper(t *testing.T) {
 	}
 
 	// Should honor already Authorization header set.
-	bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsRoundTripper("Bearer", newBearerToken, fakeRoundTripper)
+	bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsRoundTripper("Bearer", &inlineSecret{text: newBearerToken}, fakeRoundTripper)
 	request, _ = http.NewRequest("GET", "/hitchhiker", nil)
 	request.Header.Set("Authorization", ExpectedBearer)
 	_, err = bearerAuthRoundTripperShouldNotModifyExistingAuthorization.RoundTrip(request)
@@ -749,7 +753,7 @@ func TestBearerAuthFileRoundTripper(t *testing.T) {
 	}, nil, nil)
 
 	// Normal flow.
-	bearerAuthRoundTripper := NewAuthorizationCredentialsFileRoundTripper("Bearer", BearerTokenFile, fakeRoundTripper)
+	bearerAuthRoundTripper := NewAuthorizationCredentialsRoundTripper("Bearer", &fileSecret{file: BearerTokenFile}, fakeRoundTripper)
 	request, _ := http.NewRequest("GET", "/hitchhiker", nil)
 	request.Header.Set("User-Agent", "Douglas Adams mind")
 	_, err := bearerAuthRoundTripper.RoundTrip(request)
@@ -758,7 +762,7 @@ func TestBearerAuthFileRoundTripper(t *testing.T) {
 	}
 
 	// Should honor already Authorization header set.
-	bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsFileRoundTripper("Bearer", MissingBearerTokenFile, fakeRoundTripper)
+	bearerAuthRoundTripperShouldNotModifyExistingAuthorization := NewAuthorizationCredentialsRoundTripper("Bearer", &fileSecret{file: MissingBearerTokenFile}, fakeRoundTripper)
 	request, _ = http.NewRequest("GET", "/hitchhiker", nil)
 	request.Header.Set("Authorization", ExpectedBearer)
 	_, err = bearerAuthRoundTripperShouldNotModifyExistingAuthorization.RoundTrip(request)
@@ -857,7 +861,7 @@ func TestTLSConfigInvalidCA(t *testing.T) {
 				ServerName:         "",
 				InsecureSkipVerify: false,
 			},
-			errorMessage: fmt.Sprintf("unable to load specified CA cert %s:", MissingCA),
+			errorMessage: fmt.Sprintf("unable to read CA cert: unable to read file %s", MissingCA),
 		},
 		{
 			configTLSConfig: TLSConfig{
@@ -867,7 +871,7 @@ func TestTLSConfigInvalidCA(t *testing.T) {
 				ServerName:         "",
 				InsecureSkipVerify: false,
 			},
-			errorMessage: fmt.Sprintf("unable to read specified client cert (%s):", MissingCert),
+			errorMessage: fmt.Sprintf("unable to read specified client cert: unable to read file %s", MissingCert),
 		},
 		{
 			configTLSConfig: TLSConfig{
@@ -877,7 +881,7 @@ func TestTLSConfigInvalidCA(t *testing.T) {
 				ServerName:         "",
 				InsecureSkipVerify: false,
 			},
-			errorMessage: fmt.Sprintf("unable to read specified client key (%s):", MissingKey),
+			errorMessage: fmt.Sprintf("unable to read specified client key: unable to read file %s", MissingKey),
 		},
 		{
 			configTLSConfig: TLSConfig{
@@ -930,14 +934,11 @@ func TestBasicAuthNoPassword(t *testing.T) {
 		t.Fatalf("Error casting to basic auth transport, %v", client.Transport)
 	}
 
-	if rt.username != "user" {
-		t.Errorf("Bad HTTP client username: %s", rt.username)
-	}
-	if string(rt.password) != "" {
-		t.Errorf("Expected empty HTTP client password: %s", rt.password)
+	if username, _ := rt.username.fetch(); username != "user" {
+		t.Errorf("Bad HTTP client username: %s", username)
 	}
-	if string(rt.passwordFile) != "" {
-		t.Errorf("Expected empty HTTP client passwordFile: %s", rt.passwordFile)
+	if rt.password != nil {
+		t.Errorf("Expected empty HTTP client password")
 	}
 }
 
@@ -956,14 +957,11 @@ func TestBasicAuthNoUsername(t *testing.T) {
 		t.Fatalf("Error casting to basic auth transport, %v", client.Transport)
 	}
 
-	if rt.username != "" {
-		t.Errorf("Got unexpected username: %s", rt.username)
+	if rt.username != nil {
+		t.Errorf("Got unexpected username")
 	}
-	if string(rt.password) != "secret" {
-		t.Errorf("Unexpected HTTP client password: %s", string(rt.password))
-	}
-	if string(rt.passwordFile) != "" {
-		t.Errorf("Expected empty HTTP client passwordFile: %s", rt.passwordFile)
+	if password, _ := rt.password.fetch(); password != "secret" {
+		t.Errorf("Unexpected HTTP client password: %s", password)
 	}
 }
 
@@ -982,14 +980,11 @@ func TestBasicAuthPasswordFile(t *testing.T) {
 		t.Fatalf("Error casting to basic auth transport, %v", client.Transport)
 	}
 
-	if rt.username != "user" {
-		t.Errorf("Bad HTTP client username: %s", rt.username)
-	}
-	if string(rt.password) != "" {
-		t.Errorf("Bad HTTP client password: %s", rt.password)
+	if username, _ := rt.username.fetch(); username != "user" {
+		t.Errorf("Bad HTTP client username: %s", username)
 	}
-	if string(rt.passwordFile) != "testdata/basic-auth-password" {
-		t.Errorf("Bad HTTP client passwordFile: %s", rt.passwordFile)
+	if password, _ := rt.password.fetch(); password != "foobar" {
+		t.Errorf("Bad HTTP client password: %s", password)
 	}
 }
 
@@ -1008,14 +1003,11 @@ func TestBasicUsernameFile(t *testing.T) {
 		t.Fatalf("Error casting to basic auth transport, %v", client.Transport)
 	}
 
-	if rt.username != "" {
-		t.Errorf("Bad HTTP client username: %s", rt.username)
-	}
-	if string(rt.usernameFile) != "testdata/basic-auth-username" {
-		t.Errorf("Bad HTTP client usernameFile: %s", rt.usernameFile)
+	if username, _ := rt.username.fetch(); username != "testuser" {
+		t.Errorf("Bad HTTP client username: %s", username)
 	}
-	if string(rt.passwordFile) != "testdata/basic-auth-password" {
-		t.Errorf("Bad HTTP client passwordFile: %s", rt.passwordFile)
+	if password, _ := rt.password.fetch(); password != "foobar" {
+		t.Errorf("Bad HTTP client passwordFile: %s", password)
 	}
 }
 
@@ -1244,7 +1236,7 @@ func TestTLSRoundTripper_Inline(t *testing.T) {
 			certText: readFile(t, ClientCertificatePath),
 			keyText:  readFile(t, ClientKeyNoPassPath),
 
-			errMsg: "unable to use inline CA cert",
+			errMsg: "unable to use specified CA cert inline",
 		},
 		{
 			// Invalid cert.
@@ -1439,7 +1431,7 @@ func TestInvalidHTTPConfigs(t *testing.T) {
 	for _, ee := range invalidHTTPClientConfigs {
 		_, _, err := LoadHTTPConfigFile(ee.httpClientConfigFile)
 		if err == nil {
-			t.Error("Expected error with config but got none")
+			t.Errorf("Expected error with config %q but got none", ee.httpClientConfigFile)
 			continue
 		}
 		if !strings.Contains(err.Error(), ee.errMsg) {
@@ -2230,3 +2222,63 @@ func readFile(t *testing.T, filename string) string {
 
 	return string(content)
 }
+
+func TestHeaders(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		for k, v := range map[string]string{
+			"One":   "value1",
+			"Two":   "value2",
+			"Three": "value3",
+		} {
+			if r.Header.Get(k) != v {
+				t.Errorf("expected %q, got %q", v, r.Header.Get(k))
+			}
+		}
+		w.WriteHeader(http.StatusNoContent)
+	}))
+	t.Cleanup(ts.Close)
+
+	cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.headers.good.yaml")
+	if err != nil {
+		t.Fatalf("Error loading HTTP client config: %v", err)
+	}
+	client, err := NewClientFromConfig(*cfg, "test")
+	if err != nil {
+		t.Fatalf("Error creating HTTP Client: %v", err)
+	}
+
+	_, err = client.Get(ts.URL)
+	if err != nil {
+		t.Fatalf("can't fetch URL: %v", err)
+	}
+}
+
+func TestMultipleHeaders(t *testing.T) {
+	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+		for k, v := range map[string][]string{
+			"One":   {"value1a", "value1b", "value1c"},
+			"Two":   {"value2a", "value2b", "value2c"},
+			"Three": {"value3a", "value3b", "value3c"},
+		} {
+			if !reflect.DeepEqual(r.Header.Values(k), v) {
+				t.Errorf("expected %v, got %v", v, r.Header.Values(k))
+			}
+		}
+		w.WriteHeader(http.StatusNoContent)
+	}))
+	t.Cleanup(ts.Close)
+
+	cfg, _, err := LoadHTTPConfigFile("testdata/http.conf.headers-multiple.good.yaml")
+	if err != nil {
+		t.Fatalf("Error loading HTTP client config: %v", err)
+	}
+	client, err := NewClientFromConfig(*cfg, "test")
+	if err != nil {
+		t.Fatalf("Error creating HTTP Client: %v", err)
+	}
+
+	_, err = client.Get(ts.URL)
+	if err != nil {
+		t.Fatalf("can't fetch URL: %v", err)
+	}
+}
diff --git a/config/testdata/headers-file b/config/testdata/headers-file
new file mode 100644
index 0000000..44cd5c8
--- /dev/null
+++ b/config/testdata/headers-file
@@ -0,0 +1,1 @@
+value3
diff --git a/config/testdata/headers-file-a b/config/testdata/headers-file-a
new file mode 100644
index 0000000..0d2a9cd
--- /dev/null
+++ b/config/testdata/headers-file-a
@@ -0,0 +1,3 @@
+value3a
+
+
diff --git a/config/testdata/headers-file-b b/config/testdata/headers-file-b
new file mode 100644
index 0000000..c1240b4
--- /dev/null
+++ b/config/testdata/headers-file-b
@@ -0,0 +1,1 @@
+value3b 
diff --git a/config/testdata/headers-file-c b/config/testdata/headers-file-c
new file mode 100644
index 0000000..264b69d
--- /dev/null
+++ b/config/testdata/headers-file-c
@@ -0,0 +1,1 @@
+value3c
diff --git a/config/testdata/http.conf.headers-multiple.good.yaml b/config/testdata/http.conf.headers-multiple.good.yaml
new file mode 100644
index 0000000..f9dcdc0
--- /dev/null
+++ b/config/testdata/http.conf.headers-multiple.good.yaml
@@ -0,0 +1,8 @@
+http_headers:
+  one:
+    values: [value1a, value1b, value1c]
+  two:
+    values: [value2a]
+    secrets: [value2b, value2c]
+  three:
+    files: [testdata/headers-file-a, testdata/headers-file-b, testdata/headers-file-c]
diff --git a/config/testdata/http.conf.headers-reserved.bad.yaml b/config/testdata/http.conf.headers-reserved.bad.yaml
new file mode 100644
index 0000000..4e48856
--- /dev/null
+++ b/config/testdata/http.conf.headers-reserved.bad.yaml
@@ -0,0 +1,3 @@
+http_headers:
+  user-Agent:
+    values: [bar]
diff --git a/config/testdata/http.conf.headers.good.yaml b/config/testdata/http.conf.headers.good.yaml
new file mode 100644
index 0000000..f8a0236
--- /dev/null
+++ b/config/testdata/http.conf.headers.good.yaml
@@ -0,0 +1,7 @@
+http_headers:
+  one:
+    values: [value1]
+  two:
+    secrets: [value2]
+  three:
+    files: [testdata/headers-file]