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

[v14] Reuse existing SAMLConnector signing key when possible #46887

Merged
merged 1 commit into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions lib/auth/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ func (a *Server) UpsertSAMLConnector(ctx context.Context, connector types.SAMLCo
if err := services.ValidateSAMLConnector(connector, a); err != nil {
return trace.Wrap(err)
}

// If someone is applying a SAML Connector obtained with `tctl get` without secrets, the signing key pair is
// not empty (cert is set) but the private key is missing. Such a SAML resource is invalid and not usable.
if connector.GetSigningKeyPair().PrivateKey == "" {
err := services.FillSAMLSigningKeyFromExisting(ctx, connector, a.Services)
if err != nil {
return trace.Wrap(err)
}
}

if err := a.Services.UpsertSAMLConnector(ctx, connector); err != nil {
return trace.Wrap(err)
}
Expand Down
26 changes: 26 additions & 0 deletions lib/services/saml.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ import (
"github.com/gravitational/teleport/lib/utils"
)

type SAMLConnectorGetter interface {
GetSAMLConnector(ctx context.Context, id string, withSecrets bool) (types.SAMLConnector, error)
}

const ErrMsgHowToFixMissingPrivateKey = "You must either specify the singing key pair (obtain the existing one with `tctl get saml --with-secrets`) or let Teleport generate a new one (remove singing_key_pair in the resource you're trying to create)."

// ValidateSAMLConnector validates the SAMLConnector and sets default values.
// If a remote to fetch roles is specified, roles will be validated to exist.
func ValidateSAMLConnector(sc types.SAMLConnector, rg RoleGetter) error {
Expand Down Expand Up @@ -339,3 +345,23 @@ func MarshalSAMLConnector(samlConnector types.SAMLConnector, opts ...MarshalOpti
return nil, trace.BadParameter("unrecognized SAML connector version %T", samlConnector)
}
}

// FillSAMLSigningKeyFromExisting looks up the existing SAML connector and populates the signing key if it's missing.
// This must be called only if the SAML Connector signing key pair has been initialized (ValidateSAMLConnector) and
// the private key is still empty.
func FillSAMLSigningKeyFromExisting(ctx context.Context, connector types.SAMLConnector, sg SAMLConnectorGetter) error {
existing, err := sg.GetSAMLConnector(ctx, connector.GetName(), true /* with secrets */)
switch {
case trace.IsNotFound(err):
return trace.BadParameter("failed to create SAML connector, the SAML connector has no signing key set. " + ErrMsgHowToFixMissingPrivateKey)
case err != nil:
return trace.BadParameter("failed to update SAML connector, the SAML connector has no signing key set and looking up the existing connector failed with the error: %s. %s", err.Error(), ErrMsgHowToFixMissingPrivateKey)
}

existingSkp := existing.GetSigningKeyPair()
if existingSkp == nil || existingSkp.Cert != connector.GetSigningKeyPair().Cert {
return trace.BadParameter("failed to update the SAML connector, the SAML connector has no signing key and its signing certificate does not match the existing one. " + ErrMsgHowToFixMissingPrivateKey)
}
connector.SetSigningKeyPair(existingSkp)
return nil
}
112 changes: 112 additions & 0 deletions lib/services/saml_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@ package services

import (
"context"
"crypto/x509/pkix"
"strings"
"testing"
"time"

"github.com/google/go-cmp/cmp"
"github.com/gravitational/trace"
Expand All @@ -29,6 +31,7 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/utils"
)

func TestParseFromMetadata(t *testing.T) {
Expand Down Expand Up @@ -137,3 +140,112 @@ func (rs roleSet) GetRole(ctx context.Context, name string) (types.Role, error)
}
return nil, trace.NotFound("unknown role: %s", name)
}

type mockSAMLGetter map[string]types.SAMLConnector

func (m mockSAMLGetter) GetSAMLConnector(_ context.Context, id string, withSecrets bool) (types.SAMLConnector, error) {
connector, ok := m[id]
if !ok {
return nil, trace.NotFound("%s not found", id)
}
return connector, nil
}

func TestFillSAMLSigningKeyFromExisting(t *testing.T) {
t.Parallel()

// Test setup: generate the fixtures
existingKeyPEM, existingCertPEM, err := utils.GenerateSelfSignedSigningCert(pkix.Name{
Organization: []string{"Teleport OSS"},
CommonName: "teleport.localhost.localdomain",
}, nil, 10*365*24*time.Hour)
require.NoError(t, err)

existingSkp := &types.AsymmetricKeyPair{
PrivateKey: string(existingKeyPEM),
Cert: string(existingCertPEM),
}

existingConnectorName := "existing"
existingConnectors := mockSAMLGetter{
existingConnectorName: &types.SAMLConnectorV2{
Spec: types.SAMLConnectorSpecV2{
SigningKeyPair: existingSkp,
},
},
}

_, unrelatedCertPEM, err := utils.GenerateSelfSignedSigningCert(pkix.Name{
Organization: []string{"Teleport OSS"},
CommonName: "teleport.localhost.localdomain",
}, nil, 10*365*24*time.Hour)
require.NoError(t, err)

// Test setup: define test cases
testCases := []struct {
name string
connectorName string
connectorSpec types.SAMLConnectorSpecV2
assertErr require.ErrorAssertionFunc
assertResult require.ValueAssertionFunc
}{
{
name: "should read singing key from existing connector with matching cert",
connectorName: existingConnectorName,
connectorSpec: types.SAMLConnectorSpecV2{
SigningKeyPair: &types.AsymmetricKeyPair{
PrivateKey: "",
Cert: string(existingCertPEM),
},
},
assertErr: require.NoError,
assertResult: func(t require.TestingT, value interface{}, args ...interface{}) {
require.Implements(t, (*types.SAMLConnector)(nil), value)
connector := value.(types.SAMLConnector)
skp := connector.GetSigningKeyPair()
require.Equal(t, existingSkp, skp)
},
},
{
name: "should error when there's no existing connector",
connectorName: "non-existing",
connectorSpec: types.SAMLConnectorSpecV2{
SigningKeyPair: &types.AsymmetricKeyPair{
PrivateKey: "",
Cert: string(unrelatedCertPEM),
},
},
assertErr: require.Error,
},
{
name: "should error when existing connector cert is not matching",
connectorName: existingConnectorName,
connectorSpec: types.SAMLConnectorSpecV2{
SigningKeyPair: &types.AsymmetricKeyPair{
PrivateKey: "",
Cert: string(unrelatedCertPEM),
},
},
assertErr: require.Error,
},
}

// Test execution
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
connector := &types.SAMLConnectorV2{
Metadata: types.Metadata{
Name: tc.connectorName,
},
Spec: tc.connectorSpec,
}

err := FillSAMLSigningKeyFromExisting(ctx, connector, existingConnectors)
tc.assertErr(t, err)
if tc.assertResult != nil {
tc.assertResult(t, connector)
}
})
}
}
Loading