Skip to content

Fixed app session start audit events for Device Trust failures#62019

Merged
21KennethTran merged 1 commit intomasterfrom
kennethtran/device-trust-app-rejection
Jan 23, 2026
Merged

Fixed app session start audit events for Device Trust failures#62019
21KennethTran merged 1 commit intomasterfrom
kennethtran/device-trust-app-rejection

Conversation

@21KennethTran
Copy link
Copy Markdown
Contributor

@21KennethTran 21KennethTran commented Dec 5, 2025

When Device Trust and MFA is enabled for accessing a resource and a user without a trusted device tries to access that resource, their session should get rejected and an auth attempt fail event is generated in the audit log.

Since this type of session rejection is an auth failure, the type of event generated in the audit logs should be an AppSessionStart. The AppMetadata struct is added to this to provide more information about the resource the user tried to access.

Following the recreation steps from #54139 ,

Before:

device-trust-before.mov

After:

device-trust-after2.mov

Here is the audit log fields with the changes from the demo above:

{
  "app_name": "debug-app",
  "app_public_addr": "debug-app.localhost",
  "app_uri": "http://localhost:8000/",
  "cluster_name": "localhost",
  "code": "T2007E",
  "ei": 0,
  "error": "access to resource requires a trusted device",
  "event": "app.session.start",
  "message": "access to application debug-app requires a trusted device",
  "namespace": "default",
  "public_addr": "",
  "server_id": "4948fc26-21e7-4aaf-a45b-02b39d392a0d",
  "server_version": "19.0.0-dev",
  "sid": "",
  "time": "2026-01-21T00:27:54.033Z",
  "uid": "b53c7358-bdc9-4a99-94a7-0a56477fa2f1",
  "user": "kenneth.tran@goteleport.com",
  "user_cluster_name": "localhost",
  "user_kind": 1,
  "user_roles": [
    "access",
    "editor",
    "mfa-device-trust",
    "require-trusted-device"
  ],
  "user_traits": {
    "acr": [
      "1"
    ],
    "at_hash": [
      "_J7AxLN1qCDTGoeMQp5y8Q"
    ],
    "aud": [
      "teleport-oidc"
    ],
    "azp": [
      "teleport-oidc"
    ],
    "email": [
      "kenneth.tran@goteleport.com"
    ],
    "family_name": [
      "tran"
    ],
    "given_name": [
      "kenneth"
    ],
    "groups": [
      "teleport-admins"
    ],
    "iss": [
      "https://localhost:8443/realms/teleport"
    ],
    "jti": [
      "38610d7d-fad0-c7de-03c2-60b1b4cacd4c"
    ],
    "name": [
      "kenneth tran"
    ],
    "preferred_username": [
      "kenneth-keycloak"
    ],
    "sid": [
      "3c62224c-e0bf-37b9-5c57-a76a2dd53550"
    ],
    "sub": [
      "80814091-50d6-4214-8b23-229a1f13c596"
    ],
    "typ": [
      "ID"
    ],
    "username": [
      "kenneth-keycloak"
    ]
  },
  "with_mfa": "77879faa-6ed8-4799-8b0e-e46af9f3facb"
}

changelog: Reject application session requests and emit a failed session start audit event when a trusted device is required but not provided

@github-actions github-actions bot requested review from kshi36 and r0mant December 5, 2025 00:40
Copy link
Copy Markdown
Collaborator

@zmb3 zmb3 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR title could be more descriptive. It refers to "session start" events, but the change is actually about preventing "app session start" events from being emitted for sessions that are unsuccessful.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if we should change this doc comment.

Are there any cases where this is being emitted on success?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There doesn't seem to be any instances where an AuthAttempt has a success in the code field. It seems that successful events have more granular types and success codes. I will change the doc comment for more clarity.


// Validate that the request has the deviceID extension; non-empty value means user authenticated with that device with CA
if deviceTrustRequired {
if req.DeviceExtensions.DeviceID == "" {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codingllama do you think this is the right place to check whether a trusted device credential is being presented?

Context:

Prior to this change, when an app session is rejected due to device trust, the request traverses through the proxy, and the app agent itself sends back an HTTP response saying that device trust is required (instead of connecting to the app).

The problem is from the proxy's perspective, traffic is being routed to an agent, so an app session start event gets emitted. This is a problem for cases when you alert on accesses that come from untrusted devices (which is somethign we do internally!)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the early check, but I dislike the addition (and duplication!) of authz-related device trust logic to this handler. I think this is the responsibility of the AccessChecker, so I would prefer an early CheckAccess if possible, like you suggested above.

If for some reason that doesn't work, then we could expose an AccessChecker.CheckDeviceAccess method instead.

WDYT?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I opted to expose an AccessChecker.CheckDeviceAccess method since we don't have an app session to use CheckAccess on yet at this point. Let me know if this implementation looks good!

Comment on lines +544 to +552
for _, role := range checker.Roles() {
mode := role.GetOptions().DeviceTrustMode

if mode == constants.DeviceTrustModeRequired ||
mode == constants.DeviceTrustModeRequiredForHumans {
deviceTrustRequired = true
break
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks very similar to (RoleSet).checkAccess():

		// Device verification.
		var deviceVerificationPassed bool
		switch role.GetOptions().DeviceTrustMode {
		case constants.DeviceTrustModeOff, constants.DeviceTrustModeOptional, "":
			// OK, extensions not enforced.
			deviceVerificationPassed = true
		case constants.DeviceTrustModeRequiredForHumans:
			// Humans must use trusted devices, bots can use untrusted devices.
			deviceVerificationPassed = deviceTrusted || state.IsBot
		case constants.DeviceTrustModeRequired:
			// Only trusted devices allowed for bot human and bot users.
			deviceVerificationPassed = deviceTrusted
		}
		if !deviceVerificationPassed {
			logger.LogAttrs(ctx, logutils.TraceLevel, "Access to resource denied, role requires a trusted device",
				slog.String("role", role.GetName()),
			)
			return ErrTrustedDeviceRequired
		}

I wonder if we should be running the access check sooner rather than duplicating some of its code here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is true. I am not sure if we really need all of the other checks in roleset.checkAccess() like label matching or login usernames, before issuing an app session certificate, which from my understanding comes from the Auth server. The access checks comes after the proxy connects to the app service, which should be when authorization takes place.

Let me know if doing the access checks earlier would be the better choice here. I could also refactor the device trust section into a utility function to use in these two parts.

Comment on lines +564 to +568
ServerMetadata: apievents.ServerMetadata{
ServerVersion: teleport.Version,
ServerID: a.ServerID,
ServerNamespace: apidefaults.Namespace,
},
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we omit ServerMetadata here since we know the access attempt was for an app, and we have app metadata below?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ServerMetadata tells what specific Auth server emitted the event, which could be useful for audit logs? I also chose to include this to mirror the AppSessionStart emitted in the same function on a successful event.

@21KennethTran 21KennethTran changed the title Fixed session start audit events for Device Trust failures Fixed app session start audit events for Device Trust failures Dec 8, 2025
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from 8aa218d to 83120c3 Compare December 16, 2025 20:22

// Validate that the request has the deviceID extension; non-empty value means user authenticated with that device with CA
if deviceTrustRequired {
if req.DeviceExtensions.DeviceID == "" {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind the early check, but I dislike the addition (and duplication!) of authz-related device trust logic to this handler. I think this is the responsibility of the AccessChecker, so I would prefer an early CheckAccess if possible, like you suggested above.

If for some reason that doesn't work, then we could expose an AccessChecker.CheckDeviceAccess method instead.

WDYT?

@codingllama
Copy link
Copy Markdown
Contributor

Somewhat related: #62523. Reviews welcome!

@public-teleport-github-review-bot
Copy link
Copy Markdown

@21KennethTran - this PR will require admin approval to merge due to its size. Consider breaking it up into a series smaller changes.

@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from 2d17ca2 to 4f871d0 Compare January 6, 2026 19:31
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from 2eb50bb to 4317a09 Compare January 23, 2026 01:41
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from 4317a09 to c488c31 Compare January 23, 2026 02:58
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from c488c31 to ab2ffa6 Compare January 23, 2026 02:59
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from ab2ffa6 to e35505c Compare January 23, 2026 16:37
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from e35505c to 8813f3a Compare January 23, 2026 17:33
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from 8813f3a to bd545ad Compare January 23, 2026 20:12
@21KennethTran 21KennethTran force-pushed the kennethtran/device-trust-app-rejection branch from bd545ad to 0db0e97 Compare January 23, 2026 20:50
@21KennethTran 21KennethTran added this pull request to the merge queue Jan 23, 2026
Merged via the queue into master with commit ddc4586 Jan 23, 2026
47 checks passed
@21KennethTran 21KennethTran deleted the kennethtran/device-trust-app-rejection branch January 23, 2026 21:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants