Skip to content

Commit

Permalink
Support foreign trust domains admin ids config (spiffe#3642)
Browse files Browse the repository at this point in the history
Signed-off-by: Guilherme Carvalho <guilhermbrsp@gmail.com>
  • Loading branch information
guilhermocc authored and stevend-uber committed Oct 13, 2023
1 parent c299036 commit ffc508f
Show file tree
Hide file tree
Showing 9 changed files with 504 additions and 125 deletions.
5 changes: 1 addition & 4 deletions cmd/spire-server/cli/run/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
8 changes: 5 additions & 3 deletions cmd/spire-server/cli/run/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
},
},
{
Expand Down
8 changes: 4 additions & 4 deletions conf/server/server_full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
58 changes: 29 additions & 29 deletions doc/spire_server.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pkg/server/api/middleware/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
163 changes: 163 additions & 0 deletions pkg/server/endpoints/auth.go
Original file line number Diff line number Diff line change
@@ -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)
}
143 changes: 143 additions & 0 deletions pkg/server/endpoints/auth_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
})
}
}
Loading

0 comments on commit ffc508f

Please sign in to comment.