Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[v8] backport #9220 #9518

Merged
merged 3 commits into from
Dec 22, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 58 additions & 20 deletions lib/client/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -428,12 +428,18 @@ func (p *ProfileStatus) KeyPath() string {
return keypaths.UserKeyPath(p.Dir, p.Name, p.Username)
}

// DatabaseCertPath returns path to the specified database access certificate
// for this profile.
// DatabaseCertPathForCluster returns path to the specified database access
// certificate for this profile, for the specified cluster.
//
// It's kept in <profile-dir>/keys/<proxy>/<user>-db/<cluster>/<name>-x509.pem
func (p *ProfileStatus) DatabaseCertPath(name string) string {
return keypaths.DatabaseCertPath(p.Dir, p.Name, p.Username, p.Cluster, name)
//
// If the input cluster name is an empty string, the selected cluster in the
// profile will be used.
func (p *ProfileStatus) DatabaseCertPathForCluster(clusterName string, databaseName string) string {
if clusterName == "" {
clusterName = p.Cluster
}
return keypaths.DatabaseCertPath(p.Dir, p.Name, p.Username, clusterName, databaseName)
}

// AppCertPath returns path to the specified app access certificate
Expand All @@ -460,6 +466,30 @@ func (p *ProfileStatus) DatabaseServices() (result []string) {
return result
}

// DatabasesForCluster returns a list of databases for this profile, for the
// specified cluster name.
func (p *ProfileStatus) DatabasesForCluster(clusterName string) ([]tlsca.RouteToDatabase, error) {
if clusterName == "" || clusterName == p.Cluster {
return p.Databases, nil
}

idx := KeyIndex{
ProxyHost: p.Name,
Username: p.Username,
ClusterName: clusterName,
}

store, err := NewFSLocalKeyStore(p.Dir)
if err != nil {
return nil, trace.Wrap(err)
}
key, err := store.GetKey(idx, WithDBCerts{})
if err != nil {
return nil, trace.Wrap(err)
}
return findActiveDatabases(key)
}

// AppNames returns a list of app names this profile is logged into.
func (p *ProfileStatus) AppNames() (result []string) {
for _, app := range p.Apps {
Expand Down Expand Up @@ -596,25 +626,10 @@ func readProfile(profileDir string, profileName string) (*ProfileStatus, error)
return nil, trace.Wrap(err)
}

dbCerts, err := key.DBTLSCertificates()
databases, err := findActiveDatabases(key)
if err != nil {
return nil, trace.Wrap(err)
}
var databases []tlsca.RouteToDatabase
for _, cert := range dbCerts {
tlsID, err := tlsca.FromSubject(cert.Subject, time.Time{})
if err != nil {
return nil, trace.Wrap(err)
}
// If the cert expiration time is less than 5s consider cert as expired and don't add
// it to the user profile as an active database.
if time.Until(cert.NotAfter) < 5*time.Second {
continue
}
if tlsID.RouteToDatabase.ServiceName != "" {
databases = append(databases, tlsID.RouteToDatabase)
}
}

appCerts, err := key.AppTLSCertificates()
if err != nil {
Expand Down Expand Up @@ -3323,3 +3338,26 @@ func playSession(sessionEvents []events.EventFields, stream []byte) error {
return trace.Wrap(err)
}
}

func findActiveDatabases(key *Key) ([]tlsca.RouteToDatabase, error) {
dbCerts, err := key.DBTLSCertificates()
if err != nil {
return nil, trace.Wrap(err)
}
var databases []tlsca.RouteToDatabase
for _, cert := range dbCerts {
tlsID, err := tlsca.FromSubject(cert.Subject, time.Time{})
if err != nil {
return nil, trace.Wrap(err)
}
// If the cert expiration time is less than 5s consider cert as expired and don't add
// it to the user profile as an active database.
if time.Until(cert.NotAfter) < 5*time.Second {
continue
}
if tlsID.RouteToDatabase.ServiceName != "" {
databases = append(databases, tlsID.RouteToDatabase)
}
}
return databases, nil
}
2 changes: 1 addition & 1 deletion lib/client/db/profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ func New(tc *client.TeleportClient, db tlsca.RouteToDatabase, clientProfile clie
Database: db.Database,
Insecure: tc.InsecureSkipVerify,
CACertPath: clientProfile.CACertPath(),
CertPath: clientProfile.DatabaseCertPath(db.ServiceName),
CertPath: clientProfile.DatabaseCertPathForCluster(tc.SiteName, db.ServiceName),
KeyPath: clientProfile.KeyPath(),
}
}
Expand Down
2 changes: 1 addition & 1 deletion lib/client/db/profile_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func TestAddProfile(t *testing.T) {
Host: test.profileHostOut,
Port: test.profilePortOut,
CACertPath: ps.CACertPath(),
CertPath: ps.DatabaseCertPath(db.ServiceName),
CertPath: ps.DatabaseCertPathForCluster(tc.SiteName, db.ServiceName),
KeyPath: ps.KeyPath(),
}, actual)
})
Expand Down
30 changes: 24 additions & 6 deletions lib/client/keyagent.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,8 +437,27 @@ func (a *LocalKeyAgent) defaultHostPromptFunc(host string, key ssh.PublicKey, wr
// AddKey activates a new signed session key by adding it into the keystore and also
// by loading it into the SSH agent.
func (a *LocalKeyAgent) AddKey(key *Key) (*agent.AddedKey, error) {
if err := a.addKey(key); err != nil {
return nil, trace.Wrap(err)
}

// Load key into the teleport agent and system agent.
return a.LoadKey(*key)
}

// AddDatabaseKey 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) AddDatabaseKey(key *Key) error {
if len(key.DBTLSCerts) == 0 {
return trace.BadParameter("key must contains at least one database access certificate")
}
return a.addKey(key)
}

// addKey activates a new signed session key by adding it into the keystore.
func (a *LocalKeyAgent) addKey(key *Key) error {
if key == nil {
return nil, trace.BadParameter("key is nil")
return trace.BadParameter("key is nil")
}
if key.ProxyHost == "" {
key.ProxyHost = a.proxyHost
Expand All @@ -453,23 +472,22 @@ func (a *LocalKeyAgent) AddKey(key *Key) (*agent.AddedKey, error) {
storedKey, err := a.keyStore.GetKey(key.KeyIndex)
if err != nil {
if !trace.IsNotFound(err) {
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
} else {
if subtle.ConstantTimeCompare(storedKey.Priv, key.Priv) == 0 {
a.log.Debugf("Deleting obsolete stored key with index %+v.", storedKey.KeyIndex)
if err := a.keyStore.DeleteKey(storedKey.KeyIndex); err != nil {
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
}
}

// Save the new key to the keystore (usually into ~/.tsh).
if err := a.keyStore.AddKey(key); err != nil {
return nil, trace.Wrap(err)
return trace.Wrap(err)
}
// Load key into the teleport agent and system agent.
return a.LoadKey(*key)
return nil
}

// DeleteKey removes the key with all its certs from the key store
Expand Down
36 changes: 35 additions & 1 deletion lib/client/keyagent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/utils/keypaths"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/fixtures"
Expand All @@ -54,6 +55,7 @@ type KeyAgentTestSuite struct {
hostname string
clusterName string
tlsca *tlsca.CertAuthority
tlscaCert auth.TrustedCerts
}

func makeSuite(t *testing.T) *KeyAgentTestSuite {
Expand All @@ -72,7 +74,7 @@ func makeSuite(t *testing.T) *KeyAgentTestSuite {
pemBytes, ok := fixtures.PEMBytes["rsa"]
require.True(t, ok)

s.tlsca, _, err = newSelfSignedCA(pemBytes)
s.tlsca, s.tlscaCert, err = newSelfSignedCA(pemBytes)
require.NoError(t, err)

s.key, err = s.makeKey(s.username, []string{s.username}, 1*time.Minute)
Expand Down Expand Up @@ -431,6 +433,38 @@ func TestDefaultHostPromptFunc(t *testing.T) {
}
}

func TestLocalKeyAgent_AddDatabaseKey(t *testing.T) {
s := makeSuite(t)

// make a new local agent
keystore, err := NewFSLocalKeyStore(s.keyDir)
require.NoError(t, err)
lka, err := NewLocalAgent(
LocalAgentConfig{
Keystore: keystore,
ProxyHost: s.hostname,
Username: s.username,
KeysOption: AddKeysToAgentAuto,
})
require.NoError(t, err)

t.Run("no database cert", func(t *testing.T) {
require.Error(t, lka.AddDatabaseKey(s.key))
})

t.Run("success", func(t *testing.T) {
// modify key to have db cert
addKey := *s.key
addKey.DBTLSCerts = map[string][]byte{"some-db": addKey.TLSCert}
require.NoError(t, lka.SaveTrustedCerts([]auth.TrustedCerts{s.tlscaCert}))
require.NoError(t, lka.AddDatabaseKey(&addKey))

getKey, err := lka.GetKey(addKey.ClusterName, WithDBCerts{})
require.NoError(t, err)
require.Contains(t, getKey.DBTLSCerts, "some-db")
})
}

func (s *KeyAgentTestSuite) makeKey(username string, allowedLogins []string, ttl time.Duration) (*Key, error) {
keygen := testauthority.New()

Expand Down
7 changes: 5 additions & 2 deletions lib/client/keystore.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,9 +138,12 @@ func (fs *FSLocalKeyStore) AddKey(key *Key) error {
}

// Store per-cluster key data.
if err := fs.writeBytes(key.Cert, fs.sshCertPath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
if len(key.Cert) > 0 {
if err := fs.writeBytes(key.Cert, fs.sshCertPath(key.KeyIndex)); err != nil {
return trace.Wrap(err)
}
}

// TODO(awly): unit test this.
for kubeCluster, cert := range key.KubeTLSCerts {
// Prevent directory traversal via a crafted kubernetes cluster name.
Expand Down
21 changes: 21 additions & 0 deletions lib/client/keystore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,27 @@ hRdXE63PXwAfzj0P/H4qWsFfwdeCo/fuIQIDAQAB
require.NoError(t, err)
}

func TestAddKey_withoutSSHCert(t *testing.T) {
s, cleanup := newTest(t)
defer cleanup()

// without ssh cert, db certs only
idx := KeyIndex{"host.a", "bob", "root"}
key := s.makeSignedKey(t, idx, false)
key.Cert = nil
require.NoError(t, s.addKey(key))

// ssh cert path should NOT exist
sshCertPath := s.store.sshCertPath(key.KeyIndex)
_, err := os.Stat(sshCertPath)
require.ErrorIs(t, err, os.ErrNotExist)

// check db certs
keyCopy, err := s.store.GetKey(idx, WithDBCerts{})
require.NoError(t, err)
require.Len(t, keyCopy.DBTLSCerts, 1)
}

type keyStoreTest struct {
storeDir string
store *FSLocalKeyStore
Expand Down
2 changes: 2 additions & 0 deletions tool/teleport/common/teleport.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
appStartCmd.Flag("name", "Name of the application to start.").StringVar(&ccf.AppName)
appStartCmd.Flag("uri", "Internal address of the application to proxy.").StringVar(&ccf.AppURI)
appStartCmd.Flag("public-addr", "Public address of the application to proxy.").StringVar(&ccf.AppPublicAddr)
appStartCmd.Flag("diag-addr", "Start diagnostic prometheus and healthz endpoint.").Hidden().StringVar(&ccf.DiagnosticAddr)
appStartCmd.Alias(appUsageExamples) // We're using "alias" section to display usage examples.

// "teleport db" command and its subcommands
Expand All @@ -200,6 +201,7 @@ func Run(options Options) (app *kingpin.Application, executedCommand string, con
dbStartCmd.Flag("aws-rds-cluster-id", "(Only for Aurora) Aurora cluster identifier.").StringVar(&ccf.DatabaseAWSRDSClusterID)
dbStartCmd.Flag("gcp-project-id", "(Only for Cloud SQL) GCP Cloud SQL project identifier.").StringVar(&ccf.DatabaseGCPProjectID)
dbStartCmd.Flag("gcp-instance-id", "(Only for Cloud SQL) GCP Cloud SQL instance identifier.").StringVar(&ccf.DatabaseGCPInstanceID)
dbStartCmd.Flag("diag-addr", "Start diagnostic prometheus and healthz endpoint.").Hidden().StringVar(&ccf.DiagnosticAddr)
dbStartCmd.Alias(dbUsageExamples) // We're using "alias" section to display usage examples.

// define a hidden 'scp' command (it implements server-side implementation of handling
Expand Down
Loading