Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support foreign trust domains admin ids config #3642

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
bd3af89
Fix comments typos
guilhermocc Nov 28, 2022
a42ecfb
Remove foreign trust domain validation for adminIDs server config
guilhermocc Nov 28, 2022
aebc767
Update getCerts method to include federated bundles for admin Tls con…
guilhermocc Nov 28, 2022
890fd0f
Update documentation regarding new admin_ids behavior
guilhermocc Nov 28, 2022
1ba2fb3
Merge branch 'main' into support-foreign-trust-domain-admin-ids-config
guilhermocc Nov 28, 2022
6ad6274
Implement spiffe authentication in spire server
guilhermocc Dec 1, 2022
e1c306e
Add error log when some problem occurs creating TLS config for server
guilhermocc Dec 2, 2022
7291f2c
Merge branch 'main' into support-foreign-trust-domain-admin-ids-config
guilhermocc Dec 2, 2022
8d98af1
Merge branch 'main' into support-foreign-trust-domain-admin-ids-config
guilhermocc Dec 5, 2022
af453cf
Use tlsconfig config and isolate auth logic in new file
guilhermocc Dec 14, 2022
c9df0ee
Improve unfederated foreign admin id warning message
guilhermocc Dec 14, 2022
0be5985
Merge branch 'main' into support-foreign-trust-domain-admin-ids-config
guilhermocc Dec 14, 2022
55c936f
Remove duplicated opts
guilhermocc Dec 14, 2022
fa07255
Fix flaky test
guilhermocc Dec 14, 2022
ec23d9c
Improve code addressing review comments
guilhermocc Dec 14, 2022
25a95b9
Merge branch 'main' into support-foreign-trust-domain-admin-ids-config
guilhermocc Dec 19, 2022
3f743fc
Fix typo in function name
guilhermocc Dec 20, 2022
e70e7b0
Use concrete trust domain id type and fix log
guilhermocc Jan 4, 2023
ec21334
Improve argument naming
guilhermocc Jan 4, 2023
5c51de8
Remove unnecessary allocations from the handshake
guilhermocc Jan 4, 2023
47abc90
Merge branch 'main' into support-foreign-trust-domain-admin-ids-config
amartinezfayo Jan 5, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -453,11 +453,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
4 changes: 2 additions & 2 deletions conf/server/server_full.conf
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
# 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
# that caller admin privileges. The admin IDs must reside in a federated trust domain or on the same as the server,
# and need not have a corresponding admin registration.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is something up with the indentation here. Also, i think this file has imposed a column limit on comments, we should probably stick with that.

# entry with the server.
# admin_ids = ["spiffe://example.org/my/admin"]

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
170 changes: 170 additions & 0 deletions pkg/server/endpoints/auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
package endpoints

import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"sync/atomic"
"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/common/x509util"
"github.com/spiffe/spire/pkg/server/cache/dscache"
"github.com/spiffe/spire/proto/spire/common"
)

var (
lastMisconfigLogTime = new(atomic.Int64)
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() bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... should we be doing this logging rate limiting per trust domain?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, that's a good idea!

now := misconfigClk.Now()

lastLogTime := lastMisconfigLogTime.Load()
if now.Sub(time.Unix(lastLogTime, 0)) >= misconfigLogEvery {
lastMisconfigLogTime.Store(now.Unix())
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() {
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. " +
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"No bundle found for foreign admin trust domain, admins from this trust domain will not be able to connect. " +
"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 %s", e.TrustDomain.String())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String() method is implicitly called when using %s or %q formatting:

Suggested change
return nil, fmt.Errorf("no bundle found for trust domain %s", e.TrustDomain.String())
return nil, fmt.Errorf("no bundle found for trust domain %q", e.TrustDomain)

}

serverBundle, err := parseBundle(e.TrustDomain, commonServerBundle)
if err != nil {
return nil, fmt.Errorf("parse bundle: %w", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this error will result in: "parse bundle: parse bundle: SOME ERROR" I think we may remove fmt error from here or from parseBundle

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}

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 machMemberOrOneOff.
// 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 {
return func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
if rawCerts == nil {
return nil
}

return tlsconfig.VerifyPeerCertificate(
bundleSource,
tlsconfig.AdaptMatcher(machMemberOrOneOff(e.TrustDomain, e.AdminIDs...)),
)(rawCerts, nil)
}
}

// machMemberOrOneOff is a custom spiffeid.Matcher, which will validate that the peerSpiffeID belongs to the server
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// machMemberOrOneOff is a custom spiffeid.Matcher, which will validate that the peerSpiffeID belongs to the server
// matchMemberOrOneOf is a custom spiffeid.Matcher which will validate that the peerID belongs to the server

// trust domain or if it is included in the admin_ids configuration permissive list.
func machMemberOrOneOff(trustDomain spiffeid.TrustDomain, adminIds ...spiffeid.ID) spiffeid.Matcher {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func machMemberOrOneOff(trustDomain spiffeid.TrustDomain, adminIds ...spiffeid.ID) spiffeid.Matcher {
func matchMemberOrOneOf(trustDomain spiffeid.TrustDomain, adminIds ...spiffeid.ID) spiffeid.Matcher {

return func(peerSpiffeID spiffeid.ID) error {
permissiveIDsSet := make(map[spiffeid.ID]struct{})
for _, adminID := range adminIds {
permissiveIDsSet[adminID] = struct{}{}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's lift this out of the closure so that we don't recompute this map on every invocation of the matcher.

Suggested change
return func(peerSpiffeID spiffeid.ID) error {
permissiveIDsSet := make(map[spiffeid.ID]struct{})
for _, adminID := range adminIds {
permissiveIDsSet[adminID] = struct{}{}
}
permissiveIDsSet := make(map[spiffeid.ID]struct{})
for _, adminID := range adminIds {
permissiveIDsSet[adminID] = struct{}{}
}
return func(peerID spiffeid.ID) error {


if !peerSpiffeID.MemberOf(trustDomain) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if !peerSpiffeID.MemberOf(trustDomain) {
if !peerID.MemberOf(trustDomain) {

if _, ok := permissiveIDsSet[peerSpiffeID]; !ok {
return fmt.Errorf("unexpected ID %q", permissiveIDsSet)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return fmt.Errorf("unexpected ID %q", permissiveIDsSet)
return fmt.Errorf("unexpected trust domain in ID %q", peerSpiffeID)

}
}

return nil
}
}

// parseBundle parses a *x509bundle.Bundle from a *common.bundle.
func parseBundle(td spiffeid.TrustDomain, commonBundle *common.Bundle) (*x509bundle.Bundle, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a little unfortunate that we parse this on each handshake. I think we are already doing that today, but we should probably push this conversion behind the cache so we don't have to do it so much. That can happen in a later PR though.

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() *tls.Certificate
}

func newX509SVIDSource(getter func() *tls.Certificate) x509svid.Source {
return &x509SVIDSource{getter: getter}
}

func (xs *x509SVIDSource) GetX509SVID() (*x509svid.SVID, error) {
tlsCert := xs.getter()

certificates, err := x509util.RawCertsToCertificates(tlsCert.Certificate)
if err != nil {
return nil, err
}
if len(certificates) == 0 {
return nil, fmt.Errorf("no certificates found")
}

id, err := x509svid.IDFromCert(certificates[0])
if err != nil {
return nil, err
}

privateKey, ok := tlsCert.PrivateKey.(crypto.Signer)
if !ok {
return nil, fmt.Errorf("agent certificate private key type %T is unexpectedly not a signer", tlsCert.PrivateKey)
}

return &x509svid.SVID{
ID: id,
Certificates: certificates,
PrivateKey: privateKey,
}, nil
}

type bundleSource struct {
getter func(*spiffeid.TrustDomain) ([]*x509.Certificate, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we to pass a pointer to the trust domain into the getter. The trust domain struct type is intended to be passed by value (it holds a single string for state).

Suggested change
getter func(*spiffeid.TrustDomain) ([]*x509.Certificate, error)
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)
}
173 changes: 173 additions & 0 deletions pkg/server/endpoints/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
package endpoints

import (
"crypto/tls"
"crypto/x509"
"encoding/hex"
"fmt"
"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/test/testca"
"github.com/stretchr/testify/assert"
)

var (
testRSACertificate = fromHex("3082024b308201b4a003020102020900e8f09d3fe25beaa6300d06092a864886f70d01010b0500301f310b3009060355040a1302476f3110300e06035504031307476f20526f6f74301e170d3136303130313030303030305a170d3235303130313030303030305a301a310b3009060355040a1302476f310b300906035504031302476f30819f300d06092a864886f70d010101050003818d0030818902818100db467d932e12270648bc062821ab7ec4b6a25dfe1e5245887a3647a5080d92425bc281c0be97799840fb4f6d14fd2b138bc2a52e67d8d4099ed62238b74a0b74732bc234f1d193e596d9747bf3589f6c613cc0b041d4d92b2b2423775b1c3bbd755dce2054cfa163871d1e24c4f31d1a508baab61443ed97a77562f414c852d70203010001a38193308190300e0603551d0f0101ff0404030205a0301d0603551d250416301406082b0601050507030106082b06010505070302300c0603551d130101ff0402300030190603551d0e041204109f91161f43433e49a6de6db680d79f60301b0603551d230414301280104813494d137e1631bba301d5acab6e7b30190603551d1104123010820e6578616d706c652e676f6c616e67300d06092a864886f70d01010b0500038181009d30cc402b5b50a061cbbae55358e1ed8328a9581aa938a495a1ac315a1a84663d43d32dd90bf297dfd320643892243a00bccf9c7db74020015faad3166109a276fd13c3cce10c5ceeb18782f16c04ed73bbb343778d0c1cf10fa1d8408361c94c722b9daedb4606064df4c1b33ec0d1bd42d4dbfe3d1360845c21d33be9fae7")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically we include the PEM encoded certificate in tests and use the pemutil.ParseCertificate. It's a little more readable that way.

)

func Test_x509SVIDSource_GetX509SVID(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should try and follow the naming guidelines from the testing package. In this case, since there is only one part of the x509svid source that we are testing, I'd suggest the following:

Suggested change
func Test_x509SVIDSource_GetX509SVID(t *testing.T) {
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
}

type fields struct {
getter func() *tls.Certificate
}
tests := []struct {
name string
fields fields
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just have getter here? what benefit do we get from the fields struct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

usually i create the fields struct for increasing the test readability, but in this case it can be simplified since there's just one field

want *x509svid.SVID
wantErr error
}{
{
name: "success, with certificate",
fields: fields{func() *tls.Certificate {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: we generally discourage struct initialization using positional fields and prefer naming the fields explicitly.

return &tls.Certificate{
Certificate: certRaw,
PrivateKey: serverKey,
}
}},
want: &x509svid.SVID{
ID: spiffeid.RequireFromString("spiffe://example.org/spire/server"),
Certificates: serverCert,
PrivateKey: serverKey,
},
},
{
name: "error, malformed certificate",
fields: fields{func() *tls.Certificate {
return &tls.Certificate{
Certificate: [][]byte{testRSACertificate[1:]},
PrivateKey: serverKey,
}
}},
wantErr: fmt.Errorf("x509: malformed certificate"),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: when not using formatting, prefer errors.New (there are multiple instances in these tests)

},
{
name: "error, certificate with no uri",
fields: fields{func() *tls.Certificate {
return &tls.Certificate{
Certificate: [][]byte{testRSACertificate},
PrivateKey: serverKey,
}
}},
wantErr: fmt.Errorf("certificate contains no URI SAN"),
},
{
name: "error, with empty certificates",
fields: fields{func() *tls.Certificate {
return &tls.Certificate{
Certificate: [][]byte{},
PrivateKey: serverKey,
}
}},
wantErr: fmt.Errorf("no certificates found"),
},
{
name: "error, with no private key",
fields: fields{func() *tls.Certificate {
return &tls.Certificate{
Certificate: [][]byte{serverCert[0].Raw},
}
}},
wantErr: fmt.Errorf("agent certificate private key type <nil> is unexpectedly not a signer"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
xs := newX509SVIDSource(tt.fields.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 Test_bundleSource_GetX509BundleForTrustDomain(t *testing.T) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func Test_bundleSource_GetX509BundleForTrustDomain(t *testing.T) {
func TestBundleSource(t *testing.T) {

type fields struct {
getter func(*spiffeid.TrustDomain) ([]*x509.Certificate, error)
}
type args struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What benefit do we get from the args struct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same thing about the fields struct, but we can simplify here also 😄

trustDomain spiffeid.TrustDomain
}
tests := []struct {
name string
fields fields
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here.

args args
want *x509bundle.Bundle
wantErr error
}{
{
name: "success, with authorities",
fields: fields{func(domain *spiffeid.TrustDomain) ([]*x509.Certificate, error) {
return []*x509.Certificate{&x509.Certificate{}}, nil
}},
args: args{
trustDomain: spiffeid.RequireTrustDomainFromString("example.org"),
},
want: x509bundle.FromX509Authorities(
spiffeid.RequireTrustDomainFromString("example.org"),
[]*x509.Certificate{&x509.Certificate{}}),
},
{
name: "success, empty authorities list",
fields: fields{func(domain *spiffeid.TrustDomain) ([]*x509.Certificate, error) {
return []*x509.Certificate{}, nil
}},
args: args{
trustDomain: spiffeid.RequireTrustDomainFromString("example.org"),
},
want: x509bundle.FromX509Authorities(spiffeid.RequireTrustDomainFromString("example.org"), []*x509.Certificate{}),
},
{
name: "error, error on getter function",
fields: fields{func(domain *spiffeid.TrustDomain) ([]*x509.Certificate, error) {
return nil, fmt.Errorf("some error")
}},
args: args{
trustDomain: spiffeid.RequireTrustDomainFromString("example.org"),
},
wantErr: fmt.Errorf("some error"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
bs := newBundleSource(tt.fields.getter)
got, err := bs.GetX509BundleForTrustDomain(tt.args.trustDomain)
if tt.wantErr != nil {
assert.EqualError(t, err, tt.wantErr.Error())
} else {
assert.Equal(t, tt.want, got)
}
})
}
}

func fromHex(s string) []byte {
b, _ := hex.DecodeString(s)
return b
}
Loading