Project: Teleport (Provisioning / Security & Compliance)
File: 0155-scoped-webauthn-credentials.md

authors: Brian Joerger (bjoerger@goteleport.com) state: draft Engineering: @rosstimothy && @codingllama Product: @klizhentas || @xinding33 Security: @jentfoo Add and enforce scope for webauthn credentials. Today, when a client performs a webauthn ceremony, the resulting webauthn credentials can be used to perform actions other than what the client initially intended. For example, webauthn credentials intended for per-session MFA could be used instead for login. This means that if an attacker gets a hold of a user's webauthn credentials before the client consumes them, an attacker with the user's password could login as the user. Adding scope to the webauthn credentials would prevent the attacker from using the stolen webauthn credentials freely. Adding scope also enables us to allow webauthn credentials to be reused within specific scopes. For example, for admin actions it may make sense to allow the client to reuse a single webauthn credential for a string of requests that are functionally a single action, such as creating a new user and their corresponding invite token. Enforcing scope for webauthn credentials strictly improves security, as it is a purely restrictive change that prevents certain attack vectors, as described in . On the other hand, allowing clients to reuse webauthn credentials could have negative security implications if not handled carefully. Specifically, the client and server must clearly define: The webauthn credential's scope is provided by the client Reuse is requested by the client Reuse is permitted for the action - server enforced The expiration of the credentials - server enforced (5 minutes) Note: Sensitive operations like "login" and "recovery" must never allow reuse. ```diff // webauthn.proto message SessionData { bytes challenge = 1 [(gogoproto.jsontag) = "challenge,omitempty"]; bytes user_id = 2 [(gogoproto.jsontag) = "userId,omitempty"]; repeated bytes allow_credentials = 3 [(gogoproto.jsontag) = "allowCredentials,omitempty"]; bool resident_key = 4 [(gogoproto.jsontag) = "residentKey,omitempty"]; string user_verification = 5 [(gogoproto.jsontag) = "userVerification,omitempty"]; // Scope authorized by this webauthn session. ChallengeScope scope = 6 [(gogoproto.jsontag) = "scope,omitempty"]; // AllowReuse indicates that this session can be used multiple times for // authentication, until the session expires. bool AllowReuse = 7 [(gogoproto.jsontag) = "allow_reuse,omitempty"]; } +// Scope is a scope authorized by a webauthn challenge resolution. +// +// Note: New scopes can be added to cover new use cases, or to split existing +// scopes into more granular scopes. For example, passwordless and headless login +// could be included under the "login" scope, but splitting them into their own +// scopes improves the security of each. +enum ChallengeScope { SCOPE_UNSPECIFIED = 0; // Standard webauthn login. SCOPE_LOGIN = 1; // Passwordless webauthn login. SCOPEPASSWORDLESSLOGIN = 2; // MFA device management. SCOPEMANAGEDEVICES = 3; // Account recovery. SCOPE_RECOVERY = 4; // Used for per-session MFA and moderated session presence checks. SCOPE_SESSION = 5; // Headless login approval. SCOPE_HEADLESS = 6; // Used for various administrative actions, such as adding, updating, or // deleting administrative resources (users, roles,

// // Note: this scope should not be used for new MFA capabilities that have // more precise scope. Instead, new scopes should be added. This scope may // also be split into multiple smaller scopes in the future. SCOPEADMINACTION = 7; +} // authservice.proto message CreateAuthenticateChallengeRequest { oneof Request { UserCredentials UserCredentials = 1 [(gogoproto.jsontag) = "user_credentials,omitempty"]; string RecoveryStartTokenID = 2 [(gogoproto.jsontag) = "recoverystarttoken_id,omitempty"]; ContextUser ContextUser = 3 [(gogoproto.jsontag) = "context_user,omitempty"]; Passwordless Passwordless = 4 [(gogoproto.jsontag) = "passwordless,omitempty"]; } IsMFARequiredRequest MFARequiredCheck = 5 [(gogoproto.jsontag) = "mfarequiredcheck,omitempty"]; // Scope is a authorization scope for this MFA challenge. // Required. Only applies to webauthn challenges. webauthn.ChallengeScope Scope = 6 [(gogoproto.jsontag) = "scope,omitempty"]; // AllowReuse means webauthn credentials resolved from this challenge can be // reused for a short span of time before the challenge expires. // // Reuse is only permitted for specific actions by the discretion of the server. // See the server implementation for details. bool AllowReuse = 7 [(gogoproto.jsontag) = "allow_reuse,omitempty"]; } ``` Clients will be expected to provide a `Scope` when requesting an MFA challenge through `rpc CreateAuthenticateChallenge`. For specific login and device management endpoints, the scope will be automatically set on the server side. Clients can optionally provide `AllowReuse=true` if the client wants to reuse the resulting webauthn credentials for multiple requests. However, the server will only permit reuse for a select number of actions. See . When verifying a webauthn credential against the user's stored webauthn challenge, the Auth Server will check that the stored scope matches the scope that the Auth server is verifying for. For example, if the Auth server is verifying a webauthn credential for scope "login", but webauthn challenge stored for the user has scope "headless", the verification will fail. If an MFA action does not allow reuse, the Auth Server will validate that the webauthn challenge has `AllowReuse=false`. Additionally, challenges with `AllowReuse=true` will not be deleted immediately, instead letting them expire in the backend after 5 minutes. Initially, reuse will only be permitted for the following admin action RPCs: `rpc CreateUser` `rpc UpdateUser` `rpc UpsertUser` `rpc CreateRole` `rpc UpdateRole` `rpc UpsertRoleV2` `rpc UpsertRole` `rpc CreateUserGroup` `rpc UpdateUserGroup` `rpc CreateResetPasswordToken` `rpc SetClusterNetworkingConfig` `rpc SetSessionRecordingConfig` `rpc SetAuthPreference` `rpc SetNetworkRestrictions` `rpc CreateOIDCConnector` `rpc UpdateOIDCConnector` `rpc UpsertOIDCConnector` `rpc UpsertSAMLConnector` `rpc CreateSAMLConnector` `rpc UpdateSAMLConnector` `rpc UpsertSAMLConnector` `rpc CreateGithubConnector` `rpc UpdateGithubConnector` `rpc UpsertGithubConnector` `rpc CreateSAMLIdPServiceProvider` `rpc UpdateSAMLIdPServiceProvider` `rpc UpsertAccessList` `rpc UpsertAccessListWithMembers` `rpc UpsertTokenV2` `rpc CreateTokenV2` `rpc GenerateToken` `rpc CreateBot` `rpc UpdateBot` `rpc UpsertBot` `rpc DeleteBot` These RPCs are currently used for "bulk" requests such as: `tctl create -f multiple-resources.yaml` which can perform several updates and creates at once. `tctl users add` which creates a user and a reset password token for the user. The list above should be kept to a minimum, but can be extended on a case by case basis. In many cases, it would be best to create new endpoints which contain the full string of actions when

For example, we could replace our current add user flow, which calls `CreateUser` and `CreatePasswordToken`, to call a single `AddUser` endpoint which does both operations. Other times, it may be overly cumbersome or complicated to move an operation into a single endpoint. For example, it does not currently seem worth it to create a single `UpsertResources` endpoint to handle all cases of `tctl create -f multiple-resources.yaml`. Instead we call individual CRUD endpoints for each resource contained in the file, reusing the same Webauthn challenge along the way. Warning: Reuse should not be extended to sensitive operations, including all of the non admin action scopes laid out above and the following admin action endpoints: Account recovery management `rpc ChangeUserAuthentication` `rpc StartAccountRecovery` `rpc VerifyAccountRecovery` `rpc CompleteAccountRecovery` `rpc CreateAccountRecoveryCodes` Dynamic access `rpc CreateAccessRequest` `rpc SetAccessRequestState` `rpc SubmitAccessReview` `rpc AccessRequestPromote` `rpc CreateAccessListReview` CA management `http rotateCertAuthority` `http rotateExternalCertAuthority` `rpc UpsertCertAuthority` `rpc DeleteCertAuthority` `rpc DeleteCertAuthority` Certificate generation `rpc GenerateHostCert` `rpc GenerateHostCerts` `rpc GenerateUserCerts` `http createWebSession` `http deleteWebSession` Webauthn challenges are always set to expire after 5 minutes. However, as we've seen with Dynamo DB, these expirations are not always strictly respected. Therefore, the Auth server will start checking the expiration of stored webauthn challenges. If the challenge is past it's expiration, the Auth server will delete it from the backend explicitly. Note: This change should be backported since this is an existing issue for unconsumed webauthn challenges. There should be no changes to UX, other than reducing the number of MFA taps required for certain admin actions from several down to one. In order to maintain backwards compatibility with old clients, scope will only be enforced opportunistically when provided by the client until the next major version after this feature is released. However, any new features tied to scope, such as webauthn credential reuse, will only be permitted to clients that do provide scope. Currently, MFA audit details are quite limited. We only emit the MFA device used for some MFA actions, such as login. We will add 2 new audit events for better coverage. ```proto // events.proto message CreateMFAAuthChallenge { Metadata Metadata = 1; UserMetadata User = 2; webauthn.Scope Scope = 3; bool AllowReuse = 4; } message ValidateMFAAuthResponse { Metadata Metadata = 1; UserMetadata User = 2; MFADeviceMetadata Device = 3; webauthn.Scope Scope = 4; bool AllowReuse = 5; } ``` ```proto // MFAVerificationEvent is emitted when an MFA verification event is completed. message MFAVerificationEvent { // anonymized string user_name = 1; // the mfa device type used for verification. e.g. Webauthn, U2F, or TOTP. // Matches api/types.MFADevice.MFAType. string mfadevicetype = 2; string scope = 3; } ``` This RFD does not cover adding scope for TOTP. However, if necessary, we could extend this RFD to TOTP by creating some TOTP session data in the backend to match the webauthn session data flow. This TOTP session would just hold the scope and whether reuse is allowed. This TOTP session would replace the existing "Used TOTP token" we store in the backend.