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

(v9) Add auth'd tunnel mode to tsh proxy db command #11808

Merged
merged 2 commits into from
Apr 7, 2022
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
5 changes: 4 additions & 1 deletion lib/client/db/postgres/connstring.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (
)

// GetConnString returns formatted Postgres connection string for the profile.
func GetConnString(c *profile.ConnectProfile) string {
func GetConnString(c *profile.ConnectProfile, noTLS bool) string {
connStr := "postgres://"
if c.User != "" {
// Username may contain special characters in which case it should
Expand All @@ -39,6 +39,9 @@ func GetConnString(c *profile.ConnectProfile) string {
if c.Database != "" {
connStr += "/" + c.Database
}
if noTLS {
return connStr
}
params := []string{
fmt.Sprintf("sslrootcert=%v", c.CACertPath),
fmt.Sprintf("sslcert=%v", c.CertPath),
Expand Down
2 changes: 1 addition & 1 deletion lib/client/db/postgres/connstring_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ func TestConnString(t *testing.T) {
CACertPath: caPath,
CertPath: certPath,
KeyPath: keyPath,
}))
}, false))
})
}
}
113 changes: 113 additions & 0 deletions lib/srv/db/access_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1355,6 +1355,30 @@ func (c *testContext) postgresClientWithAddr(ctx context.Context, address, telep
})
}

// postgresClientLocalProxy connects to test Postgres through local ALPN proxy.
func (c *testContext) postgresClientLocalProxy(ctx context.Context, teleportUser, dbService, dbUser, dbName string) (*pgconn.PgConn, *alpnproxy.LocalProxy, error) {
route := tlsca.RouteToDatabase{
ServiceName: dbService,
Protocol: defaults.ProtocolPostgres,
Username: dbUser,
Database: dbName,
}

// Start local proxy which client will connect to.
proxy, err := c.startLocalALPNProxy(ctx, c.webListener.Addr().String(), teleportUser, route)
if err != nil {
return nil, nil, trace.Wrap(err)
}

// Client connects to the local proxy without TLS.
conn, err := pgconn.Connect(ctx, fmt.Sprintf("postgres://%v@%v/%v", dbUser, proxy.GetAddr(), dbName))
if err != nil {
return nil, nil, trace.Wrap(err)
}

return conn, proxy, nil
}

// mysqlClient connects to test MySQL through database access as a specified
// Teleport user and database account.
func (c *testContext) mysqlClient(teleportUser, dbService, dbUser string) (*mysqlclient.Conn, error) {
Expand All @@ -1377,6 +1401,29 @@ func (c *testContext) mysqlClientWithAddr(address, teleportUser, dbService, dbUs
})
}

// mysqlClientLocalProxy connects to test MySQL through local ALPN proxy.
func (c *testContext) mysqlClientLocalProxy(ctx context.Context, teleportUser, dbService, dbUser string) (*mysqlclient.Conn, *alpnproxy.LocalProxy, error) {
route := tlsca.RouteToDatabase{
ServiceName: dbService,
Protocol: defaults.ProtocolMySQL,
Username: dbUser,
}

// Start local proxy which client will connect to.
proxy, err := c.startLocalALPNProxy(ctx, c.webListener.Addr().String(), teleportUser, route)
if err != nil {
return nil, nil, trace.Wrap(err)
}

// Client connects to the local proxy without TLS.
conn, err := mysqlclient.Connect(proxy.GetAddr(), dbUser, "", "")
if err != nil {
return nil, nil, trace.Wrap(err)
}

return conn, proxy, nil
}

// mongoClient connects to test MongoDB through database access as a
// specified Teleport user and database account.
func (c *testContext) mongoClient(ctx context.Context, teleportUser, dbService, dbUser string, opts ...*options.ClientOptions) (*mongo.Client, error) {
Expand All @@ -1399,6 +1446,41 @@ func (c *testContext) mongoClientWithAddr(ctx context.Context, address, teleport
}, opts...)
}

// mongoClientLocalProxy connects to test MongoDB through local ALPN proxy.
func (c *testContext) mongoClientLocalProxy(ctx context.Context, teleportUser, dbService, dbUser string) (*mongo.Client, *alpnproxy.LocalProxy, error) {
route := tlsca.RouteToDatabase{
ServiceName: dbService,
Protocol: defaults.ProtocolMongoDB,
Username: dbUser,
}

// Start local proxy which client will connect to.
proxy, err := c.startLocalALPNProxy(ctx, c.webListener.Addr().String(), teleportUser, route)
if err != nil {
return nil, nil, trace.Wrap(err)
}

// Client connects to the local proxy without TLS.
client, err := mongo.Connect(ctx, options.Client().
ApplyURI("mongodb://"+proxy.GetAddr()).
SetHeartbeatInterval(500*time.Millisecond).
SetServerSelectionTimeout(5*time.Second))
if err != nil {
return nil, nil, trace.Wrap(err)
}

// Ping to make sure it connected successfully.
errPing := client.Ping(ctx, nil)
if errPing != nil {
if err := client.Disconnect(ctx); err != nil {
return nil, nil, trace.NewAggregate(errPing, err)
}
return nil, nil, trace.Wrap(errPing)
}

return client, proxy, nil
}

// redisClient connects to test Redis through database access as a specified Teleport user and database account.
func (c *testContext) redisClient(ctx context.Context, teleportUser, dbService, dbUser string, opts ...redis.ClientOptions) (*redis.Client, error) {
return c.redisClientWithAddr(ctx, c.webListener.Addr().String(), teleportUser, dbService, dbUser, opts...)
Expand All @@ -1420,6 +1502,37 @@ func (c *testContext) redisClientWithAddr(ctx context.Context, proxyAddress, tel
}, opts...)
}

// redisClientLocalProxy connects to test Redis through local ALPN proxy.
func (c *testContext) redisClientLocalProxy(ctx context.Context, teleportUser, dbService, dbUser string) (*redis.Client, *alpnproxy.LocalProxy, error) {
route := tlsca.RouteToDatabase{
ServiceName: dbService,
Protocol: defaults.ProtocolRedis,
Username: dbUser,
}

// Start local proxy which client will connect to.
proxy, err := c.startLocalALPNProxy(ctx, c.webListener.Addr().String(), teleportUser, route)
if err != nil {
return nil, nil, trace.Wrap(err)
}

// Client connects to the local proxy without TLS.
client := goredis.NewClient(&goredis.Options{
Addr: proxy.GetAddr(),
})

// Ping to make sure connection is successful.
errPing := client.Ping(ctx).Err()
if errPing != nil {
if err := client.Close(); err != nil {
return nil, nil, trace.NewAggregate(errPing, err)
}
return nil, nil, trace.Wrap(errPing)
}

return client, proxy, nil
}

// sqlServerClient connects to the specified SQL Server address.
func (c *testContext) sqlServerClient(ctx context.Context, teleportUser, dbService, dbUser, dbName string) (*mssql.Conn, *alpnproxy.LocalProxy, error) {
route := tlsca.RouteToDatabase{
Expand Down
131 changes: 131 additions & 0 deletions lib/srv/db/local_proxy_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
Copyright 2022 Gravitational, Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package db

import (
"context"
"testing"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/srv/db/postgres"

"github.com/jackc/pgconn"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/bson"
)

// TestLocalProxyPostgres verifies connecting to a Postgres database
// through the local authenticated ALPN proxy.
func TestLocalProxyPostgres(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t, withSelfHostedPostgres("postgres"))
go testCtx.startHandlingConnections()

// Create test user/role.
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{types.Wildcard}, []string{types.Wildcard})

// Try to connect to the database as this user.
conn, proxy, err := testCtx.postgresClientLocalProxy(ctx, "alice", "postgres", "postgres", "postgres")
require.NoError(t, err)

// Close connection and local proxy after the test.
t.Cleanup(func() {
require.NoError(t, conn.Close(ctx))
require.NoError(t, proxy.Close())
})

// Execute a query.
result, err := conn.Exec(ctx, "select 1").ReadAll()
require.NoError(t, err)
require.Equal(t, []*pgconn.Result{postgres.TestQueryResponse}, result)
}

// TestLocalProxyMySQL verifies connecting to a MySQL database
// through the local authenticated ALPN proxy.
func TestLocalProxyMySQL(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t, withSelfHostedMySQL("mysql"))
go testCtx.startHandlingConnections()

// Create test user/role.
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{types.Wildcard}, []string{types.Wildcard})

// Connect to the database as this user.
conn, proxy, err := testCtx.mysqlClientLocalProxy(ctx, "alice", "mysql", "alice")
require.NoError(t, err)

// Close connection and local proxy after the test.
t.Cleanup(func() {
require.NoError(t, conn.Close())
require.NoError(t, proxy.Close())
})

// Execute a query.
_, err = conn.Execute("select 1")
require.NoError(t, err)
}

// TestLocalProxyMongoDB verifies connecting to a MongoDB database
// through the local authenticated ALPN proxy.
func TestLocalProxyMongoDB(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t, withSelfHostedMongo("mongo"))
go testCtx.startHandlingConnections()

// Create test user/role.
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{types.Wildcard}, []string{types.Wildcard})

// Connect to the database as this user.
client, proxy, err := testCtx.mongoClientLocalProxy(ctx, "alice", "mongo", "admin")
require.NoError(t, err)

// Close connection and local proxy after the test.
t.Cleanup(func() {
require.NoError(t, client.Disconnect(ctx))
require.NoError(t, proxy.Close())
})

// Execute a query.
_, err = client.Database("admin").Collection("test").Find(ctx, bson.M{})
require.NoError(t, err)
}

// TestLocalProxyRedis verifies connecting to a Redis database
// through the local authenticated ALPN proxy.
func TestLocalProxyRedis(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t, withSelfHostedRedis("redis"))
go testCtx.startHandlingConnections()

// Create test user/role.
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{types.Wildcard}, []string{types.Wildcard})

// Connect to the database as this user.
client, proxy, err := testCtx.redisClientLocalProxy(ctx, "alice", "redis", "admin")
require.NoError(t, err)

// Close connection and local proxy after the test.
t.Cleanup(func() {
require.NoError(t, client.Close())
require.NoError(t, proxy.Close())
})

// Execute a query.
result := client.Echo(ctx, "ping")
require.NoError(t, result.Err())
require.Equal(t, "ping", result.Val())
}
17 changes: 13 additions & 4 deletions lib/srv/db/mysql/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ func (p *Proxy) makeServer(clientConn net.Conn) *server.Conn {
mysql.DEFAULT_COLLATION_ID,
mysql.AUTH_NATIVE_PASSWORD,
nil,
// TLS config can actually be nil if the client is connecting
// through local TLS proxy without TLS.
p.TLSConfig),
&credentialProvider{},
server.EmptyHandler{})
Expand Down Expand Up @@ -170,11 +172,18 @@ func (p *Proxy) performHandshake(conn *multiplexer.Conn, server *server.Conn) (*
// First part of the handshake completed and the connection has been
// upgraded to TLS so now we can look at the client certificate and
// see which database service to route the connection to.
tlsConn, ok := server.Conn.Conn.(*tls.Conn)
if !ok {
return nil, trace.BadParameter("expected TLS connection")
switch c := server.Conn.Conn.(type) {
case *tls.Conn:
return c, nil
case *multiplexer.Conn:
tlsConn, ok := c.Conn.(*tls.Conn)
if !ok {
return nil, trace.BadParameter("expected TLS connection, got: %T", c.Conn)
}
return tlsConn, nil
}
return tlsConn, nil
return nil, trace.BadParameter("expected *tls.Conn or *multiplexer.Conn, got: %T",
server.Conn.Conn)
}

// maybeReadProxyLine peeks into the connection to see if instead of regular
Expand Down
23 changes: 16 additions & 7 deletions lib/srv/db/postgres/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,14 +125,23 @@ func (p *Proxy) handleStartup(ctx context.Context, clientConn net.Conn) (*pgprot
// https://www.postgresql.org/docs/13/protocol-flow.html#id-1.10.5.7.11
switch m := startupMessage.(type) {
case *pgproto3.SSLRequest:
// Send 'S' back to indicate TLS support to the client.
_, err := clientConn.Write([]byte("S"))
if err != nil {
return nil, nil, nil, trace.Wrap(err)
if p.TLSConfig == nil {
// Send 'N' back to make the client connect without TLS. Happens
// when client connects through the local TLS proxy.
_, err := clientConn.Write([]byte("N"))
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
} else {
// Send 'S' back to indicate TLS support to the client.
_, err := clientConn.Write([]byte("S"))
if err != nil {
return nil, nil, nil, trace.Wrap(err)
}
// Upgrade the connection to TLS and wait for the next message
// which should be of the StartupMessage type.
clientConn = tls.Server(clientConn, p.TLSConfig)
}
// Upgrade the connection to TLS and wait for the next message
// which should be of the StartupMessage type.
clientConn = tls.Server(clientConn, p.TLSConfig)
return p.handleStartup(ctx, clientConn)
case *pgproto3.StartupMessage:
// TLS connection between the client and this proxy has been
Expand Down
Loading