Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/ISSUE_TEMPLATE/testplan.md
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,8 @@ tsh ssh node-that-requires-device-trust
- [ ] K8s Access
- [ ] App Access NOT enforced in global mode
- [ ] Desktop Access NOT enforced in global mode
- [ ] device_trust.mode="required-for-humans" enforces enrolled devices for
humans, but bots (e.g. `tbot`) function on any device
- [ ] Role-based authz enforces enrolled devices
(device_trust.mode="optional" and role.spec.options.device_trust_mode="required")
- [ ] SSH
Expand Down
4 changes: 4 additions & 0 deletions api/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,10 @@ const (
// DeviceTrustModeRequired enforces the presence of device extensions for
// sensitive endpoints.
DeviceTrustModeRequired DeviceTrustMode = "required"
// DeviceTrustModeRequiredForHumans enforces the presence of device
// extensions for sensitive endpoints if the user is human. In this mode,
// bots are exempt from device trust checks.
DeviceTrustModeRequiredForHumans DeviceTrustMode = "required-for-humans"
)

const (
Expand Down
2 changes: 2 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -2658,6 +2658,8 @@ message DeviceTrust {
// endpoints.
// - "required": enforces the presence of device extensions for sensitive
// endpoints.
// - "required-for-humans": enforces the presence of device extensions for
// sensitive endpoints, for human users only (bots are exempt).
//
// Mode is always "off" for OSS.
// Defaults to "optional" for Enterprise.
Expand Down
3 changes: 2 additions & 1 deletion api/types/authentication.go
Original file line number Diff line number Diff line change
Expand Up @@ -837,7 +837,8 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error {
case "": // OK, "default" mode. Varies depending on OSS or Enterprise.
case constants.DeviceTrustModeOff,
constants.DeviceTrustModeOptional,
constants.DeviceTrustModeRequired: // OK.
constants.DeviceTrustModeRequired,
constants.DeviceTrustModeRequiredForHumans: // OK.
default:
return trace.BadParameter("device trust mode %q not supported", dt.Mode)
}
Expand Down
10 changes: 10 additions & 0 deletions api/types/authentication_authpreference_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,16 @@ func TestAuthPreferenceV2_CheckAndSetDefaults_deviceTrust(t *testing.T) {
},
},
},
{
name: "Mode=required-for-humans",
authPref: &types.AuthPreferenceV2{
Spec: types.AuthPreferenceSpecV2{
DeviceTrust: &types.DeviceTrust{
Mode: constants.DeviceTrustModeRequiredForHumans,
},
},
},
},
{
name: "Mode invalid",
authPref: &types.AuthPreferenceV2{
Expand Down
2 changes: 2 additions & 0 deletions api/types/types.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions docs/pages/identity-governance/device-trust/device-trust.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,11 @@ the better the ongoing guarantees that the device itself is trustworthy.

## Device Trust enforcement

Enforcing Device Trust means configuring Teleport with Device Trust mode, i.e. applying
`device_trust_mode: required` rule, which tells Teleport Auth Service to only allow access
with a trusted and an authenticated device, in addition to establishing the user's identity and enforcing
the necessary roles.
Enforcing Device Trust means configuring Teleport with Device Trust mode, i.e.
applying a `device_trust_mode: required` or `device_trust_mode: required-for-humans`
rule, which tells Teleport Auth Service to only allow access with a trusted and
an authenticated device, in addition to establishing the user's identity and
enforcing the necessary roles.

Teleport supports two methods for device enforcement: Role-based
enforcement and Cluster-wide enforcement.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ by the `device_trust_mode` authentication setting:
- `required` - enables device authentication and device-aware audit.
Additionally, it requires a trusted device for all SSH, Database and
Kubernetes connections.
- `required-for-humans` - enables device authentication and device-aware audit.
Additionally, it requires a trusted device for all SSH, Database and
Kubernetes connections, for human users only (bots are exempt).

### Prerequisites
(!docs/pages/includes/edition-prereqs-tabs.mdx edition="Teleport Enterprise"!)
Expand All @@ -52,8 +55,8 @@ works similarly to [`require_session_mfa`](../../admin-guides/access-controls/gu

To enforce authenticated device checks for a specific role when a user accesses
databases, Kubernetes clusters, and servers with Teleport, update the role with
the `device_trust_mode` field assigned to `"required"`. The following example
updates the preset `require-trusted-device` role:
the `device_trust_mode` field assigned to `"required"` or `"required-for-humans"`.
The following example updates the preset `require-trusted-device` role:

```yaml
kind: role
Expand Down
3 changes: 3 additions & 0 deletions docs/pages/includes/config-reference/auth-service.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,9 @@ auth_service:
# - 'required' - enables device authentication and device-aware audit.
# Additionally, it requires a trusted device for all SSH, Database
# and Kubernetes connections.
# - 'required-for-humans' - enables device authentication and device-aware
# audit. Additionally, it requires a trusted device for all SSH, Database
# and Kubernetes connections, for human users only (bots are exempt).
mode: optional # always "off" for Teleport Community Edition

# Determines the default time to live for user certificates
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/includes/role-spec.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ spec:
# clients and servers through the proxy
permit_x11_forwarding: true
# device_trust_mode enforces authenticated device access for assigned user of this role.
device_trust_mode: optional|required|off
device_trust_mode: optional|required|required-for-humans|off
# require_session_mfa require per-session MFA for any assigned user of this role
require_session_mfa: true
# mfa_verification_interval optionally defines the maximum duration that can elapse between successive MFA verifications.
Expand Down
2 changes: 1 addition & 1 deletion docs/pages/reference/access-controls/roles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ user:
| `max_sessions` | Total number of session channels which can be established across a single SSH connection via Teleport | The lowest value takes precedence. |
| `enhanced_recording` | Indicates which events should be recorded by the BFP-based session recorder | |
| `permit_x11_forwarding` | Allow users to enable X11 forwarding with OpenSSH clients and servers | |
| `device_trust_mode` | Enforce authenticated device access for users assigned this role (`required`, `optional`, `off`). Applies only to the resources in the roles' allow field. | |
| `device_trust_mode` | Enforce authenticated device access for users assigned this role (`required`, `required-for-humans`, `optional`, `off`). Applies only to the resources in the roles' allow field. | |
| `require_session_mfa` | Enforce per-session MFA or PIV-hardware key restrictions on user login sessions (`no`, `yes`, `hardware_key`, `hardware_key_touch`). Applies only to the resources in the roles' allow field. | For per-session MFA, Logical "OR" i.e. evaluates to "yes" if at least one role requires session MFA |
| `mfa_verification_interval` | Define the maximum duration that can elapse between successive MFA verifications | The shortest interval wins |
| `lock` | Locking mode (`strict` or `best_effort`) | `strict` wins in case of conflict |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Optional:

- `auto_enroll` (Boolean) Enable device auto-enroll. Auto-enroll lets any user issue a device enrollment token for a known device that is not already enrolled. `tsh` takes advantage of auto-enroll to automatically enroll devices on user login, when appropriate. The effective cluster Mode still applies: AutoEnroll=true is meaningless if Mode="off".
- `ekcert_allowed_cas` (List of String) Allow list of EKCert CAs in PEM format. If present, only TPM devices that present an EKCert that is signed by a CA specified here may be enrolled (existing enrollments are unchanged). If not present, then the CA of TPM EKCerts will not be checked during enrollment, this allows any device to enroll.
- `mode` (String) Mode of verification for trusted devices. The following modes are supported: - "off": disables both device authentication and authorization. - "optional": allows both device authentication and authorization, but doesn't enforce the presence of device extensions for sensitive endpoints. - "required": enforces the presence of device extensions for sensitive endpoints. Mode is always "off" for OSS. Defaults to "optional" for Enterprise.
- `mode` (String) Mode of verification for trusted devices. The following modes are supported: - "off": disables both device authentication and authorization. - "optional": allows both device authentication and authorization, but doesn't enforce the presence of device extensions for sensitive endpoints. - "required": enforces the presence of device extensions for sensitive endpoints. - "required-for-humans": enforces the presence of device extensions for sensitive endpoints, for human users only (bots are exempt). Mode is always "off" for OSS. Defaults to "optional" for Enterprise.


### Nested Schema for `spec.hardware_key`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ Optional:

- `auto_enroll` (Boolean) Enable device auto-enroll. Auto-enroll lets any user issue a device enrollment token for a known device that is not already enrolled. `tsh` takes advantage of auto-enroll to automatically enroll devices on user login, when appropriate. The effective cluster Mode still applies: AutoEnroll=true is meaningless if Mode="off".
- `ekcert_allowed_cas` (List of String) Allow list of EKCert CAs in PEM format. If present, only TPM devices that present an EKCert that is signed by a CA specified here may be enrolled (existing enrollments are unchanged). If not present, then the CA of TPM EKCerts will not be checked during enrollment, this allows any device to enroll.
- `mode` (String) Mode of verification for trusted devices. The following modes are supported: - "off": disables both device authentication and authorization. - "optional": allows both device authentication and authorization, but doesn't enforce the presence of device extensions for sensitive endpoints. - "required": enforces the presence of device extensions for sensitive endpoints. Mode is always "off" for OSS. Defaults to "optional" for Enterprise.
- `mode` (String) Mode of verification for trusted devices. The following modes are supported: - "off": disables both device authentication and authorization. - "optional": allows both device authentication and authorization, but doesn't enforce the presence of device extensions for sensitive endpoints. - "required": enforces the presence of device extensions for sensitive endpoints. - "required-for-humans": enforces the presence of device extensions for sensitive endpoints, for human users only (bots are exempt). Mode is always "off" for OSS. Defaults to "optional" for Enterprise.


### Nested Schema for `spec.hardware_key`
Expand Down
2 changes: 1 addition & 1 deletion integrations/terraform/tfschema/types_terraform.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 9 additions & 2 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2589,7 +2589,7 @@ func (a *Server) GenerateUserTestCertsWithContext(ctx context.Context, req Gener
return nil, nil, trace.Wrap(err)
}

certs, err := a.generateUserCert(ctx, certRequest{
certReq := certRequest{
user: userState,
ttl: req.TTL,
compatibility: req.Compatibility,
Expand All @@ -2611,7 +2611,14 @@ func (a *Server) GenerateUserTestCertsWithContext(ctx context.Context, req Gener
activeRequests: req.ActiveRequests,
kubernetesCluster: req.KubernetesCluster,
usage: req.Usage,
})
}

if botName, isBot := userState.GetLabel(types.BotLabel); isBot {
certReq.botName = botName
certReq.botInstanceID = uuid.NewString()
}

certs, err := a.generateUserCert(ctx, certReq)
if err != nil {
return nil, nil, trace.Wrap(err)
}
Expand Down
40 changes: 40 additions & 0 deletions lib/auth/grpcserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1096,6 +1096,21 @@ func TestGenerateUserCerts_deviceAuthz(t *testing.T) {
}))
require.NoError(t, err, "NewClient failed")

// Create bot user for testing.
botUser, _, err := authtest.CreateUserAndRole(testServer.Auth(), "wall-e", []string{"wall-e-role"}, nil)
require.NoError(t, err, "CreateUserAndRole failed")

botMeta := botUser.GetMetadata()
botMeta.Labels = map[string]string{
types.BotLabel: "wall-e",
}
botUser.SetMetadata(botMeta)
botUser, err = testServer.Auth().UpsertUser(ctx, botUser)
require.NoError(t, err)

botClient, err := testServer.NewClient(authtest.TestUser(botUser.GetName()))
require.NoError(t, err)

// updateAuthPref is a helper used throughout the test.
updateAuthPref := func(t *testing.T, modify func(ap types.AuthPreference)) {
authPref, err := authServer.GetAuthPreference(ctx)
Expand Down Expand Up @@ -1165,6 +1180,15 @@ func TestGenerateUserCerts_deviceAuthz(t *testing.T) {
Login: username,
},
}
botSSHReq := proto.UserCertsRequest{
SSHPublicKey: sshPub,
Username: botUser.GetName(),
Expires: expires,
RouteToCluster: clusterName,
NodeName: "mynode",
Usage: proto.UserCertsRequest_SSH,
SSHLogin: "llama",
}

assertSuccess := func(t *testing.T, err error) {
assert.NoError(t, err, "GenerateUserCerts error mismatch")
Expand Down Expand Up @@ -1257,6 +1281,22 @@ func TestGenerateUserCerts_deviceAuthz(t *testing.T) {
req: winReq,
assertErr: assertSuccess,
},
{
name: "nok: mode=required with bot",
clusterDeviceMode: constants.DeviceTrustModeRequired,
client: botClient,
req: botSSHReq,
skipSingleUseCerts: true,
assertErr: assertAccessDenied,
},
{
name: "ok: mode=required-for-humans with bot",
clusterDeviceMode: constants.DeviceTrustModeRequiredForHumans,
client: botClient,
req: botSSHReq,
skipSingleUseCerts: true,
assertErr: assertSuccess,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
Expand Down
1 change: 1 addition & 0 deletions lib/authz/permissions.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,7 @@ func (c *Context) GetAccessState(authPref readonly.AuthPreference) services.Acce

state.EnableDeviceVerification = !c.disableDeviceRoleMode
state.DeviceVerified = isService || dtauthz.IsTLSDeviceVerified(&identity.DeviceExtensions)
state.IsBot = identity.IsBot()

return state
}
Expand Down
28 changes: 28 additions & 0 deletions lib/authz/permissions_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,9 @@ func TestAuthorizer_Authorize_deviceTrust(t *testing.T) {
AssetTag: "assettag1",
CredentialID: "credentialid1",
}
botUser := userWithoutExtensions
botUser.Identity.BotName = "wall-e"
botUser.Identity.BotInstanceID = uuid.NewString()

// Enterprise is necessary for mode=optional and mode=required to work.
modulestest.SetTestModules(t, modulestest.Modules{
Expand All @@ -416,6 +419,17 @@ func TestAuthorizer_Authorize_deviceTrust(t *testing.T) {
user: userWithoutExtensions,
wantErr: "access denied",
},
{
name: "nok: bot user and mode=required",
deviceMode: constants.DeviceTrustModeRequired,
user: botUser,
wantErr: "access denied",
},
{
name: "ok: bot user and mode=required-for-humans",
deviceMode: constants.DeviceTrustModeRequiredForHumans,
user: botUser,
},
{
name: "global mode disabled only",
deviceMode: constants.DeviceTrustModeRequired,
Expand Down Expand Up @@ -871,6 +885,20 @@ func TestContext_GetAccessState(t *testing.T) {
DeviceVerified: true, // Identity extensions
},
},
{
name: "bot user",
createAuthCtx: func() *authz.Context {
ctx := localCtx
localUser := ctx.Identity.(authz.LocalUser)
localUser.Identity.BotName = "wall-e"
ctx.Identity = localUser
return &ctx
},
want: services.AccessState{
EnableDeviceVerification: true,
IsBot: true,
},
},
}
for _, test := range tests {
test := test
Expand Down
30 changes: 20 additions & 10 deletions lib/devicetrust/authz/authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ func IsTLSDeviceVerified(ext *tlsca.DeviceExtensions) bool {
// VerifyTLSUser verifies if the TLS identity has the required extensions to
// fulfill the device trust configuration.
func VerifyTLSUser(ctx context.Context, dt *types.DeviceTrust, identity tlsca.Identity) error {
return verifyDeviceExtensions(ctx, dt, identity.Username, IsTLSDeviceVerified(&identity.DeviceExtensions))
return verifyDeviceExtensions(ctx, dt, identity.Username, identity.IsBot(), IsTLSDeviceVerified(&identity.DeviceExtensions))
}

// IsSSHDeviceVerified returns true if cert contains all required device
Expand Down Expand Up @@ -89,19 +89,29 @@ func VerifySSHUser(ctx context.Context, dt *types.DeviceTrust, ident *sshca.Iden
if ident == nil {
return trace.BadParameter("ssh identity required")
}

return verifyDeviceExtensions(ctx, dt, ident.Username, IsSSHDeviceVerified(ident))
return verifyDeviceExtensions(ctx, dt, ident.Username, ident.IsBot(), IsSSHDeviceVerified(ident))
}

func verifyDeviceExtensions(ctx context.Context, dt *types.DeviceTrust, username string, verified bool) error {
func verifyDeviceExtensions(ctx context.Context, dt *types.DeviceTrust, username string, isBot bool, verified bool) error {
mode := dtconfig.GetEnforcementMode(dt)
switch {
case mode != constants.DeviceTrustModeRequired:
return nil // OK, extensions not enforced.
case !verified:

var pass bool
switch mode {
case constants.DeviceTrustModeOff, constants.DeviceTrustModeOptional:
// OK, extensions not enforced.
pass = true
case constants.DeviceTrustModeRequiredForHumans:
// Humans must use trusted devices, bots can use untrusted devices.
pass = verified || isBot
case constants.DeviceTrustModeRequired:
// Only trusted devices allowed for bot human and bot users.
pass = verified
}

if !pass {
slog.DebugContext(ctx, "Device Trust: denied access for unidentified device", "user", username)
return trace.Wrap(ErrTrustedDeviceRequired)
default:
return nil
}

return nil
}
Loading
Loading