Skip to content
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
8 changes: 7 additions & 1 deletion lib/srv/alpnproxy/local_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,12 @@ func (l *LocalProxy) CheckDBCerts(dbRoute tlsca.RouteToDatabase) error {
return trace.Wrap(err)
}

// Check the subject matches.
return trace.Wrap(CheckCertSubject(cert, dbRoute))
}

// CheckCertSubject checks if the route to the database from the cert matches the provided route in
// terms of username and database (if present).
func CheckCertSubject(cert *x509.Certificate, dbRoute tlsca.RouteToDatabase) error {
identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
if err != nil {
return trace.Wrap(err)
Expand All @@ -305,6 +310,7 @@ func (l *LocalProxy) CheckDBCerts(dbRoute tlsca.RouteToDatabase) error {
return trace.Errorf("certificate subject is for database name %s, but need %s",
identity.RouteToDatabase.Database, dbRoute.Database)
}

return nil
}

Expand Down
17 changes: 17 additions & 0 deletions lib/teleterm/api/uri/uri.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import (

var pathClusters = urlpath.New("/clusters/:cluster/*")
var pathLeafClusters = urlpath.New("/clusters/:cluster/leaves/:leaf/*")
var pathDbs = urlpath.New("/clusters/:cluster/dbs/:dbName")
var pathLeafDbs = urlpath.New("/clusters/:cluster/leaves/:leaf/dbs/:dbName")

// New creates an instance of ResourceURI
func New(path string) ResourceURI {
Expand Down Expand Up @@ -92,6 +94,21 @@ func (r ResourceURI) GetLeafClusterName() string {
return result.Params["leaf"]
}

// GetDbName extracts the database name from r. Returns an empty string if path is not a database URI.
func (r ResourceURI) GetDbName() string {
result, ok := pathDbs.Match(r.path)
if ok {
return result.Params["dbName"]
}

result, ok = pathLeafDbs.Match(r.path)
if ok {
return result.Params["dbName"]
}

return ""
}

// AppendServer appends server segment to the URI
func (r ResourceURI) AppendServer(id string) ResourceURI {
r.path = fmt.Sprintf("%v/servers/%v", r.path, id)
Expand Down
54 changes: 46 additions & 8 deletions lib/teleterm/api/uri/uri_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
)

func TestString(t *testing.T) {
t.Parallel()
tests := []struct {
in uri.ResourceURI
out string
Expand All @@ -46,18 +45,14 @@ func TestString(t *testing.T) {
}

for _, tt := range tests {
tt := tt
t.Run(fmt.Sprintf("%v", tt.in), func(t *testing.T) {
t.Parallel()

out := tt.in.String()
require.Equal(t, tt.out, out)
})
}
}

func TestParseClusterURI(t *testing.T) {
t.Parallel()
tests := []struct {
in string
out uri.ResourceURI
Expand All @@ -81,13 +76,56 @@ func TestParseClusterURI(t *testing.T) {
}

for _, tt := range tests {
tt := tt
t.Run(tt.in, func(t *testing.T) {
t.Parallel()

out, err := uri.ParseClusterURI(tt.in)
require.NoError(t, err)
require.Equal(t, tt.out, out)
})
}
}

func TestGetDbName(t *testing.T) {
tests := []struct {
name string
in uri.ResourceURI
out string
}{
{
name: "returns root cluster db name",
in: uri.NewClusterURI("foo").AppendDB("postgres"),
out: "postgres",
},
{
name: "returns leaf cluster db name",
in: uri.NewClusterURI("foo").AppendLeafCluster("bar").AppendDB("postgres"),
out: "postgres",
},
{
name: "returns empty string when given root cluster URI",
in: uri.NewClusterURI("foo"),
out: "",
},
{
name: "returns empty string when given leaf cluster URI",
in: uri.NewClusterURI("foo").AppendLeafCluster("bar"),
out: "",
},
{
name: "returns empty string when given root cluster non-db resource URI",
in: uri.NewClusterURI("foo").AppendKube("k8s"),
out: "",
},
{
name: "returns empty string when given leaf cluster non-db resource URI",
in: uri.NewClusterURI("foo").AppendLeafCluster("bar").AppendKube("k8s"),
out: "",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
out := tt.in.GetDbName()
require.Equal(t, tt.out, out)
})
}
}
2 changes: 1 addition & 1 deletion lib/teleterm/clusters/cluster_databases.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ func (c *Cluster) GetDatabases(ctx context.Context, r *api.GetDatabasesRequest)
return response, nil
}

// ReissueDBCerts issues new certificates for specific DB access
// ReissueDBCerts issues new certificates for specific DB access and saves them to disk.
func (c *Cluster) ReissueDBCerts(ctx context.Context, routeToDatabase tlsca.RouteToDatabase) error {
// When generating certificate for MongoDB access, database username must
// be encoded into it. This is required to be able to tell which database
Expand Down
33 changes: 29 additions & 4 deletions lib/teleterm/clusters/dbcmd_cli_command_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
"github.com/gravitational/teleport/lib/teleterm/gateway"
"github.com/gravitational/teleport/lib/teleterm/gatewaytest"
"github.com/gravitational/teleport/lib/tlsca"
)

type fakeExec struct{}
Expand Down Expand Up @@ -89,17 +91,29 @@ func TestDbcmdCLICommandProviderGetCommand(t *testing.T) {
clusters: []*Cluster{&cluster},
}
dbcmdCLICommandProvider := NewDbcmdCLICommandProvider(fakeStorage, fakeExec{})

keyPairPaths := gatewaytest.MustGenAndSaveCert(t, tlsca.Identity{
Username: "alice",
Groups: []string{"test-group"},
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: "foo",
Protocol: defaults.ProtocolPostgres,
Username: "alice",
},
})

gateway, err := gateway.New(
gateway.Config{
TargetURI: cluster.URI.AppendDB("foo").String(),
TargetName: "foo",
TargetUser: "alice",
TargetSubresourceName: tc.targetSubresourceName,
Protocol: defaults.ProtocolPostgres,
LocalAddress: "localhost",
WebProxyAddr: "localhost:1337",
Insecure: true,
CertPath: "../../../fixtures/certs/proxy1.pem",
KeyPath: "../../../fixtures/certs/proxy1-key.pem",
CertPath: keyPairPaths.CertPath,
KeyPath: keyPairPaths.KeyPath,
CLICommandProvider: dbcmdCLICommandProvider,
TCPPortAllocator: gateway.NetTCPPortAllocator{},
},
Expand All @@ -122,6 +136,17 @@ func TestDbcmdCLICommandProviderGetCommand_ReturnsErrorIfClusterIsNotFound(t *te
clusters: []*Cluster{},
}
dbcmdCLICommandProvider := NewDbcmdCLICommandProvider(fakeStorage, fakeExec{})

keyPairPaths := gatewaytest.MustGenAndSaveCert(t, tlsca.Identity{
Username: "alice",
Groups: []string{"test-group"},
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: "foo",
Protocol: defaults.ProtocolPostgres,
Username: "alice",
},
})

gateway, err := gateway.New(
gateway.Config{
TargetURI: uri.NewClusterURI("quux").AppendDB("foo").String(),
Expand All @@ -132,8 +157,8 @@ func TestDbcmdCLICommandProviderGetCommand_ReturnsErrorIfClusterIsNotFound(t *te
LocalAddress: "localhost",
WebProxyAddr: "localhost:1337",
Insecure: true,
CertPath: "../../../fixtures/certs/proxy1.pem",
KeyPath: "../../../fixtures/certs/proxy1-key.pem",
CertPath: keyPairPaths.CertPath,
KeyPath: keyPairPaths.KeyPath,
CLICommandProvider: dbcmdCLICommandProvider,
TCPPortAllocator: gateway.NetTCPPortAllocator{},
},
Expand Down
17 changes: 15 additions & 2 deletions lib/teleterm/daemon/daemon_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/gravitational/teleport/lib/teleterm/clusters"
"github.com/gravitational/teleport/lib/teleterm/gateway"
"github.com/gravitational/teleport/lib/teleterm/gatewaytest"
"github.com/gravitational/teleport/lib/tlsca"
)

type mockGatewayCreator struct {
Expand All @@ -46,15 +47,27 @@ func (m *mockGatewayCreator) CreateGateway(ctx context.Context, params clusters.
hs.Close()
})

resourceURI := uri.New(params.TargetURI)

keyPairPaths := gatewaytest.MustGenAndSaveCert(m.t, tlsca.Identity{
Username: params.TargetUser,
Groups: []string{"test-group"},
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: resourceURI.GetDbName(),
Protocol: defaults.ProtocolPostgres,
Username: params.TargetUser,
},
})

gateway, err := gateway.New(gateway.Config{
LocalPort: params.LocalPort,
TargetURI: params.TargetURI,
TargetUser: params.TargetUser,
TargetName: params.TargetURI,
TargetSubresourceName: params.TargetSubresourceName,
Protocol: defaults.ProtocolPostgres,
CertPath: "../../../fixtures/certs/proxy1.pem",
KeyPath: "../../../fixtures/certs/proxy1-key.pem",
CertPath: keyPairPaths.CertPath,
KeyPath: keyPairPaths.KeyPath,
Insecure: true,
WebProxyAddr: hs.Listener.Addr().String(),
CLICommandProvider: params.CLICommandProvider,
Expand Down
35 changes: 35 additions & 0 deletions lib/teleterm/gateway/gateway.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
alpn "github.com/gravitational/teleport/lib/srv/alpnproxy"
alpncommon "github.com/gravitational/teleport/lib/srv/alpnproxy/common"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
)

Expand Down Expand Up @@ -82,6 +83,11 @@ func New(cfg Config) (*Gateway, error) {
return nil, trace.Wrap(err)
}

if err := checkCertSubject(tlsCert, cfg.RouteToDatabase()); err != nil {
return nil, trace.Wrap(err,
"database certificate check failed, try restarting the database connection")
}

localProxyConfig := alpn.LocalProxyConfig{
InsecureSkipVerify: cfg.Insecure,
RemoteProxyAddr: cfg.WebProxyAddr,
Expand Down Expand Up @@ -226,6 +232,14 @@ func (g *Gateway) CLICommand() (string, error) {
return cliCommand, nil
}

// RouteToDatabase returns tlsca.RouteToDatabase based on the config of the gateway.
//
// The tlsca.RouteToDatabase.Database field is skipped, as it's an optional field and gateways can
// change their Config.TargetSubresourceName at any moment.
func (g *Gateway) RouteToDatabase() tlsca.RouteToDatabase {
return g.cfg.RouteToDatabase()
}

// ReloadCert loads the key pair from cfg.CertPath & cfg.KeyPath and updates the cert of the running
// local proxy. This is typically done after the cert is reissued and saved to disk.
//
Expand All @@ -239,11 +253,32 @@ func (g *Gateway) ReloadCert() error {
return trace.Wrap(err)
}

if err := checkCertSubject(tlsCert, g.RouteToDatabase()); err != nil {
return trace.Wrap(err,
"database certificate check failed, try restarting the database connection")
}

g.localProxy.SetCerts([]tls.Certificate{tlsCert})

return nil
}

// checkCertSubject checks if the cert subject matches the expected db route.
//
// Database certs are scoped per database server but not per database user or database name.
// It might happen that after we save the cert but before we load it, another process obtains a
// cert for another db user.
//
// Before using the cert for the proxy, we have to perform this check.
func checkCertSubject(tlsCert tls.Certificate, dbRoute tlsca.RouteToDatabase) error {
cert, err := utils.TLSCertToX509(tlsCert)
if err != nil {
return trace.Wrap(err)
}

return trace.Wrap(alpn.CheckCertSubject(cert, dbRoute))
}

// Gateway describes local proxy that creates a gateway to the remote Teleport resource.
//
// Gateway is not safe for concurrent use in itself. However, all access to gateways is gated by
Expand Down
29 changes: 25 additions & 4 deletions lib/teleterm/gateway/gateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/teleterm/api/uri"
"github.com/gravitational/teleport/lib/teleterm/gatewaytest"
"github.com/gravitational/teleport/lib/tlsca"
)

func TestCLICommandUsesCLICommandProvider(t *testing.T) {
Expand All @@ -52,14 +53,24 @@ func TestGatewayStart(t *testing.T) {
hs.Close()
})

keyPairPaths := gatewaytest.MustGenAndSaveCert(t, tlsca.Identity{
Username: "alice",
Groups: []string{"test-group"},
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: "foo",
Protocol: defaults.ProtocolPostgres,
Username: "alice",
},
})

gateway, err := New(
Config{
TargetName: "foo",
TargetURI: uri.NewClusterURI("bar").AppendDB("foo").String(),
TargetUser: "alice",
Protocol: defaults.ProtocolPostgres,
CertPath: "../../../fixtures/certs/proxy1.pem",
KeyPath: "../../../fixtures/certs/proxy1-key.pem",
CertPath: keyPairPaths.CertPath,
KeyPath: keyPairPaths.KeyPath,
Insecure: true,
WebProxyAddr: hs.Listener.Addr().String(),
CLICommandProvider: mockCLICommandProvider{},
Expand Down Expand Up @@ -148,14 +159,24 @@ func createGateway(t *testing.T, tcpPortAllocator TCPPortAllocator) *Gateway {
hs.Close()
})

keyPairPaths := gatewaytest.MustGenAndSaveCert(t, tlsca.Identity{
Username: "alice",
Groups: []string{"test-group"},
RouteToDatabase: tlsca.RouteToDatabase{
ServiceName: "foo",
Protocol: defaults.ProtocolPostgres,
Username: "alice",
},
})

gateway, err := New(
Config{
TargetName: "foo",
TargetURI: uri.NewClusterURI("bar").AppendDB("foo").String(),
TargetUser: "alice",
Protocol: defaults.ProtocolPostgres,
CertPath: "../../../fixtures/certs/proxy1.pem",
KeyPath: "../../../fixtures/certs/proxy1-key.pem",
CertPath: keyPairPaths.CertPath,
KeyPath: keyPairPaths.KeyPath,
Insecure: true,
WebProxyAddr: hs.Listener.Addr().String(),
CLICommandProvider: mockCLICommandProvider{},
Expand Down
Loading