forked from spiffe/spire
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support foreign trust domains admin ids config (spiffe#3642)
Signed-off-by: Guilherme Carvalho <guilhermbrsp@gmail.com>
- Loading branch information
1 parent
c299036
commit ffc508f
Showing
9 changed files
with
504 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
Oops, something went wrong.