Skip to content

Commit

Permalink
[v10] Fixes dissonance between disconnect_expired_cert vs `require_…
Browse files Browse the repository at this point in the history
…session_mfa` (#18607) (#19204)
  • Loading branch information
ibeckermayer authored and fheinecke committed Dec 16, 2022
1 parent f6533b0 commit c897f67
Show file tree
Hide file tree
Showing 21 changed files with 998 additions and 699 deletions.
9 changes: 9 additions & 0 deletions api/proto/teleport/legacy/types/events/events.proto
Original file line number Diff line number Diff line change
Expand Up @@ -3067,6 +3067,15 @@ message Identity {
(gogoproto.jsontag) = "allowed_resource_ids,omitempty",
(gogoproto.nullable) = false
];
// PreviousIdentityExpires is the expiry time of the identity/cert that this
// identity/cert was derived from. It is used to determine a session's hard
// deadline in cases where both require_session_mfa and disconnect_expired_cert
// are enabled. See https://github.com/gravitational/teleport/issues/18544.
google.protobuf.Timestamp PreviousIdentityExpires = 23 [
(gogoproto.stdtime) = true,
(gogoproto.nullable) = false,
(gogoproto.jsontag) = "prev_identity_expires"
];
}

// RouteToApp contains parameters for application access certificate requests.
Expand Down
1,173 changes: 613 additions & 560 deletions api/types/events/events.pb.go

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ const (
// CertExtensionMFAVerified is used to mark certificates issued after an MFA
// check.
CertExtensionMFAVerified = "mfa-verified"
// CertExtensionPreviousIdentityExpires is the extension that stores an RFC3339
// timestamp representing the expiry time of the identity/cert that this
// identity/cert was derived from. It is used to determine a session's hard
// deadline in cases where both require_session_mfa and disconnect_expired_cert
// are enabled. See https://github.com/gravitational/teleport/issues/18544.
CertExtensionPreviousIdentityExpires = "prev-identity-expires"
// CertExtensionClientIP is used to embed the IP of the client that created
// the certificate.
CertExtensionClientIP = "client-ip"
Expand Down
77 changes: 44 additions & 33 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,11 @@ type certRequest struct {
// mfaVerified is the UUID of an MFA device when this certRequest was
// created immediately after an MFA check.
mfaVerified string
// previousIdentityExpires is the expiry time of the identity/cert that this
// identity/cert was derived from. It is used to determine a session's hard
// deadline in cases where both require_session_mfa and disconnect_expired_cert
// are enabled. See https://github.com/gravitational/teleport/issues/18544.
previousIdentityExpires time.Time
// clientIP is an IP of the client requesting the certificate.
clientIP string
// sourceIP is an IP this certificate should be pinned to
Expand Down Expand Up @@ -1031,6 +1036,10 @@ func certRequestMFAVerified(mfaID string) certRequestOption {
return func(r *certRequest) { r.mfaVerified = mfaID }
}

func certRequestPreviousIdentityExpires(previousIdentityExpires time.Time) certRequestOption {
return func(r *certRequest) { r.previousIdentityExpires = previousIdentityExpires }
}

func certRequestClientIP(ip string) certRequestOption {
return func(r *certRequest) { r.clientIP = ip }
}
Expand Down Expand Up @@ -1298,29 +1307,30 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
}

params := services.UserCertParams{
CASigner: caSigner,
PublicUserKey: req.publicKey,
Username: req.user.GetName(),
Impersonator: req.impersonator,
AllowedLogins: allowedLogins,
TTL: sessionTTL,
Roles: req.checker.RoleNames(),
CertificateFormat: certificateFormat,
PermitPortForwarding: req.checker.CanPortForward(),
PermitAgentForwarding: req.checker.CanForwardAgents(),
PermitX11Forwarding: req.checker.PermitX11Forwarding(),
RouteToCluster: req.routeToCluster,
Traits: req.traits,
ActiveRequests: req.activeRequests,
MFAVerified: req.mfaVerified,
ClientIP: req.clientIP,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
CertificateExtensions: req.checker.CertificateExtensions(),
AllowedResourceIDs: requestedResourcesStr,
SourceIP: req.sourceIP,
ConnectionDiagnosticID: req.connectionDiagnosticID,
CASigner: caSigner,
PublicUserKey: req.publicKey,
Username: req.user.GetName(),
Impersonator: req.impersonator,
AllowedLogins: allowedLogins,
TTL: sessionTTL,
Roles: req.checker.RoleNames(),
CertificateFormat: certificateFormat,
PermitPortForwarding: req.checker.CanPortForward(),
PermitAgentForwarding: req.checker.CanForwardAgents(),
PermitX11Forwarding: req.checker.PermitX11Forwarding(),
RouteToCluster: req.routeToCluster,
Traits: req.traits,
ActiveRequests: req.activeRequests,
MFAVerified: req.mfaVerified,
PreviousIdentityExpires: req.previousIdentityExpires,
ClientIP: req.clientIP,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
CertificateExtensions: req.checker.CertificateExtensions(),
AllowedResourceIDs: requestedResourcesStr,
SourceIP: req.sourceIP,
ConnectionDiagnosticID: req.connectionDiagnosticID,
}
sshCert, err := a.Authority.GenerateUserCert(params)
if err != nil {
Expand Down Expand Up @@ -1392,16 +1402,17 @@ func (a *Server) generateUserCert(req certRequest) (*proto.Certs, error) {
Username: req.dbUser,
Database: req.dbName,
},
DatabaseNames: dbNames,
DatabaseUsers: dbUsers,
MFAVerified: req.mfaVerified,
ClientIP: req.clientIP,
AWSRoleARNs: roleARNs,
ActiveRequests: req.activeRequests.AccessRequests,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
AllowedResourceIDs: req.checker.GetAllowedResourceIDs(),
DatabaseNames: dbNames,
DatabaseUsers: dbUsers,
MFAVerified: req.mfaVerified,
PreviousIdentityExpires: req.previousIdentityExpires,
ClientIP: req.clientIP,
AWSRoleARNs: roleARNs,
ActiveRequests: req.activeRequests.AccessRequests,
DisallowReissue: req.disallowReissue,
Renewable: req.renewable,
Generation: req.generation,
AllowedResourceIDs: req.checker.GetAllowedResourceIDs(),
}
subject, err := identity.Subject()
if err != nil {
Expand Down
9 changes: 7 additions & 2 deletions lib/auth/grpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -2399,7 +2399,7 @@ func (g *GRPCServer) GenerateUserSingleUseCerts(stream proto.AuthService_Generat
return trace.Wrap(err)
}

// Generate the cert.
// Generate the cert
respCert, err := userSingleUseCertsGenerate(ctx, actx, *initReq, mfaDev)
if err != nil {
g.Entry.Warningf("Failed to generate single-use cert: %v", err)
Expand Down Expand Up @@ -2498,7 +2498,12 @@ func userSingleUseCertsGenerate(ctx context.Context, actx *grpcContext, req prot
}

// Generate the cert.
certs, err := actx.generateUserCerts(ctx, req, certRequestMFAVerified(mfaDev.Id), certRequestClientIP(clientIP))
certs, err := actx.generateUserCerts(
ctx, req,
certRequestMFAVerified(mfaDev.Id),
certRequestPreviousIdentityExpires(actx.Identity.GetIdentity().Expires),
certRequestClientIP(clientIP),
)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down
28 changes: 18 additions & 10 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,8 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
ctx := context.Background()
srv := newTestTLSServer(t)
clock := srv.Clock()
userCertTTL := 12 * time.Hour
userCertExpires := clock.Now().Add(userCertTTL)

authPref, err := types.NewAuthPreference(types.AuthPreferenceSpecV2{
Type: constants.Local,
Expand Down Expand Up @@ -806,7 +808,9 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
role.SetOptions(roleOpt)
err = srv.Auth().UpsertRole(ctx, role)
require.NoError(t, err)
cl, err := srv.NewClient(TestUser(user.GetName()))
testUser := TestUser(user.GetName())
testUser.TTL = userCertTTL
cl, err := srv.NewClient(testUser)
require.NoError(t, err)

// Register MFA devices for the fake user.
Expand Down Expand Up @@ -850,9 +854,10 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
cert, err := sshutils.ParseCertificate(crt)
require.NoError(t, err)

require.Equal(t, cert.Extensions[teleport.CertExtensionMFAVerified], webDevID)
require.Equal(t, webDevID, cert.Extensions[teleport.CertExtensionMFAVerified])
require.Equal(t, userCertExpires.Format(time.RFC3339), cert.Extensions[teleport.CertExtensionPreviousIdentityExpires])
require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionClientIP]).IsLoopback())
require.Equal(t, cert.ValidBefore, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()))
require.Equal(t, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()), cert.ValidBefore)
},
},
},
Expand All @@ -878,9 +883,10 @@ func TestGenerateUserSingleUseCert(t *testing.T) {
cert, err := sshutils.ParseCertificate(crt)
require.NoError(t, err)

require.Equal(t, cert.Extensions[teleport.CertExtensionMFAVerified], webDevID)
require.Equal(t, webDevID, cert.Extensions[teleport.CertExtensionMFAVerified])
require.Equal(t, userCertExpires.Format(time.RFC3339), cert.Extensions[teleport.CertExtensionPreviousIdentityExpires])
require.True(t, net.ParseIP(cert.Extensions[teleport.CertExtensionClientIP]).IsLoopback())
require.Equal(t, cert.ValidBefore, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()))
require.Equal(t, uint64(clock.Now().Add(teleport.UserSingleUseCertTTL).Unix()), cert.ValidBefore)
},
},
},
Expand All @@ -907,10 +913,11 @@ func TestGenerateUserSingleUseCert(t *testing.T) {

identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, identity.MFAVerified, webDevID)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.ClientIP).IsLoopback())
require.Equal(t, identity.Usage, []string{teleport.UsageKubeOnly})
require.Equal(t, identity.KubernetesCluster, "kube-a")
require.Equal(t, []string{teleport.UsageKubeOnly}, identity.Usage)
require.Equal(t, "kube-a", identity.KubernetesCluster)
},
},
},
Expand Down Expand Up @@ -939,9 +946,10 @@ func TestGenerateUserSingleUseCert(t *testing.T) {

identity, err := tlsca.FromSubject(cert.Subject, cert.NotAfter)
require.NoError(t, err)
require.Equal(t, identity.MFAVerified, webDevID)
require.Equal(t, webDevID, identity.MFAVerified)
require.Equal(t, userCertExpires, identity.PreviousIdentityExpires)
require.True(t, net.ParseIP(identity.ClientIP).IsLoopback())
require.Equal(t, identity.Usage, []string{teleport.UsageDatabaseOnly})
require.Equal(t, []string{teleport.UsageDatabaseOnly}, identity.Usage)
require.Equal(t, identity.RouteToDatabase.ServiceName, "db-a")
},
},
Expand Down
7 changes: 5 additions & 2 deletions lib/auth/native/native.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func (k *Keygen) GenerateHostCertWithoutValidation(c services.HostCertParams) ([
return ssh.MarshalAuthorizedKey(cert), nil
}

// GenerateUserCert generates a user certificate with the passed in parameters.
// GenerateUserCert generates a user ssh 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.CheckAndSetDefaults(); err != nil {
Expand All @@ -229,7 +229,7 @@ func (k *Keygen) GenerateUserCert(c services.UserCertParams) ([]byte, error) {
// See: https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL.certkeys?annotate=HEAD.
const sourceAddress = "source-address"

// GenerateUserCertWithoutValidation generates a user certificate with the
// GenerateUserCertWithoutValidation generates a user ssh certificate with the
// passed in parameters without validating them.
func (k *Keygen) GenerateUserCertWithoutValidation(c services.UserCertParams) ([]byte, error) {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey(c.PublicUserKey)
Expand Down Expand Up @@ -266,6 +266,9 @@ func (k *Keygen) GenerateUserCertWithoutValidation(c services.UserCertParams) ([
if c.MFAVerified != "" {
cert.Permissions.Extensions[teleport.CertExtensionMFAVerified] = c.MFAVerified
}
if !c.PreviousIdentityExpires.IsZero() {
cert.Permissions.Extensions[teleport.CertExtensionPreviousIdentityExpires] = c.PreviousIdentityExpires.Format(time.RFC3339)
}
if c.ClientIP != "" {
cert.Permissions.Extensions[teleport.CertExtensionClientIP] = c.ClientIP
}
Expand Down
23 changes: 15 additions & 8 deletions lib/auth/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ func (a *authorizer) authorizeRemoteUser(ctx context.Context, u RemoteUser) (*Co
// Adjust expiry based on locally mapped roles.
ttl := time.Until(u.Identity.Expires)
ttl = checker.AdjustSessionTTL(ttl)
var previousIdentityExpires time.Time
if u.Identity.MFAVerified != "" {
prevIdentityTTL := time.Until(u.Identity.PreviousIdentityExpires)
prevIdentityTTL = checker.AdjustSessionTTL(prevIdentityTTL)
previousIdentityExpires = time.Now().Add(prevIdentityTTL)
}

kubeUsers, kubeGroups, err := checker.CheckKubeGroupsAndUsers(ttl, false)
// IsNotFound means that the user has no k8s users or groups, which is fine
Expand All @@ -255,14 +261,15 @@ func (a *authorizer) authorizeRemoteUser(ctx context.Context, u RemoteUser) (*Co
// This prevents downstream users from accidentally using the unmapped
// identity information and confusing who's accessing a resource.
identity := tlsca.Identity{
Username: user.GetName(),
Groups: user.GetRoles(),
Traits: accessInfo.Traits,
Principals: principals,
KubernetesGroups: kubeGroups,
KubernetesUsers: kubeUsers,
TeleportCluster: a.clusterName,
Expires: time.Now().Add(ttl),
Username: user.GetName(),
Groups: user.GetRoles(),
Traits: accessInfo.Traits,
Principals: principals,
KubernetesGroups: kubeGroups,
KubernetesUsers: kubeUsers,
TeleportCluster: a.clusterName,
Expires: time.Now().Add(ttl),
PreviousIdentityExpires: previousIdentityExpires,

// These fields are for routing and restrictions, safe to re-use from
// unmapped identity.
Expand Down
24 changes: 24 additions & 0 deletions lib/auth/test/suite.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,30 @@ func (s *AuthSuite) GenerateUserCert(c *check.C) {

outImpersonator := parsedCert.Extensions[teleport.CertExtensionImpersonator]
c.Assert(outImpersonator, check.DeepEquals, impersonator)

// Check that MFAVerified and PreviousIdentityExpires are encoded into ssh cert
clock := clockwork.NewFakeClock()
cert, err = s.A.GenerateUserCert(services.UserCertParams{
CASigner: caSigner,
PublicUserKey: pub,
Username: "user",
AllowedLogins: []string{"root"},
TTL: time.Minute,
CertificateFormat: constants.CertificateFormatStandard,
MFAVerified: "mfa-device-id",
PreviousIdentityExpires: clock.Now().Add(time.Hour),
})
c.Assert(err, check.IsNil)
parsedCert, err = sshutils.ParseCertificate(cert)
c.Assert(err, check.IsNil)
devID, ok := parsedCert.Extensions[teleport.CertExtensionMFAVerified]
c.Assert(ok, check.Equals, true)
c.Assert(devID, check.Equals, "mfa-device-id")
prevIDExpiresStr, ok := parsedCert.Extensions[teleport.CertExtensionPreviousIdentityExpires]
c.Assert(ok, check.Equals, true)
prevIDExpires, err := time.Parse(time.RFC3339, prevIDExpiresStr)
c.Assert(err, check.IsNil)
c.Assert(prevIDExpires, check.Equals, clock.Now().Add(time.Hour))
}

func checkCertExpiry(cert []byte, after, before time.Time) error {
Expand Down

0 comments on commit c897f67

Please sign in to comment.