diff --git a/cmd/spire-server/cli/run/run.go b/cmd/spire-server/cli/run/run.go index f0bc3a20159..b76683ff3ee 100644 --- a/cmd/spire-server/cli/run/run.go +++ b/cmd/spire-server/cli/run/run.go @@ -459,11 +459,8 @@ func NewServerConfig(c *Config, logOptions []log.Option, allowUnknownConfig bool for _, adminID := range c.Server.AdminIDs { id, err := spiffeid.FromString(adminID) - switch { - case err != nil: + if err != nil { return nil, fmt.Errorf("could not parse admin ID %q: %w", adminID, err) - case !id.MemberOf(sc.TrustDomain): - return nil, fmt.Errorf("admin ID %q does not belong to trust domain %q", id, sc.TrustDomain) } sc.AdminIDs = append(sc.AdminIDs, id) } diff --git a/cmd/spire-server/cli/run/run_test.go b/cmd/spire-server/cli/run/run_test.go index d4fa675a3b4..3933ef592d1 100644 --- a/cmd/spire-server/cli/run/run_test.go +++ b/cmd/spire-server/cli/run/run_test.go @@ -994,15 +994,17 @@ func TestNewServerConfig(t *testing.T) { }, }, { - msg: "admin ID does not belong to the trust domain", + msg: "admin ID of foreign trust domain", input: func(c *Config) { c.Server.AdminIDs = []string{ "spiffe://otherdomain.test/my/admin", } }, - expectError: true, + expectError: false, test: func(t *testing.T, c *server.Config) { - require.Nil(t, c) + require.Equal(t, []spiffeid.ID{ + spiffeid.RequireFromString("spiffe://otherdomain.test/my/admin"), + }, c.AdminIDs) }, }, { diff --git a/conf/server/server_full.conf b/conf/server/server_full.conf index e5c0e41cbbb..242d81c68b7 100644 --- a/conf/server/server_full.conf +++ b/conf/server/server_full.conf @@ -3,10 +3,10 @@ # server: Contains core configuration parameters. server { - # admin_ids: SPIFFE IDs that, when present in a caller's X509-SVID, grant - # that caller admin privileges. The admin IDs must reside in the same trust - # domain as the server and need not have a corresponding admin registration - # entry with the server. + # admin_ids: SPIFFE IDs that, when present in a caller's X509-SVID, grant + # that caller admin privileges. The admin IDs must reside in the server + # trust domain or a federated one, and need not have a corresponding + # admin registration entry with the server. # admin_ids = ["spiffe://example.org/my/admin"] # bind_address: IP address or DNS name of the SPIRE server. diff --git a/doc/spire_server.md b/doc/spire_server.md index 33a86b1f258..8e1ba3c231b 100644 --- a/doc/spire_server.md +++ b/doc/spire_server.md @@ -48,35 +48,35 @@ SPIRE configuration files may be represented in either HCL or JSON. Please see t If the -expandEnv flag is passed to SPIRE, `$VARIABLE` or `${VARIABLE}` style environment variables are expanded before parsing. This may be useful for templating configuration files, for example across different trust domains, or for inserting secrets like database connection passwords. -| Configuration | Description | Default | -|:------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------| -| `admin_ids` | SPIFFE IDs that, when present in a caller's X509-SVID, grant that caller admin privileges. The admin IDs must reside in the same trust domain as the server and need not have a corresponding admin registration entry with the server. | | -| `agent_ttl` | The TTL to use for agent SVIDs | The value of `default_svid_ttl` | -| `audit_log_enabled` | If true, enables audit logging | false | -| `bind_address` | IP address or DNS name of the SPIRE server | 0.0.0.0 | -| `bind_port` | HTTP Port number of the SPIRE server | 8081 | -| `ca_key_type` | The key type used for the server CA (both X509 and JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | ec-p256 (the JWT key type can be overridden by `jwt_key_type`) | -| `ca_subject` | The Subject that CA certificates should use (see below) | | -| `ca_ttl` | The default CA/signing key TTL | 24h | -| `data_dir` | A directory the server can use for its runtime | | -| `default_svid_ttl` | The default SVID TTL. This field is deprecated in favor of default_x509_svid_ttl and default_jwt_svid_ttl and will be removed in a future version. | 1h | -| `default_x509_svid_ttl` | The default X509-SVID TTL (overrides `default_svid_ttl` if set) | 1h | -| `default_jwt_svid_ttl` | The default JWT-SVID TTL (overrides `default_svid_ttl` if set) | 5m | -| `experimental` | The experimental options that are subject to change or removal (see below) | | -| `federation` | Bundle endpoints configuration section used for [federation](#federation-configuration) | | -| `jwt_key_type` | The key type used for the server CA (JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | The value of `ca_key_type` or ec-p256 if not defined | -| `jwt_issuer` | The issuer claim used when minting JWT-SVIDs | | -| `log_file` | File to write logs to | | -| `log_level` | Sets the logging level <DEBUG|INFO|WARN|ERROR> | INFO | -| `log_format` | Format of logs, <text|json> | text | -| `omit_x509svid_uid` | If true, the subject on X509-SVIDs will not contain the unique ID attribute (deprecated) | false | -| `profiling_enabled` | If true, enables a [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint | false | -| `profiling_freq` | Frequency of dumping profiling data to disk. Only enabled when `profiling_enabled` is `true` and `profiling_freq` > 0. | | -| `profiling_names` | List of profile names that will be dumped to disk on each profiling tick, see [Profiling Names](#profiling-names) | | -| `profiling_port` | Port number of the [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint. Only used when `profiling_enabled` is `true`. | | -| `ratelimit` | Rate limiting configurations, usually used when the server is behind a load balancer (see below) | | -| `socket_path` | Path to bind the SPIRE Server API socket to (Unix only) | /tmp/spire-server/private/api.sock | -| `trust_domain` | The trust domain that this server belongs to (should be no more than 255 characters) | | +| Configuration | Description | Default | +|:------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------| +| `admin_ids` | SPIFFE IDs that, when present in a caller's X509-SVID, grant that caller admin privileges. The admin IDs must reside on the server trust domain or a federated one, and need not have a corresponding admin registration entry with the server. | | +| `agent_ttl` | The TTL to use for agent SVIDs | The value of `default_svid_ttl` | +| `audit_log_enabled` | If true, enables audit logging | false | +| `bind_address` | IP address or DNS name of the SPIRE server | 0.0.0.0 | +| `bind_port` | HTTP Port number of the SPIRE server | 8081 | +| `ca_key_type` | The key type used for the server CA (both X509 and JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | ec-p256 (the JWT key type can be overridden by `jwt_key_type`) | +| `ca_subject` | The Subject that CA certificates should use (see below) | | +| `ca_ttl` | The default CA/signing key TTL | 24h | +| `data_dir` | A directory the server can use for its runtime | | +| `default_svid_ttl` | The default SVID TTL. This field is deprecated in favor of default_x509_svid_ttl and default_jwt_svid_ttl and will be removed in a future version. | 1h | +| `default_x509_svid_ttl` | The default X509-SVID TTL (overrides `default_svid_ttl` if set) | 1h | +| `default_jwt_svid_ttl` | The default JWT-SVID TTL (overrides `default_svid_ttl` if set) | 5m | +| `experimental` | The experimental options that are subject to change or removal (see below) | | +| `federation` | Bundle endpoints configuration section used for [federation](#federation-configuration) | | +| `jwt_key_type` | The key type used for the server CA (JWT), <rsa-2048|rsa-4096|ec-p256|ec-p384> | The value of `ca_key_type` or ec-p256 if not defined | +| `jwt_issuer` | The issuer claim used when minting JWT-SVIDs | | +| `log_file` | File to write logs to | | +| `log_level` | Sets the logging level <DEBUG|INFO|WARN|ERROR> | INFO | +| `log_format` | Format of logs, <text|json> | text | +| `omit_x509svid_uid` | If true, the subject on X509-SVIDs will not contain the unique ID attribute (deprecated) | false | +| `profiling_enabled` | If true, enables a [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint | false | +| `profiling_freq` | Frequency of dumping profiling data to disk. Only enabled when `profiling_enabled` is `true` and `profiling_freq` > 0. | | +| `profiling_names` | List of profile names that will be dumped to disk on each profiling tick, see [Profiling Names](#profiling-names) | | +| `profiling_port` | Port number of the [net/http/pprof](https://pkg.go.dev/net/http/pprof) endpoint. Only used when `profiling_enabled` is `true`. | | +| `ratelimit` | Rate limiting configurations, usually used when the server is behind a load balancer (see below) | | +| `socket_path` | Path to bind the SPIRE Server API socket to (Unix only) | /tmp/spire-server/private/api.sock | +| `trust_domain` | The trust domain that this server belongs to (should be no more than 255 characters) | | | ca_subject | Description | Default | |:----------------------------|--------------------------------|----------------| diff --git a/pkg/server/api/middleware/authorization.go b/pkg/server/api/middleware/authorization.go index 227af7b2e25..a8e8a63b321 100644 --- a/pkg/server/api/middleware/authorization.go +++ b/pkg/server/api/middleware/authorization.go @@ -44,7 +44,7 @@ func (m *authorizationMiddleware) Preprocess(ctx context.Context, methodName str if id, ok := rpccontext.CallerID(ctx); ok { fields[telemetry.CallerID] = id.String() } - // Add request ID to logger, it simplify debugging when calling batch endpints + // Add request ID to logger, it simplifies debugging when calling batch endpoints requestID, err := uuid.NewV4() if err != nil { return nil, status.Errorf(codes.Internal, "failed to create request ID: %v", err) diff --git a/pkg/server/endpoints/auth.go b/pkg/server/endpoints/auth.go new file mode 100644 index 00000000000..9df2002540a --- /dev/null +++ b/pkg/server/endpoints/auth.go @@ -0,0 +1,163 @@ +package endpoints + +import ( + "context" + "crypto/x509" + "errors" + "fmt" + "sync" + "time" + + "github.com/andres-erbsen/clock" + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "github.com/spiffe/spire/pkg/common/telemetry" + "github.com/spiffe/spire/pkg/server/cache/dscache" + "github.com/spiffe/spire/pkg/server/svid" + "github.com/spiffe/spire/proto/spire/common" +) + +var ( + misconfigLogMtx sync.Mutex + misconfigLogTimes = make(map[spiffeid.TrustDomain]time.Time) + misconfigClk = clock.New() +) + +const misconfigLogEvery = time.Minute + +// shouldLogFederationMisconfiguration returns true if the last time a misconfiguration +// was logged was more than misconfigLogEvery ago. +func shouldLogFederationMisconfiguration(td spiffeid.TrustDomain) bool { + misconfigLogMtx.Lock() + defer misconfigLogMtx.Unlock() + + now := misconfigClk.Now() + last, ok := misconfigLogTimes[td] + if !ok || now.Sub(last) >= misconfigLogEvery { + misconfigLogTimes[td] = now + return true + } + return false +} + +// bundleGetter fetches the bundle for the given trust domain and parse it as x509 certificates. +func (e *Endpoints) bundleGetter(ctx context.Context, td spiffeid.TrustDomain) ([]*x509.Certificate, error) { + commonServerBundle, err := e.DataStore.FetchBundle(dscache.WithCache(ctx), td.IDString()) + if err != nil { + return nil, fmt.Errorf("get bundle from datastore: %w", err) + } + if commonServerBundle == nil { + if td != e.TrustDomain && shouldLogFederationMisconfiguration(td) { + e.Log. + WithField(telemetry.TrustDomain, td.String()). + Warn( + "No bundle found for foreign admin trust domain; admins from this trust domain will not be able to connect. " + + "Make sure this trust domain is correctly federated.", + ) + } + return nil, fmt.Errorf("no bundle found for trust domain %q", td) + } + + serverBundle, err := parseBundle(e.TrustDomain, commonServerBundle) + if err != nil { + return nil, err + } + + return serverBundle.X509Authorities(), nil +} + +// serverSpiffeVerificationFunc returns a function that is used for peer certificate verification on TLS connections. +// The returned function will verify that the peer certificate is valid, and apply a custom authorization with matchMemberOrOneOf. +// If the peer certificate is not provided, the function will not make any verification and return nil. +func (e *Endpoints) serverSpiffeVerificationFunc(bundleSource x509bundle.Source) func(_ [][]byte, _ [][]*x509.Certificate) error { + verifyPeerCertificate := tlsconfig.VerifyPeerCertificate( + bundleSource, + tlsconfig.AdaptMatcher(matchMemberOrOneOf(e.TrustDomain, e.AdminIDs...)), + ) + + return func(rawCerts [][]byte, _ [][]*x509.Certificate) error { + if rawCerts == nil { + return nil + } + + return verifyPeerCertificate(rawCerts, nil) + } +} + +// matchMemberOrOneOf is a custom spiffeid.Matcher which will validate that the peerSpiffeID belongs to the server +// trust domain or if it is included in the admin_ids configuration permissive list. +func matchMemberOrOneOf(trustDomain spiffeid.TrustDomain, adminIds ...spiffeid.ID) spiffeid.Matcher { + permissiveIDsSet := make(map[spiffeid.ID]struct{}) + for _, adminID := range adminIds { + permissiveIDsSet[adminID] = struct{}{} + } + + return func(peerID spiffeid.ID) error { + if !peerID.MemberOf(trustDomain) { + if _, ok := permissiveIDsSet[peerID]; !ok { + return fmt.Errorf("unexpected trust domain in ID %q", peerID) + } + } + + return nil + } +} + +// parseBundle parses a *x509bundle.Bundle from a *common.bundle. +func parseBundle(td spiffeid.TrustDomain, commonBundle *common.Bundle) (*x509bundle.Bundle, error) { + var caCerts []*x509.Certificate + for _, rootCA := range commonBundle.RootCas { + rootCACerts, err := x509.ParseCertificates(rootCA.DerBytes) + if err != nil { + return nil, fmt.Errorf("parse bundle: %w", err) + } + caCerts = append(caCerts, rootCACerts...) + } + + return x509bundle.FromX509Authorities(td, caCerts), nil +} + +type x509SVIDSource struct { + getter func() svid.State +} + +func newX509SVIDSource(getter func() svid.State) x509svid.Source { + return &x509SVIDSource{getter: getter} +} + +func (xs *x509SVIDSource) GetX509SVID() (*x509svid.SVID, error) { + svidState := xs.getter() + + if len(svidState.SVID) == 0 { + return nil, errors.New("no certificates found") + } + + id, err := x509svid.IDFromCert(svidState.SVID[0]) + if err != nil { + return nil, err + } + return &x509svid.SVID{ + ID: id, + Certificates: svidState.SVID, + PrivateKey: svidState.Key, + }, nil +} + +type bundleSource struct { + getter func(spiffeid.TrustDomain) ([]*x509.Certificate, error) +} + +func newBundleSource(getter func(spiffeid.TrustDomain) ([]*x509.Certificate, error)) x509bundle.Source { + return &bundleSource{getter: getter} +} + +func (bs *bundleSource) GetX509BundleForTrustDomain(trustDomain spiffeid.TrustDomain) (*x509bundle.Bundle, error) { + authorities, err := bs.getter(trustDomain) + if err != nil { + return nil, err + } + bundle := x509bundle.FromX509Authorities(trustDomain, authorities) + return bundle.GetX509BundleForTrustDomain(trustDomain) +} diff --git a/pkg/server/endpoints/auth_test.go b/pkg/server/endpoints/auth_test.go new file mode 100644 index 00000000000..b2ff8e51171 --- /dev/null +++ b/pkg/server/endpoints/auth_test.go @@ -0,0 +1,143 @@ +package endpoints + +import ( + "crypto/x509" + "errors" + "testing" + + "github.com/spiffe/go-spiffe/v2/bundle/x509bundle" + "github.com/spiffe/go-spiffe/v2/spiffeid" + "github.com/spiffe/go-spiffe/v2/svid/x509svid" + "github.com/spiffe/spire/pkg/common/pemutil" + "github.com/spiffe/spire/pkg/server/svid" + "github.com/spiffe/spire/test/testca" + "github.com/stretchr/testify/assert" +) + +var ( + certWithoutURI, _ = pemutil.ParseCertificates([]byte(` +-----BEGIN CERTIFICATE----- +MIIBFzCBvaADAgECAgEBMAoGCCqGSM49BAMCMBExDzANBgNVBAMTBkNFUlQtQTAi +GA8wMDAxMDEwMTAwMDAwMFoYDzAwMDEwMTAxMDAwMDAwWjARMQ8wDQYDVQQDEwZD +RVJULUEwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAS6qfd5FtzLYW+p7NgjqqJu +EAyewtzk4ypsM7PfePnL+45U+mSSypopiiyXvumOlU3uIHpnVhH+dk26KXGHeh2i +owIwADAKBggqhkjOPQQDAgNJADBGAiEAom6HzKAkMs3wiQJUwJiSjp9q9PHaWgGh +m7Ins/ReHk4CIQCncVaUC6i90RxiUJNfxPPMwSV9kulsj67reucS+UkBIw== +-----END CERTIFICATE----- +`)) +) + +func TestX509SVIDSource(t *testing.T) { + ca := testca.New(t, spiffeid.RequireTrustDomainFromString("example.org")) + + serverCert, serverKey := ca.CreateX509Certificate( + testca.WithID(spiffeid.RequireFromPath(trustDomain, "/spire/server")), + ) + certRaw := make([][]byte, len(serverCert)) + for i, cert := range serverCert { + certRaw[i] = cert.Raw + } + + tests := []struct { + name string + getter func() svid.State + want *x509svid.SVID + wantErr error + }{ + { + name: "success, with certificate", + getter: func() svid.State { + return svid.State{ + SVID: serverCert, + Key: serverKey, + } + }, + want: &x509svid.SVID{ + ID: spiffeid.RequireFromString("spiffe://example.org/spire/server"), + Certificates: serverCert, + PrivateKey: serverKey, + }, + }, + { + name: "error, certificate with no uri", + getter: func() svid.State { + return svid.State{ + SVID: certWithoutURI, + Key: serverKey, + } + }, + wantErr: errors.New("certificate contains no URI SAN"), + }, + { + name: "error, with empty certificates", + getter: func() svid.State { + return svid.State{ + SVID: []*x509.Certificate{}, + Key: serverKey, + } + }, + wantErr: errors.New("no certificates found"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + xs := newX509SVIDSource(tt.getter) + got, err := xs.GetX509SVID() + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.Equal(t, tt.want.ID, got.ID) + + assert.Equal(t, tt.want, got) + } + }) + } +} + +func TestBundleSource(t *testing.T) { + tests := []struct { + name string + getter func(spiffeid.TrustDomain) ([]*x509.Certificate, error) + trustDomain spiffeid.TrustDomain + want *x509bundle.Bundle + wantErr error + }{ + { + name: "success, with authorities", + getter: func(domain spiffeid.TrustDomain) ([]*x509.Certificate, error) { + return []*x509.Certificate{&x509.Certificate{}}, nil + }, + trustDomain: spiffeid.RequireTrustDomainFromString("example.org"), + want: x509bundle.FromX509Authorities( + spiffeid.RequireTrustDomainFromString("example.org"), + []*x509.Certificate{{}}), + }, + { + name: "success, empty authorities list", + getter: func(domain spiffeid.TrustDomain) ([]*x509.Certificate, error) { + return []*x509.Certificate{}, nil + }, + trustDomain: spiffeid.RequireTrustDomainFromString("example.org"), + want: x509bundle.FromX509Authorities(spiffeid.RequireTrustDomainFromString("example.org"), []*x509.Certificate{}), + }, + { + name: "error, error on getter function", + getter: func(domain spiffeid.TrustDomain) ([]*x509.Certificate, error) { + return nil, errors.New("some error") + }, + trustDomain: spiffeid.RequireTrustDomainFromString("example.org"), + wantErr: errors.New("some error"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bs := newBundleSource(tt.getter) + got, err := bs.GetX509BundleForTrustDomain(tt.trustDomain) + if tt.wantErr != nil { + assert.EqualError(t, err, tt.wantErr.Error()) + } else { + assert.Equal(t, tt.want, got) + } + }) + } +} diff --git a/pkg/server/endpoints/endpoints.go b/pkg/server/endpoints/endpoints.go index 76ac981931c..c6439859fa2 100644 --- a/pkg/server/endpoints/endpoints.go +++ b/pkg/server/endpoints/endpoints.go @@ -4,11 +4,11 @@ import ( "crypto/tls" "crypto/x509" "errors" - "fmt" "net" "os" "time" + "github.com/spiffe/go-spiffe/v2/spiffetls/tlsconfig" "github.com/spiffe/spire/pkg/server/cache/entrycache" "golang.org/x/net/context" "golang.org/x/net/http2" @@ -32,7 +32,6 @@ import ( "github.com/spiffe/spire/pkg/common/util" "github.com/spiffe/spire/pkg/server/api/middleware" "github.com/spiffe/spire/pkg/server/authpolicy" - "github.com/spiffe/spire/pkg/server/cache/dscache" "github.com/spiffe/spire/pkg/server/datastore" "github.com/spiffe/spire/pkg/server/svid" ) @@ -289,65 +288,22 @@ func (e *Endpoints) runLocalAccess(ctx context.Context, server *grpc.Server) err // getTLSConfig returns a TLS Config hook for the gRPC server func (e *Endpoints) getTLSConfig(ctx context.Context) func(*tls.ClientHelloInfo) (*tls.Config, error) { return func(hello *tls.ClientHelloInfo) (*tls.Config, error) { - certs, roots, err := e.getCerts(ctx) - if err != nil { - e.Log.WithError(err).WithField(telemetry.Address, hello.Conn.RemoteAddr().String()).Error("Could not generate TLS config for gRPC client") - return nil, err - } - - return &tls.Config{ - // Not all server APIs required a client certificate. Though if one - // is presented, verify it. - ClientAuth: tls.VerifyClientCertIfGiven, - - Certificates: certs, - ClientCAs: roots, - - MinVersion: tls.VersionTLS12, - - NextProtos: []string{http2.NextProtoTLS}, - }, nil - } -} - -// getCerts queries the datastore and returns a TLS serving certificate(s) plus -// the current CA root bundle. -func (e *Endpoints) getCerts(ctx context.Context) ([]tls.Certificate, *x509.CertPool, error) { - bundle, err := e.DataStore.FetchBundle(dscache.WithCache(ctx), e.TrustDomain.IDString()) - if err != nil { - return nil, nil, fmt.Errorf("get bundle from datastore: %w", err) + svidSrc := newX509SVIDSource(func() svid.State { + return e.SVIDObserver.State() + }) + bundleSrc := newBundleSource(func(td spiffeid.TrustDomain) ([]*x509.Certificate, error) { + return e.bundleGetter(ctx, td) + }) + + spiffeTLSConfig := tlsconfig.MTLSServerConfig(svidSrc, bundleSrc, nil) + // provided client certificates will be validated using the custom VerifyPeerCertificate hook + spiffeTLSConfig.ClientAuth = tls.RequestClientCert + spiffeTLSConfig.MinVersion = tls.VersionTLS12 + spiffeTLSConfig.NextProtos = []string{http2.NextProtoTLS} + spiffeTLSConfig.VerifyPeerCertificate = e.serverSpiffeVerificationFunc(bundleSrc) + + return spiffeTLSConfig, nil } - if bundle == nil { - return nil, nil, errors.New("bundle not found") - } - - var caCerts []*x509.Certificate - for _, rootCA := range bundle.RootCas { - rootCACerts, err := x509.ParseCertificates(rootCA.DerBytes) - if err != nil { - return nil, nil, fmt.Errorf("parse bundle: %w", err) - } - caCerts = append(caCerts, rootCACerts...) - } - - caPool := x509.NewCertPool() - for _, c := range caCerts { - caPool.AddCert(c) - } - - svidState := e.SVIDObserver.State() - - certChain := [][]byte{} - for _, cert := range svidState.SVID { - certChain = append(certChain, cert.Raw) - } - - tlsCert := tls.Certificate{ - Certificate: certChain, - PrivateKey: svidState.Key, - } - - return []tls.Certificate{tlsCert}, caPool, nil } func (e *Endpoints) makeInterceptors() (grpc.UnaryServerInterceptor, grpc.StreamServerInterceptor) { diff --git a/pkg/server/endpoints/endpoints_test.go b/pkg/server/endpoints/endpoints_test.go index d3a2896cb26..d226df31598 100644 --- a/pkg/server/endpoints/endpoints_test.go +++ b/pkg/server/endpoints/endpoints_test.go @@ -6,6 +6,7 @@ import ( "errors" "net" "reflect" + "strings" "sync" "testing" "time" @@ -46,10 +47,17 @@ import ( ) var ( - testTD = spiffeid.RequireTrustDomainFromString("domain.test") - serverID = spiffeid.RequireFromPath(testTD, "/spire/server") - agentID = spiffeid.RequireFromPath(testTD, "/spire/agent/foo") - adminID = spiffeid.RequireFromPath(testTD, "/admin") + testTD = spiffeid.RequireTrustDomainFromString("domain.test") + foreignFederatedTD = spiffeid.RequireTrustDomainFromString("foreign-domain.test") + foreignUnfederatedTD = spiffeid.RequireTrustDomainFromString("foreign-domain-not-federated.test") + serverID = spiffeid.RequireFromPath(testTD, "/spire/server") + agentID = spiffeid.RequireFromPath(testTD, "/spire/agent/foo") + adminID = spiffeid.RequireFromPath(testTD, "/admin") + foreignAdminID = spiffeid.RequireFromPath(foreignFederatedTD, "/admin/foreign") + unauthorizedForeignAdminID = spiffeid.RequireFromPath(foreignFederatedTD, "/admin/foreign-not-authorized") + unfederatedForeignAdminID = spiffeid.RequireFromPath(foreignUnfederatedTD, "/admin/foreign-not-federated") + unauthenticatedForeignAdminID = spiffeid.RequireFromPath(foreignFederatedTD, "/admin/foreign-not-authenticated") + downstreamID = spiffeid.RequireFromPath(testTD, "/downstream") rateLimit = RateLimitConfig{ Attestation: true, @@ -176,9 +184,15 @@ func TestNewErrorCreatingAuthorizedEntryFetcher(t *testing.T) { func TestListenAndServe(t *testing.T) { ctx := context.Background() ca := testca.New(t, testTD) + federatedCA := testca.New(t, foreignFederatedTD) + unfederatedCA := testca.New(t, foreignUnfederatedTD) serverSVID := ca.CreateX509SVID(serverID) agentSVID := ca.CreateX509SVID(agentID) adminSVID := ca.CreateX509SVID(adminID) + foreignAdminSVID := federatedCA.CreateX509SVID(foreignAdminID) + unauthorizedForeignAdminSVID := federatedCA.CreateX509SVID(unauthorizedForeignAdminID) + unauthenticatedForeignAdminSVID := unfederatedCA.CreateX509SVID(unauthenticatedForeignAdminID) + unfederatedForeignAdminSVID := federatedCA.CreateX509SVID(unfederatedForeignAdminID) downstreamSVID := ca.CreateX509SVID(downstreamID) listener, err := net.Listen("tcp", "localhost:0") @@ -223,6 +237,7 @@ func TestListenAndServe(t *testing.T) { RateLimit: rateLimit, EntryFetcherCacheRebuildTask: ef.RunRebuildCacheTask, AuthPolicyEngine: pe, + AdminIDs: []spiffeid.ID{foreignAdminSVID.ID}, } // Prime the datastore with the: @@ -230,7 +245,7 @@ func TestListenAndServe(t *testing.T) { // - agent attested node information // - admin registration entry // - downstream registration entry - prepareDataStore(t, ds, ca, agentSVID) + prepareDataStore(t, ds, []*testca.CA{ca, federatedCA}, agentSVID) ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -269,6 +284,9 @@ func TestListenAndServe(t *testing.T) { downstreamConn := dialTCP(tlsconfig.MTLSClientConfig(downstreamSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) defer downstreamConn.Close() + federatedAdminConn := dialTCP(tlsconfig.MTLSClientConfig(foreignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID))) + defer downstreamConn.Close() + t.Run("Bad Client SVID", func(t *testing.T) { // Create an SVID from a different CA. This ensures that we verify // incoming certificates against the trust bundle. @@ -285,31 +303,55 @@ func TestListenAndServe(t *testing.T) { }) t.Run("Agent", func(t *testing.T) { - testAgentAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testAgentAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("Debug", func(t *testing.T) { - testDebugAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testDebugAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("Health", func(t *testing.T) { - testHealthAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testHealthAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("Bundle", func(t *testing.T) { - testBundleAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testBundleAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("Entry", func(t *testing.T) { - testEntryAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testEntryAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("SVID", func(t *testing.T) { - testSVIDAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testSVIDAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("TrustDomain", func(t *testing.T) { - testTrustDomainAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, downstreamConn) + testTrustDomainAPI(ctx, t, localConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn) }) t.Run("Access denied to remote caller", func(t *testing.T) { testRemoteCaller(ctx, t, target) }) + t.Run("Invalidate connection with misconfigured foreign admin caller", func(t *testing.T) { + unauthenticatedConfig := tlsconfig.MTLSClientConfig(unauthenticatedForeignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + unauthorizedConfig := tlsconfig.MTLSClientConfig(unauthorizedForeignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + unfederatedConfig := tlsconfig.MTLSClientConfig(unfederatedForeignAdminSVID, ca.X509Bundle(), tlsconfig.AuthorizeID(serverID)) + + for _, config := range []*tls.Config{unauthenticatedConfig, unauthorizedConfig, unfederatedConfig} { + conn, err := grpc.DialContext(ctx, endpoints.TCPAddr.String(), + grpc.WithTransportCredentials(credentials.NewTLS(config)), + ) + require.NoError(t, err) + + _, err = entryv1.NewEntryClient(conn).ListEntries(ctx, nil) + require.Error(t, err) + + switch { + case strings.Contains(err.Error(), "connection reset by peer"): + case strings.Contains(err.Error(), "tls: bad certificate"): + return + default: + t.Error("expected invalid connection for misconfigured foreign admin caller") + } + } + }) + // Assert that the bundle endpoint server was called to listen and serve require.True(t, bundleEndpointServer.Used(), "bundle server was not called to listen and serve") @@ -324,13 +366,15 @@ func TestListenAndServe(t *testing.T) { } } -func prepareDataStore(t *testing.T, ds datastore.DataStore, ca *testca.CA, agentSVID *x509svid.SVID) { +func prepareDataStore(t *testing.T, ds datastore.DataStore, rootCAs []*testca.CA, agentSVID *x509svid.SVID) { // Prepare the bundle - _, err := ds.CreateBundle(context.Background(), makeBundle(ca)) - require.NoError(t, err) + for _, rootCA := range rootCAs { + _, err := ds.CreateBundle(context.Background(), makeBundle(rootCA)) + require.NoError(t, err) + } // Create the attested node - _, err = ds.CreateAttestedNode(context.Background(), &common.AttestedNode{ + _, err := ds.CreateAttestedNode(context.Background(), &common.AttestedNode{ SpiffeId: agentID.String(), CertSerialNumber: agentSVID.Certificates[0].SerialNumber.String(), }) @@ -355,7 +399,7 @@ func prepareDataStore(t *testing.T, ds datastore.DataStore, ca *testca.CA, agent require.NoError(t, err) } -func testAgentAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testAgentAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, agentv1.NewAgentClient(udsConn), map[string]bool{ "CountAgents": true, @@ -408,6 +452,19 @@ func testAgentAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentC }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, agentv1.NewAgentClient(federatedAdminConn), map[string]bool{ + "CountAgents": true, + "ListAgents": true, + "GetAgent": true, + "DeleteAgent": true, + "BanAgent": true, + "AttestAgent": true, + "RenewAgent": false, + "CreateJoinToken": true, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, agentv1.NewAgentClient(downstreamConn), map[string]bool{ "CountAgents": false, @@ -422,7 +479,7 @@ func testAgentAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentC }) } -func testHealthAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testHealthAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, grpc_health_v1.NewHealthClient(udsConn), map[string]bool{ "Check": true, @@ -451,6 +508,13 @@ func testHealthAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agent }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, grpc_health_v1.NewHealthClient(federatedAdminConn), map[string]bool{ + "Check": true, + "Watch": true, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, grpc_health_v1.NewHealthClient(downstreamConn), map[string]bool{ "Check": true, @@ -459,7 +523,7 @@ func testHealthAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agent }) } -func testDebugAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testDebugAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, debugv1.NewDebugClient(udsConn), map[string]bool{ "GetInfo": true, @@ -484,6 +548,12 @@ func testDebugAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentC }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, debugv1.NewDebugClient(federatedAdminConn), map[string]bool{ + "GetInfo": true, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, debugv1.NewDebugClient(downstreamConn), map[string]bool{ "GetInfo": true, @@ -491,7 +561,7 @@ func testDebugAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentC }) } -func testBundleAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testBundleAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, bundlev1.NewBundleClient(udsConn), map[string]bool{ "GetBundle": true, @@ -552,6 +622,21 @@ func testBundleAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agent }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, bundlev1.NewBundleClient(federatedAdminConn), map[string]bool{ + "GetBundle": true, + "AppendBundle": true, + "PublishJWTAuthority": false, + "CountBundles": true, + "ListFederatedBundles": true, + "GetFederatedBundle": true, + "BatchCreateFederatedBundle": true, + "BatchUpdateFederatedBundle": true, + "BatchSetFederatedBundle": true, + "BatchDeleteFederatedBundle": true, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, bundlev1.NewBundleClient(downstreamConn), map[string]bool{ "GetBundle": true, @@ -568,7 +653,7 @@ func testBundleAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agent }) } -func testEntryAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testEntryAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, entryv1.NewEntryClient(udsConn), map[string]bool{ "CountEntries": true, @@ -617,6 +702,18 @@ func testEntryAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentC }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, entryv1.NewEntryClient(federatedAdminConn), map[string]bool{ + "CountEntries": true, + "ListEntries": true, + "GetEntry": true, + "BatchCreateEntry": true, + "BatchUpdateEntry": true, + "BatchDeleteEntry": true, + "GetAuthorizedEntries": false, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, entryv1.NewEntryClient(downstreamConn), map[string]bool{ "CountEntries": false, @@ -630,7 +727,7 @@ func testEntryAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentC }) } -func testSVIDAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testSVIDAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, svidv1.NewSVIDClient(udsConn), map[string]bool{ "MintX509SVID": true, @@ -671,6 +768,16 @@ func testSVIDAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentCo }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, svidv1.NewSVIDClient(federatedAdminConn), map[string]bool{ + "MintX509SVID": true, + "MintJWTSVID": true, + "BatchNewX509SVID": false, + "NewJWTSVID": false, + "NewDownstreamX509CA": false, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, svidv1.NewSVIDClient(downstreamConn), map[string]bool{ "MintX509SVID": false, @@ -682,7 +789,7 @@ func testSVIDAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentCo }) } -func testTrustDomainAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, downstreamConn *grpc.ClientConn) { +func testTrustDomainAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, agentConn, adminConn, federatedAdminConn, downstreamConn *grpc.ClientConn) { t.Run("UDS", func(t *testing.T) { testAuthorization(ctx, t, trustdomainv1.NewTrustDomainClient(udsConn), map[string]bool{ "ListFederationRelationships": true, @@ -727,6 +834,17 @@ func testTrustDomainAPI(ctx context.Context, t *testing.T, udsConn, noauthConn, }) }) + t.Run("Federated Admin", func(t *testing.T) { + testAuthorization(ctx, t, trustdomainv1.NewTrustDomainClient(federatedAdminConn), map[string]bool{ + "ListFederationRelationships": true, + "GetFederationRelationship": true, + "BatchCreateFederationRelationship": true, + "BatchUpdateFederationRelationship": true, + "BatchDeleteFederationRelationship": true, + "RefreshBundle": true, + }) + }) + t.Run("Downstream", func(t *testing.T) { testAuthorization(ctx, t, trustdomainv1.NewTrustDomainClient(downstreamConn), map[string]bool{ "ListFederationRelationships": false, @@ -836,7 +954,7 @@ func (s *bundleEndpointServer) Used() bool { func makeBundle(ca *testca.CA) *common.Bundle { bundle := &common.Bundle{ - TrustDomainId: testTD.IDString(), + TrustDomainId: ca.Bundle().TrustDomain().IDString(), } for _, x509Authority := range ca.X509Authorities() {