diff --git a/api/utils/keypaths/keypaths.go b/api/utils/keypaths/keypaths.go index 0361d1dd20670..62f1e2b9381a7 100644 --- a/api/utils/keypaths/keypaths.go +++ b/api/utils/keypaths/keypaths.go @@ -99,11 +99,14 @@ const ( // │ │ └── appC.key --> private key for app service "appC" // │ ├── foo-db --> Database access certs for user "foo" // │ │ ├── root --> Database access certs for cluster "root" -// │ │ │ ├── dbA-x509.pem --> TLS cert for database service "dbA" -// │ │ │ ├── dbB-x509.pem --> TLS cert for database service "dbB" +// │ │ │ ├── dbA.crt --> TLS cert for database service "dbA" +// │ │ │ ├── dbA.key --> private key for database service "dbA" +// │ │ │ ├── dbB.crt --> TLS cert for database service "dbB" +// │ │ │ ├── dbB.key --> private key for database service "dbB" // │ │ │ └── dbC-wallet --> Oracle Client wallet Configuration directory. // │ │ ├── leaf --> Database access certs for cluster "leaf" -// │ │ │ └── dbC-x509.pem --> TLS cert for database service "dbC" +// │ │ │ ├── dbC.crt --> TLS cert for database service "dbC" +// │ │ │ └── dbC.key --> private key for database service "dbC" // │ │ └── proxy-localca.pem --> Self-signed TLS Routing local proxy CA // │ ├── foo-kube --> Kubernetes certs for user "foo" // │ | ├── root --> Kubernetes certs for Teleport cluster "root" @@ -274,27 +277,35 @@ func DatabaseDir(baseDir, proxy, username string) string { return filepath.Join(ProxyKeyDir(baseDir, proxy), username+dbDirSuffix) } -// DatabaseCertDir returns the path to the user's database cert directory +// DatabaseCredentialDir returns the path to the user's database cert directory // for the given proxy and cluster. // // /keys//-db/ -func DatabaseCertDir(baseDir, proxy, username, cluster string) string { +func DatabaseCredentialDir(baseDir, proxy, username, cluster string) string { return filepath.Join(DatabaseDir(baseDir, proxy, username), cluster) } // DatabaseCertPath returns the path to the user's TLS certificate // for the given proxy, cluster, and database. // -// /keys//-db//-x509.pem +// /keys//-db//.crt func DatabaseCertPath(baseDir, proxy, username, cluster, dbname string) string { - return filepath.Join(DatabaseCertDir(baseDir, proxy, username, cluster), dbname+fileExtTLSCertLegacy) + return filepath.Join(DatabaseCredentialDir(baseDir, proxy, username, cluster), dbname+fileExtTLSCert) +} + +// DatabaseKeyPath returns the path to the user's TLS private key +// for the given proxy, cluster, and database. +// +// /keys//-db//.key +func DatabaseKeyPath(baseDir, proxy, username, cluster, dbname string) string { + return filepath.Join(DatabaseCredentialDir(baseDir, proxy, username, cluster), dbname+fileExtTLSKey) } // DatabaseOracleWalletDirectory returns the path to the user's Oracle Wallet configuration directory. // for the given proxy, cluster and database. // /keys//-db//dbname-wallet/ func DatabaseOracleWalletDirectory(baseDir, proxy, username, cluster, dbname string) string { - return filepath.Join(DatabaseCertDir(baseDir, proxy, username, cluster), dbname+oracleWalletDirSuffix) + return filepath.Join(DatabaseCredentialDir(baseDir, proxy, username, cluster), dbname+oracleWalletDirSuffix) } // KubeDir returns the path to the user's kube directory diff --git a/lib/client/api.go b/lib/client/api.go index 9f9da3b9e52d1..2ab4a55185e8b 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -544,12 +544,18 @@ func VirtualPathCAParams(caType types.CertAuthType) VirtualPathParams { } } -// VirtualPathDatabaseParams returns parameters for selecting specific database -// certificates. -func VirtualPathDatabaseParams(databaseName string) VirtualPathParams { +// VirtualPathDatabaseCertParams returns parameters for selecting a specific database +// certificate by name. +func VirtualPathDatabaseCertParams(databaseName string) VirtualPathParams { return VirtualPathParams{databaseName} } +// VirtualPathDatabaseKeyParams returns parameters for selecting a specific database +// key by name. +func VirtualPathDatabaseKeyParams(databaseName string) VirtualPathParams { + return VirtualPathParams{"DB", databaseName} +} + // VirtualPathAppCertParams returns parameters for selecting specific app cert by name. func VirtualPathAppCertParams(appName string) VirtualPathParams { return VirtualPathParams{appName} @@ -1468,8 +1474,8 @@ func (tc *TeleportClient) IssueUserCertsWithMFA(ctx context.Context, params Reis } defer clusterClient.Close() - key, _, err := clusterClient.IssueUserCertsWithMFA(ctx, params, tc.NewMFAPrompt(mfaPromptOpts...)) - return key, trace.Wrap(err) + keyRing, _, err := clusterClient.IssueUserCertsWithMFA(ctx, params, tc.NewMFAPrompt(mfaPromptOpts...)) + return keyRing, trace.Wrap(err) } // CreateAccessRequestV2 registers a new access request with the auth server. @@ -3713,7 +3719,10 @@ func (tc *TeleportClient) SSHLogin(ctx context.Context, sshLoginFunc SSHLoginFun keyRing.KubeTLSCerts[tc.KubernetesCluster] = response.TLSCert } if tc.DatabaseService != "" { - keyRing.DBTLSCerts[tc.DatabaseService] = response.TLSCert + keyRing.DBTLSCredentials[tc.DatabaseService] = TLSCredential{ + Cert: response.TLSCert, + PrivateKey: priv, + } } // Store the requested cluster name in the keyring. diff --git a/lib/client/api_test.go b/lib/client/api_test.go index 611a3e4217945..fa96596dc32b3 100644 --- a/lib/client/api_test.go +++ b/lib/client/api_test.go @@ -824,12 +824,22 @@ func TestVirtualPathNames(t *testing.T) { { name: "database", kind: VirtualPathDatabase, - params: VirtualPathDatabaseParams("foo"), + params: VirtualPathDatabaseCertParams("foo"), expected: []string{ "TSH_VIRTUAL_PATH_DB_FOO", "TSH_VIRTUAL_PATH_DB", }, }, + { + name: "database key", + kind: VirtualPathKey, + params: VirtualPathDatabaseKeyParams("foo"), + expected: []string{ + "TSH_VIRTUAL_PATH_KEY_DB_FOO", + "TSH_VIRTUAL_PATH_KEY_DB", + "TSH_VIRTUAL_PATH_KEY", + }, + }, { name: "app", kind: VirtualPathAppCert, diff --git a/lib/client/client.go b/lib/client/client.go index dd5da6b2d659d..fa5fd71edf8d0 100644 --- a/lib/client/client.go +++ b/lib/client/client.go @@ -47,6 +47,7 @@ import ( tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" + "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/api/utils/retryutils" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/defaults" @@ -205,10 +206,10 @@ const ( // makeDatabaseClientPEM returns appropriate client PEM file contents for the // specified database type. Some databases only need certificate in the PEM // file, others both certificate and key. -func makeDatabaseClientPEM(proto string, cert []byte, pk *KeyRing) ([]byte, error) { +func makeDatabaseClientPEM(proto string, cert []byte, pk *keys.PrivateKey) ([]byte, error) { // MongoDB expects certificate and key pair in the same pem file. if proto == defaults.ProtocolMongoDB { - keyPEM, err := pk.PrivateKey.SoftwarePrivateKeyPEM() + keyPEM, err := pk.SoftwarePrivateKeyPEM() if err == nil { return append(cert, keyPEM...), nil } else if !trace.IsBadParameter(err) { diff --git a/lib/client/client_store_test.go b/lib/client/client_store_test.go index a9eb3124f00ec..cd9de2f65d354 100644 --- a/lib/client/client_store_test.go +++ b/lib/client/client_store_test.go @@ -108,7 +108,10 @@ func (s *testAuthority) makeSignedKeyRing(t *testing.T, idx KeyRingIndex, makeEx keyRing.Cert = cert keyRing.TLSCert = tlsCert keyRing.TrustedCerts = []authclient.TrustedCerts{s.trustedCerts} - keyRing.DBTLSCerts["example-db"] = tlsCert + keyRing.DBTLSCredentials["example-db"] = TLSCredential{ + Cert: tlsCert, + PrivateKey: priv, + } return keyRing } diff --git a/lib/client/cluster_client.go b/lib/client/cluster_client.go index 470b36698df2d..7fb8d839621bf 100644 --- a/lib/client/cluster_client.go +++ b/lib/client/cluster_client.go @@ -232,11 +232,14 @@ func (c *ClusterClient) generateUserCerts(ctx context.Context, cachePolicy CertC Cert: certs.TLS, } case proto.UserCertsRequest_Database: - dbCert, err := makeDatabaseClientPEM(params.RouteToDatabase.Protocol, certs.TLS, keyRing) + dbCert, err := makeDatabaseClientPEM(params.RouteToDatabase.Protocol, certs.TLS, privKey) if err != nil { return nil, trace.Wrap(err) } - keyRing.DBTLSCerts[params.RouteToDatabase.ServiceName] = dbCert + keyRing.DBTLSCredentials[params.RouteToDatabase.ServiceName] = TLSCredential{ + Cert: dbCert, + PrivateKey: privKey, + } case proto.UserCertsRequest_Kubernetes: keyRing.KubeTLSCerts[params.KubernetesCluster] = certs.TLS case proto.UserCertsRequest_WindowsDesktop: @@ -359,6 +362,15 @@ func (c *ClusterClient) prepareUserCertsRequest(ctx context.Context, params Reis if err != nil { return nil, nil, trace.Wrap(err) } + case proto.UserCertsRequest_Database: + privateKey, err = keyRing.GenerateKey(ctx, c.tc, cryptosuites.UserDatabase) + if err != nil { + return nil, nil, trace.Wrap(err) + } + tlsPublicKey, err = keys.MarshalPublicKey(privateKey.Public()) + if err != nil { + return nil, nil, trace.Wrap(err) + } default: // TODO(nklaassen): split the SSH and TLS key for remaining protocols. privateKey = keyRing.PrivateKey @@ -664,14 +676,17 @@ func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (* keyRing.KubeTLSCerts[certsReq.KubernetesCluster] = newCerts.TLS case proto.UserCertsRequest_Database: - dbCert, err := makeDatabaseClientPEM(certsReq.RouteToDatabase.Protocol, newCerts.TLS, keyRing) + dbCert, err := makeDatabaseClientPEM(certsReq.RouteToDatabase.Protocol, newCerts.TLS, params.PrivateKey) if err != nil { return nil, nil, trace.Wrap(err) } - if keyRing.DBTLSCerts == nil { - keyRing.DBTLSCerts = make(map[string][]byte) + if keyRing.DBTLSCredentials == nil { + keyRing.DBTLSCredentials = make(map[string]TLSCredential) + } + keyRing.DBTLSCredentials[certsReq.RouteToDatabase.ServiceName] = TLSCredential{ + Cert: dbCert, + PrivateKey: params.PrivateKey, } - keyRing.DBTLSCerts[certsReq.RouteToDatabase.ServiceName] = dbCert case proto.UserCertsRequest_WindowsDesktop: if keyRing.WindowsDesktopCerts == nil { diff --git a/lib/client/db/dbcmd/dbcmd.go b/lib/client/db/dbcmd/dbcmd.go index ca28321031358..4b6b2cefe4bf6 100644 --- a/lib/client/db/dbcmd/dbcmd.go +++ b/lib/client/db/dbcmd/dbcmd.go @@ -317,8 +317,9 @@ func (c *CLICommandBuilder) getMariaDBArgs() []string { } sslCertPath := c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName) + sslKeyPath := c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName) - args = append(args, []string{"--ssl-key", c.profile.KeyPath()}...) + args = append(args, []string{"--ssl-key", sslKeyPath}...) args = append(args, []string{"--ssl-ca", c.profile.CACertPathForCluster(c.rootCluster)}...) args = append(args, []string{"--ssl-cert", sslCertPath}...) @@ -589,7 +590,7 @@ func (c *CLICommandBuilder) getRedisCommand() *exec.Cmd { if !c.options.noTLS { args = append(args, "--tls", - "--key", c.profile.KeyPath(), + "--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName), "--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName)) if c.tc.InsecureSkipVerify { @@ -691,7 +692,9 @@ func (c *CLICommandBuilder) getOpenSearchCommand() (*exec.Cmd, error) { func (c *CLICommandBuilder) getOpenSearchCLICommand() (*exec.Cmd, error) { cfg := opensearch.ConfigNoTLS(c.host, c.port) if !c.options.noTLS { - cfg = opensearch.ConfigTLS(c.host, c.port, c.options.caPath, c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName), c.profile.KeyPath()) + cfg = opensearch.ConfigTLS(c.host, c.port, c.options.caPath, + c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName), + c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName)) } baseDir := path.Join(c.profile.Dir, c.profile.Cluster, c.db.ServiceName) @@ -825,7 +828,7 @@ func (c *CLICommandBuilder) getElasticsearchAlternativeCommands() []CommandAlter } else { args := []string{ fmt.Sprintf("https://%v:%v/", c.host, c.port), - "--key", c.profile.KeyPath(), + "--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName), "--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName), } @@ -870,7 +873,7 @@ func (c *CLICommandBuilder) getOpenSearchAlternativeCommands() []CommandAlternat } else { args := []string{ fmt.Sprintf("https://%v:%v/", c.host, c.port), - "--key", c.profile.KeyPath(), + "--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName), "--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName), } diff --git a/lib/client/db/dbcmd/dbcmd_clickhouse.go b/lib/client/db/dbcmd/dbcmd_clickhouse.go index d43966804a23f..6e75cf579041c 100644 --- a/lib/client/db/dbcmd/dbcmd_clickhouse.go +++ b/lib/client/db/dbcmd/dbcmd_clickhouse.go @@ -37,7 +37,7 @@ func (c *CLICommandBuilder) getClickhouseHTTPCommand() (*exec.Cmd, error) { } else { args := []string{ fmt.Sprintf("https://%v:%v/", c.host, c.port), - "--key", c.profile.KeyPath(), + "--key", c.profile.DatabaseKeyPathForCluster(c.tc.SiteName, c.db.ServiceName), "--cert", c.profile.DatabaseCertPathForCluster(c.tc.SiteName, c.db.ServiceName), } diff --git a/lib/client/db/dbcmd/dbcmd_test.go b/lib/client/db/dbcmd/dbcmd_test.go index e53e5e25e160e..4e70703ca2e12 100644 --- a/lib/client/db/dbcmd/dbcmd_test.go +++ b/lib/client/db/dbcmd/dbcmd_test.go @@ -94,8 +94,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { execer: &fakeExec{}, cmd: []string{"psql", "postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/cas/root.pem&" + - "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" + - "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full"}, + "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql.crt&" + + "sslkey=/tmp/keys/example.com/bob-db/db.example.com/mysql.key&sslmode=verify-full"}, wantErr: false, }, { @@ -116,8 +116,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { execer: &fakeExec{}, cmd: []string{"psql", "\"postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/cas/root.pem&" + - "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" + - "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full\""}, + "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql.crt&" + + "sslkey=/tmp/keys/example.com/bob-db/db.example.com/mysql.key&sslmode=verify-full\""}, wantErr: false, }, { @@ -131,8 +131,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { }, cmd: []string{"cockroach", "sql", "--url", "postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/cas/root.pem&" + - "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" + - "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full"}, + "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql.crt&" + + "sslkey=/tmp/keys/example.com/bob-db/db.example.com/mysql.key&sslmode=verify-full"}, wantErr: false, }, { @@ -161,8 +161,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { }, cmd: []string{"cockroach", "sql", "--url", "\"postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/cas/root.pem&" + - "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" + - "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full\""}, + "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql.crt&" + + "sslkey=/tmp/keys/example.com/bob-db/db.example.com/mysql.key&sslmode=verify-full\""}, wantErr: false, }, { @@ -172,8 +172,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { execer: &fakeExec{}, cmd: []string{"psql", "postgres://myUser@localhost:12345/mydb?sslrootcert=/tmp/keys/example.com/cas/root.pem&" + - "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem&" + - "sslkey=/tmp/keys/example.com/bob&sslmode=verify-full"}, + "sslcert=/tmp/keys/example.com/bob-db/db.example.com/mysql.crt&" + + "sslkey=/tmp/keys/example.com/bob-db/db.example.com/mysql.key&sslmode=verify-full"}, wantErr: false, }, { @@ -191,9 +191,9 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { "--port", "12345", "--host", "localhost", "--protocol", "TCP", - "--ssl-key", "/tmp/keys/example.com/bob", + "--ssl-key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--ssl-ca", "/tmp/keys/example.com/cas/root.pem", - "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "--ssl-verify-server-cert"}, wantErr: false, }, @@ -230,9 +230,9 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { "--port", "12345", "--host", "localhost", "--protocol", "TCP", - "--ssl-key", "/tmp/keys/example.com/bob", + "--ssl-key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--ssl-ca", "/tmp/keys/example.com/cas/root.pem", - "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "--ssl-verify-server-cert"}, wantErr: false, }, @@ -313,9 +313,9 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { "--database", "mydb", "--port", "3036", "--host", "proxy.example.com", - "--ssl-key", "/tmp/keys/example.com/bob", + "--ssl-key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--ssl-ca", "/tmp/keys/example.com/cas/root.pem", - "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--ssl-cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "--ssl-verify-server-cert"}, wantErr: false, }, @@ -331,7 +331,7 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { }, cmd: []string{"mongo", "--ssl", - "--sslPEMKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--sslPEMKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "mongodb://localhost:12345/mydb?serverSelectionTimeoutMS=5000", }, wantErr: false, @@ -363,7 +363,7 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { }, cmd: []string{"mongosh", "--tls", - "--tlsCertificateKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--tlsCertificateKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "--tlsUseSystemCA", "mongodb://localhost:12345/mydb?serverSelectionTimeoutMS=5000", }, @@ -383,7 +383,7 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { }, cmd: []string{"mongosh", "--tls", - "--tlsCertificateKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--tlsCertificateKeyFile", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "--tlsCAFile", "/tmp/keys/example.com/cas/example.com.pem", "mongodb://localhost:12345/mydb?serverSelectionTimeoutMS=5000", }, @@ -485,8 +485,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { "-h", "localhost", "-p", "12345", "--tls", - "--key", "/tmp/keys/example.com/bob", - "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem"}, + "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", + "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt"}, wantErr: false, }, { @@ -498,8 +498,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { "-h", "localhost", "-p", "12345", "--tls", - "--key", "/tmp/keys/example.com/bob", - "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", + "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "-n", "2"}, wantErr: false, }, @@ -522,8 +522,8 @@ func TestCLICommandBuilderGetConnectCommand(t *testing.T) { "-h", "proxy.example.com", "-p", "3080", "--tls", - "--key", "/tmp/keys/example.com/bob", - "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem", + "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", + "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt", "--sni", "proxy.example.com"}, wantErr: false, }, @@ -841,7 +841,7 @@ func TestCLICommandBuilderGetConnectCommandAlternatives(t *testing.T) { opts: []ConnectCommandFunc{}, execer: &fakeExec{}, databaseName: "warehouse1", - cmd: map[string][]string{"run single request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem"}}, + cmd: map[string][]string{"run single request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt"}}, wantErr: false, }, { @@ -855,7 +855,7 @@ func TestCLICommandBuilderGetConnectCommandAlternatives(t *testing.T) { }, databaseName: "warehouse1", cmd: map[string][]string{ - "run single request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem"}}, + "run single request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt"}}, wantErr: false, }, { @@ -882,7 +882,7 @@ func TestCLICommandBuilderGetConnectCommandAlternatives(t *testing.T) { execer: &fakeExec{}, databaseName: "warehouse1", cmd: map[string][]string{ - "run request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem"}}, + "run request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt"}}, wantErr: false, }, { @@ -906,8 +906,8 @@ func TestCLICommandBuilderGetConnectCommandAlternatives(t *testing.T) { }, databaseName: "warehouse1", cmd: map[string][]string{ - "run request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql-x509.pem"}, - "run request with opensearch-cli": {"opensearch-cli", "--profile", "teleport", "--config", "/tmp/mysql/opensearch-cli/fb135a4d.yml", "curl", "get", "--path", "/"}}, + "run request with curl": {"curl", "https://localhost:12345/", "--key", "/tmp/keys/example.com/bob-db/db.example.com/mysql.key", "--cert", "/tmp/keys/example.com/bob-db/db.example.com/mysql.crt"}, + "run request with opensearch-cli": {"opensearch-cli", "--profile", "teleport", "--config", "/tmp/mysql/opensearch-cli/7e266ec0.yml", "curl", "get", "--path", "/"}}, wantErr: false, }, diff --git a/lib/client/db/profile.go b/lib/client/db/profile.go index 1be919044975e..3c4090017abf6 100644 --- a/lib/client/db/profile.go +++ b/lib/client/db/profile.go @@ -94,7 +94,7 @@ func New(tc *client.TeleportClient, db tlsca.RouteToDatabase, clientProfile clie Insecure: tc.InsecureSkipVerify, CACertPath: clientProfile.CACertPathForCluster(rootCluster), CertPath: clientProfile.DatabaseCertPathForCluster(tc.SiteName, db.ServiceName), - KeyPath: clientProfile.KeyPath(), + KeyPath: clientProfile.DatabaseKeyPathForCluster(tc.SiteName, db.ServiceName), } } diff --git a/lib/client/db/profile_test.go b/lib/client/db/profile_test.go index d9e44791fce83..73d062ee2a861 100644 --- a/lib/client/db/profile_test.go +++ b/lib/client/db/profile_test.go @@ -117,7 +117,7 @@ func TestAddProfile(t *testing.T) { Port: test.profilePortOut, CACertPath: ps.CACertPathForCluster("root-cluster"), CertPath: ps.DatabaseCertPathForCluster(tc.SiteName, db.ServiceName), - KeyPath: ps.KeyPath(), + KeyPath: ps.DatabaseKeyPathForCluster(tc.SiteName, db.ServiceName), }, actual) }) } diff --git a/lib/client/identityfile/identity.go b/lib/client/identityfile/identity.go index 410c73136f108..39137c872fc7f 100644 --- a/lib/client/identityfile/identity.go +++ b/lib/client/identityfile/identity.go @@ -767,7 +767,11 @@ func KeyRingFromIdentityFile(identityPath, proxyHost, clusterName string) (*clie // If this identity file has any database certs, copy it into the DBTLSCerts map. if parsedIdent.RouteToDatabase.ServiceName != "" { - keyRing.DBTLSCerts[parsedIdent.RouteToDatabase.ServiceName] = ident.Certs.TLS + keyRing.DBTLSCredentials[parsedIdent.RouteToDatabase.ServiceName] = client.TLSCredential{ + // Identity files only have room for one private key, it must match the db cert. + PrivateKey: priv, + Cert: ident.Certs.TLS, + } } // Similarly, if this identity has any app certs, copy them in. diff --git a/lib/client/interfaces.go b/lib/client/interfaces.go index f37fdefba9412..05846ab276b56 100644 --- a/lib/client/interfaces.go +++ b/lib/client/interfaces.go @@ -113,9 +113,9 @@ type KeyRing struct { // KubeTLSCerts are TLS certificates (PEM-encoded) for individual // kubernetes clusters. Map key is a kubernetes cluster name. KubeTLSCerts map[string][]byte `json:"KubeCerts,omitempty"` - // DBTLSCerts are PEM-encoded TLS certificates for database access. + // DBTLSCredentials are TLS credentials for database access. // Map key is the database service name. - DBTLSCerts map[string][]byte `json:"DBCerts,omitempty"` + DBTLSCredentials map[string]TLSCredential // AppTLSCredetials are TLS credentials for application access. // Map key is the application name. AppTLSCredentials map[string]TLSCredential @@ -178,7 +178,7 @@ func NewKeyRing(priv *keys.PrivateKey) *KeyRing { return &KeyRing{ PrivateKey: priv, KubeTLSCerts: make(map[string][]byte), - DBTLSCerts: make(map[string][]byte), + DBTLSCredentials: make(map[string]TLSCredential), AppTLSCredentials: make(map[string]TLSCredential), WindowsDesktopCerts: make(map[string][]byte), } @@ -483,21 +483,17 @@ func (k *KeyRing) KubeTLSCert(kubeClusterName string) (tls.Certificate, error) { // DBTLSCert returns the tls.Certificate for authentication against a named database. func (k *KeyRing) DBTLSCert(dbName string) (tls.Certificate, error) { - certPem, ok := k.DBTLSCerts[dbName] + cred, ok := k.DBTLSCredentials[dbName] if !ok { return tls.Certificate{}, trace.NotFound("TLS certificate for database %q not found", dbName) } - tlsCert, err := k.PrivateKey.TLSCertificate(certPem) - if err != nil { - return tls.Certificate{}, trace.Wrap(err) - } - return tlsCert, nil + return cred.TLSCertificate() } // DBTLSCertificates returns all parsed x509 database access certificates. func (k *KeyRing) DBTLSCertificates() (certs []x509.Certificate, err error) { - for _, bytes := range k.DBTLSCerts { - cert, err := tlsca.ParseCertificatePEM(bytes) + for _, cred := range k.DBTLSCredentials { + cert, err := tlsca.ParseCertificatePEM(cred.Cert) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/client/keyagent.go b/lib/client/keyagent.go index 2df36e92d06b1..b9a8107988550 100644 --- a/lib/client/keyagent.go +++ b/lib/client/keyagent.go @@ -506,8 +506,8 @@ func (a *LocalKeyAgent) AddKeyRing(keyRing *KeyRing) error { // AddDatabaseKeyRing activates a new signed database key by adding it into the keystore. // key must contain at least one db cert. ssh cert is not required. func (a *LocalKeyAgent) AddDatabaseKeyRing(keyRing *KeyRing) error { - if len(keyRing.DBTLSCerts) == 0 { - return trace.BadParameter("key must contains at least one database access certificate") + if len(keyRing.DBTLSCredentials) == 0 { + return trace.BadParameter("key ring must contain at least one Database access certificate") } return a.addKeyRing(keyRing) } @@ -516,7 +516,7 @@ func (a *LocalKeyAgent) AddDatabaseKeyRing(keyRing *KeyRing) error { // key must contain at least one Kubernetes cert. ssh cert is not required. func (a *LocalKeyAgent) AddKubeKeyRing(keyRing *KeyRing) error { if len(keyRing.KubeTLSCerts) == 0 { - return trace.BadParameter("key must contains at least one Kubernetes access certificate") + return trace.BadParameter("key ring must contain at least one Kubernetes access certificate") } return a.addKeyRing(keyRing) } @@ -525,7 +525,7 @@ func (a *LocalKeyAgent) AddKubeKeyRing(keyRing *KeyRing) error { // key must contain at least one app credential. ssh cert is not required. func (a *LocalKeyAgent) AddAppKeyRing(keyRing *KeyRing) error { if len(keyRing.AppTLSCredentials) == 0 { - return trace.BadParameter("key must contains at least one App access certificate") + return trace.BadParameter("key ring must contain at least one App access certificate") } return a.addKeyRing(keyRing) } @@ -533,7 +533,7 @@ func (a *LocalKeyAgent) AddAppKeyRing(keyRing *KeyRing) error { // addKeyRing activates a new signed session key ring by adding it into the keystore. func (a *LocalKeyAgent) addKeyRing(keyRing *KeyRing) error { if keyRing == nil { - return trace.BadParameter("key is nil") + return trace.BadParameter("key ring is nil") } if keyRing.ProxyHost == "" { keyRing.ProxyHost = a.proxyHost diff --git a/lib/client/keyagent_test.go b/lib/client/keyagent_test.go index 59e8b61906193..976d46a0c8c25 100644 --- a/lib/client/keyagent_test.go +++ b/lib/client/keyagent_test.go @@ -706,13 +706,18 @@ func TestLocalKeyAgent_AddDatabaseKey(t *testing.T) { t.Run("success", func(t *testing.T) { // modify key to have db cert addKey := *s.keyRing - addKey.DBTLSCerts = map[string][]byte{"some-db": addKey.TLSCert} + addKey.DBTLSCredentials = map[string]TLSCredential{ + "some-db": TLSCredential{ + PrivateKey: addKey.PrivateKey, + Cert: addKey.TLSCert, + }, + } require.NoError(t, lka.SaveTrustedCerts([]authclient.TrustedCerts{s.tlscaCert})) require.NoError(t, lka.AddDatabaseKeyRing(&addKey)) getKeyRing, err := lka.GetKeyRing(addKey.ClusterName, WithDBCerts{}) require.NoError(t, err) - require.Contains(t, getKeyRing.DBTLSCerts, "some-db") + require.Contains(t, getKeyRing.DBTLSCredentials, "some-db") }) } diff --git a/lib/client/keystore.go b/lib/client/keystore.go index 261ff5b715ddc..bd968fba10735 100644 --- a/lib/client/keystore.go +++ b/lib/client/keystore.go @@ -155,6 +155,11 @@ func (fs *FSKeyStore) databaseCertPath(idx KeyRingIndex, dbname string) string { return keypaths.DatabaseCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, dbname) } +// databaseCertPath returns the private key path for the given KeyRingIndex and database name. +func (fs *FSKeyStore) databaseKeyPath(idx KeyRingIndex, dbname string) string { + return keypaths.DatabaseKeyPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, dbname) +} + // kubeCertPath returns the TLS certificate path for the given KeyRingIndex and kube cluster name. func (fs *FSKeyStore) kubeCertPath(idx KeyRingIndex, kubename string) string { return keypaths.KubeCertPath(fs.KeyDir, idx.ProxyHost, idx.Username, idx.ClusterName, kubename) @@ -214,9 +219,14 @@ func (fs *FSKeyStore) AddKeyRing(keyRing *KeyRing) error { return trace.Wrap(err) } } - for db, cert := range keyRing.DBTLSCerts { - path := fs.databaseCertPath(keyRing.KeyRingIndex, filepath.Clean(db)) - if err := fs.writeBytes(cert, path); err != nil { + for db, cred := range keyRing.DBTLSCredentials { + db = filepath.Clean(db) + certPath := fs.databaseCertPath(keyRing.KeyRingIndex, db) + if err := fs.writeBytes(cred.Cert, certPath); err != nil { + return trace.Wrap(err) + } + keyPath := fs.databaseKeyPath(keyRing.KeyRingIndex, db) + if err := fs.writeBytes(cred.PrivateKey.PrivateKeyPEM(), keyPath); err != nil { return trace.Wrap(err) } } @@ -520,12 +530,12 @@ type WithDBCerts struct { } func (o WithDBCerts) updateKeyRing(keyDir string, idx KeyRingIndex, keyRing *KeyRing) error { - certDir := keypaths.DatabaseCertDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) - certsByName, err := getCertsByName(certDir) + credentialDir := keypaths.DatabaseCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName) + credsByName, err := getCredentialsByName(credentialDir) if err != nil { return trace.Wrap(err) } - keyRing.DBTLSCerts = certsByName + keyRing.DBTLSCredentials = credsByName return nil } @@ -534,13 +544,16 @@ func (o WithDBCerts) pathsToDelete(keyDir string, idx KeyRingIndex) []string { return []string{keypaths.DatabaseDir(keyDir, idx.ProxyHost, idx.Username)} } if o.dbName == "" { - return []string{keypaths.DatabaseCertDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName)} + return []string{keypaths.DatabaseCredentialDir(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName)} + } + return []string{ + keypaths.DatabaseCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName, o.dbName), + keypaths.DatabaseKeyPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName, o.dbName), } - return []string{keypaths.DatabaseCertPath(keyDir, idx.ProxyHost, idx.Username, idx.ClusterName, o.dbName)} } func (o WithDBCerts) deleteFromKeyRing(keyRing *KeyRing) { - keyRing.DBTLSCerts = make(map[string][]byte) + keyRing.DBTLSCredentials = make(map[string]TLSCredential) } // WithAppCerts is a CertOption for handling application access certificates. @@ -650,7 +663,7 @@ func (ms *MemKeyStore) GetKeyRing(idx KeyRingIndex, opts ...CertOption) (*KeyRin case WithKubeCerts: retKeyRing.KubeTLSCerts = keyRing.KubeTLSCerts case WithDBCerts: - retKeyRing.DBTLSCerts = keyRing.DBTLSCerts + retKeyRing.DBTLSCredentials = keyRing.DBTLSCredentials case WithAppCerts: retKeyRing.AppTLSCredentials = keyRing.AppTLSCredentials } diff --git a/lib/client/keystore_test.go b/lib/client/keystore_test.go index 77547eca5863a..092d9ff7afa7b 100644 --- a/lib/client/keystore_test.go +++ b/lib/client/keystore_test.go @@ -69,13 +69,13 @@ func TestKeyStore(t *testing.T) { keyRing.TrustedCerts = nil require.Equal(t, keyRing, retrievedKeyRing) - // Delete just the db cert, reload & verify it's gone + // Delete just the db cred, reload & verify it's gone err = keyStore.DeleteUserCerts(idx, WithDBCerts{}) require.NoError(t, err) retrievedKeyRing, err = keyStore.GetKeyRing(idx, WithSSHCerts{}, WithDBCerts{}) require.NoError(t, err) expectKeyRing := keyRing.Copy() - expectKeyRing.DBTLSCerts = make(map[string][]byte) + expectKeyRing.DBTLSCredentials = make(map[string]TLSCredential) require.Equal(t, expectKeyRing, retrievedKeyRing) // check for the key, now without cluster name @@ -270,10 +270,10 @@ func TestAddKey_withoutSSHCert(t *testing.T) { _, err := os.Stat(sshCertPath) require.ErrorIs(t, err, os.ErrNotExist) - // check db certs + // check db creds keyCopy, err := keyStore.GetKeyRing(idx, WithDBCerts{}) require.NoError(t, err) - require.Len(t, keyCopy.DBTLSCerts, 1) + require.Len(t, keyCopy.DBTLSCredentials, 1) } func TestConfigDirNotDeleted(t *testing.T) { diff --git a/lib/client/profile.go b/lib/client/profile.go index d863d6281e2b7..48f526bd5cfc3 100644 --- a/lib/client/profile.go +++ b/lib/client/profile.go @@ -444,7 +444,7 @@ func (p *ProfileStatus) KeyPath() string { // DatabaseCertPathForCluster returns path to the specified database access // certificate for this profile, for the specified cluster. // -// It's kept in /keys//-db//-x509.pem +// It's kept in /keys//-db//.crt // // If the input cluster name is an empty string, the selected cluster in the // profile will be used. @@ -453,13 +453,32 @@ func (p *ProfileStatus) DatabaseCertPathForCluster(clusterName string, databaseN clusterName = p.Cluster } - if path, ok := p.virtualPathFromEnv(VirtualPathDatabase, VirtualPathDatabaseParams(databaseName)); ok { + if path, ok := p.virtualPathFromEnv(VirtualPathDatabase, VirtualPathDatabaseCertParams(databaseName)); ok { return path } return keypaths.DatabaseCertPath(p.Dir, p.Name, p.Username, clusterName, databaseName) } +// DatabaseKeyPathForCluster returns path to the specified database access +// private key for this profile, for the specified cluster. +// +// It's kept in /keys//-db//.key +// +// If the input cluster name is an empty string, the selected cluster in the +// profile will be used. +func (p *ProfileStatus) DatabaseKeyPathForCluster(clusterName string, databaseName string) string { + if clusterName == "" { + clusterName = p.Cluster + } + + if path, ok := p.virtualPathFromEnv(VirtualPathKey, VirtualPathDatabaseKeyParams(databaseName)); ok { + return path + } + + return keypaths.DatabaseKeyPath(p.Dir, p.Name, p.Username, clusterName, databaseName) +} + // OracleWalletDir returns path to the specified database access // certificate for this profile, for the specified cluster. // @@ -472,7 +491,7 @@ func (p *ProfileStatus) OracleWalletDir(clusterName string, databaseName string) clusterName = p.Cluster } - if path, ok := p.virtualPathFromEnv(VirtualPathDatabase, VirtualPathDatabaseParams(databaseName)); ok { + if path, ok := p.virtualPathFromEnv(VirtualPathDatabase, VirtualPathDatabaseCertParams(databaseName)); ok { return path } diff --git a/lib/cryptosuites/suites.go b/lib/cryptosuites/suites.go index 5631cbdcc11bb..82a3d5c584e9c 100644 --- a/lib/cryptosuites/suites.go +++ b/lib/cryptosuites/suites.go @@ -83,6 +83,8 @@ const ( UserSSH // UserTLS represents a user TLS key. UserTLS + // UserDatabase represents a user Database key. + UserDatabase // TODO(nklaassen): define remaining key purposes. @@ -143,6 +145,7 @@ var ( SPIFFECAJWT: RSA2048, UserSSH: RSA2048, UserTLS: RSA2048, + UserDatabase: RSA2048, // We could consider updating this algorithm even in the legacy suite, only database agents need to // accept these connections and they have never restricted algorithm support. ProxyToDatabaseAgent: RSA2048, @@ -167,6 +170,7 @@ var ( SPIFFECAJWT: ECDSAP256, UserSSH: Ed25519, UserTLS: ECDSAP256, + UserDatabase: RSA2048, ProxyToDatabaseAgent: ECDSAP256, // TODO(nklaassen): define remaining key purposes. } @@ -189,6 +193,7 @@ var ( SPIFFECAJWT: ECDSAP256, UserSSH: ECDSAP256, UserTLS: ECDSAP256, + UserDatabase: RSA2048, ProxyToDatabaseAgent: ECDSAP256, // TODO(nklaassen): define remaining key purposes. } @@ -213,6 +218,7 @@ var ( SPIFFECAJWT: ECDSAP256, UserSSH: Ed25519, UserTLS: ECDSAP256, + UserDatabase: RSA2048, ProxyToDatabaseAgent: ECDSAP256, // TODO(nklaassen): define remaining key purposes. } diff --git a/lib/tbot/output_utils.go b/lib/tbot/output_utils.go index 80fdad50352bc..351decdb0b0e6 100644 --- a/lib/tbot/output_utils.go +++ b/lib/tbot/output_utils.go @@ -122,8 +122,8 @@ func NewClientKeyRing(ident *identity.Identity, hostCAs []types.CertAuthority) ( // Note: these fields are never used or persisted with identity files, // so we won't bother to set them. (They may need to be reconstituted // on tsh's end based on cert fields, though.) - KubeTLSCerts: make(map[string][]byte), - DBTLSCerts: make(map[string][]byte), + KubeTLSCerts: make(map[string][]byte), + DBTLSCredentials: make(map[string]client.TLSCredential), }, nil } diff --git a/tool/tsh/common/db.go b/tool/tsh/common/db.go index f49b772cd5339..dd204485c43fa 100644 --- a/tool/tsh/common/db.go +++ b/tool/tsh/common/db.go @@ -550,7 +550,7 @@ func onDatabaseConfig(cf *CLIConf) error { database.ServiceName, host, port, database.Username, database.Database, profile.CACertPathForCluster(rootCluster), profile.DatabaseCertPathForCluster(tc.SiteName, database.ServiceName), - profile.KeyPath(), + profile.DatabaseKeyPathForCluster(tc.SiteName, database.ServiceName), } out, err := serializeDatabaseConfig(configInfo, format) if err != nil { @@ -569,7 +569,9 @@ Key: %v `, database.ServiceName, host, port, database.Username, database.Database, profile.CACertPathForCluster(rootCluster), - profile.DatabaseCertPathForCluster(tc.SiteName, database.ServiceName), profile.KeyPath()) + profile.DatabaseCertPathForCluster(tc.SiteName, database.ServiceName), + profile.DatabaseKeyPathForCluster(tc.SiteName, database.ServiceName), + ) } return nil } diff --git a/tool/tsh/common/db_test.go b/tool/tsh/common/db_test.go index ccb239057cf3d..7ae5ba046f8b8 100644 --- a/tool/tsh/common/db_test.go +++ b/tool/tsh/common/db_test.go @@ -441,7 +441,7 @@ func testDatabaseLogin(t *testing.T) { } args := append([]string{ // default --db-user and --db-name are selected from roles. - "db", "login", + "db", "login", "--insecure", }, selectors...) args = append(args, test.extraLoginOptions...) diff --git a/tool/tsh/common/proxy.go b/tool/tsh/common/proxy.go index 0370d621ce0c3..8a9bdf568c60d 100644 --- a/tool/tsh/common/proxy.go +++ b/tool/tsh/common/proxy.go @@ -268,7 +268,7 @@ func onProxyCommandDB(cf *CLIConf) error { "address": listener.Addr().String(), "ca": profile.CACertPathForCluster(rootCluster), "cert": profile.DatabaseCertPathForCluster(cf.SiteName, dbInfo.ServiceName), - "key": profile.KeyPath(), + "key": profile.DatabaseKeyPathForCluster(cf.SiteName, dbInfo.ServiceName), "randomPort": randomPort, "databaseUser": dbInfo.Username, "databaseName": dbInfo.Database,