diff --git a/docs/pages/choose-an-edition/teleport-enterprise/gcp-kms.mdx b/docs/pages/choose-an-edition/teleport-enterprise/gcp-kms.mdx index 5179fccb974fd..0bf4b2c324f8c 100644 --- a/docs/pages/choose-an-edition/teleport-enterprise/gcp-kms.mdx +++ b/docs/pages/choose-an-edition/teleport-enterprise/gcp-kms.mdx @@ -234,42 +234,16 @@ certificates can be signed without error. ## Migrating an existing cluster -If you have an existing Teleport cluster it will have already created CA keys -during its first start. +If you have an existing Teleport cluster it will have already created software +CA keys during its first start. Those existing CA keys will have been used to sign all existing user and host certificates, and will be trusted by all other services in your cluster. -When an Auth Server starts up with a `gcp_kms` keyring configured in its -`ca_key_params`, it will refuse to sign any certificates with any existing -software keys in the CA. -This will prevent any new user logins or new hosts from joining your cluster if -their requests are directed to that Auth Server and effectively cause downtime -for that server until a CA rotation is completed. - -If some downtime until you can complete a CA rotation is acceptable, the -migration can be performed in three steps: - -1. Configure all Auth Servers `ca_key_params` to use your desired KMS keyring, -as described in [Step 4](#step-45-configure-your-auth-server-to-use-kms-keys). -1. Restart all Auth Servers. -1. Perform a full [CA rotation](../../management/operations/ca-rotation.mdx). - -To avoid any downtime while migrating your cluster, do the following procedure -instead: - -1. Start a new Auth Server with an identical backend configuration to your - existing Auth Servers and with `ca_key_params` configured to use your KMS key - ring. Make sure no requests are routed to this new, temporary, Auth Server by - not adding it to your load balancer. You can run this anywhere with access to - your existing backend and new KMS key ring, one option would be to run it - locally on an existing Auth Server host (make sure to give it its own - `teleport.yaml` and unique `data_dir`). -1. Perform a full [CA rotation](../../management/operations/ca-rotation.mdx). - The temporary Auth Server will generate new KMS keys and include their names in - the backend CA state. -1. Stop/remove/delete the temporary Auth Server as it is no longer necessary. -1. Configure all other existing Auth Servers with identical `ca_key_params` and - reload/restart them, one by one. They will now use the KMS keys generated by the - temporary Auth Server. -1. Perform one more full CA rotation to evict all now-unused software keys from the - CA backend state so that hosts will no longer trust them. +In order for the Teleport Auth Service to generate new keys in GCP KMS and have +them trusted by the rest of the cluster, you will need to rotate all of your +Teleport CAs. + +`teleport start` will print a warning during startup if any CA needs to be rotated. +CA rotation can be performed manually or semi-automatically, see our admin guide +on [Certificate rotation](../../management/operations/ca-rotation.mdx). +All CAs listed in the output of `tctl status` must be rotated. diff --git a/docs/pages/choose-an-edition/teleport-enterprise/hsm.mdx b/docs/pages/choose-an-edition/teleport-enterprise/hsm.mdx index 0bad62333d5ec..bccf198936b0a 100644 --- a/docs/pages/choose-an-edition/teleport-enterprise/hsm.mdx +++ b/docs/pages/choose-an-edition/teleport-enterprise/hsm.mdx @@ -110,7 +110,7 @@ to use. https://docs.aws.amazon.com/cloudhsm/latest/userguide/gs_cloudhsm_cli-install.html Bootstrap the CLI by configuring the IP address of the new HSM: - + ```code $ sudo /opt/cloudhsm/bin/configure-cli -a ``` @@ -170,7 +170,7 @@ to use. ```code $ sudo /opt/cloudhsm/bin/configure-pkcs11 --disable-key-availability-check -a ``` - + @@ -300,48 +300,26 @@ auth_service: ## Step 3/5. (Re)start Teleport Auth -If this is a new auth server which has not been started yet, starting a brand -new cluster with an empty backend, HSM keys will be automatically generated at -startup and no further action is required, skip to step 5. Otherwise, continue -reading. +If this is a new Teleport Auth Service which has not been started yet, starting +a brand new cluster with an empty backend, HSM keys will be automatically +generated at startup and no further action is required, skip to step 5. +Otherwise, continue reading. If you are connecting an HSM to an existing Teleport cluster, restart the auth -server for the configuration changes to take effect. New CA keys will -automatically be generated in the HSM. For these keys to be trusted by the rest -of the cluster you will need to perform a CA rotation, see -[Step 4](#step-45-certificate-rotation-with-hsm). The auth server will not perform -any signing operations until the rotation has started. In an HA cluster you -should add the HSM to the auth configuration one server at a time, and do not -route any traffic to the auth server where the HSM is currently being added. +server for the configuration changes to take effect. +New CA keys will automatically be generated in the HSM during the next CA +rotation. +Until a CA rotation is completed, the Auth Service will continue signing new +certificates with the existing software keys. ## Step 4/5. Certificate Rotation with HSM When adding a new HSM to an existing Teleport cluster, or adding a new -HSM-connected Auth server to an HA Teleport cluster, you will need to rotate all -Certificate Authorities in order for new certificates to be issued and +HSM-connected Auth Service to an HA Teleport cluster, you will need to rotate +all Certificate Authorities in order for new certificates to be issued and trusted. -`tctl status` will print a warning if CA rotation is required: -```code -$ tctl status -WARNING: One or more auth servers has a newly added or removed HSM or KMS configured. You should not route traffic to that server until a CA rotation has been completed. -Cluster cluster-one -Version (=teleport.version=) -host CA never updated -user CA never updated -db CA never updated -openssh CA never updated -jwt CA never updated -saml_idp CA never updated -CA pin (=presets.ca_pin=) -CA pin sha256:e758c8f0f6cd95116d5da8171e0ff4adfa99dab3b1f171bfe854070884955524 -``` - -`teleport start` will also print a warning during startup if any CA needs to be rotated. -Until rotation is completed, the auth server will not sign any new certificates -(except the `Admin` certificate used by `tctl` which will be signed by a -temporary HSM key). - +`teleport start` will print a warning during startup if any CA needs to be rotated. CA rotation can be performed manually or semi-automatically, see our admin guide on [Certificate rotation](../../management/operations/ca-rotation.mdx). All CAs listed in the output of `tctl status` must be rotated. @@ -351,5 +329,3 @@ All CAs listed in the output of `tctl status` must be rotated. You are all set! Check the teleport logs for `Creating new HSM key pair` to confirm that the feature is working. You can also check that keys were created in your HSM using your HSM's admin tool. - - diff --git a/integration/hsm/helpers.go b/integration/hsm/helpers.go index 9f9e70acde31b..216d784e87800 100644 --- a/integration/hsm/helpers.go +++ b/integration/hsm/helpers.go @@ -21,7 +21,6 @@ package hsm import ( "context" "net" - "os" "path/filepath" "testing" "time" @@ -229,9 +228,6 @@ func (s teleportServices) waitForPhaseChange(ctx context.Context) error { } func newAuthConfig(t *testing.T, log utils.Logger) *servicecfg.Config { - hostName, err := os.Hostname() - require.NoError(t, err) - config := servicecfg.MakeDefaultConfig() config.DataDir = t.TempDir() config.Auth.StorageConfig.Params["path"] = filepath.Join(config.DataDir, defaults.BackendDir) @@ -244,13 +240,14 @@ func newAuthConfig(t *testing.T, log utils.Logger) *servicecfg.Config { config.Auth.Enabled = true config.Auth.NoAudit = true - config.Auth.ListenAddr.Addr = net.JoinHostPort(hostName, "0") + config.Auth.ListenAddr.Addr = net.JoinHostPort("localhost", "0") config.Auth.PublicAddrs = []utils.NetAddr{ { AddrNetwork: "tcp", - Addr: hostName, + Addr: "localhost", }, } + var err error config.Auth.ClusterName, err = services.NewClusterNameWithRandomID(types.ClusterNameSpecV2{ ClusterName: "testcluster", }) @@ -270,9 +267,6 @@ func newAuthConfig(t *testing.T, log utils.Logger) *servicecfg.Config { } func newProxyConfig(t *testing.T, authAddr utils.NetAddr, log utils.Logger) *servicecfg.Config { - hostName, err := os.Hostname() - require.NoError(t, err) - config := servicecfg.MakeDefaultConfig() config.DataDir = t.TempDir() config.CachePolicy.Enabled = true @@ -289,8 +283,8 @@ func newProxyConfig(t *testing.T, authAddr utils.NetAddr, log utils.Logger) *ser config.Proxy.DisableWebInterface = true config.Proxy.DisableWebService = true config.Proxy.DisableReverseTunnel = true - config.Proxy.SSHAddr.Addr = net.JoinHostPort(hostName, "0") - config.Proxy.WebAddr.Addr = net.JoinHostPort(hostName, "0") + config.Proxy.SSHAddr.Addr = net.JoinHostPort("localhost", "0") + config.Proxy.WebAddr.Addr = net.JoinHostPort("localhost", "0") return config } diff --git a/integration/hsm/hsm_test.go b/integration/hsm/hsm_test.go index 5e9d99d416797..7afa0499e7ce7 100644 --- a/integration/hsm/hsm_test.go +++ b/integration/hsm/hsm_test.go @@ -142,13 +142,14 @@ func TestHSMRotation(t *testing.T) { log.Debug("TestHSMRotation: starting auth server") authConfig := newHSMAuthConfig(t, liteBackendConfig(t), log) auth1 := newTeleportService(t, authConfig, "auth1") - t.Cleanup(func() { - require.NoError(t, auth1.process.GetAuthServer().GetKeyStore().DeleteUnusedKeys(ctx, nil)) - }) allServices := teleportServices{auth1} log.Debug("TestHSMRotation: waiting for auth server to start") - require.NoError(t, auth1.start(ctx)) + err := auth1.start(ctx) + require.NoError(t, err, trace.DebugReport(err)) + t.Cleanup(func() { + require.NoError(t, auth1.process.GetAuthServer().GetKeyStore().DeleteUnusedKeys(ctx, nil)) + }) // start a proxy to make sure it can get creds at each stage of rotation log.Debug("TestHSMRotation: starting proxy") @@ -157,7 +158,7 @@ func TestHSMRotation(t *testing.T) { allServices = append(allServices, proxy) log.Debug("TestHSMRotation: sending rotation request init") - err := auth1.process.GetAuthServer().RotateCertAuthority(ctx, types.RotateRequest{ + err = auth1.process.GetAuthServer().RotateCertAuthority(ctx, types.RotateRequest{ Type: types.HostCA, TargetPhase: types.RotationPhaseInit, Mode: types.RotationModeManual, @@ -261,11 +262,9 @@ func TestHSMDualAuthRotation(t *testing.T) { require.NoError(t, authServices.start(ctx), "auth service failed initial startup") log.Debug("TestHSMDualAuthRotation: Starting load balancer") - hostName, err := os.Hostname() - require.NoError(t, err) lb, err := utils.NewLoadBalancer( ctx, - *utils.MustParseAddr(net.JoinHostPort(hostName, "0")), + *utils.MustParseAddr(net.JoinHostPort("localhost", "0")), auth1.authAddr(t), ) require.NoError(t, err) @@ -487,12 +486,15 @@ func TestHSMMigrate(t *testing.T) { require.NoError(t, auth1.start(ctx)) require.NoError(t, auth2.start(ctx)) + // Replace configured addresses with port set to 0 with the actual port + // number so they are stable across hard restarts. + auth1Config.Auth.ListenAddr = auth1.authAddr(t) + auth2Config.Auth.ListenAddr = auth2.authAddr(t) + log.Debug("TestHSMMigrate: Starting load balancer") - hostName, err := os.Hostname() - require.NoError(t, err) lb, err := utils.NewLoadBalancer( ctx, - *utils.MustParseAddr(net.JoinHostPort(hostName, "0")), + *utils.MustParseAddr(net.JoinHostPort("localhost", "0")), auth1.authAddr(t), auth2.authAddr(t), ) @@ -508,12 +510,11 @@ func TestHSMMigrate(t *testing.T) { require.NoError(t, proxy.start(ctx)) testClient := func(t *testing.T) { - testAdminClient(t, auth2Config.DataDir, auth2.authAddrString(t)) + testAdminClient(t, auth1Config.DataDir, lb.Addr().String()) } testClient(t) // Phase 1: migrate auth1 to HSM - lb.RemoveBackend(auth1.authAddr(t)) auth1.process.Close() require.NoError(t, auth1.waitForShutdown(ctx)) auth1Config.Auth.KeyStore = keystore.SetupSoftHSMTest(t) @@ -560,7 +561,7 @@ func TestHSMMigrate(t *testing.T) { }, } - // do a full rotation + // Do a full rotation to get HSM keys for auth1 into the CA. for _, stage := range stages { log.Debugf("TestHSMMigrate: Sending rotate request %s", stage.targetPhase) require.NoError(t, auth1.process.GetAuthServer().RotateCertAuthority(ctx, types.RotateRequest{ @@ -571,11 +572,7 @@ func TestHSMMigrate(t *testing.T) { stage.verify(t) } - // Safe to send traffic to new auth1 again - lb.AddBackend(auth1.authAddr(t)) - // Phase 2: migrate auth2 to HSM - lb.RemoveBackend(auth2.authAddr(t)) auth2.process.Close() require.NoError(t, auth2.waitForShutdown(ctx)) auth2Config.Auth.KeyStore = keystore.SetupSoftHSMTest(t) @@ -587,10 +584,10 @@ func TestHSMMigrate(t *testing.T) { testClient(t) - // do a full rotation + // Do another full rotation to get HSM keys for auth2 into the CA. for _, stage := range stages { log.Debugf("TestHSMMigrate: Sending rotate request %s", stage.targetPhase) - require.NoError(t, auth1.process.GetAuthServer().RotateCertAuthority(ctx, types.RotateRequest{ + require.NoError(t, auth2.process.GetAuthServer().RotateCertAuthority(ctx, types.RotateRequest{ Type: types.HostCA, TargetPhase: stage.targetPhase, Mode: types.RotationModeManual, @@ -598,7 +595,5 @@ func TestHSMMigrate(t *testing.T) { stage.verify(t) } - // Safe to send traffic to new auth2 again - lb.AddBackend(auth2.authAddr(t)) testClient(t) } diff --git a/integration/hsm/reload_test.go b/integration/hsm/reload_test.go index a214e17fecba0..af81c9c08cb55 100644 --- a/integration/hsm/reload_test.go +++ b/integration/hsm/reload_test.go @@ -24,6 +24,7 @@ import ( "testing" "time" + "github.com/gravitational/trace" "github.com/stretchr/testify/require" "github.com/gravitational/teleport/lib/service" @@ -54,7 +55,8 @@ func testReloads(t *testing.T) { authConfig := newAuthConfig(t, log) auth := newTeleportService(t, authConfig, "auth") - require.NoError(t, auth.start(testCtx)) + err := auth.start(testCtx) + require.NoError(t, err, trace.DebugReport(err)) t.Cleanup(func() { require.NoError(t, auth.close()) }) proxyConfig := newProxyConfig(t, auth.authAddr(t), log) diff --git a/lib/auth/keystore/aws_kms.go b/lib/auth/keystore/aws_kms.go index a34279dba2feb..f164846498fca 100644 --- a/lib/auth/keystore/aws_kms.go +++ b/lib/auth/keystore/aws_kms.go @@ -298,7 +298,7 @@ func (a *awsKMSKeystore) canSignWithKey(ctx context.Context, raw []byte, keyType // DeleteUnusedKeys deletes all keys readable from the AWS KMS account and // region if they: -// 1. Are not included in the argument activeKys +// 1. Are not included in the argument activeKeys // 2. Are labeled in AWS KMS as being created by this Teleport cluster // 3. Were not created in the past 5 minutes. // @@ -310,7 +310,7 @@ func (a *awsKMSKeystore) canSignWithKey(ctx context.Context, raw []byte, keyType // 1. A different auth server (auth2) creates a new key in GCP KMS // 2. This function (running on auth1) deletes that new key // 3. auth2 saves the id of this deleted key to the backend CA -func (a *awsKMSKeystore) DeleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { +func (a *awsKMSKeystore) deleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { activeAWSKMSKeys := make(map[string]int) for _, activeKey := range activeKeys { keyIsRelevent, err := a.canSignWithKey(ctx, activeKey, keyType(activeKey)) @@ -381,7 +381,7 @@ func (a *awsKMSKeystore) DeleteUnusedKeys(ctx context.Context, activeKeys [][]by a.logger.WithFields(logrus.Fields{ "key_arn": keyARN, "key_state": keyState, - }).Info("DeleteUnusedKeys skipping AWS KMS key which is not in enabled state.") + }).Info("deleteUnusedKeys skipping AWS KMS key which is not in enabled state.") return nil } creationDate := aws.TimeValue(describeOutput.KeyMetadata.CreationDate) @@ -391,7 +391,7 @@ func (a *awsKMSKeystore) DeleteUnusedKeys(ctx context.Context, activeKeys [][]by // the backend CA yet (which is why they don't appear in activeKeys). a.logger.WithFields(logrus.Fields{ "key_arn": keyARN, - }).Info("DeleteUnusedKeys skipping AWS KMS key which was created in the past 5 minutes.") + }).Info("deleteUnusedKeys skipping AWS KMS key which was created in the past 5 minutes.") return nil } diff --git a/lib/auth/keystore/aws_kms_test.go b/lib/auth/keystore/aws_kms_test.go index a973b4175e383..a84f6ed88a1e8 100644 --- a/lib/auth/keystore/aws_kms_test.go +++ b/lib/auth/keystore/aws_kms_test.go @@ -43,7 +43,7 @@ import ( "github.com/gravitational/teleport/lib/utils" ) -// TestAWSKMS_DeleteUnusedKeys tests the AWS KMS keystore's DeleteUnusedKeys +// TestAWSKMS_deleteUnusedKeys tests the AWS KMS keystore's deleteUnusedKeys // method under conditions where the ListKeys response is paginated and/or // includes keys created by other clusters. // @@ -322,7 +322,6 @@ func (f *fakeAWSKMSService) ListKeysWithContext(ctx aws.Context, input *kms.List output.NextMarker = aws.String(strconv.Itoa(i)) output.Truncated = aws.Bool(true) } - fmt.Println("NIC ListKeys", aws.StringValue(input.Marker), len(output.Keys), output.NextMarker) return output, nil } diff --git a/lib/auth/keystore/gcp_kms.go b/lib/auth/keystore/gcp_kms.go index f8a6f9ebec31f..f204df91cfdc9 100644 --- a/lib/auth/keystore/gcp_kms.go +++ b/lib/auth/keystore/gcp_kms.go @@ -231,7 +231,7 @@ func (g *gcpKMSKeyStore) canSignWithKey(ctx context.Context, raw []byte, keyType return true, nil } -// DeleteUnusedKeys deletes all keys from the configured KMS keyring if they: +// deleteUnusedKeys deletes all keys from the configured KMS keyring if they: // 1. Are not included in the argument activeKeys // 2. Are labeled with hostLabel (teleport_auth_host) // 3. The hostLabel value matches the local host UUID @@ -248,7 +248,7 @@ func (g *gcpKMSKeyStore) canSignWithKey(ctx context.Context, raw []byte, keyType // or a simpler case where: the other auth server is running in a completely // different Teleport cluster and the keys it's actively using will never appear // in the activeKeys argument. -func (g *gcpKMSKeyStore) DeleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { +func (g *gcpKMSKeyStore) deleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { // Make a map of currently active key versions, this is used for lookups to // check which keys in KMS are unused. activeKmsKeyVersions := make(map[string]int) diff --git a/lib/auth/keystore/keystore_test.go b/lib/auth/keystore/keystore_test.go index bb87ed57a7be6..0b6caed6f004a 100644 --- a/lib/auth/keystore/keystore_test.go +++ b/lib/auth/keystore/keystore_test.go @@ -24,12 +24,11 @@ import ( "crypto/rand" "crypto/rsa" "crypto/sha256" - "crypto/x509/pkix" - "log" "os" "testing" "time" + "github.com/aws/aws-sdk-go/aws/arn" "github.com/google/uuid" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" @@ -39,9 +38,6 @@ import ( "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/cloud" - "github.com/gravitational/teleport/lib/defaults" - "github.com/gravitational/teleport/lib/modules" - "github.com/gravitational/teleport/lib/tlsca" "github.com/gravitational/teleport/lib/utils" ) @@ -141,230 +137,31 @@ JhuTMEqUaAOZBoQLn+txjl3nu9WwTThJzlY0L4w= } ) -func TestKeyStore(t *testing.T) { - modules.SetTestModules(t, &modules.TestModules{ - TestBuildType: modules.BuildEnterprise, - TestFeatures: modules.Features{ - HSM: true, - }, - }) - +func TestBackends(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) - clock := clockwork.NewFakeClock() - - skipSoftHSM := os.Getenv("SOFTHSM2_PATH") == "" - var softHSMConfig Config - if !skipSoftHSM { - softHSMConfig = SetupSoftHSMTest(t) - softHSMConfig.PKCS11.HostUUID = "server1" - } - - hostUUID := uuid.NewString() - - gcpKMSConfig := GCPKMSConfig{ - HostUUID: hostUUID, - ProtectionLevel: "HSM", - } - if keyRing := os.Getenv("TEST_GCP_KMS_KEYRING"); keyRing != "" { - t.Logf("Running test with real GCP KMS keyring %s", keyRing) - gcpKMSConfig.KeyRing = keyRing - } else { - t.Log("Running test with fake GCP KMS service") - _, dialer := newTestGCPKMSService(t) - testClient := newTestGCPKMSClient(t, dialer) - gcpKMSConfig.kmsClientOverride = testClient - gcpKMSConfig.KeyRing = "test-keyring" - } - - awsKMSAccount := os.Getenv("TEST_AWS_KMS_ACCOUNT") - awsKMSRegion := os.Getenv("TEST_AWS_KMS_REGION") - - yubiSlotNumber := 0 - backends := []struct { - desc string - config Config - isSoftware bool - shouldSkip func() bool - // unusedRawKey should return passable raw key identifier for this - // backend that would not actually exist in the backend. - unusedRawKey func(t *testing.T) []byte - }{ - { - desc: "software", - config: Config{ - Software: SoftwareConfig{ - RSAKeyPairSource: native.GenerateKeyPair, - }, - }, - isSoftware: true, - shouldSkip: func() bool { return false }, - unusedRawKey: func(t *testing.T) []byte { - rawKey, _, err := native.GenerateKeyPair() - require.NoError(t, err) - return rawKey - }, - }, - { - desc: "softhsm", - config: softHSMConfig, - shouldSkip: func() bool { - if skipSoftHSM { - log.Println("Skipping softhsm test because SOFTHSM2_PATH is not set.") - return true - } - return false - }, - unusedRawKey: func(t *testing.T) []byte { - rawKey, err := keyID{ - HostID: softHSMConfig.PKCS11.HostUUID, - KeyID: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", - }.marshal() - require.NoError(t, err) - return rawKey - }, - }, - { - desc: "yubihsm", - config: Config{ - PKCS11: PKCS11Config{ - Path: os.Getenv("YUBIHSM_PKCS11_PATH"), - SlotNumber: &yubiSlotNumber, - Pin: "0001password", - HostUUID: hostUUID, - }, - }, - shouldSkip: func() bool { - if os.Getenv("YUBIHSM_PKCS11_CONF") == "" || os.Getenv("YUBIHSM_PKCS11_PATH") == "" { - log.Println("Skipping yubihsm test because YUBIHSM_PKCS11_CONF or YUBIHSM_PKCS11_PATH is not set.") - return true - } - return false - }, - unusedRawKey: func(t *testing.T) []byte { - rawKey, err := keyID{ - HostID: hostUUID, - KeyID: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", - }.marshal() - require.NoError(t, err) - return rawKey - }, - }, - { - desc: "cloudhsm", - config: Config{ - PKCS11: PKCS11Config{ - Path: "/opt/cloudhsm/lib/libcloudhsm_pkcs11.so", - TokenLabel: "cavium", - Pin: os.Getenv("CLOUDHSM_PIN"), - HostUUID: hostUUID, - }, - }, - shouldSkip: func() bool { - if os.Getenv("CLOUDHSM_PIN") == "" { - log.Println("Skipping cloudhsm test because CLOUDHSM_PIN is not set.") - return true - } - return false - }, - unusedRawKey: func(t *testing.T) []byte { - rawKey, err := keyID{ - HostID: hostUUID, - KeyID: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", - }.marshal() - require.NoError(t, err) - return rawKey - }, - }, - { - desc: "gcp kms", - config: Config{ - GCPKMS: gcpKMSConfig, - }, - shouldSkip: func() bool { - return false - }, - unusedRawKey: func(t *testing.T) []byte { - return gcpKMSKeyID{ - keyVersionName: gcpKMSConfig.KeyRing + "/cryptoKeys/FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF" + keyVersionSuffix, - }.marshal() - }, - }, - { - desc: "aws kms", - config: Config{ - AWSKMS: AWSKMSConfig{ - Cluster: "test-cluster", - AWSAccount: awsKMSAccount, - AWSRegion: awsKMSRegion, - }, - }, - shouldSkip: func() bool { - return awsKMSAccount == "" || awsKMSRegion == "" - }, - unusedRawKey: func(t *testing.T) []byte { - return awsKMSKeyID{ - arn: "arn:aws:kms:" + awsKMSAccount + ":" + awsKMSRegion + ":key/unused", - account: awsKMSAccount, - region: awsKMSRegion, - }.marshal() - }, - }, - { - desc: "fake aws kms", - config: Config{ - AWSKMS: AWSKMSConfig{ - Cluster: "test-cluster", - AWSAccount: "123456789012", - AWSRegion: "us-west-2", - CloudClients: &cloud.TestCloudClients{ - KMS: newFakeAWSKMSService(t, clock, "123456789012", "us-west-2", 100), - STS: &fakeAWSSTSClient{ - account: "123456789012", - }, - }, - clock: clock, - }, - }, - shouldSkip: func() bool { - return false - }, - unusedRawKey: func(t *testing.T) []byte { - return awsKMSKeyID{ - arn: "arn:aws:kms:us-west-2:123456789012:key/unused", - account: "123456789012", - region: "us-west-2", - }.marshal() - }, - }, - } - message := []byte("Lorem ipsum dolor sit amet...") messageHash := sha256.Sum256(message) - for _, tc := range backends { - tc := tc - t.Run(tc.desc, func(t *testing.T) { - if tc.shouldSkip() { - t.SkipNow() - } + pack := newTestPack(ctx, t) - // create the keystore manager - keyStore, err := NewManager(ctx, tc.config) - require.NoError(t, err, trace.DebugReport(err)) + for _, backendDesc := range pack.backends { + t.Run(backendDesc.name, func(t *testing.T) { + backend := backendDesc.backend // create a key - key, signer, err := keyStore.generateRSA(ctx) + key, signer, err := backend.generateRSA(ctx) require.NoError(t, err, trace.DebugReport(err)) require.NotNil(t, key) require.NotNil(t, signer) + require.Equal(t, backendDesc.expectedKeyType, keyType(key)) // delete the key when we're done with it - t.Cleanup(func() { require.NoError(t, keyStore.deleteKey(ctx, key)) }) + t.Cleanup(func() { require.NoError(t, backend.deleteKey(ctx, key)) }) // get a signer from the key - signer, err = keyStore.getSigner(ctx, key, signer.Public()) + signer, err = backend.getSigner(ctx, key, signer.Public()) require.NoError(t, err) require.NotNil(t, signer) @@ -375,140 +172,21 @@ func TestKeyStore(t *testing.T) { // make sure we can verify the signature with a "known good" rsa implementation err = rsa.VerifyPKCS1v15(signer.Public().(*rsa.PublicKey), crypto.SHA256, messageHash[:], signature) require.NoError(t, err) - - // make sure we can get the ssh public key - sshSigner, err := ssh.NewSignerFromSigner(signer) - require.NoError(t, err) - sshPublicKey := ssh.MarshalAuthorizedKey(sshSigner.PublicKey()) - - // make sure we can get a tls cert - tlsCert, err := tlsca.GenerateSelfSignedCAWithSigner( - signer, - pkix.Name{ - CommonName: "server1", - Organization: []string{"server1"}, - }, nil, defaults.CATTL) - require.NoError(t, err) - - jwtPublicKey, err := utils.MarshalPublicKey(signer) - require.NoError(t, err) - - // test CA with multiple active keypairs - ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ - Type: types.HostCA, - ClusterName: "example.com", - ActiveKeys: types.CAKeySet{ - SSH: []*types.SSHKeyPair{ - testPKCS11SSHKeyPair, - &types.SSHKeyPair{ - PrivateKey: key, - PrivateKeyType: keyType(key), - PublicKey: sshPublicKey, - }, - }, - TLS: []*types.TLSKeyPair{ - testPKCS11TLSKeyPair, - &types.TLSKeyPair{ - Key: key, - KeyType: keyType(key), - Cert: tlsCert, - }, - }, - JWT: []*types.JWTKeyPair{ - testPKCS11JWTKeyPair, - &types.JWTKeyPair{ - PrivateKey: key, - PrivateKeyType: keyType(key), - PublicKey: jwtPublicKey, - }, - }, - }, - }) - require.NoError(t, err) - - // test that keyStore is able to select the correct key and get a signer - sshSigner, err = keyStore.GetSSHSigner(ctx, ca) - require.NoError(t, err, trace.DebugReport(err)) - require.NotNil(t, sshSigner) - - tlsCert, tlsSigner, err := keyStore.GetTLSCertAndSigner(ctx, ca) - require.NoError(t, err) - require.NotNil(t, tlsCert) - require.NotEqual(t, testPKCS11TLSKeyPair.Cert, tlsCert) - require.NotNil(t, tlsSigner) - - jwtSigner, err := keyStore.GetJWTSigner(ctx, ca) - require.NoError(t, err, trace.DebugReport(err)) - require.NotNil(t, jwtSigner) - - // test CA with only raw keys - ca, err = types.NewCertAuthority(types.CertAuthoritySpecV2{ - Type: types.HostCA, - ClusterName: "example.com", - ActiveKeys: types.CAKeySet{ - SSH: []*types.SSHKeyPair{ - testRawSSHKeyPair, - }, - TLS: []*types.TLSKeyPair{ - testRawTLSKeyPair, - }, - JWT: []*types.JWTKeyPair{ - testRawJWTKeyPair, - }, - }, - }) - require.NoError(t, err) - - if !tc.isSoftware { - // hsm keyStore should not get any signer from raw keys - _, err = keyStore.GetSSHSigner(ctx, ca) - require.True(t, trace.IsNotFound(err)) - - _, _, err = keyStore.GetTLSCertAndSigner(ctx, ca) - require.True(t, trace.IsNotFound(err)) - - _, err = keyStore.GetJWTSigner(ctx, ca) - require.True(t, trace.IsNotFound(err)) - } else { - // software keyStore should be able to get a signer - sshSigner, err = keyStore.GetSSHSigner(ctx, ca) - require.NoError(t, err) - require.NotNil(t, sshSigner) - - tlsCert, tlsSigner, err = keyStore.GetTLSCertAndSigner(ctx, ca) - require.NoError(t, err) - require.NotNil(t, tlsCert) - require.NotNil(t, tlsSigner) - - jwtSigner, err = keyStore.GetJWTSigner(ctx, ca) - require.NoError(t, err) - require.NotNil(t, jwtSigner) - } }) } - for _, tc := range backends { - t.Run(tc.desc+"_DeleteUnusedKeys", func(t *testing.T) { - if tc.shouldSkip() { - t.SkipNow() - } - if tc.isSoftware { - // deleting keys is a no-op for software, we won't get the error - // we're expecting - t.SkipNow() - } - - // create the keystore manager - keyStore, err := NewManager(ctx, tc.config) - require.NoError(t, err) + for _, backendDesc := range pack.backends { + t.Run(backendDesc.name+"_deleteUnusedKeys", func(t *testing.T) { + backend := backendDesc.backend - // create some keys to test DeleteUnusedKeys + // create some keys to test deleteUnusedKeys const numKeys = 3 rawPrivateKeys := make([][]byte, numKeys) rawPublicKeys := make([][]byte, numKeys) for i := 0; i < numKeys; i++ { var signer crypto.Signer - rawPrivateKeys[i], signer, err = keyStore.generateRSA(ctx) + var err error + rawPrivateKeys[i], signer, err = backend.generateRSA(ctx) require.NoError(t, err) rawPublicKeys[i], err = utils.MarshalPublicKey(signer) require.NoError(t, err) @@ -516,41 +194,397 @@ func TestKeyStore(t *testing.T) { // AWS KMS keystore will not delete any keys created in the past 5 // minutes. - clock.Advance(6 * time.Minute) + pack.clock.Advance(6 * time.Minute) // say that only the first key is in use, delete the rest usedKeys := [][]byte{rawPrivateKeys[0]} - err = keyStore.DeleteUnusedKeys(ctx, usedKeys) + err := backend.deleteUnusedKeys(ctx, usedKeys) require.NoError(t, err, trace.DebugReport(err)) // make sure the first key is still good - signer, err := keyStore.getSigner(ctx, rawPrivateKeys[0], rawPublicKeys[0]) + signer, err := backend.getSigner(ctx, rawPrivateKeys[0], rawPublicKeys[0]) require.NoError(t, err) _, err = signer.Sign(rand.Reader, messageHash[:], crypto.SHA256) require.NoError(t, err) // make sure all other keys are deleted for i := 1; i < numKeys; i++ { - signer, err := keyStore.getSigner(ctx, rawPrivateKeys[i], rawPublicKeys[0]) + signer, err := backend.getSigner(ctx, rawPrivateKeys[i], rawPublicKeys[0]) if err != nil { // For PKCS11 we expect to fail to get the signer, for cloud - // KMS backends it won't fail until signing + // KMS backends it won't fail until actually signing. continue } _, err = signer.Sign(rand.Reader, messageHash[:], crypto.SHA256) - require.Error(t, err) + if backendDesc.deletionDoesNothing { + require.NoError(t, err) + } else { + require.Error(t, err) + } } // Make sure key deletion is aborted when one of the active keys // cannot be found. This makes sure that we don't accidentally // delete current active keys in case the ListKeys operation fails. - fakeActiveKey := tc.unusedRawKey(t) - err = keyStore.DeleteUnusedKeys(ctx, [][]byte{fakeActiveKey}) - require.True(t, trace.IsNotFound(err), "expected NotFound error, got %v", err) + fakeActiveKey := backendDesc.unusedRawKey + err = backend.deleteUnusedKeys(ctx, [][]byte{fakeActiveKey}) + if backendDesc.deletionDoesNothing { + require.NoError(t, err) + } else { + require.True(t, trace.IsNotFound(err), "expected NotFound error, got %v", err) + } // delete the final key so we don't leak it - err = keyStore.deleteKey(ctx, rawPrivateKeys[0]) + err = backend.deleteKey(ctx, rawPrivateKeys[0]) require.NoError(t, err) }) } } + +func TestManager(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + + pack := newTestPack(ctx, t) + + const clusterName = "test-cluster" + + for _, backendDesc := range pack.backends { + manager, err := NewManager(ctx, backendDesc.config) + require.NoError(t, err) + + // Delete all keys to clean up the test. + t.Cleanup(func() { + require.NoError(t, manager.DeleteUnusedKeys(context.Background(), nil /*activeKeys*/)) + }) + + sshKeyPair, err := manager.NewSSHKeyPair(ctx) + require.NoError(t, err) + require.Equal(t, backendDesc.expectedKeyType, sshKeyPair.PrivateKeyType) + + tlsKeyPair, err := manager.NewTLSKeyPair(ctx, clusterName) + require.NoError(t, err) + require.Equal(t, backendDesc.expectedKeyType, tlsKeyPair.KeyType) + + jwtKeyPair, err := manager.NewJWTKeyPair(ctx) + require.NoError(t, err) + require.Equal(t, backendDesc.expectedKeyType, jwtKeyPair.PrivateKeyType) + + // Test a CA with multiple active keypairs. Each element of ActiveKeys + // includes a keypair generated above and a PKCS11 keypair with a + // different hostID that this manager should not be able to use. + ca, err := types.NewCertAuthority(types.CertAuthoritySpecV2{ + Type: types.HostCA, + ClusterName: clusterName, + ActiveKeys: types.CAKeySet{ + SSH: []*types.SSHKeyPair{ + testPKCS11SSHKeyPair, + sshKeyPair, + }, + TLS: []*types.TLSKeyPair{ + testPKCS11TLSKeyPair, + tlsKeyPair, + }, + JWT: []*types.JWTKeyPair{ + testPKCS11JWTKeyPair, + jwtKeyPair, + }, + }, + }) + require.NoError(t, err) + + // Test that the manager is able to select the correct key and get a + // signer. + sshSigner, err := manager.GetSSHSigner(ctx, ca) + require.NoError(t, err, trace.DebugReport(err)) + require.Equal(t, sshKeyPair.PublicKey, ssh.MarshalAuthorizedKey(sshSigner.PublicKey())) + + tlsCert, tlsSigner, err := manager.GetTLSCertAndSigner(ctx, ca) + require.NoError(t, err) + require.Equal(t, tlsKeyPair.Cert, tlsCert) + require.NotNil(t, tlsSigner) + + jwtSigner, err := manager.GetJWTSigner(ctx, ca) + require.NoError(t, err, trace.DebugReport(err)) + pubkeyPem, err := utils.MarshalPublicKey(jwtSigner) + require.NoError(t, err) + require.Equal(t, jwtKeyPair.PublicKey, pubkeyPem) + + // Test what happens when the CA has only raw keys, which will be the + // initial state when migrating from software to a HSM/KMS backend. + ca, err = types.NewCertAuthority(types.CertAuthoritySpecV2{ + Type: types.HostCA, + ClusterName: clusterName, + ActiveKeys: types.CAKeySet{ + SSH: []*types.SSHKeyPair{ + testRawSSHKeyPair, + }, + TLS: []*types.TLSKeyPair{ + testRawTLSKeyPair, + }, + JWT: []*types.JWTKeyPair{ + testRawJWTKeyPair, + }, + }, + }) + require.NoError(t, err) + + // Manager should always be able to get a signer for software keys. + hasUsableKeys, err := manager.HasUsableActiveKeys(ctx, ca) + require.NoError(t, err) + require.True(t, hasUsableKeys) + + sshSigner, err = manager.GetSSHSigner(ctx, ca) + require.NoError(t, err) + require.NotNil(t, sshSigner) + + tlsCert, tlsSigner, err = manager.GetTLSCertAndSigner(ctx, ca) + require.NoError(t, err) + require.NotNil(t, tlsCert) + require.NotNil(t, tlsSigner) + + jwtSigner, err = manager.GetJWTSigner(ctx, ca) + require.NoError(t, err) + require.NotNil(t, jwtSigner) + + // Test a CA with only unusable keypairs - PKCS11 keypairs with a + // different hostID that this manager should not be able to use. + ca, err = types.NewCertAuthority(types.CertAuthoritySpecV2{ + Type: types.HostCA, + ClusterName: clusterName, + ActiveKeys: types.CAKeySet{ + SSH: []*types.SSHKeyPair{ + testPKCS11SSHKeyPair, + }, + TLS: []*types.TLSKeyPair{ + testPKCS11TLSKeyPair, + }, + JWT: []*types.JWTKeyPair{ + testPKCS11JWTKeyPair, + }, + }, + }) + require.NoError(t, err) + + // The manager should not be able to select a key. + hasUsableKeys, err = manager.HasUsableActiveKeys(ctx, ca) + require.NoError(t, err) + require.False(t, hasUsableKeys) + + _, err = manager.GetSSHSigner(ctx, ca) + require.True(t, trace.IsNotFound(err), "expected NotFound error, got %v", err) + + _, _, err = manager.GetTLSCertAndSigner(ctx, ca) + require.True(t, trace.IsNotFound(err), "expected NotFound error, got %v", err) + + _, err = manager.GetJWTSigner(ctx, ca) + require.True(t, trace.IsNotFound(err), "expected NotFound error, got %v", err) + } +} + +type testPack struct { + backends []*backendDesc + clock clockwork.FakeClock +} + +type backendDesc struct { + name string + config Config + backend backend + expectedKeyType types.PrivateKeyType + unusedRawKey []byte + deletionDoesNothing bool +} + +func newTestPack(ctx context.Context, t *testing.T) *testPack { + clock := clockwork.NewFakeClock() + var backends []*backendDesc + + hostUUID := uuid.NewString() + logger := utils.NewLoggerForTests() + + unusedPKCS11Key, err := keyID{ + HostID: hostUUID, + KeyID: "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF", + }.marshal() + require.NoError(t, err) + + softwareConfig := Config{Software: SoftwareConfig{ + RSAKeyPairSource: native.GenerateKeyPair, + }} + softwareBackend := newSoftwareKeyStore(&softwareConfig.Software, logger) + backends = append(backends, &backendDesc{ + name: "software", + config: softwareConfig, + backend: softwareBackend, + unusedRawKey: testRawPrivateKey, + deletionDoesNothing: true, + }) + + if os.Getenv("SOFTHSM2_PATH") != "" { + config := SetupSoftHSMTest(t) + config.PKCS11.HostUUID = hostUUID + backend, err := newPKCS11KeyStore(&config.PKCS11, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "softhsm", + config: config, + backend: backend, + expectedKeyType: types.PrivateKeyType_PKCS11, + unusedRawKey: unusedPKCS11Key, + }) + } + + if yubiHSMPath := os.Getenv("YUBIHSM_PKCS11_PATH"); yubiHSMPath != "" { + slotNumber := 0 + config := Config{ + PKCS11: PKCS11Config{ + Path: os.Getenv(yubiHSMPath), + SlotNumber: &slotNumber, + Pin: "0001password", + HostUUID: hostUUID, + }, + } + backend, err := newPKCS11KeyStore(&config.PKCS11, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "yubihsm", + config: config, + backend: backend, + expectedKeyType: types.PrivateKeyType_PKCS11, + unusedRawKey: unusedPKCS11Key, + }) + } + + if cloudHSMPin := os.Getenv("CLOUDHSM_PIN"); cloudHSMPin != "" { + config := Config{ + PKCS11: PKCS11Config{ + Path: "/opt/cloudhsm/lib/libcloudhsm_pkcs11.so", + TokenLabel: "cavium", + Pin: cloudHSMPin, + HostUUID: hostUUID, + }, + } + backend, err := newPKCS11KeyStore(&config.PKCS11, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "yubihsm", + config: config, + backend: backend, + expectedKeyType: types.PrivateKeyType_PKCS11, + unusedRawKey: unusedPKCS11Key, + }) + } + + if gcpKMSKeyring := os.Getenv("TEST_GCP_KMS_KEYRING"); gcpKMSKeyring != "" { + config := Config{ + GCPKMS: GCPKMSConfig{ + HostUUID: hostUUID, + ProtectionLevel: "HSM", + KeyRing: gcpKMSKeyring, + }, + } + backend, err := newGCPKMSKeyStore(ctx, &config.GCPKMS, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "gcp_kms", + config: config, + backend: backend, + expectedKeyType: types.PrivateKeyType_GCP_KMS, + unusedRawKey: gcpKMSKeyID{ + keyVersionName: gcpKMSKeyring + "/cryptoKeys/FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF" + keyVersionSuffix, + }.marshal(), + }) + } + _, gcpKMSDialer := newTestGCPKMSService(t) + testGCPKMSClient := newTestGCPKMSClient(t, gcpKMSDialer) + fakeGCPKMSConfig := Config{ + GCPKMS: GCPKMSConfig{ + HostUUID: hostUUID, + ProtectionLevel: "HSM", + KeyRing: "test-keyring", + kmsClientOverride: testGCPKMSClient, + }, + } + fakeGCPKMSBackend, err := newGCPKMSKeyStore(ctx, &fakeGCPKMSConfig.GCPKMS, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "fake_gcp_kms", + config: fakeGCPKMSConfig, + backend: fakeGCPKMSBackend, + expectedKeyType: types.PrivateKeyType_GCP_KMS, + unusedRawKey: gcpKMSKeyID{ + keyVersionName: "test-keyring/cryptoKeys/FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF" + keyVersionSuffix, + }.marshal(), + }) + + awsKMSAccount := os.Getenv("TEST_AWS_KMS_ACCOUNT") + awsKMSRegion := os.Getenv("TEST_AWS_KMS_REGION") + if awsKMSAccount != "" && awsKMSRegion != "" { + config := Config{ + AWSKMS: AWSKMSConfig{ + Cluster: "test-cluster", + AWSAccount: awsKMSAccount, + AWSRegion: awsKMSRegion, + }, + } + backend, err := newAWSKMSKeystore(ctx, &config.AWSKMS, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "aws_kms", + config: config, + backend: backend, + expectedKeyType: types.PrivateKeyType_AWS_KMS, + unusedRawKey: awsKMSKeyID{ + arn: arn.ARN{ + Partition: "aws", + Service: "kms", + Region: awsKMSRegion, + AccountID: awsKMSAccount, + Resource: "unused", + }.String(), + account: awsKMSAccount, + region: awsKMSRegion, + }.marshal(), + }) + } + + fakeAWSKMSConfig := Config{ + AWSKMS: AWSKMSConfig{ + Cluster: "test-cluster", + AWSAccount: "123456789012", + AWSRegion: "us-west-2", + CloudClients: &cloud.TestCloudClients{ + KMS: newFakeAWSKMSService(t, clock, "123456789012", "us-west-2", 100), + STS: &fakeAWSSTSClient{ + account: "123456789012", + }, + }, + clock: clock, + }, + } + fakeAWSKMSBackend, err := newAWSKMSKeystore(ctx, &fakeAWSKMSConfig.AWSKMS, logger) + require.NoError(t, err) + backends = append(backends, &backendDesc{ + name: "fake_aws_kms", + config: fakeAWSKMSConfig, + backend: fakeAWSKMSBackend, + expectedKeyType: types.PrivateKeyType_AWS_KMS, + unusedRawKey: awsKMSKeyID{ + arn: arn.ARN{ + Partition: "aws", + Service: "kms", + Region: "us-west-2", + AccountID: "123456789012", + Resource: "unused", + }.String(), + account: "123456789012", + region: "us-west-2", + }.marshal(), + }) + + return &testPack{ + backends: backends, + clock: clock, + } +} diff --git a/lib/auth/keystore/manager.go b/lib/auth/keystore/manager.go index 13a59078f38b4..c0bf84d081a79 100644 --- a/lib/auth/keystore/manager.go +++ b/lib/auth/keystore/manager.go @@ -39,7 +39,14 @@ import ( // Manager provides an interface to interact with teleport CA private keys, // which may be software keys or held in an HSM or other key manager. type Manager struct { - backend + // backendForNewKeys is the preferred backend the Manager is configured to + // use, all new keys will be generated in this backend. + backendForNewKeys backend + + // usableSigningBackends is a list of all backends the manager can get + // signers from, in preference order. [backendForNewKeys] is expected to be + // the first element. + usableSigningBackends []backend } // RSAKeyOptions configure options for RSA key generation. @@ -59,14 +66,9 @@ func WithDigestAlgorithm(alg crypto.Hash) RSAKeyOption { // backend is an interface that holds private keys and provides signing // operations. type backend interface { - // DeleteUnusedKeys deletes all keys from the KeyStore if they are: - // 1. Labeled by this KeyStore when they were created - // 2. Not included in the argument activeKeys - DeleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error - - // generateRSA creates a new RSA private key and returns its identifier and - // a crypto.Signer. The returned identifier can be passed to getSigner - // later to get the same crypto.Signer. + // generateRSA creates a new RSA key pair and returns its identifier and a + // crypto.Signer. The returned identifier can be passed to getSigner later + // to get the same crypto.Signer. generateRSA(context.Context, ...RSAKeyOption) (keyID []byte, signer crypto.Signer, err error) // getSigner returns a crypto.Signer for the given key identifier, if it is found. @@ -74,12 +76,19 @@ type backend interface { // from the underlying backend, and it is always stored in the CA anyway. getSigner(ctx context.Context, keyID []byte, pub crypto.PublicKey) (crypto.Signer, error) - // deleteKey deletes the given key from the KeyStore. - deleteKey(ctx context.Context, keyID []byte) error - - // canSignWithKey returns true if this KeyStore is able to sign with the + // canSignWithKey returns true if this backend is able to sign with the // given key. canSignWithKey(ctx context.Context, raw []byte, keyType types.PrivateKeyType) (bool, error) + + // deleteKey deletes the given key from the backend. + deleteKey(ctx context.Context, keyID []byte) error + + // deleteUnusedKeys deletes all keys from the backend if they are: + // 1. Not included in the argument activeKeys which is meant to contain all + // active keys currently referenced in the backend CA. + // 2. Created in the backend by this Teleport cluster. + // 3. Each backend may apply extra restrictions to which keys may be deleted. + deleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error } // Config holds configuration parameters for the keystore. A software keystore @@ -101,6 +110,14 @@ type Config struct { } func (cfg *Config) CheckAndSetDefaults() error { + if cfg.Logger == nil { + cfg.Logger = logrus.StandardLogger() + } + + if err := cfg.Software.CheckAndSetDefaults(); err != nil { + return trace.Wrap(err) + } + // We check for mutual exclusion when parsing the file config. if (cfg.PKCS11 != PKCS11Config{}) { return trace.Wrap(cfg.PKCS11.CheckAndSetDefaults()) @@ -111,7 +128,7 @@ func (cfg *Config) CheckAndSetDefaults() error { if (cfg.AWSKMS != AWSKMSConfig{}) { return trace.Wrap(cfg.AWSKMS.CheckAndSetDefaults()) } - return trace.Wrap(cfg.Software.CheckAndSetDefaults()) + return nil } // NewManager returns a new keystore Manager @@ -120,24 +137,33 @@ func NewManager(ctx context.Context, cfg Config) (*Manager, error) { return nil, trace.Wrap(err) } - logger := cfg.Logger - if logger == nil { - logger = logrus.StandardLogger() - } + softwareBackend := newSoftwareKeyStore(&cfg.Software, cfg.Logger) if (cfg.PKCS11 != PKCS11Config{}) { - backend, err := newPKCS11KeyStore(&cfg.PKCS11, logger) - return &Manager{backend: backend}, trace.Wrap(err) + pkcs11Backend, err := newPKCS11KeyStore(&cfg.PKCS11, cfg.Logger) + return &Manager{ + backendForNewKeys: pkcs11Backend, + usableSigningBackends: []backend{pkcs11Backend, softwareBackend}, + }, trace.Wrap(err) } if (cfg.GCPKMS != GCPKMSConfig{}) { - backend, err := newGCPKMSKeyStore(ctx, &cfg.GCPKMS, logger) - return &Manager{backend: backend}, trace.Wrap(err) + gcpBackend, err := newGCPKMSKeyStore(ctx, &cfg.GCPKMS, cfg.Logger) + return &Manager{ + backendForNewKeys: gcpBackend, + usableSigningBackends: []backend{gcpBackend, softwareBackend}, + }, trace.Wrap(err) } if (cfg.AWSKMS != AWSKMSConfig{}) { - backend, err := newAWSKMSKeystore(ctx, &cfg.AWSKMS, logger) - return &Manager{backend: backend}, trace.Wrap(err) + awsBackend, err := newAWSKMSKeystore(ctx, &cfg.AWSKMS, cfg.Logger) + return &Manager{ + backendForNewKeys: awsBackend, + usableSigningBackends: []backend{awsBackend, softwareBackend}, + }, trace.Wrap(err) } - return &Manager{backend: newSoftwareKeyStore(&cfg.Software, logger)}, nil + return &Manager{ + backendForNewKeys: softwareBackend, + usableSigningBackends: []backend{softwareBackend}, + }, nil } // GetSSHSigner selects a usable SSH keypair from the given CA ActiveKeys and @@ -155,28 +181,30 @@ func (m *Manager) GetAdditionalTrustedSSHSigner(ctx context.Context, ca types.Ce } func (m *Manager) getSSHSigner(ctx context.Context, keySet types.CAKeySet) (ssh.Signer, error) { - for _, keyPair := range keySet.SSH { - canSign, err := m.backend.canSignWithKey(ctx, keyPair.PrivateKey, keyPair.PrivateKeyType) - if err != nil { - return nil, trace.Wrap(err) - } - if !canSign { - continue - } - pub, err := publicKeyFromSSHAuthorizedKey(keyPair.PublicKey) - if err != nil { - return nil, trace.Wrap(err, "failed to parse SSH public key") - } - signer, err := m.backend.getSigner(ctx, keyPair.PrivateKey, pub) - if err != nil { - return nil, trace.Wrap(err) + for _, backend := range m.usableSigningBackends { + for _, keyPair := range keySet.SSH { + canSign, err := backend.canSignWithKey(ctx, keyPair.PrivateKey, keyPair.PrivateKeyType) + if err != nil { + return nil, trace.Wrap(err) + } + if !canSign { + continue + } + pub, err := publicKeyFromSSHAuthorizedKey(keyPair.PublicKey) + if err != nil { + return nil, trace.Wrap(err, "failed to parse SSH public key") + } + signer, err := backend.getSigner(ctx, keyPair.PrivateKey, pub) + if err != nil { + return nil, trace.Wrap(err) + } + sshSigner, err := ssh.NewSignerFromSigner(signer) + if err != nil { + return nil, trace.Wrap(err) + } + // SHA-512 to match NewSSHKeyPair. + return toRSASHA512Signer(sshSigner), trace.Wrap(err) } - sshSigner, err := ssh.NewSignerFromSigner(signer) - if err != nil { - return nil, trace.Wrap(err) - } - // SHA-512 to match NewSSHKeyPair. - return toRSASHA512Signer(sshSigner), trace.Wrap(err) } return nil, trace.NotFound("no usable SSH key pairs found") } @@ -226,23 +254,25 @@ func (m *Manager) GetAdditionalTrustedTLSCertAndSigner(ctx context.Context, ca t } func (m *Manager) getTLSCertAndSigner(ctx context.Context, keySet types.CAKeySet) ([]byte, crypto.Signer, error) { - for _, keyPair := range keySet.TLS { - canSign, err := m.backend.canSignWithKey(ctx, keyPair.Key, keyPair.KeyType) - if err != nil { - return nil, nil, trace.Wrap(err) - } - if !canSign { - continue - } - pub, err := publicKeyFromTLSCertPem(keyPair.Cert) - if err != nil { - return nil, nil, trace.Wrap(err) - } - signer, err := m.backend.getSigner(ctx, keyPair.Key, pub) - if err != nil { - return nil, nil, trace.Wrap(err) + for _, backend := range m.usableSigningBackends { + for _, keyPair := range keySet.TLS { + canSign, err := backend.canSignWithKey(ctx, keyPair.Key, keyPair.KeyType) + if err != nil { + return nil, nil, trace.Wrap(err) + } + if !canSign { + continue + } + pub, err := publicKeyFromTLSCertPem(keyPair.Cert) + if err != nil { + return nil, nil, trace.Wrap(err) + } + signer, err := backend.getSigner(ctx, keyPair.Key, pub) + if err != nil { + return nil, nil, trace.Wrap(err) + } + return keyPair.Cert, signer, nil } - return keyPair.Cert, signer, nil } return nil, nil, trace.NotFound("no usable TLS key pairs found") } @@ -262,20 +292,22 @@ func publicKeyFromTLSCertPem(certPem []byte) (crypto.PublicKey, error) { // GetJWTSigner selects a usable JWT keypair from the given keySet and returns // a [crypto.Signer]. func (m *Manager) GetJWTSigner(ctx context.Context, ca types.CertAuthority) (crypto.Signer, error) { - for _, keyPair := range ca.GetActiveKeys().JWT { - canSign, err := m.backend.canSignWithKey(ctx, keyPair.PrivateKey, keyPair.PrivateKeyType) - if err != nil { - return nil, trace.Wrap(err) + for _, backend := range m.usableSigningBackends { + for _, keyPair := range ca.GetActiveKeys().JWT { + canSign, err := backend.canSignWithKey(ctx, keyPair.PrivateKey, keyPair.PrivateKeyType) + if err != nil { + return nil, trace.Wrap(err) + } + if !canSign { + continue + } + pub, err := utils.ParsePublicKey(keyPair.PublicKey) + if err != nil { + return nil, trace.Wrap(err) + } + signer, err := backend.getSigner(ctx, keyPair.PrivateKey, pub) + return signer, trace.Wrap(err) } - if !canSign { - continue - } - pub, err := utils.ParsePublicKey(keyPair.PublicKey) - if err != nil { - return nil, trace.Wrap(err) - } - signer, err := m.backend.getSigner(ctx, keyPair.PrivateKey, pub) - return signer, trace.Wrap(err) } return nil, trace.NotFound("no usable JWT key pairs found") } @@ -283,7 +315,7 @@ func (m *Manager) GetJWTSigner(ctx context.Context, ca types.CertAuthority) (cry // NewSSHKeyPair generates a new SSH keypair in the keystore backend and returns it. func (m *Manager) NewSSHKeyPair(ctx context.Context) (*types.SSHKeyPair, error) { // The default hash length for SSH signers is 512 bits. - sshKey, cryptoSigner, err := m.backend.generateRSA(ctx, WithDigestAlgorithm(crypto.SHA512)) + sshKey, cryptoSigner, err := m.backendForNewKeys.generateRSA(ctx, WithDigestAlgorithm(crypto.SHA512)) if err != nil { return nil, trace.Wrap(err) } @@ -301,7 +333,7 @@ func (m *Manager) NewSSHKeyPair(ctx context.Context) (*types.SSHKeyPair, error) // NewTLSKeyPair creates a new TLS keypair in the keystore backend and returns it. func (m *Manager) NewTLSKeyPair(ctx context.Context, clusterName string) (*types.TLSKeyPair, error) { - tlsKey, signer, err := m.backend.generateRSA(ctx) + tlsKey, signer, err := m.backendForNewKeys.generateRSA(ctx) if err != nil { return nil, trace.Wrap(err) } @@ -324,7 +356,7 @@ func (m *Manager) NewTLSKeyPair(ctx context.Context, clusterName string) (*types // New JWTKeyPair create a new JWT keypair in the keystore backend and returns // it. func (m *Manager) NewJWTKeyPair(ctx context.Context) (*types.JWTKeyPair, error) { - jwtKey, signer, err := m.backend.generateRSA(ctx) + jwtKey, signer, err := m.backendForNewKeys.generateRSA(ctx) if err != nil { return nil, trace.Wrap(err) } @@ -353,36 +385,42 @@ func (m *Manager) HasUsableAdditionalKeys(ctx context.Context, ca types.CertAuth } func (m *Manager) hasUsableKeys(ctx context.Context, keySet types.CAKeySet) (bool, error) { - for _, sshKeyPair := range keySet.SSH { - usable, err := m.backend.canSignWithKey(ctx, sshKeyPair.PrivateKey, sshKeyPair.PrivateKeyType) - if err != nil { - return false, trace.Wrap(err) + for _, backend := range m.usableSigningBackends { + for _, sshKeyPair := range keySet.SSH { + usable, err := backend.canSignWithKey(ctx, sshKeyPair.PrivateKey, sshKeyPair.PrivateKeyType) + if err != nil { + return false, trace.Wrap(err) + } + if usable { + return true, nil + } } - if usable { - return true, nil + for _, tlsKeyPair := range keySet.TLS { + usable, err := backend.canSignWithKey(ctx, tlsKeyPair.Key, tlsKeyPair.KeyType) + if err != nil { + return false, trace.Wrap(err) + } + if usable { + return true, nil + } } - } - for _, tlsKeyPair := range keySet.TLS { - usable, err := m.backend.canSignWithKey(ctx, tlsKeyPair.Key, tlsKeyPair.KeyType) - if err != nil { - return false, trace.Wrap(err) - } - if usable { - return true, nil - } - } - for _, jwtKeyPair := range keySet.JWT { - usable, err := m.backend.canSignWithKey(ctx, jwtKeyPair.PrivateKey, jwtKeyPair.PrivateKeyType) - if err != nil { - return false, trace.Wrap(err) - } - if usable { - return true, nil + for _, jwtKeyPair := range keySet.JWT { + usable, err := backend.canSignWithKey(ctx, jwtKeyPair.PrivateKey, jwtKeyPair.PrivateKeyType) + if err != nil { + return false, trace.Wrap(err) + } + if usable { + return true, nil + } } } return false, nil } +func (m *Manager) DeleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { + return trace.Wrap(m.backendForNewKeys.deleteUnusedKeys(ctx, activeKeys)) +} + // keyType returns the type of the given private key. func keyType(key []byte) types.PrivateKeyType { if bytes.HasPrefix(key, pkcs11Prefix) { diff --git a/lib/auth/keystore/pkcs11.go b/lib/auth/keystore/pkcs11.go index 72057643dbbce..8dddb08566a7d 100644 --- a/lib/auth/keystore/pkcs11.go +++ b/lib/auth/keystore/pkcs11.go @@ -238,12 +238,12 @@ func (p *pkcs11KeyStore) deleteKey(_ context.Context, rawKey []byte) error { return trace.Wrap(signer.Delete()) } -// DeleteUnusedKeys deletes all keys from the KeyStore if they are: +// deleteUnusedKeys deletes all keys from the KeyStore if they are: // 1. Labeled with the local HostUUID when they were created // 2. Not included in the argument activeKeys // This is meant to delete unused keys after they have been rotated out by a CA // rotation. -func (p *pkcs11KeyStore) DeleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { +func (p *pkcs11KeyStore) deleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { p.log.Debug("Deleting unused keys from HSM") // It's necessary to fetch all PublicKeys for the known activeKeys in order to diff --git a/lib/auth/keystore/software.go b/lib/auth/keystore/software.go index 893ee0b518793..a460ed6cd4257 100644 --- a/lib/auth/keystore/software.go +++ b/lib/auth/keystore/software.go @@ -26,6 +26,7 @@ import ( "github.com/sirupsen/logrus" "github.com/gravitational/teleport/api/types" + "github.com/gravitational/teleport/lib/auth/native" "github.com/gravitational/teleport/lib/utils" ) @@ -42,7 +43,7 @@ type SoftwareConfig struct { func (cfg *SoftwareConfig) CheckAndSetDefaults() error { if cfg.RSAKeyPairSource == nil { - return trace.BadParameter("must provide RSAKeyPairSource") + cfg.RSAKeyPairSource = native.GenerateKeyPair } return nil } @@ -84,16 +85,14 @@ func (s *softwareKeyStore) canSignWithKey(ctx context.Context, _ []byte, keyType return keyType == types.PrivateKeyType_RAW, nil } -// deleteKey deletes the given key from the KeyStore. This is a no-op for -// softwareKeyStore. +// deleteKey is a no-op for softwareKeyStore because the keys are not actually +// stored in any external backend. func (s *softwareKeyStore) deleteKey(_ context.Context, _ []byte) error { return nil } -// DeleteUnusedKeys deletes all keys from the KeyStore if they are: -// 1. Labeled by this KeyStore when they were created -// 2. Not included in the argument activeKeys -// This is a no-op for rawKeyStore. -func (s *softwareKeyStore) DeleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { +// deleteUnusedKeys is a no-op for softwareKeyStore because the keys are not +// actually stored in any external backend. +func (s *softwareKeyStore) deleteUnusedKeys(ctx context.Context, activeKeys [][]byte) error { return nil }