From 201669c9d8e36fe005bd439a8924e46ded9f52b4 Mon Sep 17 00:00:00 2001 From: Kim Vandry Date: Sun, 7 Sep 2025 11:41:27 +0100 Subject: [PATCH] Add support for target scraping with SPIFFE This is the first step toward https://github.com/prometheus/common/issues/702 The feature is not available until the calling code invokes the WithSpiffeSourceFactory HTTPClientOptions, so no behaviour is yet changed. Use of that option will be the subject of a future change to https://github.com/prometheus/prometheus SPIFFE replaces the usual peer X.509 certificate verification algorithm with its own that checks SPIFFE IDs in URI SANs and checks against different sets of trust roots for different SPIFFE trust domains. Accordingly, none of the other TLS configuration parameters are applicable when SPIFFE is configured and vice versa and this mutual exclusivity is enforced in tls_config. There are two ways to set which SPIFFE ID should be expected from a scrape endpoint that is using HTTPS and SPIFFE: in the tls_config, and per-request. The former would be expected mainly on static scrape configs that define a single (perhaps replicated) scrape endpoint. For scrape configs with target discovery it is expected that different endpoints would present certificates with different SPIFFE IDs and so the per-request version would be used. It is intended that the peer's expected SPIFFE ID should come from a new special label __spiffe_id__ which would be populated by target discovery. That too will be part of the next change. For the purposes of this library, the per-request peer SPIFFE ID is supplied in the Context accompanying the Request. Signed-off-by: Kim Vandry --- config/http_config.go | 72 ++++++++ config/http_config_test.go | 167 ++++++++++++++++++ config/testdata/spiffe.bundle.pem | 13 ++ config/testdata/spiffe.workload1.cert.pem | 14 ++ config/testdata/spiffe.workload1.key.pem | 5 + config/testdata/spiffe.workload2.cert.pem | 14 ++ config/testdata/spiffe.workload2.key.pem | 5 + .../tls_config.spiffe_and_other.bad.json | 2 + config/tls_config_test.go | 4 + go.mod | 6 + go.sum | 34 ++++ model/labels.go | 4 + 12 files changed, 340 insertions(+) create mode 100644 config/testdata/spiffe.bundle.pem create mode 100644 config/testdata/spiffe.workload1.cert.pem create mode 100644 config/testdata/spiffe.workload1.key.pem create mode 100644 config/testdata/spiffe.workload2.cert.pem create mode 100644 config/testdata/spiffe.workload2.key.pem create mode 100644 config/testdata/tls_config.spiffe_and_other.bad.json diff --git a/config/http_config.go b/config/http_config.go index 4e5ff92a2..8ec9ef65f 100644 --- a/config/http_config.go +++ b/config/http_config.go @@ -32,6 +32,11 @@ import ( "time" conntrack "github.com/mwitkow/go-conntrack" + spiffebundle "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/spiffetls" + spiffetlsconfig "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" + spiffesvid "github.com/spiffe/go-spiffe/v2/svid/x509svid" "go.yaml.in/yaml/v2" "golang.org/x/net/http/httpproxy" "golang.org/x/net/http2" @@ -456,6 +461,11 @@ type DialContextFunc func(context.Context, string, string) (net.Conn, error) // NewTLSConfigFunc returns tls.Config. type NewTLSConfigFunc func(context.Context, *TLSConfig, ...TLSConfigOption) (*tls.Config, error) +type SpiffeSvidAndBundleSource interface { + spiffesvid.Source + spiffebundle.Source +} + type httpClientOptions struct { dialContextFunc DialContextFunc newTLSConfigFunc NewTLSConfigFunc @@ -465,6 +475,7 @@ type httpClientOptions struct { userAgent string host string secretManager SecretManager + spiffeSourceFn func() (SpiffeSvidAndBundleSource, error) } // HTTPClientOption defines an option that can be applied to the HTTP client. @@ -530,6 +541,20 @@ func WithHost(host string) HTTPClientOption { }) } +// WithSpiffeSourceFactory allows SPIFFE to be used with this HTTP client. +// The provided function should return the same X509Source on every call +// since all clients can share the same source. The source may either +// already exist (in which case the function can just return a fixed value) +// or be created on demand (in which case no X509Source will be created unless +// SPIFFE is configured and used). The returned X509Source will not be closed +// during the lifetime of the HTTPClient. The default is that there is no +// factory function and SPIFFE is not available. +func WithSpiffeSourceFactory(fn func() (SpiffeSvidAndBundleSource, error)) HTTPClientOption { + return httpClientOptionFunc(func(opts *httpClientOptions) { + opts.spiffeSourceFn = fn + }) +} + type secretManagerOption struct { secretManager SecretManager } @@ -584,6 +609,28 @@ func NewRoundTripperFromConfig(cfg HTTPClientConfig, name string, optFuncs ...HT return NewRoundTripperFromConfigWithContext(context.Background(), cfg, name, optFuncs...) } +func makeSpiffeDialer(configuredSpiffeID string, getSource func() (SpiffeSvidAndBundleSource, error)) func(ctx context.Context, network, addr string) (net.Conn, error) { + return func(ctx context.Context, network, addr string) (net.Conn, error) { + ids := configuredSpiffeID + if idc := ctx.Value(SpiffeIDContextValue); idc != nil { + ids = idc.(string) + } + peer, err := spiffeid.FromString(ids) + if err != nil { + return nil, fmt.Errorf("unparsable SPIFFE ID %q: %w", ids, err) + } + if getSource == nil { + return nil, errors.New("SPIFFE requested but not configured") + } + source, err := getSource() + if err != nil { + return nil, err + } + mode := spiffetls.MTLSClientWithRawConfig(spiffetlsconfig.AuthorizeID(peer), source, source) + return spiffetls.DialWithMode(ctx, network, addr, mode) + } +} + // NewRoundTripperFromConfigWithContext returns a new HTTP RoundTripper configured for the // given config.HTTPClientConfig and config.HTTPClientOption. // The name is used as go-conntrack metric label. @@ -607,6 +654,11 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon } newRT := func(tlsConfig *tls.Config) (http.RoundTripper, error) { + var dialTLS func(ctx context.Context, network, addr string) (net.Conn, error) + if tlsConfig == nil { + // Use SPIFFE + dialTLS = makeSpiffeDialer(cfg.TLSConfig.SpiffeID, opts.spiffeSourceFn) + } // The only timeout we care about is the configured scrape timeout. // It is applied on request. So we leave out any timings here. var rt http.RoundTripper = &http.Transport{ @@ -621,6 +673,7 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, DialContext: dialContext, + DialTLSContext: dialTLS, } if opts.http2Enabled && cfg.EnableHTTP2 { // HTTP/2 support is golang had many problematic cornercases where @@ -691,6 +744,10 @@ func NewRoundTripperFromConfigWithContext(ctx context.Context, cfg HTTPClientCon return rt, nil } + if cfg.TLSConfig.SpiffeID != "" || cfg.TLSConfig.UseSpiffe { + return newRT(nil) + } + tlsConfig, err := opts.newTLSConfigFunc(ctx, &cfg.TLSConfig, WithSecretManager(opts.secretManager)) if err != nil { return nil, err @@ -1140,6 +1197,13 @@ type TLSConfig struct { MinVersion TLSVersion `yaml:"min_version,omitempty" json:"min_version,omitempty"` // Maximum TLS version. MaxVersion TLSVersion `yaml:"max_version,omitempty" json:"max_version,omitempty"` + // Use SPIFFE to configure TLS. The special label `__spiffe_id__` on the + // scrape target configures the SPIFFE ID that must be presented. + UseSpiffe bool `yaml:"use_spiffe,omitempty" json:"use_spiffe,omitempty"` + // Use SPIFFE to configure TLS. The special label `__spiffe_id__` on the + // scrape target (first) or the value of this parameter (otherwise) + // configures the SPIFFE ID that must be presented. + SpiffeID string `yaml:"spiffe_id,omitempty" json:"spiffe_id,omitempty"` } // SetDirectory joins any relative file paths with dir. @@ -1181,6 +1245,10 @@ func (c *TLSConfig) Validate() error { return errors.New("exactly one of cert or cert_file must be configured when a client key is configured") } + if (len(c.CA) > 0 || len(c.CAFile) > 0 || len(c.CARef) > 0 || len(c.Cert) > 0 || len(c.CertFile) > 0 || len(c.CertRef) > 0 || len(c.Key) > 0 || len(c.KeyFile) > 0 || len(c.KeyRef) > 0 || len(c.ServerName) > 0 || c.InsecureSkipVerify) && (len(c.SpiffeID) > 0 || c.UseSpiffe) { + return errors.New("either SPIFFE settings or other TLSConfig settings may be set but not both") + } + return nil } @@ -1536,3 +1604,7 @@ func (c *ProxyConfig) Proxy() (fn func(*http.Request) (*url.URL, error)) { func (c *ProxyConfig) GetProxyConnectHeader() http.Header { return c.ProxyConnectHeader.HTTPHeader() } + +type spiffeIDContextValue bool + +const SpiffeIDContextValue = spiffeIDContextValue(false) diff --git a/config/http_config_test.go b/config/http_config_test.go index c8853592e..795d2b3e9 100644 --- a/config/http_config_test.go +++ b/config/http_config_test.go @@ -15,9 +15,11 @@ package config import ( "context" + "crypto" "crypto/tls" "crypto/x509" "encoding/json" + "encoding/pem" "errors" "fmt" "io" @@ -35,6 +37,9 @@ import ( "testing" "time" + spiffebundle "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + spiffesvid "github.com/spiffe/go-spiffe/v2/svid/x509svid" "github.com/stretchr/testify/require" "go.yaml.in/yaml/v2" ) @@ -53,6 +58,12 @@ const ( MissingCert = "missing/cert.crt" MissingKey = "missing/secret.key" + SpiffeWorkload1Cert = "testdata/spiffe.workload1.cert.pem" + SpiffeWorkload1Key = "testdata/spiffe.workload1.key.pem" + SpiffeWorkload2Cert = "testdata/spiffe.workload2.cert.pem" + SpiffeWorkload2Key = "testdata/spiffe.workload2.key.pem" + SpiffeBundle = "testdata/spiffe.bundle.pem" + ExpectedMessage = "I'm here to serve you!!!" ExpectedError = "expected error" AuthorizationCredentials = "theanswertothegreatquestionoflifetheuniverseandeverythingisfortytwo" @@ -166,6 +177,37 @@ func newTestServer(handler func(w http.ResponseWriter, r *http.Request)) (*httpt return testServer, nil } +func newSpiffeTestServer() (*httptest.Server, error) { + handler := func(w http.ResponseWriter, _ *http.Request) { + fmt.Fprint(w, ExpectedMessage) + } + testServer := httptest.NewUnstartedServer(http.HandlerFunc(handler)) + + tlsCAChain, err := os.ReadFile(SpiffeBundle) + if err != nil { + return nil, fmt.Errorf("Can't read %s", SpiffeBundle) + } + serverCertificate, err := tls.LoadX509KeyPair(SpiffeWorkload1Cert, SpiffeWorkload1Key) + if err != nil { + return nil, fmt.Errorf("Can't load X509 key pair %s - %s", SpiffeWorkload1Cert, SpiffeWorkload1Key) + } + + rootCAs := x509.NewCertPool() + rootCAs.AppendCertsFromPEM(tlsCAChain) + + testServer.TLS = &tls.Config{ + Certificates: make([]tls.Certificate, 1), + RootCAs: rootCAs, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCAs: rootCAs, + } + testServer.TLS.Certificates[0] = serverCertificate + + testServer.StartTLS() + + return testServer, nil +} + func TestNewClientFromConfig(t *testing.T) { newClientValidConfig := []struct { clientConfig HTTPClientConfig @@ -1275,6 +1317,131 @@ func TestTLSRoundTripper_Inline(t *testing.T) { } } +type testSpiffeSource struct{} + +func (*testSpiffeSource) GetX509SVID() (*spiffesvid.SVID, error) { + cert, err := tls.LoadX509KeyPair(SpiffeWorkload2Cert, SpiffeWorkload2Key) + if err != nil { + return nil, fmt.Errorf("Can't load X509 key pair %s - %s", SpiffeWorkload2Cert, SpiffeWorkload2Key) + } + if signer, ok := cert.PrivateKey.(crypto.Signer); ok { + return &spiffesvid.SVID{ + ID: spiffeid.RequireFromString("spiffe://example.org/workload2"), + Certificates: []*x509.Certificate{cert.Leaf}, + PrivateKey: signer, + }, nil + } + return nil, errors.New("private key is not crypto.Signer") +} + +func (*testSpiffeSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*spiffebundle.Bundle, error) { + if trustDomain.Name() == "example.org" { + bundlePem, err := os.ReadFile(SpiffeBundle) + if err != nil { + return nil, fmt.Errorf("Can't read %s", SpiffeBundle) + } + var bundle []*x509.Certificate + for len(bundlePem) > 0 { + b, more := pem.Decode(bundlePem) + c, err := x509.ParseCertificate(b.Bytes) + if err != nil { + return nil, fmt.Errorf("Can't parse %s as a certificate", SpiffeBundle) + } + bundlePem = more + bundle = append(bundle, c) + } + return spiffebundle.FromX509Authorities(trustDomain, bundle), nil + } + return nil, fmt.Errorf("No bundle for trust domain %v", trustDomain) +} + +func spiffeMaker() (SpiffeSvidAndBundleSource, error) { + return &testSpiffeSource{}, nil +} + +func TestTLSRoundTripper_SPIFFE(t *testing.T) { + testServer, err := newSpiffeTestServer() + require.NoError(t, err) + defer testServer.Close() + + testCases := []struct { + disabled bool + useSpiffe bool + confID string + ctxID string + + errMsg string + }{ + { + disabled: true, + confID: "spiffe://trust.domain/foo", + errMsg: "SPIFFE requested but not configured", + }, + { + confID: "spiffe://example.org/workload1", + }, + { + confID: "spiffe://example.org/wrong", + errMsg: "unexpected ID", + }, + { + confID: "unparsable", + errMsg: "unparsable", + }, + { + useSpiffe: true, + ctxID: "spiffe://example.org/workload1", + }, + { + confID: "spiffe://overridden/ignored", + ctxID: "spiffe://example.org/workload1", + }, + } + + for i, tc := range testCases { + tc := tc + t.Run(strconv.Itoa(i), func(t *testing.T) { + cfg := HTTPClientConfig{ + TLSConfig: TLSConfig{ + UseSpiffe: tc.useSpiffe, + SpiffeID: tc.confID, + }, + } + + var opts []HTTPClientOption + if !tc.disabled { + opts = append(opts, WithSpiffeSourceFactory(spiffeMaker)) + } + c, err := NewClientFromConfig(cfg, "test", opts...) + require.NoErrorf(t, err, "Error creating HTTP client: %v", err) + req, err := http.NewRequest(http.MethodGet, testServer.URL, nil) + require.NoErrorf(t, err, "Error creating HTTP request: %v", err) + ctx := context.Background() + if tc.ctxID != "" { + ctx = context.WithValue(ctx, SpiffeIDContextValue, tc.ctxID) + } + r, err := c.Do(req.WithContext(ctx)) + if tc.errMsg != "" { + require.ErrorContainsf(t, err, tc.errMsg, "Expected error message to contain %q, got %q", tc.errMsg, err) + return + } else if err != nil { + t.Fatalf("Error executing HTTP request: %v", err) + } + + b, err := io.ReadAll(r.Body) + r.Body.Close() + if err != nil { + t.Errorf("Can't read the server response body") + } + + got := strings.TrimSpace(string(b)) + if ExpectedMessage != got { + t.Errorf("The expected message %q differs from the obtained message %q", ExpectedMessage, got) + } + }) + } +} + func TestTLSRoundTripperRaces(t *testing.T) { bs := getCertificateBlobs(t) diff --git a/config/testdata/spiffe.bundle.pem b/config/testdata/spiffe.bundle.pem new file mode 100644 index 000000000..6153481cd --- /dev/null +++ b/config/testdata/spiffe.bundle.pem @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIICBDCCAaqgAwIBAgIRAP6xM1DuTb3X0lAhRTLmNhMwCgYIKoZIzj0EAwIwUDEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTEwMC4GA1UEBRMnMzM4NTQzOTg4 +Mjg4MjIzNDk5NzE4MjE1ODYwMjk1NDUyNDcyODUxMCAXDTI1MDkwNzA5MjQzMVoY +DzIyMjUwNzIxMDkyNDQxWjBQMQswCQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZF +MTAwLgYDVQQFEyczMzg1NDM5ODgyODgyMjM0OTk3MTgyMTU4NjAyOTU0NTI0NzI4 +NTEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAQtQEBKDICx1ywQOjIiYGOO6DI8 +edAy3MxMQrWs737AgD45ixo1fNIGHfpD9v0WH3kq5C9ycB5YNpIaSdfXJiiJo2Mw +YTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5LV +HKgEZ5Kvs50NaJboDBt+U/owHwYDVR0RBBgwFoYUc3BpZmZlOi8vZXhhbXBsZS5v +cmcwCgYIKoZIzj0EAwIDSAAwRQIgOvUg85SrI2hmeCStTkCF9or0Vgcuzdifwfq5 +qQBJ9W8CIQD13tnaExaopel4BbweByAnXHqYEwomLlMbLs6ldz7a7w== +-----END CERTIFICATE----- diff --git a/config/testdata/spiffe.workload1.cert.pem b/config/testdata/spiffe.workload1.cert.pem new file mode 100644 index 000000000..d4ff780c2 --- /dev/null +++ b/config/testdata/spiffe.workload1.cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGjCCAb+gAwIBAgIQS8qrtPjeVHsY0SXsfT8B/zAKBggqhkjOPQQDAjBQMQsw +CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMTAwLgYDVQQFEyczMzg1NDM5ODgy +ODgyMjM0OTk3MTgyMTU4NjAyOTU0NTI0NzI4NTEwIBcNMjUwOTA3MDkyNjUzWhgP +MjEwMDA4MjAwOTI3MDNaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABO+FhNA7IYyFNA/BDOovyNVlwZVesbfj +knv/alVHrJ8Z5PPLv5RC4EqxDUCIdYH8IcfpXbRUsxV3Y8oIRkQFWpOjgaswgagw +DgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAM +BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBQJ/hirbRAZC32OWIHWBdVfOF67gzAfBgNV +HSMEGDAWgBRnktUcqARnkq+znQ1olugMG35T+jApBgNVHREEIjAghh5zcGlmZmU6 +Ly9leGFtcGxlLm9yZy93b3JrbG9hZDEwCgYIKoZIzj0EAwIDSQAwRgIhAPIkKNdq +SBb9ROIe2eo5ed1/0ay89UMjd+dxlkuyX8jAAiEAx1P1bBnzAVhuQx0YbzG2lJnK +QBCtPcGZ/amdWZt9G0A= +-----END CERTIFICATE----- diff --git a/config/testdata/spiffe.workload1.key.pem b/config/testdata/spiffe.workload1.key.pem new file mode 100644 index 000000000..983c5a6aa --- /dev/null +++ b/config/testdata/spiffe.workload1.key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgorRBVyov8trytD8G +v094aUhNkeOlBfV6mdZfC7rm66+hRANCAATvhYTQOyGMhTQPwQzqL8jVZcGVXrG3 +45J7/2pVR6yfGeTzy7+UQuBKsQ1AiHWB/CHH6V20VLMVd2PKCEZEBVqT +-----END PRIVATE KEY----- diff --git a/config/testdata/spiffe.workload2.cert.pem b/config/testdata/spiffe.workload2.cert.pem new file mode 100644 index 000000000..cf7a628c6 --- /dev/null +++ b/config/testdata/spiffe.workload2.cert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICGTCCAb+gAwIBAgIQLObm0tancKOMatNi8ON8VTAKBggqhkjOPQQDAjBQMQsw +CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMTAwLgYDVQQFEyczMzg1NDM5ODgy +ODgyMjM0OTk3MTgyMTU4NjAyOTU0NTI0NzI4NTEwIBcNMjUwOTA3MDkyNzAzWhgP +MjEwMDA4MjAwOTI3MTNaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABO+KKXV+1AeZpCarfpIWbjDePmzPdB8s +t5vd256f0t3qsvZ9edVAVBsW69gf49813V8KysVJSbDDM1bipiwx6T2jgaswgagw +DgYDVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAM +BgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSdcdhiF8avWCJsGmHHoU71DtYwrjAfBgNV +HSMEGDAWgBRnktUcqARnkq+znQ1olugMG35T+jApBgNVHREEIjAghh5zcGlmZmU6 +Ly9leGFtcGxlLm9yZy93b3JrbG9hZDIwCgYIKoZIzj0EAwIDSAAwRQIgGeWI3yvG +pXMQr6BRlmH1zv7uhO07aBhX86iKbp1JYCcCIQDuNzook5Y+okja2nepHjoqxNj3 +xLjI46t6wCxLvmWV/Q== +-----END CERTIFICATE----- diff --git a/config/testdata/spiffe.workload2.key.pem b/config/testdata/spiffe.workload2.key.pem new file mode 100644 index 000000000..458c1a6c4 --- /dev/null +++ b/config/testdata/spiffe.workload2.key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg9wQQ4VluDQR9MC99 +EfPcZ33IC99bY80M/UiKcM/P8WKhRANCAATviil1ftQHmaQmq36SFm4w3j5sz3Qf +LLeb3duen9Ld6rL2fXnVQFQbFuvYH+PfNd1fCsrFSUmwwzNW4qYsMek9 +-----END PRIVATE KEY----- diff --git a/config/testdata/tls_config.spiffe_and_other.bad.json b/config/testdata/tls_config.spiffe_and_other.bad.json new file mode 100644 index 000000000..32806f4b2 --- /dev/null +++ b/config/testdata/tls_config.spiffe_and_other.bad.json @@ -0,0 +1,2 @@ +{"ca_file": "something", +"spiffe_id": "spiffe://trust.domain/expected/peer"} diff --git a/config/tls_config_test.go b/config/tls_config_test.go index f02c87fdd..69fda84d8 100644 --- a/config/tls_config_test.go +++ b/config/tls_config_test.go @@ -117,6 +117,10 @@ var invalidTLSConfigs = []struct { filename: "tls_config.max_and_min_version.bad.yml", errMsg: "tls_config.max_version must be greater than or equal to tls_config.min_version if both are specified", }, + { + filename: "tls_config.spiffe_and_other.bad.json", + errMsg: "either SPIFFE settings or other TLSConfig settings may be set but not both", + }, } func TestInvalidTLSConfig(t *testing.T) { diff --git a/go.mod b/go.mod index f0eb3d693..8c6cdda95 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f github.com/prometheus/client_model v0.6.2 + github.com/spiffe/go-spiffe/v2 v2.6.0 github.com/stretchr/testify v1.11.1 go.yaml.in/yaml/v2 v2.4.3 golang.org/x/net v0.44.0 @@ -17,18 +18,23 @@ require ( ) require ( + github.com/Microsoft/go-winio v0.6.2 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-jose/go-jose/v4 v4.1.2 // indirect github.com/jpillora/backoff v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.20.4 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/xhit/go-str2duration/v2 v2.1.0 // indirect + golang.org/x/crypto v0.42.0 // indirect golang.org/x/sys v0.36.0 // indirect golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect + google.golang.org/grpc v1.75.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3c5194f10..94e0882ad 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= @@ -9,8 +11,18 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL 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/go-jose/go-jose/v4 v4.1.2 h1:TK/7NqRQZfgAh+Td8AlsrvtPoUyiHh0LqVvokh+1vHI= +github.com/go-jose/go-jose/v4 v4.1.2/go.mod h1:22cg9HWM1pOlnRiY+9cQYJ9XHmya1bYW8OeDM6Ku6Oo= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= @@ -37,6 +49,8 @@ github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0leargg github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= +github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -49,8 +63,22 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= @@ -59,6 +87,12 @@ golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4= +google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/model/labels.go b/model/labels.go index dfeb34be5..682106cf8 100644 --- a/model/labels.go +++ b/model/labels.go @@ -59,6 +59,10 @@ const ( // timeout used to scrape a target. ScrapeTimeoutLabel = "__scrape_timeout__" + // SPIFFE ID that must be presented by the scrape target if scraping over + // https and SPIFFE is enabled. + SpiffeIDLabel = "__spiffe_id__" + // ReservedLabelPrefix is a prefix which is not legal in user-supplied // label names. ReservedLabelPrefix = "__"