diff --git a/api/gen/proto/go/teleport/decision/v1alpha1/ssh_access.pb.go b/api/gen/proto/go/teleport/decision/v1alpha1/ssh_access.pb.go index 3ca35c94082..38afeca1039 100644 --- a/api/gen/proto/go/teleport/decision/v1alpha1/ssh_access.pb.go +++ b/api/gen/proto/go/teleport/decision/v1alpha1/ssh_access.pb.go @@ -163,6 +163,8 @@ const ( PreconditionKind_PRECONDITION_KIND_UNSPECIFIED PreconditionKind = 0 // PreconditionKindInBandMFA requires in-band MFA to be completed. PreconditionKind_PRECONDITION_KIND_IN_BAND_MFA PreconditionKind = 1 + // PreconditionKindPinSourceIP enforces the client IP observed at login matches the client IP of SSH connections. + PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP PreconditionKind = 2 ) // Enum value maps for PreconditionKind. @@ -170,10 +172,12 @@ var ( PreconditionKind_name = map[int32]string{ 0: "PRECONDITION_KIND_UNSPECIFIED", 1: "PRECONDITION_KIND_IN_BAND_MFA", + 2: "PRECONDITION_KIND_PIN_SOURCE_IP", } PreconditionKind_value = map[string]int32{ - "PRECONDITION_KIND_UNSPECIFIED": 0, - "PRECONDITION_KIND_IN_BAND_MFA": 1, + "PRECONDITION_KIND_UNSPECIFIED": 0, + "PRECONDITION_KIND_IN_BAND_MFA": 1, + "PRECONDITION_KIND_PIN_SOURCE_IP": 2, } ) @@ -1082,10 +1086,11 @@ const file_teleport_decision_v1alpha1_ssh_access_proto_rawDesc = "" + "\x1aHOST_USER_MODE_UNSPECIFIED\x10\x00\x12\x17\n" + "\x13HOST_USER_MODE_KEEP\x10\x01\x12\x17\n" + "\x13HOST_USER_MODE_DROP\x10\x02\x12\x19\n" + - "\x15HOST_USER_MODE_STATIC\x10\x03*X\n" + + "\x15HOST_USER_MODE_STATIC\x10\x03*}\n" + "\x10PreconditionKind\x12!\n" + "\x1dPRECONDITION_KIND_UNSPECIFIED\x10\x00\x12!\n" + - "\x1dPRECONDITION_KIND_IN_BAND_MFA\x10\x01BZZXgithub.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1;decisionpbb\x06proto3" + "\x1dPRECONDITION_KIND_IN_BAND_MFA\x10\x01\x12#\n" + + "\x1fPRECONDITION_KIND_PIN_SOURCE_IP\x10\x02BZZXgithub.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1;decisionpbb\x06proto3" var ( file_teleport_decision_v1alpha1_ssh_access_proto_rawDescOnce sync.Once diff --git a/api/proto/teleport/decision/v1alpha1/ssh_access.proto b/api/proto/teleport/decision/v1alpha1/ssh_access.proto index c0042a8ff65..b6630e78321 100644 --- a/api/proto/teleport/decision/v1alpha1/ssh_access.proto +++ b/api/proto/teleport/decision/v1alpha1/ssh_access.proto @@ -240,4 +240,6 @@ enum PreconditionKind { PRECONDITION_KIND_UNSPECIFIED = 0; // PreconditionKindInBandMFA requires in-band MFA to be completed. PRECONDITION_KIND_IN_BAND_MFA = 1; + // PreconditionKindPinSourceIP enforces the client IP observed at login matches the client IP of SSH connections. + PRECONDITION_KIND_PIN_SOURCE_IP = 2; } diff --git a/lib/authz/permissions.go b/lib/authz/permissions.go index f5beac447a5..893f8919539 100644 --- a/lib/authz/permissions.go +++ b/lib/authz/permissions.go @@ -49,6 +49,7 @@ import ( "github.com/gravitational/teleport/lib/services" "github.com/gravitational/teleport/lib/services/readonly" "github.com/gravitational/teleport/lib/tlsca" + logutils "github.com/gravitational/teleport/lib/utils/log" ) // NewAdminContext returns new admin auth context @@ -438,7 +439,12 @@ func (a *authorizer) Authorize(ctx context.Context) (authCtx *Context, err error return nil, trace.Wrap(err) } - if err := CheckIPPinning(ctx, authContext.Identity.GetIdentity(), authContext.Checker.PinSourceIP(), a.logger); err != nil { + var clientAddr string + if clientSrcAddr, err := ClientSrcAddrFromContext(ctx); err == nil { + clientAddr = clientSrcAddr.String() + } + + if err := CheckIPPinning(ctx, clientAddr, authContext.Identity.GetIdentity().PinnedIP, authContext.Checker.PinSourceIP(), a.logger); err != nil { return nil, trace.Wrap(err) } @@ -655,31 +661,40 @@ var ErrIPPinningNotAllowed = &trace.AccessDeniedError{Message: "IP pinning is no "downgraded or are behind a L4 load balancers with PROXY protocol enabled without explicitly setting 'proxy_protocol: on'" + "in the proxy_service and/or auth_service config."} -// CheckIPPinning verifies IP pinning for the identity, using the client IP taken from context. +// CheckIPPinning verifies IP pinning for the identity, using the provided client addr. // Check is considered successful if no error is returned. -func CheckIPPinning(ctx context.Context, identity tlsca.Identity, pinSourceIP bool, log *slog.Logger) error { - if identity.PinnedIP == "" { +func CheckIPPinning(ctx context.Context, clientAddr string, pinnedIP string, pinSourceIP bool, log *slog.Logger) error { + if pinnedIP == "" { if pinSourceIP { return trace.Wrap(ErrIPPinningMissing) } return nil } - clientSrcAddr, err := ClientSrcAddrFromContext(ctx) + if clientAddr == "" { + return trace.BadParameter("pinned IP is required for the user, but the client IP was not provided") + } + + clientHost, clientPort, err := net.SplitHostPort(clientAddr) if err != nil { return trace.Wrap(err) } - clientIP, clientPort, err := net.SplitHostPort(clientSrcAddr.String()) - if err != nil { - return trace.Wrap(err) + parsedClientIP := net.ParseIP(clientHost) + if parsedClientIP == nil { + return trace.BadParameter("client IP address %q is not a valid IP address", clientHost) } - if clientIP != identity.PinnedIP { + parsedPinnedIP := net.ParseIP(pinnedIP) + if parsedPinnedIP == nil { + return trace.BadParameter("pinned IP address %q is not a valid IP address", pinnedIP) + } + + if !parsedClientIP.Equal(net.ParseIP(pinnedIP)) { if log != nil { log.DebugContext(ctx, "Pinned IP and client IP mismatch", - "client_ip", clientIP, - "pinned_ip", identity.PinnedIP, + "client_ip", logutils.StringerAttr(parsedClientIP), + "pinned_ip", logutils.StringerAttr(parsedPinnedIP), ) } return trace.Wrap(ErrIPPinningMismatch) @@ -688,7 +703,7 @@ func CheckIPPinning(ctx context.Context, identity tlsca.Identity, pinSourceIP bo // For security reason we don't allow such connection for IP pinning because we can't rely on client IP being correct. if clientPort == "0" { if log != nil { - log.DebugContext(ctx, "client address is not allowed to use IP pinning", "client_ip", clientIP, "pinned_ip", identity.PinnedIP, "error", ErrIPPinningNotAllowed.Message) + log.DebugContext(ctx, "client address is not allowed to use IP pinning", "client_ip", parsedClientIP, "pinned_ip", pinnedIP, "error", ErrIPPinningNotAllowed.Message) } return trace.Wrap(ErrIPPinningNotAllowed) } diff --git a/lib/authz/permissions_test.go b/lib/authz/permissions_test.go index d5d8e2cdd32..41d2f292c7f 100644 --- a/lib/authz/permissions_test.go +++ b/lib/authz/permissions_test.go @@ -50,7 +50,6 @@ import ( "github.com/gravitational/teleport/lib/services/local" "github.com/gravitational/teleport/lib/services/readonly" "github.com/gravitational/teleport/lib/tlsca" - "github.com/gravitational/teleport/lib/utils" ) const ( @@ -980,7 +979,7 @@ func TestCheckIPPinning(t *testing.T) { desc: "IP pinning enabled, missing client IP", pinnedIP: "127.0.0.1", pinIP: true, - wantErr: "client source address was not found in the context", + wantErr: "client IP was not provided", }, { desc: "IP pinning enabled, port=0 (marked by proxyProtocolMode unspecified)", @@ -995,23 +994,51 @@ func TestCheckIPPinning(t *testing.T) { pinnedIP: "127.0.0.1", pinIP: true, }, + { + desc: "invalid client IP", + clientAddr: "localhost:1", + pinnedIP: "127.0.0.1:1", + pinIP: true, + wantErr: "\"localhost\" is not a valid IP address", + }, + { + desc: "invalid pinned IP", + clientAddr: "127.0.0.1:1", + pinnedIP: "localhost", + pinIP: true, + wantErr: "\"localhost\" is not a valid IP address", + }, + // IPv6 + { + desc: "IPv6: correct IP pinning", + clientAddr: "[2001:db8::1]:444", + pinnedIP: "2001:db8::1", + pinIP: true, + }, + { + desc: "IPv6: pinned IP doesn't match", + clientAddr: "[2001:db8::1]:444", + pinnedIP: "2001:db8::2", + pinIP: true, + wantErr: authz.ErrIPPinningMismatch.Error(), + }, + { + desc: "IPv6: equivalency with compression", + clientAddr: "[2001:db8:0:0:0:0:0:1]:444", + pinnedIP: "2001:db8::1", + pinIP: true, + }, } for _, tt := range testCases { - ctx := context.Background() - if tt.clientAddr != "" { - ctx = authz.ContextWithClientSrcAddr(ctx, utils.MustParseAddr(tt.clientAddr)) - } - identity := tlsca.Identity{PinnedIP: tt.pinnedIP} - - err := authz.CheckIPPinning(ctx, identity, tt.pinIP, nil) - - if tt.wantErr != "" { - require.ErrorContains(t, err, tt.wantErr) - } else { - require.NoError(t, err) - } - + t.Run(tt.desc, func(t *testing.T) { + err := authz.CheckIPPinning(t.Context(), tt.clientAddr, tt.pinnedIP, tt.pinIP, nil) + if tt.wantErr != "" { + require.ErrorContains(t, err, tt.wantErr) + } else { + require.NoError(t, err) + } + }) } } diff --git a/lib/decision/service.go b/lib/decision/service.go index 332b409a2ff..3734e422233 100644 --- a/lib/decision/service.go +++ b/lib/decision/service.go @@ -247,6 +247,12 @@ func (s *Service) EvaluateSSHAccess(ctx context.Context, req *decisionpb.Evaluat }, } + if accessChecker.PinSourceIP() { + permit.Preconditions = append(permit.Preconditions, &decisionpb.Precondition{ + Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP, + }) + } + return &decisionpb.EvaluateSSHAccessResponse{ Decision: &decisionpb.EvaluateSSHAccessResponse_Permit{ Permit: permit, diff --git a/lib/srv/authhandlers.go b/lib/srv/authhandlers.go index 516b9c7efe1..7d69634084b 100644 --- a/lib/srv/authhandlers.go +++ b/lib/srv/authhandlers.go @@ -752,16 +752,20 @@ func (h *AuthHandlers) VerifiedPublicKeyCallback( return nil, trace.BadParameter("failed to decode ssh identity from cert: %v %v", key.Type(), sshutils.Fingerprint(key)) } - // Determine if keyboard-interactive authentication is required to satisfy any outstanding preconditions. Assume - // that it is required until we can verify that it is not. - requiresKeyboardInteractive := true + // Determine if keyboard-interactive authentication is required to satisfy any outstanding preconditions. + requiresKeyboardInteractive := false for _, precond := range preconds { switch precond.GetKind() { case decisionpb.PreconditionKind_PRECONDITION_KIND_IN_BAND_MFA: - if requiresKeyboardInteractive, err = requiresInBandMFA(id, conn); err != nil { + required, err := requiresInBandMFA(id, conn) + if err != nil { return nil, trace.Wrap(err) } + requiresKeyboardInteractive = requiresKeyboardInteractive || required + + case decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP: + // No interactive auth needed. default: // If an unknown or unsupported precondition is provided, fail close to prevent potential auth bypasses. @@ -962,6 +966,7 @@ type proxyingPermit struct { DisconnectExpiredCert time.Time MappedRoles []string SessionRecordingMode constants.SessionRecordingMode + PinSourceIP bool } func (a *ahLoginChecker) evaluateProxying(ident *sshca.Identity, ca types.CertAuthority, clusterName string) (*proxyingPermit, error) { @@ -1008,6 +1013,7 @@ func (a *ahLoginChecker) evaluateProxying(ident *sshca.Identity, ca types.CertAu DisconnectExpiredCert: getDisconnectExpiredCertFromSSHIdentity(accessChecker, authPref, ident), MappedRoles: accessInfo.Roles, SessionRecordingMode: accessChecker.SessionRecordingMode(constants.SessionRecordingServiceSSH), + PinSourceIP: accessChecker.PinSourceIP(), }, nil } @@ -1160,7 +1166,7 @@ func (a *ahLoginChecker) evaluateScopedSSHAccess(ident *sshca.Identity, ca types return nil, trace.Wrap(err) } - return &decisionpb.SSHAccessPermit{ + permit := &decisionpb.SSHAccessPermit{ ForwardAgent: checker.SSH().CheckAgentForward(osUser) == nil, X11Forwarding: checker.SSH().PermitX11Forwarding(), MaxConnections: checker.SSH().MaxConnections(), @@ -1181,7 +1187,15 @@ func (a *ahLoginChecker) evaluateScopedSSHAccess(ident *sshca.Identity, ca types HostUserCreationAllowedBy: hostUsersDecision.AllowedBy, HostUserCreationDeniedBy: hostUsersDecision.DeniedBy, }, - }, nil + } + + if checker.PinSourceIP() { + permit.Preconditions = append(permit.Preconditions, &decisionpb.Precondition{ + Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP, + }) + } + + return permit, nil } // evaluateSSHAccess checks the given certificate (supplied by a connected @@ -1273,6 +1287,12 @@ func (a *ahLoginChecker) evaluateSSHAccess(ident *sshca.Identity, ca types.CertA return nil, trace.Wrap(err) } + if accessChecker.PinSourceIP() { + preconds = append(preconds, &decisionpb.Precondition{ + Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP, + }) + } + return &decisionpb.SSHAccessPermit{ ForwardAgent: accessChecker.CheckAgentForward(osUser) == nil, X11Forwarding: accessChecker.PermitX11Forwarding(), diff --git a/lib/srv/authhandlers_test.go b/lib/srv/authhandlers_test.go index 1ac147dc8d9..ba21cfff911 100644 --- a/lib/srv/authhandlers_test.go +++ b/lib/srv/authhandlers_test.go @@ -834,14 +834,29 @@ func TestRBACJoinMFA(t *testing.T) { _, err = server.auth.CreateRole(ctx, joinRole) require.NoError(t, err) + pinJoinRole, err := types.NewRole("pinJoin", types.RoleSpecV6{ + Options: types.RoleOptions{ + PinSourceIP: true, + }, + Allow: types.RoleConditions{ + NodeLabels: types.Labels{ + types.Wildcard: []string{types.Wildcard}, + }, + }, + }) + require.NoError(t, err) + _, err = server.auth.CreateRole(ctx, pinJoinRole) + require.NoError(t, err) + _, err = server.auth.CreateClusterNetworkingConfig(ctx, types.DefaultClusterNetworkingConfig()) require.NoError(t, err) tests := []struct { - name string - authPref types.AuthPreference - role string - testError func(t *testing.T, err error) + name string + authPref types.AuthPreference + role string + expectPinSourceIP bool + testError func(t *testing.T, err error) }{ { name: "MFA cluster auth, MFA role", @@ -878,6 +893,15 @@ func TestRBACJoinMFA(t *testing.T) { require.NoError(t, err) }, }, + { + name: "no MFA cluster auth, pin source IP role", + authPref: noMFAAuthPref, + role: pinJoinRole.GetName(), + expectPinSourceIP: true, + testError: func(t *testing.T, err error) { + require.NoError(t, err) + }, + }, } for _, tt := range tests { @@ -911,8 +935,13 @@ func TestRBACJoinMFA(t *testing.T) { ident, err := sshca.DecodeIdentity(cert) require.NoError(t, err) - _, err = ah.evaluateSSHAccess(ident, userCA, clusterName, node, teleport.SSHSessionJoinPrincipal) + permit, err := ah.evaluateSSHAccess(ident, userCA, clusterName, node, teleport.SSHSessionJoinPrincipal) tt.testError(t, err) + if err == nil && tt.expectPinSourceIP { + require.Contains(t, permit.Preconditions, &decisionpb.Precondition{ + Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP, + }) + } }) } } @@ -1133,6 +1162,17 @@ func TestVerifiedPublicKeyCallback(t *testing.T) { ) require.NoError(t, err) + ipPinningPrecondPermitRaw, err := protojson.Marshal( + &decisionpb.SSHAccessPermit{ + Preconditions: []*decisionpb.Precondition{ + { + Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP, + }, + }, + }, + ) + require.NoError(t, err) + modernClientConn := &mockConnMetadata{clientVersion: []byte(apissh.ClientVersionWithFeatures(apissh.InBandMFAFeature))} legacyClientConn := &mockConnMetadata{clientVersion: []byte("SSH-2.0-Go")} @@ -1210,6 +1250,20 @@ func TestVerifiedPublicKeyCallback(t *testing.T) { require.Same(t, inPerms, outPerms) }) + t.Run("permit with pin source ip precondition allows auth without keyboard interactive", func(t *testing.T) { + ah := &AuthHandlers{} + + inPerms := &ssh.Permissions{ + Extensions: map[string]string{ + utils.ExtIntSSHAccessPermit: string(ipPinningPrecondPermitRaw), + }, + } + + outPerms, err := ah.VerifiedPublicKeyCallback(&mockConnMetadata{}, cert, inPerms, "") + require.NoError(t, err) + require.Same(t, inPerms, outPerms) + }) + t.Run("permit with unsupported preconditions fails closed", func(t *testing.T) { ah := &AuthHandlers{} diff --git a/lib/srv/session_control.go b/lib/srv/session_control.go index 1edf73598b4..66bb3f13086 100644 --- a/lib/srv/session_control.go +++ b/lib/srv/session_control.go @@ -22,6 +22,7 @@ import ( "context" "io" "log/slog" + "slices" "strings" "github.com/gravitational/trace" @@ -33,10 +34,12 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/constants" apidefaults "github.com/gravitational/teleport/api/defaults" + decisionpb "github.com/gravitational/teleport/api/gen/proto/go/teleport/decision/v1alpha1" "github.com/gravitational/teleport/api/observability/tracing" "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/decision" dtauthz "github.com/gravitational/teleport/lib/devicetrust/authz" "github.com/gravitational/teleport/lib/events" @@ -164,6 +167,7 @@ type webSessionPermit struct { LockTargets []types.LockTarget PrivateKeyPolicy keys.PrivateKeyPolicy MaxConnections int64 + PinSourceIP bool } // WebSessionController is a wrapper around [SessionController] which can be @@ -208,6 +212,7 @@ func WebSessionController(controller *SessionController) func(ctx context.Contex PrivateKeyPolicy: privateKeyPolicy, LockTargets: lockTargets, MaxConnections: accessChecker.MaxConnections(), + PinSourceIP: accessChecker.PinSourceIP(), } identity := IdentityContext{ @@ -250,22 +255,28 @@ func (s *SessionController) AcquireSessionContext(ctx context.Context, identity var lockTargets []types.LockTarget var requiredPolicy keys.PrivateKeyPolicy var maxConnections int64 + var pinSourceIP bool switch { case identity.AccessPermit != nil: lockingMode = constants.LockingMode(identity.AccessPermit.LockingMode) lockTargets = decision.LockTargetsFromProto(identity.AccessPermit.LockTargets) requiredPolicy = keys.PrivateKeyPolicy(identity.AccessPermit.PrivateKeyPolicy) maxConnections = identity.AccessPermit.MaxConnections + pinSourceIP = slices.ContainsFunc(identity.AccessPermit.Preconditions, func(p *decisionpb.Precondition) bool { + return p.Kind == decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP + }) case identity.ProxyingPermit != nil: lockingMode = identity.ProxyingPermit.LockingMode lockTargets = identity.ProxyingPermit.LockTargets requiredPolicy = identity.ProxyingPermit.PrivateKeyPolicy maxConnections = identity.ProxyingPermit.MaxConnections + pinSourceIP = identity.ProxyingPermit.PinSourceIP case identity.webSessionPermit != nil: lockingMode = identity.webSessionPermit.LockingMode lockTargets = identity.webSessionPermit.LockTargets requiredPolicy = identity.webSessionPermit.PrivateKeyPolicy maxConnections = identity.webSessionPermit.MaxConnections + pinSourceIP = identity.webSessionPermit.PinSourceIP default: return nil, trace.BadParameter("session context requires one of AccessPermit, ProxyingPermit, or webSessionPermit to be set (this is a bug)") } @@ -279,6 +290,10 @@ func (s *SessionController) AcquireSessionContext(ctx context.Context, identity return ctx, keys.NewPrivateKeyPolicyError(requiredPolicy) } + if err := authz.CheckIPPinning(ctx, remoteAddr, identity.UnmappedIdentity.PinnedIP, pinSourceIP, s.cfg.Logger); err != nil { + return nil, trace.Wrap(err) + } + // Don't apply the following checks in non-node contexts. if s.cfg.Component != teleport.ComponentNode { return ctx, nil diff --git a/lib/srv/session_control_test.go b/lib/srv/session_control_test.go index 072fbba03f3..1e706ff212e 100644 --- a/lib/srv/session_control_test.go +++ b/lib/srv/session_control_test.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport/api/types" apievents "github.com/gravitational/teleport/api/types/events" "github.com/gravitational/teleport/api/utils/keys" + "github.com/gravitational/teleport/lib/authz" "github.com/gravitational/teleport/lib/events" "github.com/gravitational/teleport/lib/events/eventstest" "github.com/gravitational/teleport/lib/modules" @@ -456,6 +457,45 @@ func TestSessionController_AcquireSessionContext(t *testing.T) { assert.NoError(t, err, "AcquireSessionContext returned an unexpected error") }, }, + { + name: "session rejected due to pinned ip mismatch", + cfg: SessionControllerConfig{ + Clock: clock, + Semaphores: mockSemaphores{}, + AccessPoint: mockAccessPoint{ + authPreference: &types.AuthPreferenceV2{ + Spec: types.AuthPreferenceSpecV2{ + LockingMode: constants.LockingModeStrict, + }, + }, + clusterName: &types.ClusterNameV2{Spec: types.ClusterNameSpecV2{ClusterName: "llama"}}, + }, + LockEnforcer: mockLockEnforcer{}, + Emitter: emitter, + Component: teleport.ComponentNode, + ServerID: "1234", + }, + identity: IdentityContext{ + UnmappedIdentity: &sshca.Identity{ + Username: "alpaca", + PinnedIP: "1.2.3.4", + }, + TeleportUser: "alpaca", + Login: "alpaca", + AccessPermit: &decisionpb.SSHAccessPermit{ + MaxConnections: 1, + PrivateKeyPolicy: string(keys.PrivateKeyPolicyNone), + Preconditions: []*decisionpb.Precondition{ + { + Kind: decisionpb.PreconditionKind_PRECONDITION_KIND_PIN_SOURCE_IP, + }, + }, + }, + }, + assertion: func(t *testing.T, ctx context.Context, err error, emitter *eventstest.MockRecorderEmitter) { + require.ErrorIs(t, err, authz.ErrIPPinningMismatch) + }, + }, } for _, tt := range cases { t.Run(tt.name, func(t *testing.T) { diff --git a/lib/sshca/identity.go b/lib/sshca/identity.go index 813d0f6f872..c6f648f5627 100644 --- a/lib/sshca/identity.go +++ b/lib/sshca/identity.go @@ -102,7 +102,8 @@ type Identity struct { PreviousIdentityExpires time.Time // LoginIP is an observed IP of the client on the moment of certificate creation. LoginIP string - // PinnedIP is an IP from which client must communicate with Teleport. + // PinnedIP is an IP from which client must communicate with Teleport. If set, + // this implies that IP Pinning was required at the time of certificate creation. PinnedIP string // DisallowReissue flags that any attempt to request new certificates while // authenticated with this cert should be denied. diff --git a/lib/web/sessions.go b/lib/web/sessions.go index 5b43829454c..32272f091bb 100644 --- a/lib/web/sessions.go +++ b/lib/web/sessions.go @@ -1034,7 +1034,12 @@ func (s *sessionCache) getOrCreateSession(ctx context.Context, user, sessionID s } // Enforce IP Pinning if it is present in the user's certificate. - if err := authz.CheckIPPinning(ctx, *identity, false, s.log); err != nil { + var clientAddr string + if clientSrcAddr, err := authz.ClientSrcAddrFromContext(ctx); err == nil { + clientAddr = clientSrcAddr.String() + } + + if err := authz.CheckIPPinning(ctx, clientAddr, identity.PinnedIP, false, s.log); err != nil { return nil, trace.Wrap(err) } @@ -1174,7 +1179,12 @@ func (s *sessionCache) newSessionContextFromSession(ctx context.Context, session return nil, trace.Wrap(err) } - if err := authz.CheckIPPinning(ctx, *identity, false, s.log); err != nil { + var clientAddr string + if clientSrcAddr, err := authz.ClientSrcAddrFromContext(ctx); err == nil { + clientAddr = clientSrcAddr.String() + } + + if err := authz.CheckIPPinning(ctx, clientAddr, identity.PinnedIP, false, s.log); err != nil { return nil, trace.Wrap(err) }