Skip to content

Commit

Permalink
Add support for MFA for DB access (#8270)
Browse files Browse the repository at this point in the history
  • Loading branch information
smallinsky authored Oct 6, 2021
1 parent 8f783cf commit 700f9f7
Showing 11 changed files with 385 additions and 122 deletions.
8 changes: 8 additions & 0 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
@@ -71,6 +71,7 @@ import (
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/services/local"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/srv/db/common/role"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/tlsca"
@@ -3115,9 +3116,16 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck
if db == nil {
return nil, trace.Wrap(notFoundErr)
}

dbRoleMatchers := role.DatabaseRoleMatchers(
db.GetProtocol(),
t.Database.Username,
t.Database.GetDatabase(),
)
noMFAAccessErr = checker.CheckAccess(
db,
services.AccessMFAParams{},
dbRoleMatchers...,
)

default:
171 changes: 171 additions & 0 deletions lib/auth/auth_with_roles_test.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ import (
"time"

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/defaults"
"github.com/gravitational/teleport/api/types"
libdefaults "github.com/gravitational/teleport/lib/defaults"
@@ -955,6 +956,176 @@ func TestReplaceRemoteLocksRBAC(t *testing.T) {
}
}

// TestIsMFARequiredMFADB tests isMFARequest logic per database protocol where different role matchers are used.
func TestIsMFARequiredMFADB(t *testing.T) {
const (
databaseName = "test-database"
userName = "test-username"
)

type modifyRoleFunc func(role types.Role)
tests := []struct {
name string
userRoleRequireMFA bool
checkMFA require.BoolAssertionFunc
modifyRoleFunc modifyRoleFunc
dbProtocol string
req *proto.IsMFARequiredRequest
}{
{
name: "RequireSessionMFA enabled MySQL protocol doesn't match database name",
dbProtocol: libdefaults.ProtocolMySQL,
req: &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Database{
Database: &proto.RouteToDatabase{
ServiceName: databaseName,
Protocol: libdefaults.ProtocolMySQL,
Username: userName,
Database: "example",
},
},
},
modifyRoleFunc: func(role types.Role) {
roleOpt := role.GetOptions()
roleOpt.RequireSessionMFA = true
role.SetOptions(roleOpt)

role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetDatabaseNames(types.Allow, nil)
},
checkMFA: require.True,
},
{
name: "RequireSessionMFA disabled",
dbProtocol: libdefaults.ProtocolMySQL,
req: &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Database{
Database: &proto.RouteToDatabase{
ServiceName: databaseName,
Protocol: libdefaults.ProtocolMySQL,
Username: userName,
Database: "example",
},
},
},
modifyRoleFunc: func(role types.Role) {
roleOpt := role.GetOptions()
roleOpt.RequireSessionMFA = false
role.SetOptions(roleOpt)

role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetDatabaseNames(types.Allow, nil)
},
checkMFA: require.False,
},
{
name: "RequireSessionMFA enabled Postgres protocol database name doesn't match",
dbProtocol: libdefaults.ProtocolPostgres,
req: &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Database{
Database: &proto.RouteToDatabase{
ServiceName: databaseName,
Protocol: libdefaults.ProtocolPostgres,
Username: userName,
Database: "example",
},
},
},
modifyRoleFunc: func(role types.Role) {
roleOpt := role.GetOptions()
roleOpt.RequireSessionMFA = true
role.SetOptions(roleOpt)

role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetDatabaseNames(types.Allow, nil)
},
checkMFA: require.False,
},
{
name: "RequireSessionMFA enabled Postgres protocol database name matches",
dbProtocol: libdefaults.ProtocolPostgres,
req: &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Database{
Database: &proto.RouteToDatabase{
ServiceName: databaseName,
Protocol: libdefaults.ProtocolPostgres,
Username: userName,
Database: "example",
},
},
},
modifyRoleFunc: func(role types.Role) {
roleOpt := role.GetOptions()
roleOpt.RequireSessionMFA = true
role.SetOptions(roleOpt)

role.SetDatabaseUsers(types.Allow, []string{types.Wildcard})
role.SetDatabaseLabels(types.Allow, types.Labels{types.Wildcard: {types.Wildcard}})
role.SetDatabaseNames(types.Allow, []string{"example"})
},
checkMFA: require.True,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)

// Enable MFA support.
authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "teleport",
Facets: []string{"teleport"},
},
})
require.NoError(t, err)
err = srv.Auth().SetAuthPreference(ctx, authPref)
require.NoError(t, err)

database, err := types.NewDatabaseServerV3(
types.Metadata{
Name: databaseName,
Labels: map[string]string{
"env": "dev",
},
},
types.DatabaseServerSpecV3{
Protocol: tc.dbProtocol,
URI: "example.com",
Hostname: "host",
HostID: "hostID",
},
)
require.NoError(t, err)

_, err = srv.Auth().UpsertDatabaseServer(ctx, database)
require.NoError(t, err)

user, role, err := CreateUserAndRole(srv.Auth(), userName, []string{"test-role"})
require.NoError(t, err)

if tc.modifyRoleFunc != nil {
tc.modifyRoleFunc(role)
}
err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)

cl, err := srv.NewClient(TestUser(user.GetName()))
require.NoError(t, err)

resp, err := cl.IsMFARequired(ctx, tc.req)
require.NoError(t, err)
tc.checkMFA(t, resp.GetRequired())
})
}
}

// TestKindClusterConfig verifies that types.KindClusterConfig can be used
// as an alternative privilege to provide access to cluster configuration
// resources.
5 changes: 5 additions & 0 deletions lib/client/api.go
Original file line number Diff line number Diff line change
@@ -600,6 +600,11 @@ func readProfile(profileDir string, profileName string) (*ProfileStatus, error)
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)
}
9 changes: 1 addition & 8 deletions lib/client/client.go
Original file line number Diff line number Diff line change
@@ -164,14 +164,7 @@ func (p ReissueParams) usage() proto.UserCertsRequest_CertUsage {
case p.RouteToDatabase.ServiceName != "":
// Database means a request for a TLS certificate for access to a
// specific database, as specified by RouteToDatabase.

// DELETE IN 7.0
// Database certs have to be requested with CertUsage All because
// pre-7.0 servers do not accept usage-restricted certificates.
//
// In 7.0 clients, we can expect the server to be 7.0+ and set this to
// proto.UserCertsRequest_Database again.
return proto.UserCertsRequest_All
return proto.UserCertsRequest_Database
case p.RouteToApp.Name != "":
// App means a request for a TLS certificate for access to a specific
// web app, as specified by RouteToApp.
47 changes: 47 additions & 0 deletions lib/srv/db/common/role/role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/*
Copyright 2021 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 role

import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
)

// DatabaseRoleMatchers returns role matchers based on the database protocol.
func DatabaseRoleMatchers(dbProtocol string, user, database string) services.RoleMatchers {
switch dbProtocol {
case defaults.ProtocolMySQL:
// In MySQL, unlike Postgres, "database" and "schema" are the same thing
// and there's no good way to prevent users from performing cross-database
// queries once they're connected, apart from granting proper privileges
// in MySQL itself.
//
// As such, checking db_names for MySQL is quite pointless so we only
// check db_users. In future, if we implement some sort of access controls
// on queries, we might be able to restrict db_names as well e.g. by
// detecting full-qualified table names like db.table, until then the
// proper way is to use MySQL grants system.
return services.RoleMatchers{
&services.DatabaseUserMatcher{User: user},
}
default:
return services.RoleMatchers{
&services.DatabaseUserMatcher{User: user},
&services.DatabaseNameMatcher{Name: database},
}
}
}
14 changes: 11 additions & 3 deletions lib/srv/db/mongodb/engine.go
Original file line number Diff line number Diff line change
@@ -21,8 +21,10 @@ import (
"net"
"strings"

"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/srv/db/common/role"
"github.com/gravitational/teleport/lib/srv/db/mongodb/protocol"
"github.com/gravitational/teleport/lib/utils"

@@ -188,10 +190,16 @@ func (e *Engine) authorizeClientMessage(sessionCtx *common.Session, message prot
e.Log.Warnf("No database info in message: %v.", message)
return nil
}
err := sessionCtx.Checker.CheckAccess(sessionCtx.Database,
dbRoleMatchers := role.DatabaseRoleMatchers(
defaults.ProtocolMongoDB,
sessionCtx.DatabaseUser,
database,
)
err := sessionCtx.Checker.CheckAccess(
sessionCtx.Database,
services.AccessMFAParams{Verified: true},
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser},
&services.DatabaseNameMatcher{Name: database})
dbRoleMatchers...,
)
e.Audit.OnQuery(e.Context, sessionCtx, common.Query{
Database: msg.GetDatabase(),
// Commands may consist of multiple bson documents.
19 changes: 8 additions & 11 deletions lib/srv/db/mysql/engine.go
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/srv/db/common/role"
"github.com/gravitational/teleport/lib/srv/db/mysql/protocol"
"github.com/gravitational/teleport/lib/utils"

@@ -128,20 +129,16 @@ func (e *Engine) checkAccess(ctx context.Context, sessionCtx *common.Session) er
Verified: sessionCtx.Identity.MFAVerified != "",
AlwaysRequired: ap.GetRequireSessionMFA(),
}
// In MySQL, unlike Postgres, "database" and "schema" are the same thing
// and there's no good way to prevent users from performing cross-database
// queries once they're connected, apart from granting proper privileges
// in MySQL itself.
//
// As such, checking db_names for MySQL is quite pointless so we only
// check db_users. In future, if we implement some sort of access controls
// on queries, we might be able to restrict db_names as well e.g. by
// detecting full-qualified table names like db.table, until then the
// proper way is to use MySQL grants system.
dbRoleMatchers := role.DatabaseRoleMatchers(
defaults.ProtocolMySQL,
sessionCtx.DatabaseUser,
sessionCtx.DatabaseName,
)
err = sessionCtx.Checker.CheckAccess(
sessionCtx.Database,
mfaParams,
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser})
dbRoleMatchers...,
)
if err != nil {
e.Audit.OnSessionStart(e.Context, sessionCtx, err)
return trace.Wrap(err)
12 changes: 10 additions & 2 deletions lib/srv/db/postgres/engine.go
Original file line number Diff line number Diff line change
@@ -23,8 +23,10 @@ import (
"net"

"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/srv/db/common"
"github.com/gravitational/teleport/lib/srv/db/common/role"

"github.com/jackc/pgconn"
"github.com/jackc/pgproto3/v2"
@@ -179,11 +181,17 @@ func (e *Engine) checkAccess(ctx context.Context, sessionCtx *common.Session) er
Verified: sessionCtx.Identity.MFAVerified != "",
AlwaysRequired: ap.GetRequireSessionMFA(),
}

dbRoleMatchers := role.DatabaseRoleMatchers(
defaults.ProtocolPostgres,
sessionCtx.DatabaseUser,
sessionCtx.DatabaseName,
)
err = sessionCtx.Checker.CheckAccess(
sessionCtx.Database,
mfaParams,
&services.DatabaseUserMatcher{User: sessionCtx.DatabaseUser},
&services.DatabaseNameMatcher{Name: sessionCtx.DatabaseName})
dbRoleMatchers...,
)
if err != nil {
e.Audit.OnSessionStart(e.Context, sessionCtx, err)
return trace.Wrap(err)
Loading

0 comments on commit 700f9f7

Please sign in to comment.