Files
teleport/lib/srv/db/auth_test.go
T
Brian Joerger 73f4dc71cc Migrate RotateCertAuthority to gRPC (#36536)
* Add RotateCertAuthority to gRPC TrustService.

* Add gRPC server implementation.

* Move RotateRequest type to api/types.

* Mark deprecated HTTP rotate endpoint for deletion.

* Add client implemenation and HTTP fallback.
2024-01-11 17:44:40 +00:00

476 lines
16 KiB
Go

/*
* Teleport
* Copyright (C) 2023 Gravitational, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package db
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/elasticache"
"github.com/aws/aws-sdk-go/service/memorydb"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/auth"
"github.com/gravitational/teleport/lib/auth/testauthority"
"github.com/gravitational/teleport/lib/cloud/mocks"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/tlsca"
)
// TestAuthTokens verifies that proper IAM auth tokens are used when connecting
// to cloud databases such as RDS, Redshift, Cloud SQL.
func TestAuthTokens(t *testing.T) {
ctx := context.Background()
testCtx := setupTestContext(ctx, t)
withDBs := []withDatabaseOption{
withRDSPostgres("postgres-rds-correct-token", rdsAuthToken),
withRDSPostgres("postgres-rds-incorrect-token", "qwe123"),
withRedshiftPostgres("postgres-redshift-correct-token", redshiftAuthToken),
withRedshiftPostgres("postgres-redshift-incorrect-token", "qwe123"),
withCloudSQLPostgres("postgres-cloudsql-correct-token", cloudSQLAuthToken),
withCloudSQLPostgres("postgres-cloudsql-incorrect-token", "qwe123"),
withAzurePostgres("postgres-azure-correct-token", azureAccessToken),
withAzurePostgres("postgres-azure-incorrect-token", "qwe123"),
withRDSMySQL("mysql-rds-correct-token", "root", rdsAuthToken),
withRDSMySQL("mysql-rds-incorrect-token", "root", "qwe123"),
withCloudSQLMySQL("mysql-cloudsql-correct-token", "root", cloudSQLPassword),
withCloudSQLMySQL("mysql-cloudsql-incorrect-token", "root", "qwe123"),
withAzureMySQL("mysql-azure-correct-token", "root", azureAccessToken),
withAzureMySQL("mysql-azure-incorrect-token", "root", "qwe123"),
withAzureRedis("redis-azure-correct-token", azureRedisToken),
withAzureRedis("redis-azure-incorrect-token", "qwe123"),
withElastiCacheRedis("redis-elasticache-correct-token", elastiCacheRedisToken, "7.0.0"),
withElastiCacheRedis("redis-elasticache-incorrect-token", "qwe123", "7.0.0"),
withMemoryDBRedis("redis-memorydb-correct-token", memorydbToken, "7.0"),
withMemoryDBRedis("redis-memorydb-incorrect-token", "qwe123", "7.0"),
}
databases := make([]types.Database, 0, len(withDBs))
for _, withDB := range withDBs {
databases = append(databases, withDB(t, ctx, testCtx))
}
ecMock := &mocks.ElastiCacheMock{}
elastiCacheIAMUser := &elasticache.User{
UserId: aws.String("default"),
Authentication: &elasticache.Authentication{Type: aws.String("iam")},
}
ecMock.AddMockUser(elastiCacheIAMUser, nil)
memorydbMock := &mocks.MemoryDBMock{}
memorydbIAMUser := &memorydb.User{
Name: aws.String("default"),
Authentication: &memorydb.Authentication{Type: aws.String("iam")},
}
memorydbMock.AddMockUser(memorydbIAMUser, nil)
testCtx.server = testCtx.setupDatabaseServer(ctx, t, agentParams{
Databases: databases,
ElastiCache: ecMock,
MemoryDB: memorydbMock,
})
go testCtx.startHandlingConnections()
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{types.Wildcard}, []string{types.Wildcard})
tests := []struct {
desc string
service string
protocol string
err string
}{
{
desc: "correct Postgres RDS IAM auth token",
service: "postgres-rds-correct-token",
protocol: defaults.ProtocolPostgres,
},
{
desc: "incorrect Postgres RDS IAM auth token",
service: "postgres-rds-incorrect-token",
protocol: defaults.ProtocolPostgres,
// Make sure we print example RDS IAM policy.
err: "arn:aws:rds-db:us-east-1:{account_id}:dbuser:{resource_id}",
},
{
desc: "correct Postgres Redshift IAM auth token",
service: "postgres-redshift-correct-token",
protocol: defaults.ProtocolPostgres,
},
{
desc: "incorrect Postgres Redshift IAM auth token",
service: "postgres-redshift-incorrect-token",
protocol: defaults.ProtocolPostgres,
err: "invalid auth token",
},
{
desc: "correct Postgres Cloud SQL IAM auth token",
service: "postgres-cloudsql-correct-token",
protocol: defaults.ProtocolPostgres,
},
{
desc: "incorrect Postgres Cloud SQL IAM auth token",
service: "postgres-cloudsql-incorrect-token",
protocol: defaults.ProtocolPostgres,
err: "invalid auth token",
},
{
desc: "correct MySQL RDS IAM auth token",
service: "mysql-rds-correct-token",
protocol: defaults.ProtocolMySQL,
},
{
desc: "incorrect MySQL RDS IAM auth token",
service: "mysql-rds-incorrect-token",
protocol: defaults.ProtocolMySQL,
// Make sure we print example RDS IAM policy.
err: "arn:aws:rds-db:us-east-1:{account_id}:dbuser:{resource_id}",
},
{
desc: "correct MySQL Cloud SQL IAM auth token",
service: "mysql-cloudsql-correct-token",
protocol: defaults.ProtocolMySQL,
},
{
desc: "incorrect MySQL Cloud SQL IAM auth token",
service: "mysql-cloudsql-incorrect-token",
protocol: defaults.ProtocolMySQL,
err: "Access denied for user",
},
{
desc: "correct Azure Redis auth token",
service: "redis-azure-correct-token",
protocol: defaults.ProtocolRedis,
},
{
desc: "incorrect Azure Redis auth token",
service: "redis-azure-incorrect-token",
protocol: defaults.ProtocolRedis,
err: "WRONGPASS invalid username-password pair",
},
{
desc: "correct ElastiCache Redis auth token",
service: "redis-elasticache-correct-token",
protocol: defaults.ProtocolRedis,
},
{
desc: "incorrect ElastiCache Redis auth token",
service: "redis-elasticache-incorrect-token",
protocol: defaults.ProtocolRedis,
// Make sure we print a user-friendly IAM auth error.
err: "Make sure that IAM auth is enabled",
},
{
desc: "correct MemoryDB auth token",
service: "redis-memorydb-correct-token",
protocol: defaults.ProtocolRedis,
},
{
desc: "incorrect MemoryDB auth token",
service: "redis-memorydb-incorrect-token",
protocol: defaults.ProtocolRedis,
// Make sure we print a user-friendly IAM auth error.
err: "Make sure that IAM auth is enabled",
},
}
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
switch test.protocol {
case defaults.ProtocolPostgres:
conn, err := testCtx.postgresClient(ctx, "alice", test.service, "postgres", "postgres")
if test.err != "" {
require.Error(t, err)
require.Contains(t, err.Error(), test.err)
} else {
require.NoError(t, err)
require.NoError(t, conn.Close(ctx))
}
case defaults.ProtocolMySQL:
conn, err := testCtx.mysqlClient("alice", test.service, "root")
if test.err != "" {
require.Error(t, err)
require.Contains(t, err.Error(), test.err)
} else {
require.NoError(t, err)
require.NoError(t, conn.Close())
}
case defaults.ProtocolRedis:
conn, err := testCtx.redisClient(ctx, "alice", test.service, "default")
if test.err != "" {
require.Error(t, err)
require.Contains(t, err.Error(), test.err)
} else {
require.NoError(t, err)
require.NoError(t, conn.Close())
}
default:
t.Fatalf("unrecognized database protocol in test: %q", test.protocol)
}
})
}
}
// testAuth mocks cloud provider auth tokens generation for use in tests.
type testAuth struct {
// Auth is the wrapped "real" auth that handles everything except for
// cloud auth tokens generation.
common.Auth
// FieldLogger is used for logging.
logrus.FieldLogger
}
func newTestAuth(ac common.AuthConfig) (*testAuth, error) {
auth, err := common.NewAuth(ac)
if err != nil {
return nil, trace.Wrap(err)
}
return &testAuth{
Auth: auth,
FieldLogger: logrus.WithField(trace.Component, "auth:test"),
}, nil
}
const (
// rdsAuthToken is a mock RDS IAM auth token.
rdsAuthToken = "rds-auth-token"
// redshiftAuthUser is a mock Redshift IAM auth user.
redshiftAuthUser = "redshift-auth-user"
// redshiftAuthToken is a mock Redshift IAM auth token.
redshiftAuthToken = "redshift-auth-token"
// cloudSQLAuthToken is a mock Cloud SQL IAM auth token.
cloudSQLAuthToken = "cloudsql-auth-token"
// cloudSQLPassword is a mock Cloud SQL user password.
cloudSQLPassword = "cloudsql-password"
// azureAccessToken is a mock Azure access token.
azureAccessToken = "azure-access-token"
// azureRedisToken is a mock Azure Redis token.
azureRedisToken = "azure-redis-token"
// elastiCacheRedisToken is a mock ElastiCache Redis token.
elastiCacheRedisToken = "elasticache-redis-token"
// memorydbToken is a mock MemoryDB auth token.
memorydbToken = "memorydb-token"
// atlasAuthUser is a mock Mongo Atlas IAM auth user.
atlasAuthUser = "arn:aws:iam::111111111111:role/alice"
// atlasAuthToken is a mock Mongo Atlas IAM auth token.
atlasAuthToken = "atlas-auth-token"
// atlasAuthSessionToken is a mock Mongo Atlas IAM auth session token.
atlasAuthSessionToken = "atlas-session-token"
)
// GetRDSAuthToken generates RDS/Aurora auth token.
func (a *testAuth) GetRDSAuthToken(ctx context.Context, sessionCtx *common.Session) (string, error) {
a.Infof("Generating RDS auth token for %v.", sessionCtx)
return rdsAuthToken, nil
}
// GetRedshiftAuthToken generates Redshift auth token.
func (a *testAuth) GetRedshiftAuthToken(ctx context.Context, sessionCtx *common.Session) (string, string, error) {
a.Infof("Generating Redshift auth token for %v.", sessionCtx)
return redshiftAuthUser, redshiftAuthToken, nil
}
func (a *testAuth) GetRedshiftServerlessAuthToken(ctx context.Context, sessionCtx *common.Session) (string, string, error) {
return "", "", trace.NotImplemented("GetRedshiftServerlessAuthToken is not implemented")
}
func (a *testAuth) GetElastiCacheRedisToken(ctx context.Context, sessionCtx *common.Session) (string, error) {
return elastiCacheRedisToken, nil
}
func (a *testAuth) GetMemoryDBToken(ctx context.Context, sessionCtx *common.Session) (string, error) {
return memorydbToken, nil
}
// GetCloudSQLAuthToken generates Cloud SQL auth token.
func (a *testAuth) GetCloudSQLAuthToken(ctx context.Context, sessionCtx *common.Session) (string, error) {
a.Infof("Generating Cloud SQL auth token for %v.", sessionCtx)
return cloudSQLAuthToken, nil
}
// GetCloudSQLPassword generates Cloud SQL user password.
func (a *testAuth) GetCloudSQLPassword(ctx context.Context, sessionCtx *common.Session) (string, error) {
a.Infof("Generating Cloud SQL user password %v.", sessionCtx)
return cloudSQLPassword, nil
}
// GetAzureAccessToken generates Azure access token.
func (a *testAuth) GetAzureAccessToken(ctx context.Context, sessionCtx *common.Session) (string, error) {
a.Infof("Generating Azure access token for %v.", sessionCtx)
return azureAccessToken, nil
}
// GetAzureCacheForRedisToken retrieves auth token for Azure Cache for Redis.
func (a *testAuth) GetAzureCacheForRedisToken(ctx context.Context, sessionCtx *common.Session) (string, error) {
a.Infof("Generating Azure Redis token for %v.", sessionCtx)
return azureRedisToken, nil
}
// GetAWSIAMCreds returns the AWS IAM credentials, including access key, secret
// access key and session token.
func (a *testAuth) GetAWSIAMCreds(ctx context.Context, sessionCtx *common.Session) (string, string, string, error) {
a.Infof("Generating AWS IAM credentials for %v.", sessionCtx)
return atlasAuthUser, atlasAuthToken, atlasAuthSessionToken, nil
}
func TestDBCertSigning(t *testing.T) {
authServer, err := auth.NewTestAuthServer(auth.TestAuthServerConfig{
Clock: clockwork.NewFakeClockAt(time.Now()),
ClusterName: "local.me",
Dir: t.TempDir(),
})
require.NoError(t, err)
t.Cleanup(func() { require.NoError(t, authServer.Close()) })
ctx := context.Background()
privateKey, err := testauthority.New().GeneratePrivateKey()
require.NoError(t, err)
csr, err := tlsca.GenerateCertificateRequestPEM(pkix.Name{
CommonName: "localhost",
}, privateKey)
require.NoError(t, err)
// Set rotation to init phase. New CA will be generated.
// DB service should still use old key to sign certificates.
// tctl should use new key to sign certificates.
err = authServer.AuthServer.RotateCertAuthority(ctx, types.RotateRequest{
Type: types.DatabaseCA,
TargetPhase: types.RotationPhaseInit,
Mode: types.RotationModeManual,
})
require.NoError(t, err)
dbCAs, err := authServer.AuthServer.GetCertAuthorities(ctx, types.DatabaseCA, false)
require.NoError(t, err)
require.Len(t, dbCAs, 1)
require.NotNil(t, dbCAs[0].GetActiveKeys().TLS)
require.NotNil(t, dbCAs[0].GetAdditionalTrustedKeys().TLS)
tests := []struct {
name string
requester proto.DatabaseCertRequest_Requester
getCertFn func(dbCAs []types.CertAuthority) []byte
}{
{
name: "sign from DB service",
requester: proto.DatabaseCertRequest_UNSPECIFIED, // default behavior
getCertFn: func(dbCAs []types.CertAuthority) []byte {
return dbCAs[0].GetActiveKeys().TLS[0].Cert
},
},
{
name: "sign from tctl",
requester: proto.DatabaseCertRequest_TCTL,
getCertFn: func(dbCAs []types.CertAuthority) []byte {
return dbCAs[0].GetAdditionalTrustedKeys().TLS[0].Cert
},
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
certResp, err := authServer.AuthServer.GenerateDatabaseCert(ctx, &proto.DatabaseCertRequest{
CSR: csr,
ServerName: "localhost",
TTL: proto.Duration(time.Hour),
RequesterName: tt.requester,
})
require.NoError(t, err)
require.NotNil(t, certResp.Cert)
require.Len(t, certResp.CACerts, 2)
dbCert, err := tlsca.ParseCertificatePEM(certResp.Cert)
require.NoError(t, err)
certPool := x509.NewCertPool()
ok := certPool.AppendCertsFromPEM(tt.getCertFn(dbCAs))
require.True(t, ok)
opts := x509.VerifyOptions{
Roots: certPool,
}
// Verify if the generated certificate can be verified with the correct CA.
_, err = dbCert.Verify(opts)
require.NoError(t, err)
})
}
}
func TestMongoDBAtlas(t *testing.T) {
t.Parallel()
ctx := context.Background()
testCtx := setupTestContext(ctx, t,
withAtlasMongo("iam-auth", atlasAuthUser, atlasAuthSessionToken),
withAtlasMongo("certs-auth", "", ""),
)
go testCtx.startHandlingConnections()
testCtx.createUserAndRole(ctx, t, "alice", "admin", []string{types.Wildcard}, []string{types.Wildcard})
for name, tt := range map[string]struct {
service string
dbUser string
expectErr require.ErrorAssertionFunc
}{
"authenticates with arn username": {
service: "iam-auth",
dbUser: "arn:aws:iam::111111111111:role/alice",
expectErr: require.NoError,
},
"disabled iam authentication": {
service: "certs-auth",
dbUser: "arn:aws:iam::111111111111:role/alice",
expectErr: require.Error,
},
"partial arn authentication": {
service: "iam-auth",
dbUser: "role/alice",
expectErr: require.NoError,
},
"IAM user arn": {
service: "iam-auth",
dbUser: "arn:aws:iam::111111111111:user/alice",
expectErr: require.Error,
},
"certs authentication": {
service: "certs-auth",
dbUser: "alice",
expectErr: require.NoError,
},
} {
t.Run(name, func(t *testing.T) {
conn, err := testCtx.mongoClient(ctx, "alice", tt.service, tt.dbUser)
tt.expectErr(t, err)
if err != nil {
require.NoError(t, conn.Disconnect(ctx))
}
})
}
}