mfa: per-session MFA certs for SSH and Kubernetes (#5564)

* mfa: per-session MFA certs for SSH and Kubernetes

This is client-side support for requesting single-use certs with an MFA
check.

The client doesn't know whether they need MFA check when accessing a
resource, this is decided during an RBAC check on the server. So a
client will always try to get a single-use cert, and the server will
respond with NotNeeded if MFA is not required. This is an extra
round-trip for every session which causes ~20% slowdown in SSH logins:

```
$ hyperfine '/tmp/tsh-old ssh talos date' '/tmp/tsh-new ssh talos date'
Benchmark #1: /tmp/tsh-old ssh talos date
  Time (mean ± σ):      49.9 ms ±   1.0 ms    [User: 15.1 ms, System: 7.4 ms]
  Range (min … max):    48.4 ms …  54.1 ms    59 runs

Benchmark #2: /tmp/tsh-new ssh talos date
  Time (mean ± σ):      60.2 ms ±   1.6 ms    [User: 19.1 ms, System: 8.3 ms]
  Range (min … max):    59.0 ms …  69.7 ms    50 runs

  Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.

Summary
  '/tmp/tsh-old ssh talos date' ran
    1.21 ± 0.04 times faster than '/tmp/tsh-new ssh talos date'
```

Another few other internal changes:

- client.LocalKeyAgent will now always have a non-nil LocalKeyStore.
  Previously, it would be nil (e.g. in a web UI handler or when using an
  identity file) which easily causes panics. I added a noLocalKeyStore
  type instead that returns errors from all methods.

- requesting a user cert with a TTL < 1min will now succeed and return a
  1min cert instead of failing

* Capture access approvals on MFA-issued certs

* Address review feedback

* Address review feedback

* mfa: accept unknown nodes during short-term MFA cert creation

An unknown node could be an OpenSSH node set up via
https://goteleport.com/teleport/docs/openssh-teleport/

In this case, we shouldn't prevent the user from connecting.

There's a small risk of authz bypass - an attacker might know a
different name/IP for a registered node which Teleport doesn't know
about. But a Teleport node will still check RBAC and reject the
connection.

* Validate username against unmapped user identity

IssueUserCertsWithMFA is called on the leaf auth server in case of
trusted clusters. Username in the request object will be that of the
original unmapped caller.

* mfa: add IsMFARequired RPC

This RPC is ran before every connection to check whether MFA is
required. If a connection is against the leaf cluster, this request is
forwarded from root to leaf for evaluation.

* Fix integration tests

* Correctly treat "Username" as login name in IsMFARequired

Also, move the logic into auth.Server out of ServerWithRoles.

* Fix TestHA

* Address review feedback
This commit is contained in:
Andrew Lytvynov 2021-03-10 15:42:16 -08:00 committed by GitHub
parent 8490e99aaf
commit 3d02ae6279
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1813 additions and 360 deletions

View file

@ -117,7 +117,7 @@ func (c *Client) grpcDialer() grpcDialer {
}
conn, err := c.dialer.DialContext(ctx, "tcp", addr)
if err != nil {
return nil, trace.ConnectionProblem(err, "failed to dial")
return nil, trace.ConnectionProblem(err, "failed to dial: %v", err)
}
return conn, nil
}
@ -793,3 +793,11 @@ func (c *Client) GenerateUserSingleUseCerts(ctx context.Context) (proto.AuthServ
}
return stream, nil
}
func (c *Client) IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) {
resp, err := c.grpc.IsMFARequired(ctx, req)
if err != nil {
return nil, trail.FromGRPC(err)
}
return resp, nil
}

File diff suppressed because it is too large Load diff

View file

@ -711,6 +711,30 @@ message UserSingleUseCertsResponse {
}
}
// IsMFARequiredRequest is a request to check whether MFA is required to access
// the Target.
message IsMFARequiredRequest {
oneof Target {
// KubernetesCluster specifies the target kubernetes cluster.
string KubernetesCluster = 1;
// RouteToDatabase specifies the target database proxy name.
RouteToDatabase Database = 2;
// Node specifies the target SSH node.
NodeLogin Node = 3;
}
}
// NodeLogin specifies an SSH node and OS login.
message NodeLogin {
// Node can be node's hostname or UUID.
string Node = 1;
// Login is the OS login name.
string Login = 2;
}
// IsMFARequiredResponse is a response for MFA requirement check.
message IsMFARequiredResponse { bool Required = 1; }
// SingleUseUserCert is a single-use user certificate, either SSH or TLS.
message SingleUseUserCert {
oneof Cert {
@ -733,6 +757,9 @@ service AuthService {
// certificates.
rpc GenerateUserSingleUseCerts(stream UserSingleUseCertsRequest)
returns (stream UserSingleUseCertsResponse);
// IsMFARequired checks whether MFA is required to access the specified
// target.
rpc IsMFARequired(IsMFARequiredRequest) returns (IsMFARequiredResponse);
// GetUser gets a user resource by name.
rpc GetUser(GetUserRequest) returns (types.UserV2);
// GetUsers gets all current user resources.

View file

@ -1576,8 +1576,19 @@ func (s *IntSuite) TestHA(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(output.String(), check.Equals, "hello world\n")
// stop auth server a now
// Stop cluster "a" to force existing tunnels to close.
c.Assert(a.StopAuth(true), check.IsNil)
// Restart cluster "a".
c.Assert(a.Reset(), check.IsNil)
c.Assert(a.Start(), check.IsNil)
// Wait for the tunnels to reconnect.
abortTime = time.Now().Add(time.Second * 10)
for len(checkGetClusters(c, a.Tunnel)) < 2 && len(checkGetClusters(c, b.Tunnel)) < 2 {
time.Sleep(time.Millisecond * 2000)
if time.Now().After(abortTime) {
c.Fatalf("two sites do not see each other: tunnels are not working")
}
}
// try to execute an SSH command using the same old client to site-B
// "site-A" and "site-B" reverse tunnels are supposed to reconnect,

View file

@ -28,8 +28,10 @@ import (
"crypto"
"crypto/subtle"
"encoding/base64"
"errors"
"fmt"
"math/rand"
"net"
"net/url"
"strings"
"sync"
@ -1960,6 +1962,131 @@ func (a *Server) GetDatabaseServers(ctx context.Context, namespace string, opts
return a.GetCache().GetDatabaseServers(ctx, namespace, opts...)
}
func (a *Server) isMFARequired(ctx context.Context, checker services.AccessChecker, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) {
var noMFAAccessErr, notFoundErr error
switch t := req.Target.(type) {
case *proto.IsMFARequiredRequest_Node:
notFoundErr = trace.NotFound("node %q not found", t.Node)
if t.Node.Node == "" {
return nil, trace.BadParameter("empty Node field")
}
if t.Node.Login == "" {
return nil, trace.BadParameter("empty Login field")
}
// Find the target node and check whether MFA is required.
nodes, err := a.GetNodes(defaults.Namespace, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
var matches []types.Server
for _, n := range nodes {
// Get the server address without port number.
addr, _, err := net.SplitHostPort(n.GetAddr())
if err != nil {
addr = n.GetAddr()
}
// Match NodeName to UUID, hostname or self-reported server address.
if n.GetName() == t.Node.Node || n.GetHostname() == t.Node.Node || addr == t.Node.Node {
matches = append(matches, n)
}
}
if len(matches) == 0 {
// If t.Node.Node is not a known registered node, it may be an
// unregistered host running OpenSSH with a certificate created via
// `tctl auth sign`. In these cases, let the user through without
// extra checks.
//
// If t.Node.Node turns out to be an alias for a real node (e.g.
// private network IP), and MFA check was actually required, the
// Node itself will check the cert extensions and reject the
// connection.
return &proto.IsMFARequiredResponse{Required: false}, nil
}
// Check RBAC against all matching nodes and return the first error.
// If at least one node requires MFA, we'll catch it.
for _, n := range matches {
err := checker.CheckAccessToServer(t.Node.Login, n, false)
if err != nil {
noMFAAccessErr = err
break
}
}
case *proto.IsMFARequiredRequest_KubernetesCluster:
notFoundErr = trace.NotFound("kubernetes cluster %q not found", t.KubernetesCluster)
if t.KubernetesCluster == "" {
return nil, trace.BadParameter("missing KubernetesCluster field in a kubernetes-only UserCertsRequest")
}
// Find the target cluster and check whether MFA is required.
svcs, err := a.GetKubeServices(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
var cluster *types.KubernetesCluster
outer:
for _, svc := range svcs {
for _, c := range svc.GetKubernetesClusters() {
if c.Name == t.KubernetesCluster {
cluster = c
break outer
}
}
}
if cluster == nil {
return nil, trace.Wrap(notFoundErr)
}
noMFAAccessErr = checker.CheckAccessToKubernetes(defaults.Namespace, cluster, false)
case *proto.IsMFARequiredRequest_Database:
notFoundErr = trace.NotFound("database service %q not found", t.Database.ServiceName)
if t.Database.ServiceName == "" {
return nil, trace.BadParameter("missing ServiceName field in a database-only UserCertsRequest")
}
dbs, err := a.GetDatabaseServers(ctx, defaults.Namespace, services.SkipValidation())
if err != nil {
return nil, trace.Wrap(err)
}
var db types.DatabaseServer
for _, d := range dbs {
if d.GetName() == t.Database.ServiceName {
db = d
break
}
}
if db == nil {
return nil, trace.Wrap(notFoundErr)
}
noMFAAccessErr = checker.CheckAccessToDatabase(db, false)
default:
return nil, trace.BadParameter("unknown Target %T", req.Target)
}
// No error means that MFA is not required for this resource by
// AccessChecker.
if noMFAAccessErr == nil {
return &proto.IsMFARequiredResponse{Required: false}, nil
}
// Errors other than ErrSessionMFARequired mean something else is wrong,
// most likely access denied.
if !errors.Is(noMFAAccessErr, services.ErrSessionMFARequired) {
if trace.IsAccessDenied(noMFAAccessErr) {
log.Infof("Access to resource denied: %v", noMFAAccessErr)
// notFoundErr should always be set by this point, but check it
// just in case.
if notFoundErr == nil {
notFoundErr = trace.NotFound("target resource not found")
}
// Mask access denied errors to prevent resource name oracles.
return nil, trace.Wrap(notFoundErr)
}
return nil, trace.Wrap(noMFAAccessErr)
}
// If we reach here, the error from AccessChecker was
// ErrSessionMFARequired.
return &proto.IsMFARequiredResponse{Required: true}, nil
}
// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
// registered by the user.
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, u2fStorage u2f.AuthenticationStorage) (*proto.MFAAuthenticateChallenge, error) {

View file

@ -66,7 +66,7 @@ func (a *ServerWithRoles) action(namespace string, resource string, action strin
// even if they are not admins, e.g. update their own passwords,
// or generate certificates, otherwise it will require admin privileges
func (a *ServerWithRoles) currentUserAction(username string) error {
if a.hasLocalUserRole(a.context.Checker) && username == a.context.User.GetName() {
if hasLocalUserRole(a.context.Checker) && username == a.context.User.GetName() {
return nil
}
return utils.OpaqueAccessDenied(
@ -120,12 +120,17 @@ func (a *ServerWithRoles) hasRemoteBuiltinRole(name string) bool {
return true
}
// hasLocalUserRole checks if the type of the role set is a local user or not.
func (a *ServerWithRoles) hasLocalUserRole(checker services.AccessChecker) bool {
if _, ok := checker.(LocalUserRoleSet); !ok {
return false
// hasRemoteUserRole checks if the type of the role set is a remote user or
// not.
func hasRemoteUserRole(checker services.AccessChecker) bool {
_, ok := checker.(RemoteUserRoleSet)
return ok
}
return true
// hasLocalUserRole checks if the type of the role set is a local user or not.
func hasLocalUserRole(checker services.AccessChecker) bool {
_, ok := checker.(LocalUserRoleSet)
return ok
}
// AuthenticateWebUser authenticates web user, creates and returns a web session
@ -2601,6 +2606,13 @@ func (a *ServerWithRoles) GenerateUserSingleUseCerts(ctx context.Context) (proto
return nil, trace.NotImplemented("bug: GenerateUserSingleUseCerts must not be called on auth.ServerWithRoles")
}
func (a *ServerWithRoles) IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) {
if !hasLocalUserRole(a.context.Checker) && !hasRemoteUserRole(a.context.Checker) {
return nil, trace.AccessDenied("only a user role can call IsMFARequired, got %T", a.context.Checker)
}
return a.authServer.isMFARequired(ctx, a.context.Checker, req)
}
// NewAdminAuthServer returns auth server authorized as admin,
// used for auth server cached access
func NewAdminAuthServer(authServer *Server, sessions session.Service, alog events.IAuditLog) (ClientI, error) {

View file

@ -2281,6 +2281,10 @@ type IdentityService interface {
// (https://github.com/gravitational/teleport/blob/3a1cf9111c2698aede2056513337f32bfc16f1f1/rfd/0014-session-2FA.md#sessions).
GenerateUserSingleUseCerts(ctx context.Context) (proto.AuthService_GenerateUserSingleUseCertsClient, error)
// IsMFARequiredRequest is a request to check whether MFA is required to
// access the Target.
IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error)
// DeleteAllUsers deletes all users
DeleteAllUsers() error

View file

@ -40,7 +40,6 @@ import (
"github.com/golang/protobuf/ptypes/empty"
"github.com/gravitational/trace"
"github.com/gravitational/trace/trail"
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
@ -1622,7 +1621,8 @@ func (g *GRPCServer) GetMFADevices(ctx context.Context, req *proto.GetMFADevices
}
func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_GenerateUserSingleUseCertsServer) error {
actx, err := g.authenticate(stream.Context())
ctx := stream.Context()
actx, err := g.authenticate(ctx)
if err != nil {
return trail.ToGRPC(err)
}
@ -1644,7 +1644,8 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_Generat
if initReq == nil {
return trail.ToGRPC(trace.BadParameter("expected UserCertsRequest, got %T", req.Request))
}
if err := validateUserSingleUseCertRequest(initReq, g.AuthServer.GetClock()); err != nil {
if err := validateUserSingleUseCertRequest(ctx, actx, initReq); err != nil {
g.Entry.Debugf("Validation of single-use cert request failed: %v", err)
return trail.ToGRPC(err)
}
@ -1652,12 +1653,14 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_Generat
// 3. receive and validate MFAResponse
mfaDev, err := userSingleUseCertsAuthChallenge(actx, stream)
if err != nil {
g.Entry.Debugf("Failed to perform single-use cert challenge: %v", err)
return trail.ToGRPC(err)
}
// Generate the cert.
respCert, err := userSingleUseCertsGenerate(stream.Context(), actx, *initReq, mfaDev)
respCert, err := userSingleUseCertsGenerate(ctx, actx, *initReq, mfaDev)
if err != nil {
g.Entry.Warningf("Failed to generate single-use cert: %v", err)
return trail.ToGRPC(err)
}
@ -1670,7 +1673,13 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_Generat
return nil
}
func validateUserSingleUseCertRequest(req *proto.UserCertsRequest, clock clockwork.Clock) error {
// validateUserSingleUseCertRequest validates the request for a single-use user
// cert.
func validateUserSingleUseCertRequest(ctx context.Context, actx *grpcContext, req *proto.UserCertsRequest) error {
if err := actx.currentUserAction(req.Username); err != nil {
return trace.Wrap(err)
}
switch req.Usage {
case proto.UserCertsRequest_SSH:
if req.NodeName == "" {
@ -1689,7 +1698,8 @@ func validateUserSingleUseCertRequest(req *proto.UserCertsRequest, clock clockwo
default:
return trace.BadParameter("unknown certificate Usage %q", req.Usage)
}
maxExpiry := clock.Now().Add(teleport.UserSingleUseCertTTL)
maxExpiry := actx.authServer.GetClock().Now().Add(teleport.UserSingleUseCertTTL)
if req.Expires.After(maxExpiry) {
req.Expires = maxExpiry
}
@ -1709,6 +1719,9 @@ func userSingleUseCertsAuthChallenge(gctx *grpcContext, stream proto.AuthService
if err != nil {
return nil, trace.Wrap(err)
}
if authChallenge.TOTP == nil && len(authChallenge.U2F) == 0 {
return nil, trace.AccessDenied("MFA is required to access this resource but user has no MFA devices; use 'tsh mfa add' to register MFA devices")
}
if err := stream.Send(&proto.UserSingleUseCertsResponse{
Response: &proto.UserSingleUseCertsResponse_MFAChallenge{MFAChallenge: authChallenge},
}); err != nil {
@ -1758,6 +1771,18 @@ func userSingleUseCertsGenerate(ctx context.Context, actx *grpcContext, req prot
return resp, nil
}
func (g *GRPCServer) IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) {
actx, err := g.authenticate(ctx)
if err != nil {
return nil, trail.ToGRPC(err)
}
resp, err := actx.IsMFARequired(ctx, req)
if err != nil {
return nil, trail.ToGRPC(err)
}
return resp, nil
}
type grpcContext struct {
*Context
*ServerWithRoles

View file

@ -20,6 +20,7 @@ import (
"context"
"encoding/base32"
"encoding/base64"
"fmt"
"net"
"sort"
"testing"
@ -550,12 +551,24 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
U2F: &types.U2F{
AppID: "teleport",
Facets: []string{"teleport"},
},
})
}})
require.NoError(t, err)
err = srv.Auth().SetAuthPreference(authPref)
require.NoError(t, err)
// Register an SSH node.
node := &types.ServerV2{
Kind: types.KindKubeService,
Version: types.V2,
Metadata: types.Metadata{
Name: "node-a",
},
Spec: types.ServerSpecV2{
Hostname: "node-a",
},
}
_, err = srv.Auth().UpsertNode(node)
require.NoError(t, err)
// Register a k8s cluster.
k8sSrv := &types.ServerV2{
Kind: types.KindKubeService,
@ -569,9 +582,24 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
}
err = srv.Auth().UpsertKubeService(ctx, k8sSrv)
require.NoError(t, err)
// Register a database.
db := types.NewDatabaseServerV3("db-a", nil, types.DatabaseServerSpecV3{
Protocol: "postgres",
URI: "localhost",
Hostname: "localhost",
HostID: "localhost",
})
_, err = srv.Auth().UpsertDatabaseServer(ctx, db)
require.NoError(t, err)
// Create a fake user.
user, _, err := CreateUserAndRole(srv.Auth(), "mfa-user", []string{"role"})
user, role, err := CreateUserAndRole(srv.Auth(), "mfa-user", []string{"role"})
require.NoError(t, err)
// Make sure MFA is required for this user.
roleOpt := role.GetOptions()
roleOpt.RequireSessionMFA = true
role.SetOptions(roleOpt)
err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
cl, err := srv.NewClient(TestUser(user.GetName()))
require.NoError(t, err)
@ -836,3 +864,61 @@ func testGenerateUserSingleUseCert(ctx context.Context, t *testing.T, cl *Client
require.NoError(t, stream.CloseSend())
}
func TestIsMFARequired(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
// Enable MFA support.
authPref, err := services.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: teleport.Local,
SecondFactor: constants.SecondFactorOptional,
U2F: &types.U2F{
AppID: "teleport",
Facets: []string{"teleport"},
},
})
require.NoError(t, err)
err = srv.Auth().SetAuthPreference(authPref)
require.NoError(t, err)
// Register an SSH node.
node := &types.ServerV2{
Kind: types.KindKubeService,
Version: types.V2,
Metadata: types.Metadata{
Name: "node-a",
},
Spec: types.ServerSpecV2{
Hostname: "node-a",
},
}
_, err = srv.Auth().UpsertNode(node)
require.NoError(t, err)
// Create a fake user.
user, role, err := CreateUserAndRole(srv.Auth(), "no-mfa-user", []string{"role"})
require.NoError(t, err)
for _, required := range []bool{true, false} {
t.Run(fmt.Sprintf("required=%v", required), func(t *testing.T) {
roleOpt := role.GetOptions()
roleOpt.RequireSessionMFA = required
role.SetOptions(roleOpt)
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, &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{
Login: user.GetName(),
Node: "node-a",
}},
})
require.NoError(t, err)
require.Equal(t, resp.Required, required)
})
}
}

View file

@ -237,7 +237,7 @@ func (k *Keygen) GenerateHostCertWithoutValidation(c services.HostCertParams) ([
// GenerateUserCert generates a user certificate with the passed in parameters.
// The private key of the CA to sign the certificate must be provided.
func (k *Keygen) GenerateUserCert(c services.UserCertParams) ([]byte, error) {
if err := c.Check(); err != nil {
if err := c.CheckAndSetDefaults(); err != nil {
return nil, trace.Wrap(err, "error validating UserCertParams")
}
return k.GenerateUserCertWithoutValidation(c)

View file

@ -204,6 +204,8 @@ func (a *authorizer) authorizeRemoteUser(u RemoteUser) (*Context, error) {
KubernetesCluster: u.Identity.KubernetesCluster,
RouteToApp: u.Identity.RouteToApp,
RouteToDatabase: u.Identity.RouteToDatabase,
MFAVerified: u.Identity.MFAVerified,
ClientIP: u.Identity.ClientIP,
}
return &Context{

View file

@ -20,6 +20,8 @@ package test
import (
"time"
"github.com/gravitational/trace"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/services"
@ -105,21 +107,12 @@ func (s *AuthSuite) GenerateUserCert(c *check.C) {
})
c.Assert(err, check.IsNil)
certificate, err := sshutils.ParseCertificate(cert)
// Check the valid time is not more than 1 minute before and 1 hour after
// the current time.
err = checkCertExpiry(cert, s.Clock.Now().Add(-1*time.Minute), s.Clock.Now().Add(1*time.Hour))
c.Assert(err, check.IsNil)
// Check the valid time is not more than 1 minute before the current time.
validAfter := time.Unix(int64(certificate.ValidAfter), 0)
c.Assert(validAfter.Unix(), check.Equals, s.Clock.Now().UTC().Add(-1*time.Minute).Unix())
// Check the valid time is not more than 1 hour after the current time.
validBefore := time.Unix(int64(certificate.ValidBefore), 0)
c.Assert(validBefore.Unix(), check.Equals, s.Clock.Now().UTC().Add(1*time.Hour).Unix())
_, _, _, _, err = ssh.ParseAuthorizedKey(cert)
c.Assert(err, check.IsNil)
_, err = s.A.GenerateUserCert(services.UserCertParams{
cert, err = s.A.GenerateUserCert(services.UserCertParams{
PrivateCASigningKey: priv,
CASigningAlg: defaults.CASignatureAlgorithm,
PublicUserKey: pub,
@ -130,7 +123,9 @@ func (s *AuthSuite) GenerateUserCert(c *check.C) {
PermitPortForwarding: true,
CertificateFormat: teleport.CertificateFormatStandard,
})
c.Assert(err, check.NotNil)
c.Assert(err, check.IsNil)
err = checkCertExpiry(cert, s.Clock.Now().Add(-1*time.Minute), s.Clock.Now().Add(defaults.MinCertDuration))
c.Assert(err, check.IsNil)
_, err = s.A.GenerateUserCert(services.UserCertParams{
PrivateCASigningKey: priv,
@ -143,7 +138,9 @@ func (s *AuthSuite) GenerateUserCert(c *check.C) {
PermitPortForwarding: true,
CertificateFormat: teleport.CertificateFormatStandard,
})
c.Assert(err, check.NotNil)
c.Assert(err, check.IsNil)
err = checkCertExpiry(cert, s.Clock.Now().Add(-1*time.Minute), s.Clock.Now().Add(defaults.MinCertDuration))
c.Assert(err, check.IsNil)
_, err = s.A.GenerateUserCert(services.UserCertParams{
PrivateCASigningKey: priv,
@ -178,3 +175,20 @@ func (s *AuthSuite) GenerateUserCert(c *check.C) {
c.Assert(err, check.IsNil)
c.Assert(outRoles, check.DeepEquals, inRoles)
}
func checkCertExpiry(cert []byte, after, before time.Time) error {
certificate, err := sshutils.ParseCertificate(cert)
if err != nil {
return trace.Wrap(err)
}
validAfter := time.Unix(int64(certificate.ValidAfter), 0)
if !validAfter.Equal(after) {
return trace.BadParameter("ValidAfter incorrect: got %v, want %v", validAfter, after)
}
validBefore := time.Unix(int64(certificate.ValidBefore), 0)
if !validBefore.Equal(before) {
return trace.BadParameter("ValidBefore incorrect: got %v, want %v", validBefore, before)
}
return nil
}

View file

@ -940,7 +940,7 @@ func NewClient(c *Config) (tc *TeleportClient, err error) {
// if the client was passed an agent in the configuration and skip local auth, use
// the passed in agent.
if c.Agent != nil {
tc.localAgent = &LocalKeyAgent{Agent: c.Agent}
tc.localAgent = &LocalKeyAgent{Agent: c.Agent, keyStore: noLocalKeyStore{}}
}
} else {
// initialize the local agent (auth agent which uses local SSH keys signed by the CA):
@ -1021,6 +1021,25 @@ func (tc *TeleportClient) ReissueUserCerts(ctx context.Context, params ReissuePa
return proxyClient.ReissueUserCerts(ctx, params)
}
// IssueUserCertsWithMFA issues a single-use SSH or TLS certificate for
// connecting to a target (node/k8s/db/app) specified in params with an MFA
// check. A user has to be logged in, there should be a valid login cert
// available.
//
// If access to this target does not require per-connection MFA checks
// (according to RBAC), IssueCertsWithMFA will:
// - for SSH certs, return the existing Key from the keystore.
// - for TLS certs, fall back to ReissueUserCerts.
func (tc *TeleportClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams) (*Key, error) {
proxyClient, err := tc.ConnectToProxy(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
defer proxyClient.Close()
return proxyClient.IssueUserCertsWithMFA(ctx, params)
}
// CreateAccessRequest registers a new access request with the auth server.
func (tc *TeleportClient) CreateAccessRequest(ctx context.Context, req services.AccessRequest) error {
proxyClient, err := tc.ConnectToProxy(ctx)
@ -1129,6 +1148,7 @@ func (tc *TeleportClient) SSH(ctx context.Context, command []string, runLocally
if len(nodeAddrs) == 0 {
return trace.BadParameter("no target host specified")
}
nodeClient, err := proxyClient.ConnectToNode(
ctx,
NodeAddr{Addr: nodeAddrs[0], Namespace: tc.Namespace, Cluster: siteInfo.Name},

View file

@ -31,6 +31,8 @@ import (
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/gravitational/teleport"
"github.com/gravitational/teleport/api/client"
@ -137,37 +139,76 @@ func (proxy *ProxyClient) GetLeafClusters(ctx context.Context) ([]services.Remot
// user certificate reissue.
type ReissueParams struct {
RouteToCluster string
NodeName string
KubernetesCluster string
AccessRequests []string
RouteToDatabase proto.RouteToDatabase
}
func (p ReissueParams) usage() proto.UserCertsRequest_CertUsage {
switch {
case p.NodeName != "":
return proto.UserCertsRequest_SSH
case p.KubernetesCluster != "":
return proto.UserCertsRequest_Kubernetes
case p.RouteToDatabase.ServiceName != "":
return proto.UserCertsRequest_Database
default:
return proto.UserCertsRequest_All
}
}
func (p ReissueParams) isMFARequiredRequest(sshLogin string) *proto.IsMFARequiredRequest {
req := new(proto.IsMFARequiredRequest)
switch {
case p.NodeName != "":
req.Target = &proto.IsMFARequiredRequest_Node{Node: &proto.NodeLogin{Node: p.NodeName, Login: sshLogin}}
case p.KubernetesCluster != "":
req.Target = &proto.IsMFARequiredRequest_KubernetesCluster{KubernetesCluster: p.KubernetesCluster}
case p.RouteToDatabase.ServiceName != "":
req.Target = &proto.IsMFARequiredRequest_Database{Database: &p.RouteToDatabase}
}
return req
}
// ReissueUserCerts generates certificates for the user
// that have a metadata instructing server to route the requests to the cluster
func (proxy *ProxyClient) ReissueUserCerts(ctx context.Context, params ReissueParams) error {
localAgent := proxy.teleportClient.LocalAgent()
key, err := localAgent.GetKey(WithKubeCerts(params.RouteToCluster))
if err != nil {
return trace.Wrap(err)
}
cert, err := key.SSHCert()
if err != nil {
return trace.Wrap(err)
}
tlsCert, err := key.TeleportTLSCertificate()
if err != nil {
return trace.Wrap(err)
}
rootClusterName, err := tlsca.ClusterName(tlsCert.Issuer)
key, err := proxy.reissueUserCerts(ctx, params)
if err != nil {
return trace.Wrap(err)
}
clt, err := proxy.ConnectToCluster(ctx, rootClusterName, true)
if err != nil {
// save the cert to the local storage (~/.tsh usually):
_, err = proxy.teleportClient.LocalAgent().AddKey(key)
return trace.Wrap(err)
}
func (proxy *ProxyClient) reissueUserCerts(ctx context.Context, params ReissueParams) (*Key, error) {
localAgent := proxy.teleportClient.LocalAgent()
key, err := localAgent.GetKey(WithKubeCerts(params.RouteToCluster))
if err != nil {
return nil, trace.Wrap(err)
}
cert, err := key.SSHCert()
if err != nil {
return nil, trace.Wrap(err)
}
tlsCert, err := key.TeleportTLSCertificate()
if err != nil {
return nil, trace.Wrap(err)
}
rootClusterName, err := tlsca.ClusterName(tlsCert.Issuer)
if err != nil {
return nil, trace.Wrap(err)
}
clt, err := proxy.ConnectToCluster(ctx, rootClusterName, true)
if err != nil {
return nil, trace.Wrap(err)
}
defer clt.Close()
if params.RouteToCluster != "" {
// Before requesting a certificate, check if the requested cluster is valid.
_, err = clt.GetCertAuthority(services.CertAuthID{
@ -175,7 +216,7 @@ func (proxy *ProxyClient) ReissueUserCerts(ctx context.Context, params ReissuePa
DomainName: params.RouteToCluster,
}, false)
if err != nil {
return trace.NotFound("cluster %v not found", params.RouteToCluster)
return nil, trace.NotFound("cluster %v not found", params.RouteToCluster)
}
}
req := proto.UserCertsRequest{
@ -186,6 +227,8 @@ func (proxy *ProxyClient) ReissueUserCerts(ctx context.Context, params ReissuePa
KubernetesCluster: params.KubernetesCluster,
AccessRequests: params.AccessRequests,
RouteToDatabase: params.RouteToDatabase,
NodeName: params.NodeName,
Usage: proto.UserCertsRequest_All,
}
if _, ok := cert.Permissions.Extensions[teleport.CertExtensionTeleportRoles]; !ok {
req.Format = teleport.CertificateFormatOldSSH
@ -193,7 +236,7 @@ func (proxy *ProxyClient) ReissueUserCerts(ctx context.Context, params ReissuePa
certs, err := clt.GenerateUserCerts(ctx, req)
if err != nil {
return trace.Wrap(err)
return nil, trace.Wrap(err)
}
key.Cert = certs.SSH
key.TLSCert = certs.TLS
@ -203,10 +246,157 @@ func (proxy *ProxyClient) ReissueUserCerts(ctx context.Context, params ReissuePa
if params.RouteToDatabase.ServiceName != "" {
key.DBTLSCerts[params.RouteToDatabase.ServiceName] = certs.TLS
}
return key, nil
}
// save the cert to the local storage (~/.tsh usually):
_, err = localAgent.AddKey(key)
return trace.Wrap(err)
// IssueUserCertsWithMFA generates a single-use certificate for the user.
func (proxy *ProxyClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams) (*Key, error) {
localAgent := proxy.teleportClient.LocalAgent()
key, err := localAgent.GetKey(WithKubeCerts(params.RouteToCluster))
if err != nil {
return nil, trace.Wrap(err)
}
cert, err := key.SSHCert()
if err != nil {
return nil, trace.Wrap(err)
}
tlsCert, err := key.TeleportTLSCertificate()
if err != nil {
return nil, trace.Wrap(err)
}
rootClusterName, err := tlsca.ClusterName(tlsCert.Issuer)
if err != nil {
return nil, trace.Wrap(err)
}
if params.RouteToCluster == "" {
params.RouteToCluster = rootClusterName
}
// Connect to the target cluster (root or leaf) to check whether MFA is
// required.
clt, err := proxy.ConnectToCluster(ctx, params.RouteToCluster, true)
if err != nil {
return nil, trace.Wrap(err)
}
defer clt.Close()
requiredCheck, err := clt.IsMFARequired(ctx, params.isMFARequiredRequest(proxy.hostLogin))
if err != nil {
return nil, trace.Wrap(err)
}
if !requiredCheck.Required {
log.Debug("MFA not required for access.")
// MFA is not required.
// SSH certs can be used without embedding the node name.
if params.usage() == proto.UserCertsRequest_SSH {
return key, nil
}
// All other targets need their name embedded in the cert for routing,
// fall back to non-MFA reissue.
return proxy.reissueUserCerts(ctx, params)
}
if len(params.AccessRequests) == 0 {
// Get the active access requests to include in the cert.
var activeRequests services.RequestIDs
rawRequests, ok := cert.Extensions[teleport.CertExtensionTeleportActiveRequests]
if ok {
if err := activeRequests.Unmarshal([]byte(rawRequests)); err != nil {
return nil, trace.Wrap(err)
}
}
params.AccessRequests = activeRequests.AccessRequests
}
// Always connect to root for getting new credentials, but attempt to reuse
// the existing client if possible.
if params.RouteToCluster != rootClusterName {
clt.Close()
clt, err = proxy.ConnectToCluster(ctx, rootClusterName, true)
if err != nil {
return nil, trace.Wrap(err)
}
defer clt.Close()
}
log.Debug("Attempting to issue a single-use user certificate with an MFA check.")
stream, err := clt.GenerateUserSingleUseCerts(ctx)
if err != nil {
if status.Code(err) == codes.Unimplemented {
// Probably talking to an older server, use the old non-MFA endpoint.
log.WithError(err).Debug("Auth server does not implement GenerateUserSingleUseCerts.")
// SSH certs can be used without reissuing.
if params.usage() == proto.UserCertsRequest_SSH {
return key, nil
}
return proxy.reissueUserCerts(ctx, params)
}
return nil, trace.Wrap(err)
}
defer stream.CloseSend()
initReq := &proto.UserCertsRequest{
Username: proxy.hostLogin,
PublicKey: key.Pub,
Expires: time.Unix(int64(cert.ValidBefore), 0),
RouteToCluster: params.RouteToCluster,
NodeName: params.NodeName,
KubernetesCluster: params.KubernetesCluster,
AccessRequests: params.AccessRequests,
RouteToDatabase: params.RouteToDatabase,
Usage: params.usage(),
}
if _, ok := cert.Permissions.Extensions[teleport.CertExtensionTeleportRoles]; !ok {
initReq.Format = teleport.CertificateFormatOldSSH
}
err = stream.Send(&proto.UserSingleUseCertsRequest{Request: &proto.UserSingleUseCertsRequest_Init{
Init: initReq,
}})
if err != nil {
return nil, trace.Wrap(err)
}
resp, err := stream.Recv()
if err != nil {
return nil, trace.Wrap(err)
}
mfaChal := resp.GetMFAChallenge()
if mfaChal == nil {
return nil, trace.BadParameter("server sent a %T on GenerateUserSingleUseCerts, expected MFAChallenge", resp.Response)
}
mfaResp, err := PromptMFAChallenge(ctx, proxy.teleportClient.WebProxyAddr, mfaChal, "")
if err != nil {
return nil, trace.Wrap(err)
}
err = stream.Send(&proto.UserSingleUseCertsRequest{Request: &proto.UserSingleUseCertsRequest_MFAResponse{MFAResponse: mfaResp}})
if err != nil {
return nil, trace.Wrap(err)
}
resp, err = stream.Recv()
if err != nil {
return nil, trace.Wrap(err)
}
certResp := resp.GetCert()
if certResp == nil {
return nil, trace.BadParameter("server sent a %T on GenerateUserSingleUseCerts, expected SingleUseUserCert", resp.Response)
}
switch crt := certResp.Cert.(type) {
case *proto.SingleUseUserCert_SSH:
key.Cert = crt.SSH
case *proto.SingleUseUserCert_TLS:
switch initReq.Usage {
case proto.UserCertsRequest_Kubernetes:
key.KubeTLSCerts[initReq.KubernetesCluster] = crt.TLS
case proto.UserCertsRequest_Database:
key.DBTLSCerts[initReq.RouteToDatabase.ServiceName] = crt.TLS
default:
return nil, trace.BadParameter("server returned a TLS certificate but cert request usage was %s", initReq.Usage)
}
default:
return nil, trace.BadParameter("server sent a %T SingleUseUserCert in response", certResp.Cert)
}
log.Debug("Issued single-use user certificate after an MFA check.")
return key, nil
}
// RootClusterName returns name of the current cluster
@ -411,6 +601,7 @@ func (proxy *ProxyClient) ConnectToCluster(ctx context.Context, clusterName stri
if err != nil {
return nil, trace.Wrap(err, "failed to generate client TLS config")
}
tlsConfig.InsecureSkipVerify = proxy.teleportClient.InsecureSkipVerify
clt, err := auth.NewClient(client.Config{
Dialer: dialer,
Credentials: []client.Credentials{
@ -593,6 +784,11 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress NodeAdd
return proxy.PortForwardToNode(ctx, nodeAddress, user, quiet)
}
authMethod, err := proxy.sessionSSHCertificate(ctx, nodeAddress)
if err != nil {
return nil, trace.Wrap(err)
}
// parse destination first:
localAddr, err := utils.ParseAddr("tcp://" + proxy.proxyAddress)
if err != nil {
@ -677,7 +873,7 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress NodeAdd
)
sshConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{proxy.authMethod},
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: proxy.hostKeyCallback,
}
conn, chans, reqs, err := newClientConn(ctx, pipeNetConn, nodeAddress.ProxyFormat(), sshConfig)
@ -716,6 +912,11 @@ func (proxy *ProxyClient) ConnectToNode(ctx context.Context, nodeAddress NodeAdd
func (proxy *ProxyClient) PortForwardToNode(ctx context.Context, nodeAddress NodeAddr, user string, quiet bool) (*NodeClient, error) {
log.Infof("Client=%v jumping to node=%s", proxy.clientAddr, nodeAddress)
authMethod, err := proxy.sessionSSHCertificate(ctx, nodeAddress)
if err != nil {
return nil, trace.Wrap(err)
}
// after auth but before we create the first session, find out if the proxy
// is in recording mode or not
recordingProxy, err := proxy.isRecordingProxy()
@ -744,7 +945,7 @@ func (proxy *ProxyClient) PortForwardToNode(ctx context.Context, nodeAddress Nod
sshConfig := &ssh.ClientConfig{
User: user,
Auth: []ssh.AuthMethod{proxy.authMethod},
Auth: []ssh.AuthMethod{authMethod},
HostKeyCallback: proxy.hostKeyCallback,
}
conn, chans, reqs, err := newClientConn(ctx, proxyConn, nodeAddress.Addr, sshConfig)
@ -1121,3 +1322,21 @@ func (proxy *ProxyClient) currentCluster() (*services.Site, error) {
}
return nil, trace.NotFound("cluster %v not found", proxy.siteName)
}
func (proxy *ProxyClient) sessionSSHCertificate(ctx context.Context, nodeAddr NodeAddr) (ssh.AuthMethod, error) {
if _, err := proxy.teleportClient.localAgent.GetKey(); trace.IsNotFound(err) {
// Either running inside the web UI in a proxy or using an identity
// file. Fall back to whatever AuthMethod we currently have.
return proxy.authMethod, nil
}
key, err := proxy.IssueUserCertsWithMFA(ctx, ReissueParams{
NodeName: nodeName(nodeAddr.Addr),
RouteToCluster: nodeAddr.Cluster,
})
if err != nil {
return nil, trace.Wrap(err)
}
return key.AsAuthMethod()
}

View file

@ -35,8 +35,9 @@ import (
"github.com/gravitational/teleport/lib/sshutils"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/sirupsen/logrus"
"github.com/gravitational/trace"
)
const (
@ -654,3 +655,35 @@ func initKeysDir(dirPath string) (string, error) {
return dirPath, nil
}
// noLocalKeyStore is a LocalKeyStore representing the absence of a keystore.
// All methods return errors. This exists to avoid nil checking everywhere in
// LocalKeyAgent and prevent nil pointer panics.
type noLocalKeyStore struct{}
var errNoLocalKeyStore = trace.NotFound("there is no local keystore")
func (noLocalKeyStore) AddKey(proxy string, username string, key *Key) error {
return errNoLocalKeyStore
}
func (noLocalKeyStore) GetKey(proxy, username string, opts ...KeyOption) (*Key, error) {
return nil, errNoLocalKeyStore
}
func (noLocalKeyStore) DeleteKey(proxyHost, username string, opts ...KeyOption) error {
return errNoLocalKeyStore
}
func (noLocalKeyStore) DeleteKeyOption(proxyHost, username string, opts ...KeyOption) error {
return errNoLocalKeyStore
}
func (noLocalKeyStore) DeleteKeys() error { return errNoLocalKeyStore }
func (noLocalKeyStore) AddKnownHostKeys(hostname string, keys []ssh.PublicKey) error {
return errNoLocalKeyStore
}
func (noLocalKeyStore) GetKnownHostKeys(hostname string) ([]ssh.PublicKey, error) {
return nil, errNoLocalKeyStore
}
func (noLocalKeyStore) SaveCerts(proxy string, cas []auth.TrustedCerts) error {
return errNoLocalKeyStore
}
func (noLocalKeyStore) GetCerts(proxy string) (*x509.CertPool, error) { return nil, errNoLocalKeyStore }
func (noLocalKeyStore) GetCertsPEM(proxy string) ([][]byte, error) { return nil, errNoLocalKeyStore }

View file

@ -42,7 +42,7 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
return &proto.MFAAuthenticateResponse{}, nil
// TOTP only.
case c.TOTP != nil && len(c.U2F) == 0:
totpCode, err := prompt.Input(os.Stdout, os.Stdin, fmt.Sprintf("Enter an OTP code from a %sdevice", promptDevicePrefix))
totpCode, err := prompt.Input(os.Stderr, os.Stdin, fmt.Sprintf("Enter an OTP code from a %sdevice", promptDevicePrefix))
if err != nil {
return nil, trace.Wrap(err)
}
@ -51,7 +51,7 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
}}, nil
// U2F only.
case c.TOTP == nil && len(c.U2F) > 0:
fmt.Printf("Tap any %ssecurity key\n", promptDevicePrefix)
fmt.Fprintf(os.Stderr, "Tap any %ssecurity key\n", promptDevicePrefix)
return promptU2FChallenges(ctx, proxyAddr, c.U2F)
// Both TOTP and U2F.
@ -75,7 +75,7 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
}()
go func() {
totpCode, err := prompt.Input(os.Stdout, os.Stdin, fmt.Sprintf("Tap any %[1]ssecurity key or enter a code from a %[1]sOTP device", promptDevicePrefix, promptDevicePrefix))
totpCode, err := prompt.Input(os.Stderr, os.Stdin, fmt.Sprintf("Tap any %[1]ssecurity key or enter a code from a %[1]sOTP device", promptDevicePrefix, promptDevicePrefix))
res := response{kind: "TOTP", err: err}
if err == nil {
res.resp = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{
@ -99,7 +99,7 @@ func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthe
// Print a newline after the TOTP prompt, so that any future
// output doesn't print on the prompt line.
fmt.Println()
fmt.Fprintln(os.Stderr)
return res.resp, nil
case <-ctx.Done():
@ -126,6 +126,7 @@ func promptU2FChallenges(ctx context.Context, proxyAddr string, challenges []*pr
})
}
log.Debugf("prompting U2F devices with facet %q", facet)
resp, err := u2f.AuthenticateSignChallenge(ctx, facet, u2fChallenges...)
if err != nil {
return nil, trace.Wrap(err)

View file

@ -33,8 +33,9 @@ import (
"github.com/gravitational/teleport/lib/tlsca"
"github.com/gravitational/teleport/lib/utils"
"github.com/gravitational/trace"
"github.com/jonboulle/clockwork"
"github.com/gravitational/trace"
)
// NewJWTAuthority creates and returns a services.CertAuthority with a new
@ -273,12 +274,12 @@ type UserCertParams struct {
}
// Check checks the user certificate parameters
func (c UserCertParams) Check() error {
func (c *UserCertParams) CheckAndSetDefaults() error {
if len(c.PrivateCASigningKey) == 0 || c.CASigningAlg == "" {
return trace.BadParameter("PrivateCASigningKey and CASigningAlg are required")
}
if c.TTL < defaults.MinCertDuration {
return trace.BadParameter("TTL can't be less than %v", defaults.MinCertDuration)
c.TTL = defaults.MinCertDuration
}
if len(c.AllowedLogins) == 0 {
return trace.BadParameter("AllowedLogins are required")

View file

@ -90,6 +90,10 @@ var DefaultCertAuthorityRules = []Rule{
NewRule(KindCertAuthority, ReadNoSecrets()),
}
// ErrSessionMFARequired is returned by AccessChecker when access to a resource
// requires an MFA check.
var ErrSessionMFARequired = trace.AccessDenied("access to resource requires MFA")
// RoleNameForUser returns role name associated with a user.
func RoleNameForUser(name string) string {
return "user:" + name
@ -1396,7 +1400,7 @@ func (set RoleSet) CheckAccessToServer(login string, s Server, mfaVerified bool)
trace.Component: teleport.ComponentRBAC,
}).Debugf("Access to node %q denied, role %q requires per-session MFA; match(namespace=%v, label=%v, login=%v)",
s.GetHostname(), role.GetName(), namespaceMessage, labelsMessage, loginMessage)
return trace.AccessDenied("access to server requires MFA")
return ErrSessionMFARequired
}
// Check all remaining roles, even if we found a match.
// RequireSessionMFA should be enforced when at least one role has
@ -1464,7 +1468,7 @@ func (set RoleSet) CheckAccessToApp(namespace string, app *App, mfaVerified bool
trace.Component: teleport.ComponentRBAC,
}).Debugf("Access to app %q denied, role %q requires per-session MFA; match(namespace=%v, label=%v)",
app.Name, role.GetName(), namespaceMessage, labelsMessage)
return trace.AccessDenied("access to app requires MFA")
return ErrSessionMFARequired
}
// Check all remaining roles, even if we found a match.
// RequireSessionMFA should be enforced when at least one role has
@ -1532,7 +1536,7 @@ func (set RoleSet) CheckAccessToKubernetes(namespace string, kube *KubernetesClu
trace.Component: teleport.ComponentRBAC,
}).Debugf("Access to kubernetes cluster %q denied, role %q requires per-session MFA; match(namespace=%v, label=%v)",
kube.Name, role.GetName(), namespaceMessage, labelsMessage)
return trace.AccessDenied("access to kubernetes cluster requires MFA")
return ErrSessionMFARequired
}
// Check all remaining roles, even if we found a match.
// RequireSessionMFA should be enforced when at least one role has
@ -1684,7 +1688,7 @@ func (set RoleSet) CheckAccessToDatabase(server types.DatabaseServer, mfaVerifie
}
if role.GetOptions().RequireSessionMFA {
log.Debugf("Access to database %q denied, role %q requires per-session MFA", server.GetName(), role.GetName())
return trace.AccessDenied("access to database requires MFA")
return ErrSessionMFARequired
}
// Check all remaining roles, even if we found a match.
// RequireSessionMFA should be enforced when at least one role has
@ -1916,7 +1920,8 @@ const RoleSpecV3SchemaTemplate = `{
"max_connections": { "type": "number" },
"max_sessions": {"type": "number"},
"request_access": { "type": "string" },
"request_prompt": { "type": "string" }
"request_prompt": { "type": "string" },
"require_session_mfa": { "type": ["boolean", "string"] }
}
},
"allow": { "$ref": "#/definitions/role_condition" },

View file

@ -98,18 +98,19 @@ func (c *kubeCredentialsCommand) run(cf *CLIConf) error {
log.Debugf("Requesting TLS cert for kubernetes cluster %q", c.kubeCluster)
err = client.RetryWithRelogin(cf.Context, tc, func() error {
return tc.ReissueUserCerts(cf.Context, client.ReissueParams{
var err error
k, err = tc.IssueUserCertsWithMFA(cf.Context, client.ReissueParams{
RouteToCluster: c.teleportCluster,
KubernetesCluster: c.kubeCluster,
})
return err
})
if err != nil {
return trace.Wrap(err)
}
// ReissueUserCerts should cache the new cert on disk, re-read them.
k, err = tc.LocalAgent().GetKey(client.WithKubeCerts(c.teleportCluster))
if err != nil {
// Cache the new cert on disk for reuse.
if _, err := tc.LocalAgent().AddKey(k); err != nil {
return trace.Wrap(err)
}
@ -121,11 +122,15 @@ func (c *kubeCredentialsCommand) writeResponse(key *client.Key, kubeClusterName
if err != nil {
return trace.Wrap(err)
}
expiry := crt.NotAfter
// Indicate slightly earlier expiration to avoid the cert expiring
// mid-request, if possible.
if time.Until(expiry) > time.Minute {
expiry = expiry.Add(-1 * time.Minute)
}
resp := &clientauthentication.ExecCredential{
Status: &clientauthentication.ExecCredentialStatus{
// Indicate slightly earlier expiration to avoid the cert expiring
// mid-request.
ExpirationTimestamp: &metav1.Time{Time: crt.NotAfter.Add(-1 * time.Minute)},
ExpirationTimestamp: &metav1.Time{Time: expiry},
ClientCertificateData: string(key.KubeTLSCerts[kubeClusterName]),
ClientKeyData: string(key.Priv),
},

View file

@ -75,7 +75,7 @@ func (c *mfaLSCommand) run(cf *CLIConf) error {
return trace.Wrap(err)
}
defer pc.Close()
aci, err := pc.ConnectToCurrentCluster(cf.Context, false)
aci, err := pc.ConnectToRootCluster(cf.Context, false)
if err != nil {
return trace.Wrap(err)
}
@ -189,7 +189,7 @@ func (c *mfaAddCommand) addDeviceRPC(cf *CLIConf, devName string, devType proto.
return trace.Wrap(err)
}
defer pc.Close()
aci, err := pc.ConnectToCurrentCluster(cf.Context, false)
aci, err := pc.ConnectToRootCluster(cf.Context, false)
if err != nil {
return trace.Wrap(err)
}
@ -343,7 +343,7 @@ func (c *mfaRemoveCommand) run(cf *CLIConf) error {
return trace.Wrap(err)
}
defer pc.Close()
aci, err := pc.ConnectToCurrentCluster(cf.Context, false)
aci, err := pc.ConnectToRootCluster(cf.Context, false)
if err != nil {
return trace.Wrap(err)
}