Add support for Hardware Key PIN (#31743)

* Update RFD with hardware key pin policies.

* Consolidate policy logic and update tests.

* Add pin private key policies; Make PIV PIN/Touch prompts work together.

* Prompt user to set pin/puk from default.

* Handle unexpected PIN auth errors.

* Resolve RFD password prompt comment.

* Handle incompatible private key policy in role sets (future-proof).

* Resolve comment on require mfa type string godocs and tests.

* A satisfying change.

* Address PIN/PUK prompt comments and other code suggestions.

* Resolve comments.

* Fix test that prompts for pin twice.

* Fix test.
This commit is contained in:
Brian Joerger 2023-10-13 12:07:43 -07:00 committed by GitHub
parent f1fd668e55
commit c0b3299de7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 2278 additions and 1719 deletions

View file

@ -5333,9 +5333,14 @@ enum RequireMFAType {
// and login sessions must use a private key backed by a hardware key.
SESSION_AND_HARDWARE_KEY = 2;
// HARDWARE_KEY_TOUCH means login sessions must use a hardware private key that
// requires touch to be used. This touch is required for all private key operations,
// so the key is always treated as MFA verified for sessions.
// requires touch to be used.
HARDWARE_KEY_TOUCH = 3;
// HARDWARE_KEY_PIN means login sessions must use a hardware private key that
// requires pin to be used.
HARDWARE_KEY_PIN = 4;
// HARDWARE_KEY_TOUCH_AND_PIN means login sessions must use a hardware private key that
// requires touch and pin to be used.
HARDWARE_KEY_TOUCH_AND_PIN = 5;
}
// Plugin describes a single instance of a Teleport Plugin

View file

@ -389,6 +389,10 @@ func (c *AuthPreferenceV2) GetPrivateKeyPolicy() keys.PrivateKeyPolicy {
return keys.PrivateKeyPolicyHardwareKey
case RequireMFAType_HARDWARE_KEY_TOUCH:
return keys.PrivateKeyPolicyHardwareKeyTouch
case RequireMFAType_HARDWARE_KEY_PIN:
return keys.PrivateKeyPolicyHardwareKeyPIN
case RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN:
return keys.PrivateKeyPolicyHardwareKeyTouchAndPIN
default:
return keys.PrivateKeyPolicyNone
}
@ -907,8 +911,14 @@ func (r *RequireMFAType) UnmarshalJSON(data []byte) error {
}
const (
RequireMFATypeHardwareKeyString = "hardware_key"
// RequireMFATypeHardwareKeyString is the string representation of RequireMFATypeHardwareKey
RequireMFATypeHardwareKeyString = "hardware_key"
// RequireMFATypeHardwareKeyTouchString is the string representation of RequireMFATypeHardwareKeyTouch
RequireMFATypeHardwareKeyTouchString = "hardware_key_touch"
// RequireMFATypeHardwareKeyPINString is the string representation of RequireMFATypeHardwareKeyPIN
RequireMFATypeHardwareKeyPINString = "hardware_key_pin"
// RequireMFATypeHardwareKeyTouchAndPINString is the string representation of RequireMFATypeHardwareKeyTouchAndPIN
RequireMFATypeHardwareKeyTouchAndPINString = "hardware_key_touch_and_pin"
)
// encode RequireMFAType into a string or boolean. This is necessary for
@ -924,6 +934,10 @@ func (r *RequireMFAType) encode() (interface{}, error) {
return RequireMFATypeHardwareKeyString, nil
case RequireMFAType_HARDWARE_KEY_TOUCH:
return RequireMFATypeHardwareKeyTouchString, nil
case RequireMFAType_HARDWARE_KEY_PIN:
return RequireMFATypeHardwareKeyPINString, nil
case RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN:
return RequireMFATypeHardwareKeyTouchAndPINString, nil
default:
return nil, trace.BadParameter("RequireMFAType invalid value %v", *r)
}
@ -940,6 +954,10 @@ func (r *RequireMFAType) decode(val interface{}) error {
*r = RequireMFAType_SESSION_AND_HARDWARE_KEY
case RequireMFATypeHardwareKeyTouchString:
*r = RequireMFAType_HARDWARE_KEY_TOUCH
case RequireMFATypeHardwareKeyPINString:
*r = RequireMFAType_HARDWARE_KEY_PIN
case RequireMFATypeHardwareKeyTouchAndPINString:
*r = RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN
case "":
// default to off
*r = RequireMFAType_OFF

View file

@ -0,0 +1,66 @@
/*
Copyright 2022 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package types
import (
"testing"
"github.com/stretchr/testify/require"
)
// TestMarshalUnmarshalRequireMFAType tests encoding/decoding of the RequireMFAType.
func TestEncodeDecodeRequireMFAType(t *testing.T) {
for _, tt := range []struct {
requireMFAType RequireMFAType
encoded any
}{
{
requireMFAType: RequireMFAType_OFF,
encoded: false,
}, {
requireMFAType: RequireMFAType_SESSION,
encoded: true,
}, {
requireMFAType: RequireMFAType_SESSION_AND_HARDWARE_KEY,
encoded: RequireMFATypeHardwareKeyString,
}, {
requireMFAType: RequireMFAType_HARDWARE_KEY_TOUCH,
encoded: RequireMFATypeHardwareKeyTouchString,
}, {
requireMFAType: RequireMFAType_HARDWARE_KEY_PIN,
encoded: RequireMFATypeHardwareKeyPINString,
}, {
requireMFAType: RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN,
encoded: RequireMFATypeHardwareKeyTouchAndPINString,
},
} {
t.Run(tt.requireMFAType.String(), func(t *testing.T) {
t.Run("encode", func(t *testing.T) {
encoded, err := tt.requireMFAType.encode()
require.NoError(t, err)
require.Equal(t, tt.encoded, encoded)
})
t.Run("decode", func(t *testing.T) {
var decoded RequireMFAType
err := decoded.decode(tt.encoded)
require.NoError(t, err)
require.Equal(t, tt.requireMFAType, decoded)
})
})
}
}

View file

@ -889,6 +889,10 @@ func (r *RoleV6) GetPrivateKeyPolicy() keys.PrivateKeyPolicy {
return keys.PrivateKeyPolicyHardwareKey
case RequireMFAType_HARDWARE_KEY_TOUCH:
return keys.PrivateKeyPolicyHardwareKeyTouch
case RequireMFAType_HARDWARE_KEY_PIN:
return keys.PrivateKeyPolicyHardwareKeyPIN
case RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN:
return keys.PrivateKeyPolicyHardwareKeyTouchAndPIN
default:
return keys.PrivateKeyPolicyNone
}

File diff suppressed because it is too large Load diff

View file

@ -34,65 +34,138 @@ const (
// hardware key to generate and store their private keys securely, and
// this key must require touch to be accessed and used.
PrivateKeyPolicyHardwareKeyTouch PrivateKeyPolicy = "hardware_key_touch"
// PrivateKeyPolicyHardwareKeyPIN means that the client must use a valid
// hardware key to generate and store their private keys securely, and
// this key must require pin to be accessed and used.
PrivateKeyPolicyHardwareKeyPIN PrivateKeyPolicy = "hardware_key_pin"
// PrivateKeyPolicyHardwareKeyTouchAndPIN means that the client must use a valid
// hardware key to generate and store their private keys securely, and
// this key must require touch and pin to be accessed and used.
PrivateKeyPolicyHardwareKeyTouchAndPIN PrivateKeyPolicy = "hardware_key_touch_and_pin"
)
// VerifyPolicy verifies that the given policy meets the requirements of this policy.
// If not, it will return a private key policy error, which can be parsed to retrieve
// the unmet policy.
func (p PrivateKeyPolicy) VerifyPolicy(policy PrivateKeyPolicy) error {
switch p {
// IsSatisfiedBy returns whether this key policy is satisfied by the given key policy.
func (requiredPolicy PrivateKeyPolicy) IsSatisfiedBy(keyPolicy PrivateKeyPolicy) bool {
switch requiredPolicy {
case PrivateKeyPolicyNone:
return nil
return true
case PrivateKeyPolicyHardwareKey:
if policy == PrivateKeyPolicyHardwareKey || policy == PrivateKeyPolicyHardwareKeyTouch {
return nil
}
return keyPolicy.IsHardwareKeyPolicy()
case PrivateKeyPolicyHardwareKeyTouch:
if policy == PrivateKeyPolicyHardwareKeyTouch {
return nil
}
return keyPolicy.isHardwareKeyTouchVerified()
case PrivateKeyPolicyHardwareKeyPIN:
return keyPolicy.isHardwareKeyPINVerified()
case PrivateKeyPolicyHardwareKeyTouchAndPIN:
return keyPolicy.isHardwareKeyTouchVerified() && keyPolicy.isHardwareKeyPINVerified()
}
return NewPrivateKeyPolicyError(p)
return false
}
// IsHardwareKeyVerified return true if this private key policy requires a hardware key.
func (p PrivateKeyPolicy) IsHardwareKeyVerified() bool {
// Deprecated in favor of IsSatisfiedBy.
// TODO(Joerger): delete once reference in /e is replaced.
func (requiredPolicy PrivateKeyPolicy) VerifyPolicy(keyPolicy PrivateKeyPolicy) error {
if !requiredPolicy.IsSatisfiedBy(keyPolicy) {
return NewPrivateKeyPolicyError(requiredPolicy)
}
return nil
}
// IsHardwareKeyPolicy return true if this private key policy requires a hardware key.
func (p PrivateKeyPolicy) IsHardwareKeyPolicy() bool {
switch p {
case PrivateKeyPolicyHardwareKey, PrivateKeyPolicyHardwareKeyTouch:
case PrivateKeyPolicyHardwareKey,
PrivateKeyPolicyHardwareKeyTouch,
PrivateKeyPolicyHardwareKeyPIN,
PrivateKeyPolicyHardwareKeyTouchAndPIN:
return true
}
return false
}
// MFAVerified checks that meet this private key policy counts towards MFA verification.
// MFAVerified checks that private keys with this key policy count as MFA verified.
// Both Hardware key touch and pin are count as MFA verification.
func (p PrivateKeyPolicy) MFAVerified() bool {
return p == PrivateKeyPolicyHardwareKeyTouch
return p.isHardwareKeyTouchVerified() || p.isHardwareKeyPINVerified()
}
func (p PrivateKeyPolicy) isHardwareKeyTouchVerified() bool {
switch p {
case PrivateKeyPolicyHardwareKeyTouch, PrivateKeyPolicyHardwareKeyTouchAndPIN:
return true
}
return false
}
func (p PrivateKeyPolicy) isHardwareKeyPINVerified() bool {
switch p {
case PrivateKeyPolicyHardwareKeyPIN, PrivateKeyPolicyHardwareKeyTouchAndPIN:
return true
}
return false
}
func (p PrivateKeyPolicy) validate() error {
switch p {
case PrivateKeyPolicyNone, PrivateKeyPolicyHardwareKey, PrivateKeyPolicyHardwareKeyTouch:
case PrivateKeyPolicyNone,
PrivateKeyPolicyHardwareKey,
PrivateKeyPolicyHardwareKeyTouch,
PrivateKeyPolicyHardwareKeyPIN,
PrivateKeyPolicyHardwareKeyTouchAndPIN:
return nil
}
return trace.BadParameter("%q is not a valid key policy", p)
}
var privateKeyPolicyErrRegex = regexp.MustCompile(`private key policy not met: (\w+)`)
// PolicyThatSatisfiesSet returns least restrictive policy necessary to satisfy the given set of policies.
func PolicyThatSatisfiesSet(policies []PrivateKeyPolicy) (PrivateKeyPolicy, error) {
setPolicy := PrivateKeyPolicyNone
for _, policy := range policies {
if policy.IsSatisfiedBy(setPolicy) {
continue
}
switch {
case setPolicy.IsSatisfiedBy(policy):
// Upgrade set policy to stricter policy.
setPolicy = policy
case policy.IsSatisfiedBy(PrivateKeyPolicyHardwareKeyTouchAndPIN) &&
setPolicy.IsSatisfiedBy(PrivateKeyPolicyHardwareKeyTouchAndPIN):
// Neither policy is met by the other (pin or touch), but both are met by
// stricter pin+touch policy.
setPolicy = PrivateKeyPolicyHardwareKeyTouchAndPIN
default:
// Currently, "hardware_key_touch_and_pin" is the strictest policy available and
// meets every other required policy. However, in the future we may add policy
// requirements that are mutually exclusive, so this logic is future proofed.
return PrivateKeyPolicyNone, trace.BadParameter(""+
"private key policy requirements %q and %q are incompatible, "+
"please contact the cluster administrator", policy, setPolicy)
}
}
return setPolicy, nil
}
var privateKeyPolicyErrRegex = regexp.MustCompile(`private key policy not (met|satisfied): (\w+)`)
func NewPrivateKeyPolicyError(p PrivateKeyPolicy) error {
// TODO(Joerger): Replace with "private key policy not satisfied" in 16.0.0
return trace.BadParameter(fmt.Sprintf("private key policy not met: %s", p))
}
// ParsePrivateKeyPolicyError checks if the given error is a private key policy
// error and returns the contained unmet PrivateKeyPolicy.
// error and returns the contained unsatisfied PrivateKeyPolicy.
func ParsePrivateKeyPolicyError(err error) (PrivateKeyPolicy, error) {
// subMatches should have two groups - the full string and the policy "(\w+)"
subMatches := privateKeyPolicyErrRegex.FindStringSubmatch(err.Error())
if subMatches == nil || len(subMatches) != 2 {
if subMatches == nil || len(subMatches) != 3 {
return "", trace.BadParameter("provided error is not a key policy error")
}
policy := PrivateKeyPolicy(subMatches[1])
policy := PrivateKeyPolicy(subMatches[2])
if err := policy.validate(); err != nil {
return "", trace.Wrap(err)
}

View file

@ -14,85 +14,203 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
package keys
package keys_test
import (
"fmt"
"slices"
"testing"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
"github.com/gravitational/teleport/api/utils/keys"
)
// TestVerifyPolicy tests VerifyPolicy.
func TestVerifyPolicy(t *testing.T) {
t.Run("key policy none", func(t *testing.T) {
require.NoError(t, PrivateKeyPolicyNone.VerifyPolicy(PrivateKeyPolicyNone))
require.NoError(t, PrivateKeyPolicyNone.VerifyPolicy(PrivateKeyPolicyHardwareKey))
require.NoError(t, PrivateKeyPolicyNone.VerifyPolicy(PrivateKeyPolicyHardwareKeyTouch))
})
t.Run("key policy hardware_key", func(t *testing.T) {
require.Error(t, PrivateKeyPolicyHardwareKey.VerifyPolicy(PrivateKeyPolicyNone))
require.NoError(t, PrivateKeyPolicyHardwareKey.VerifyPolicy(PrivateKeyPolicyHardwareKey))
require.NoError(t, PrivateKeyPolicyHardwareKey.VerifyPolicy(PrivateKeyPolicyHardwareKeyTouch))
})
t.Run("key policy hardware_key_touch", func(t *testing.T) {
require.Error(t, PrivateKeyPolicyHardwareKeyTouch.VerifyPolicy(PrivateKeyPolicyNone))
require.Error(t, PrivateKeyPolicyHardwareKeyTouch.VerifyPolicy(PrivateKeyPolicyHardwareKey))
require.NoError(t, PrivateKeyPolicyHardwareKeyTouch.VerifyPolicy(PrivateKeyPolicyHardwareKeyTouch))
})
}
var (
privateKeyPolicies = []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyTouch,
keys.PrivateKeyPolicyHardwareKeyPIN,
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}
hardwareKeyPolicies = []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyTouch,
keys.PrivateKeyPolicyHardwareKeyPIN,
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}
hardwareKeyTouchPolicies = []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyHardwareKeyTouch,
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}
hardwareKeyPINPolicies = []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyHardwareKeyPIN,
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}
hardwareKeyTouchAndPINPolicies = []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}
)
// TestPrivateKeyPolicyError tests private key policy error logic.
func TestPrivateKeyPolicyError(t *testing.T) {
func TestIsRequiredPolicyMet(t *testing.T) {
privateKeyPolicies := []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyTouch,
keys.PrivateKeyPolicyHardwareKeyPIN,
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}
for _, tc := range []struct {
desc string
errIn error
expectIsKeyPolicy bool
expectKeyPolicyErr bool
expectKeyPolicy PrivateKeyPolicy
requiredPolicy keys.PrivateKeyPolicy
satisfyingPolicies []keys.PrivateKeyPolicy
}{
{
desc: "random error",
errIn: trace.BadParameter("random error"),
expectIsKeyPolicy: false,
expectKeyPolicyErr: true,
requiredPolicy: keys.PrivateKeyPolicyNone,
satisfyingPolicies: privateKeyPolicies,
}, {
desc: "unknown_key_policy",
errIn: NewPrivateKeyPolicyError("unknown_key_policy"),
expectIsKeyPolicy: true,
expectKeyPolicyErr: true,
requiredPolicy: keys.PrivateKeyPolicyHardwareKey,
satisfyingPolicies: hardwareKeyPolicies,
}, {
desc: string(PrivateKeyPolicyNone),
errIn: NewPrivateKeyPolicyError(PrivateKeyPolicyNone),
expectIsKeyPolicy: true,
expectKeyPolicy: PrivateKeyPolicyNone,
requiredPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
satisfyingPolicies: hardwareKeyTouchPolicies,
}, {
desc: string(PrivateKeyPolicyHardwareKey),
errIn: NewPrivateKeyPolicyError(PrivateKeyPolicyHardwareKey),
expectIsKeyPolicy: true,
expectKeyPolicy: PrivateKeyPolicyHardwareKey,
requiredPolicy: keys.PrivateKeyPolicyHardwareKeyPIN,
satisfyingPolicies: hardwareKeyPINPolicies,
}, {
desc: string(PrivateKeyPolicyHardwareKeyTouch),
errIn: NewPrivateKeyPolicyError(PrivateKeyPolicyHardwareKeyTouch),
expectIsKeyPolicy: true,
expectKeyPolicy: PrivateKeyPolicyHardwareKeyTouch,
}, {
desc: "wrapped policy error",
errIn: trace.Wrap(NewPrivateKeyPolicyError(PrivateKeyPolicyHardwareKeyTouch), "wrapped err"),
expectIsKeyPolicy: true,
expectKeyPolicy: PrivateKeyPolicyHardwareKeyTouch,
}, {
desc: "policy error string contained in error",
errIn: trace.Errorf("ssh: rejected: administratively prohibited (%s)", NewPrivateKeyPolicyError(PrivateKeyPolicyHardwareKeyTouch).Error()),
expectIsKeyPolicy: true,
expectKeyPolicy: PrivateKeyPolicyHardwareKeyTouch,
requiredPolicy: keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
satisfyingPolicies: hardwareKeyTouchAndPINPolicies,
},
} {
t.Run(tc.desc, func(t *testing.T) {
require.Equal(t, tc.expectIsKeyPolicy, IsPrivateKeyPolicyError(tc.errIn))
t.Run(string(tc.requiredPolicy), func(t *testing.T) {
for _, keyPolicy := range privateKeyPolicies {
if tc.requiredPolicy.IsSatisfiedBy(keyPolicy) {
require.Contains(t, tc.satisfyingPolicies, keyPolicy, "Policy %q does not meet %q but IsRequirePolicyMet(%v, %v) returned true", keyPolicy, tc.requiredPolicy, tc.requiredPolicy, keyPolicy)
} else {
require.NotContains(t, tc.satisfyingPolicies, keyPolicy, "Policy %q does meet %q but IsRequirePolicyMet(%v, %v) returned false", keyPolicy, tc.requiredPolicy, tc.requiredPolicy, keyPolicy)
}
}
})
}
}
keyPolicy, err := ParsePrivateKeyPolicyError(tc.errIn)
if tc.expectKeyPolicyErr {
func TestGetPolicyFromSet(t *testing.T) {
testCases := []struct {
name string
policySet []keys.PrivateKeyPolicy
wantPolicy keys.PrivateKeyPolicy
}{
{
name: "none",
policySet: []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyNone,
},
wantPolicy: keys.PrivateKeyPolicyNone,
}, {
name: "hardware key policy",
policySet: []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyHardwareKey,
},
wantPolicy: keys.PrivateKeyPolicyHardwareKey,
}, {
name: "touch policy",
policySet: []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyTouch,
},
wantPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
}, {
name: "pin policy",
policySet: []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyPIN,
},
wantPolicy: keys.PrivateKeyPolicyHardwareKeyPIN,
}, {
name: "touch policy and pin policy",
policySet: []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyNone,
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyPIN,
keys.PrivateKeyPolicyHardwareKeyTouch,
},
wantPolicy: keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
}, {
name: "touch and pin policy",
policySet: privateKeyPolicies,
wantPolicy: keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
requiredPolicy, err := keys.PolicyThatSatisfiesSet(tc.policySet)
require.NoError(t, err)
require.Equal(t, tc.wantPolicy, requiredPolicy)
// reversing the policy set shouldn't change the output
slices.Reverse(tc.policySet)
requiredPolicy, err = keys.PolicyThatSatisfiesSet(tc.policySet)
require.NoError(t, err)
require.Equal(t, tc.wantPolicy, requiredPolicy)
})
}
}
// TestParsePrivateKeyPolicyError tests private key policy error parsing and checking.
func TestParsePrivateKeyPolicyError(t *testing.T) {
type testCase struct {
desc string
errIn error
expectIsKeyPolicy bool
expectParseKeyPolicyErr bool
expectKeyPolicy keys.PrivateKeyPolicy
}
testCases := []testCase{
{
desc: "random error",
errIn: trace.BadParameter("random error"),
expectIsKeyPolicy: false,
expectParseKeyPolicyErr: true,
}, {
desc: "unknown_key_policy",
errIn: keys.NewPrivateKeyPolicyError("unknown_key_policy"),
expectIsKeyPolicy: true,
expectParseKeyPolicyErr: true,
}, {
desc: "wrapped policy error",
errIn: trace.Wrap(keys.NewPrivateKeyPolicyError(keys.PrivateKeyPolicyHardwareKeyTouch), "wrapped err"),
expectIsKeyPolicy: true,
expectKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
}, {
desc: "policy error string contained in error",
errIn: trace.Errorf("ssh: rejected: administratively prohibited (%s)", keys.NewPrivateKeyPolicyError(keys.PrivateKeyPolicyHardwareKeyTouch).Error()),
expectIsKeyPolicy: true,
expectKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
},
}
for _, policy := range privateKeyPolicies {
testCases = append(testCases, testCase{
desc: fmt.Sprintf("valid key policy: %v", policy),
errIn: keys.NewPrivateKeyPolicyError(policy),
expectIsKeyPolicy: true,
expectKeyPolicy: policy,
})
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
require.Equal(t, tc.expectIsKeyPolicy, keys.IsPrivateKeyPolicyError(tc.errIn))
keyPolicy, err := keys.ParsePrivateKeyPolicyError(tc.errIn)
if tc.expectParseKeyPolicyErr {
require.Error(t, err)
} else {
require.NoError(t, err)

View file

@ -61,6 +61,13 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva
return nil, trace.Wrap(err)
}
// If PIN is required, check that PIN and PUK are not the defaults.
if requiredKeyPolicy.isHardwareKeyPINVerified() {
if err := y.checkOrSetPIN(ctx); err != nil {
return nil, trace.Wrap(err)
}
}
promptOverwriteSlot := func(msg string) error {
promptQuestion := fmt.Sprintf("%v\nWould you like to overwrite this slot's private key and certificate?", msg)
if confirmed, confirmErr := prompt.Confirmation(ctx, os.Stderr, prompt.Stdin(), promptQuestion); confirmErr != nil {
@ -107,7 +114,7 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva
// Get the key in the slot, or generate a new one if needed.
priv, err := y.getPrivateKey(pivSlot)
switch {
case err == nil && requiredKeyPolicy.VerifyPolicy(priv.GetPrivateKeyPolicy()) != nil:
case err == nil && !requiredKeyPolicy.IsSatisfiedBy(priv.GetPrivateKeyPolicy()):
// Key does not meet the required key policy, prompt the user before we overwrite the slot.
msg := fmt.Sprintf("private key in YubiKey PIV slot %q does not meet private key policy %q.", pivSlot, requiredKeyPolicy)
if err := promptOverwriteSlot(msg); err != nil {
@ -118,7 +125,7 @@ func getOrGenerateYubiKeyPrivateKey(ctx context.Context, requiredKeyPolicy Priva
fallthrough
case trace.IsNotFound(err):
// no key found, generate a new key.
priv, err := y.generatePrivateKeyAndCert(pivSlot, requiredKeyPolicy)
priv, err = y.generatePrivateKeyAndCert(pivSlot, requiredKeyPolicy)
return priv, trace.Wrap(err)
case err != nil:
return nil, trace.Wrap(err)
@ -135,6 +142,12 @@ func GetDefaultKeySlot(policy PrivateKeyPolicy) (piv.Slot, error) {
case PrivateKeyPolicyHardwareKeyTouch:
// private_key_policy: hardware_key_touch -> 9c
return piv.SlotSignature, nil
case PrivateKeyPolicyHardwareKeyPIN:
// private_key_policy: hardware_key_pin -> 9d
return piv.SlotCardAuthentication, nil
case PrivateKeyPolicyHardwareKeyTouchAndPIN:
// private_key_policy: hardware_key_touch_and_pin -> 9e
return piv.SlotKeyManagement, nil
default:
return piv.Slot{}, trace.BadParameter("unexpected private key policy %v", policy)
}
@ -146,6 +159,10 @@ func getKeyPolicies(policy PrivateKeyPolicy) (piv.TouchPolicy, piv.PINPolicy, er
return piv.TouchPolicyNever, piv.PINPolicyNever, nil
case PrivateKeyPolicyHardwareKeyTouch:
return piv.TouchPolicyCached, piv.PINPolicyNever, nil
case PrivateKeyPolicyHardwareKeyPIN:
return piv.TouchPolicyNever, piv.PINPolicyOnce, nil
case PrivateKeyPolicyHardwareKeyTouchAndPIN:
return piv.TouchPolicyCached, piv.PINPolicyOnce, nil
default:
return piv.TouchPolicyNever, piv.PINPolicyNever, trace.BadParameter("unexpected private key policy %v", policy)
}
@ -227,7 +244,7 @@ func (y *YubiKeyPrivateKey) Public() crypto.PublicKey {
// Sign implements crypto.Signer.
func (y *YubiKeyPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
signCtx, cancel := context.WithCancel(context.Background())
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// To prevent concurrent calls to Sign from failing due to PIV only handling a
@ -235,19 +252,38 @@ func (y *YubiKeyPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.Sign
y.signMux.Lock()
defer y.signMux.Unlock()
// For generic auth errors, the smart card returns the error code 0x6982. This PIV library
// wraps error codes like this with a user readable message: "security status not satisfied".
const pivGenericAuthErrCodeString = "6982"
signature, err := y.sign(ctx, rand, digest, opts)
if err != nil && strings.Contains(err.Error(), pivGenericAuthErrCodeString) {
// If we get a generic auth error, it probably means the PIV connection didn't prompt for
// PIN when he PIV module expected PIN. This can happen in custom PIV modules that don't
// implement proper PIN caching in the connection, or potentially in very old YubiKey
// models. In these cases, modify the key's PIN policy to reflect that PIN should always
// be prompted for and try again.
y.attestation.PINPolicy = piv.PINPolicyAlways
signature, err = y.sign(ctx, rand, digest, opts)
}
if err != nil {
return nil, trace.Wrap(err)
}
return signature, nil
}
func (y *YubiKeyPrivateKey) sign(ctx context.Context, rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) {
yk, err := y.open()
if err != nil {
return nil, trace.Wrap(err)
}
defer yk.Close()
privateKey, err := yk.PrivateKey(y.pivSlot, y.Public(), piv.KeyAuth{})
if err != nil {
return nil, trace.Wrap(err)
}
var touchPromptDelayTimer *time.Timer
if y.attestation.TouchPolicy != piv.TouchPolicyNever {
touchPromptDelayTimer := time.NewTimer(signTouchPromptDelay)
touchPromptDelayTimer = time.NewTimer(signTouchPromptDelay)
defer touchPromptDelayTimer.Stop()
go func() {
@ -256,13 +292,34 @@ func (y *YubiKeyPrivateKey) Sign(rand io.Reader, digest []byte, opts crypto.Sign
// Prompt for touch after a delay, in case the function succeeds without touch due to a cached touch.
fmt.Fprintln(os.Stderr, "Tap your YubiKey")
return
case <-signCtx.Done():
case <-ctx.Done():
// touch cached, skip prompt.
return
}
}()
}
promptPIN := func() (string, error) {
// touch prompt delay is disrupted by pin prompts. To prevent misfired
// touch prompts, pause the timer for the duration of the pin prompt.
if touchPromptDelayTimer != nil {
if touchPromptDelayTimer.Stop() {
defer touchPromptDelayTimer.Reset(signTouchPromptDelay)
}
}
return prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PIN")
}
auth := piv.KeyAuth{
PINPrompt: promptPIN,
PINPolicy: y.attestation.PINPolicy,
}
privateKey, err := yk.PrivateKey(y.pivSlot, y.slotCert.PublicKey, auth)
if err != nil {
return nil, trace.Wrap(err)
}
signer, ok := privateKey.(crypto.Signer)
if !ok {
return nil, trace.BadParameter("private key type %T does not implement crypto.Signer", privateKey)
@ -317,10 +374,20 @@ func (y *YubiKeyPrivateKey) GetPrivateKeyPolicy() PrivateKeyPolicy {
return GetPrivateKeyPolicyFromAttestation(y.attestation)
}
// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy met by the given hardware key attestation.
// GetPrivateKeyPolicyFromAttestation returns the PrivateKeyPolicy satisfied by the given hardware key attestation.
func GetPrivateKeyPolicyFromAttestation(att *piv.Attestation) PrivateKeyPolicy {
switch att.TouchPolicy {
case piv.TouchPolicyCached, piv.TouchPolicyAlways:
isTouchPolicy := att.TouchPolicy == piv.TouchPolicyCached ||
att.TouchPolicy == piv.TouchPolicyAlways
isPINPolicy := att.PINPolicy == piv.PINPolicyOnce ||
att.PINPolicy == piv.PINPolicyAlways
switch {
case isPINPolicy && isTouchPolicy:
return PrivateKeyPolicyHardwareKeyTouchAndPIN
case isPINPolicy:
return PrivateKeyPolicyHardwareKeyPIN
case isTouchPolicy:
return PrivateKeyPolicyHardwareKeyTouch
default:
return PrivateKeyPolicyHardwareKey
@ -461,13 +528,6 @@ func (y *YubiKey) getPrivateKey(slot piv.Slot) (*PrivateKey, error) {
return nil, trace.Wrap(err)
}
// We don't yet support pin policies so we must return a user readable error in case
// they passed a mis-configured slot. Otherwise they will get a PIV auth error during signing.
// TODO(Joerger): remove this check once PIN prompt is supported.
if attestation.PINPolicy != piv.PINPolicyNever {
return nil, trace.NotImplemented(`PIN policy is not currently supported. Please generate a key with PIN policy "never"`)
}
priv := &YubiKeyPrivateKey{
YubiKey: y,
pivSlot: slot,
@ -481,7 +541,52 @@ func (y *YubiKey) getPrivateKey(slot piv.Slot) (*PrivateKey, error) {
return nil, trace.Wrap(err)
}
return NewPrivateKey(priv, keyPEM)
key, err := NewPrivateKey(priv, keyPEM)
if err != nil {
return nil, trace.Wrap(err)
}
return key, nil
}
// SetPin sets the YubiKey PIV PIN. This doesn't require user interaction like touch, just the correct old PIN.
func (y *YubiKey) SetPIN(oldPin, newPin string) error {
yk, err := y.open()
if err != nil {
return trace.Wrap(err)
}
defer yk.Close()
err = yk.SetPIN(oldPin, newPin)
return trace.Wrap(err)
}
// checkOrSetPIN prompts the user for PIN and verifies it with the YubiKey.
// If the user provides the default PIN, they will be prompted to set a
// non-default PIN and PUK before continuing.
func (y *YubiKey) checkOrSetPIN(ctx context.Context) error {
pin, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PIN [blank to use default PIN]")
if err != nil {
return trace.Wrap(err)
}
yk, err := y.open()
if err != nil {
return trace.Wrap(err)
}
defer yk.Close()
switch pin {
case piv.DefaultPIN:
fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN)
fallthrough
case "":
if pin, err = setPINAndPUKFromDefault(ctx, yk); err != nil {
return trace.Wrap(err)
}
}
return trace.Wrap(yk.VerifyPIN(pin))
}
// open a connection to YubiKey PIV module. The returned connection should be closed once
@ -665,3 +770,92 @@ const (
// slower cards (if there are any).
signTouchPromptDelay = time.Millisecond * 200
)
func setPINAndPUKFromDefault(ctx context.Context, yk *piv.YubiKey) (string, error) {
// YubiKey requires that PIN and PUK be 6-8 characters.
isValid := func(pin string) bool {
return len(pin) >= 6 && len(pin) <= 8
}
var pin string
for {
fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PIN.\n")
newPIN, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PIN")
if err != nil {
return "", trace.Wrap(err)
}
newPINConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PIN")
if err != nil {
return "", trace.Wrap(err)
}
if newPIN != newPINConfirm {
fmt.Fprintf(os.Stderr, "PINs do not match.\n")
continue
}
if newPIN == piv.DefaultPIN {
fmt.Fprintf(os.Stderr, "The default PIN %q is not supported.\n", piv.DefaultPIN)
continue
}
if !isValid(newPIN) {
fmt.Fprintf(os.Stderr, "PIN must be 6-8 characters long.\n")
continue
}
pin = newPIN
break
}
puk, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your YubiKey PIV PUK to reset PIN [blank to use default PUK]")
if err != nil {
return "", trace.Wrap(err)
}
switch puk {
case piv.DefaultPUK:
fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK)
fallthrough
case "":
for {
fmt.Fprintf(os.Stderr, "Please set a new 6-8 character PUK (used to reset PIN).\n")
newPUK, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Enter your new YubiKey PIV PUK")
if err != nil {
return "", trace.Wrap(err)
}
newPUKConfirm, err := prompt.Password(ctx, os.Stderr, prompt.Stdin(), "Confirm your new YubiKey PIV PUK")
if err != nil {
return "", trace.Wrap(err)
}
if newPUK != newPUKConfirm {
fmt.Fprintf(os.Stderr, "PUKs do not match.\n")
continue
}
if newPUK == piv.DefaultPUK {
fmt.Fprintf(os.Stderr, "The default PUK %q is not supported.\n", piv.DefaultPUK)
continue
}
if !isValid(newPUK) {
fmt.Fprintf(os.Stderr, "PUK must be 6-8 characters long.\n")
continue
}
if err := yk.SetPUK(piv.DefaultPUK, newPUK); err != nil {
return "", trace.Wrap(err)
}
puk = newPUK
break
}
}
if err := yk.Unblock(puk, pin); err != nil {
return "", trace.Wrap(err)
}
return pin, nil
}

View file

@ -24,6 +24,8 @@ import (
// If the slot is empty, a new private key matching the given policy will be generated in the slot.
// - hardware_key: 9a
// - hardware_key_touch: 9c
// - hardware_key_pin: 9d
// - hardware_key_touch_pin: 9e
func GetYubiKeyPrivateKey(ctx context.Context, policy PrivateKeyPolicy, slot PIVSlot) (*PrivateKey, error) {
priv, err := getOrGenerateYubiKeyPrivateKey(ctx, policy, slot)
if err != nil {

View file

@ -23,6 +23,7 @@ import (
"os"
"testing"
"github.com/go-piv/piv-go/piv"
"github.com/gravitational/trace"
"github.com/stretchr/testify/require"
@ -44,16 +45,19 @@ func TestGetYubiKeyPrivateKey_Interactive(t *testing.T) {
fmt.Println("This test is interactive, tap your YubiKey when prompted.")
ctx := context.Background()
resetYubikey(ctx, t)
t.Cleanup(func() { resetYubikey(t) })
for _, policy := range []keys.PrivateKeyPolicy{
keys.PrivateKeyPolicyHardwareKey,
keys.PrivateKeyPolicyHardwareKeyTouch,
keys.PrivateKeyPolicyHardwareKeyPIN,
keys.PrivateKeyPolicyHardwareKeyTouchAndPIN,
} {
for _, customSlot := range []bool{true, false} {
t.Run(fmt.Sprintf("policy:%q", policy), func(t *testing.T) {
t.Run(fmt.Sprintf("custom slot:%v", customSlot), func(t *testing.T) {
t.Cleanup(func() { resetYubikey(ctx, t) })
resetYubikey(t)
setupPINPrompt(t)
var slot keys.PIVSlot = ""
if customSlot {
@ -96,10 +100,7 @@ func TestOverwritePrompt(t *testing.T) {
}
ctx := context.Background()
resetYubikey(ctx, t)
oldStdin := prompt.Stdin()
t.Cleanup(func() { prompt.SetStdin(oldStdin) })
t.Cleanup(func() { resetYubikey(t) })
// Use a custom slot.
pivSlot, err := keys.GetDefaultKeySlot(keys.PrivateKeyPolicyHardwareKeyTouch)
@ -118,7 +119,7 @@ func TestOverwritePrompt(t *testing.T) {
}
t.Run("invalid metadata cert", func(t *testing.T) {
t.Cleanup(func() { resetYubikey(ctx, t) })
resetYubikey(t)
// Set a non-teleport certificate in the slot.
y, err := keys.FindYubiKey(0)
@ -130,7 +131,7 @@ func TestOverwritePrompt(t *testing.T) {
})
t.Run("invalid key policies", func(t *testing.T) {
t.Cleanup(func() { resetYubikey(ctx, t) })
resetYubikey(t)
// Generate a key that does not require touch in the slot that Teleport expects to require touch.
_, err := keys.GetYubiKeyPrivateKey(ctx, keys.PrivateKeyPolicyHardwareKey, keys.PIVSlot(pivSlot.String()))
@ -141,9 +142,24 @@ func TestOverwritePrompt(t *testing.T) {
}
// resetYubikey connects to the first yubiKey and resets it to defaults.
func resetYubikey(ctx context.Context, t *testing.T) {
func resetYubikey(t *testing.T) {
t.Helper()
y, err := keys.FindYubiKey(0)
require.NoError(t, err)
require.NoError(t, y.Reset())
}
func setupPINPrompt(t *testing.T) {
t.Helper()
y, err := keys.FindYubiKey(0)
require.NoError(t, err)
// Set pin for tests.
const testPIN = "123123"
require.NoError(t, y.SetPIN(piv.DefaultPIN, testPIN))
// Handle PIN prompt.
oldStdin := prompt.Stdin()
t.Cleanup(func() { prompt.SetStdin(oldStdin) })
prompt.SetStdin(prompt.NewFakeReader().AddString(testPIN).AddString(testPIN))
}

View file

@ -2370,7 +2370,10 @@ func generateCert(a *Server, req certRequest, caType types.CertAuthType) (*proto
}
attestedKeyPolicy := keys.PrivateKeyPolicyNone
requiredKeyPolicy := req.checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
requiredKeyPolicy, err := req.checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
if err != nil {
return nil, trace.Wrap(err)
}
if !req.skipAttestation && requiredKeyPolicy != keys.PrivateKeyPolicyNone {
// verify that the required private key policy for the requesting identity
// is met by the provided attestation statement.

View file

@ -3929,8 +3929,7 @@ func (a *ServerWithRoles) UpsertRole(ctx context.Context, role types.Role) error
}
// check that the given RequireMFAType is supported in this build.
switch role.GetOptions().RequireMFAType {
case types.RequireMFAType_SESSION_AND_HARDWARE_KEY, types.RequireMFAType_HARDWARE_KEY_TOUCH:
if role.GetPrivateKeyPolicy().IsHardwareKeyPolicy() {
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return trace.AccessDenied("Hardware Key support is only available with an enterprise license")
}
@ -4239,8 +4238,7 @@ func (a *ServerWithRoles) SetAuthPreference(ctx context.Context, newAuthPref typ
}
// check that the given RequireMFAType is supported in this build.
switch newAuthPref.GetRequireMFAType() {
case types.RequireMFAType_SESSION_AND_HARDWARE_KEY, types.RequireMFAType_HARDWARE_KEY_TOUCH:
if newAuthPref.GetPrivateKeyPolicy().IsHardwareKeyPolicy() {
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return trace.AccessDenied("Hardware Key support is only available with an enterprise license")
}

View file

@ -1733,6 +1733,8 @@ var requireMFATypes = []types.RequireMFAType{
types.RequireMFAType_SESSION,
types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
types.RequireMFAType_HARDWARE_KEY_TOUCH,
types.RequireMFAType_HARDWARE_KEY_PIN,
types.RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN,
}
func TestIsMFARequired(t *testing.T) {

View file

@ -306,10 +306,13 @@ func (a *authorizer) enforcePrivateKeyPolicy(ctx context.Context, authContext *C
// Check that the required private key policy, defined by roles and auth pref,
// is met by this Identity's tls certificate.
identityPolicy := authContext.Identity.GetIdentity().PrivateKeyPolicy
requiredPolicy := authContext.Checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
if err := requiredPolicy.VerifyPolicy(identityPolicy); err != nil {
requiredPolicy, err := authContext.Checker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
if err != nil {
return trace.Wrap(err)
}
if !requiredPolicy.IsSatisfiedBy(identityPolicy) {
return keys.NewPrivateKeyPolicyError(requiredPolicy)
}
return nil
}

View file

@ -3858,7 +3858,7 @@ func (tc *TeleportClient) GetNewLoginKey(ctx context.Context) (priv *keys.Privat
)
defer span.End()
if tc.PrivateKeyPolicy.IsHardwareKeyVerified() {
if tc.PrivateKeyPolicy.IsHardwareKeyPolicy() {
log.Debugf("Attempting to login with YubiKey private key.")
if tc.PIVSlot != "" {
log.Debugf("Using PIV slot %q specified by client or server settings.", tc.PIVSlot)
@ -4472,7 +4472,7 @@ func (tc *TeleportClient) applyAuthSettings(authSettings webclient.Authenticatio
tc.PIVSlot = authSettings.PIVSlot
// Update the private key policy from auth settings if it is stricter than the saved setting.
if authSettings.PrivateKeyPolicy != "" && authSettings.PrivateKeyPolicy.VerifyPolicy(tc.PrivateKeyPolicy) != nil {
if authSettings.PrivateKeyPolicy != "" && !authSettings.PrivateKeyPolicy.IsSatisfiedBy(tc.PrivateKeyPolicy) {
tc.PrivateKeyPolicy = authSettings.PrivateKeyPolicy
}
}

View file

@ -617,15 +617,32 @@ func TestAuthenticationConfig_RequireSessionMFA(t *testing.T) {
},
expectRequireMFAType: types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
}, {
desc: "RequireSessionMFA hardware_key",
desc: "RequireSessionMFA hardware_key_touch",
mutate: func(cfg cfgMap) {
cfg["auth_service"].(cfgMap)["authentication"] = cfgMap{
"require_session_mfa": types.RequireMFATypeHardwareKeyTouchString,
}
},
expectRequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH,
}, {
desc: "RequireSessionMFA hardware_key_pin",
mutate: func(cfg cfgMap) {
cfg["auth_service"].(cfgMap)["authentication"] = cfgMap{
"require_session_mfa": types.RequireMFATypeHardwareKeyPINString,
}
},
expectRequireMFAType: types.RequireMFAType_HARDWARE_KEY_PIN,
}, {
desc: "RequireSessionMFA hardware_key_touch_and_pin",
mutate: func(cfg cfgMap) {
cfg["auth_service"].(cfgMap)["authentication"] = cfgMap{
"require_session_mfa": types.RequireMFATypeHardwareKeyTouchAndPINString,
}
},
expectRequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
text := bytes.NewBuffer(editConfig(t, tt.mutate))

View file

@ -813,8 +813,7 @@ func NewTeleport(cfg *servicecfg.Config) (*TeleportProcess, error) {
}
}
switch cfg.Auth.Preference.GetRequireMFAType() {
case types.RequireMFAType_SESSION_AND_HARDWARE_KEY, types.RequireMFAType_HARDWARE_KEY_TOUCH:
if cfg.Auth.Preference.GetPrivateKeyPolicy().IsHardwareKeyPolicy() {
if modules.GetModules().BuildType() != modules.BuildEnterprise {
return nil, trace.AccessDenied("Hardware Key support is only available with an enterprise license")
}

View file

@ -216,7 +216,7 @@ type AccessChecker interface {
GetAccessState(authPref types.AuthPreference) AccessState
// PrivateKeyPolicy returns the enforced private key policy for this role set,
// or the provided defaultPolicy - whichever is stricter.
PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) keys.PrivateKeyPolicy
PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) (keys.PrivateKeyPolicy, error)
// GetKubeResources returns the allowed and denied Kubernetes Resources configured
// for a user.

View file

@ -1218,24 +1218,13 @@ func (set RoleSet) getMFARequired(clusterRequireMFAType types.RequireMFAType) MF
}
// PrivateKeyPolicy returns the enforced private key policy for this role set.
func (set RoleSet) PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) keys.PrivateKeyPolicy {
if defaultPolicy == keys.PrivateKeyPolicyHardwareKeyTouch {
// This is the strictest option so we can return now
return defaultPolicy
}
policy := defaultPolicy
func (set RoleSet) PrivateKeyPolicy(authPreferencePolicy keys.PrivateKeyPolicy) (keys.PrivateKeyPolicy, error) {
policySet := []keys.PrivateKeyPolicy{authPreferencePolicy}
for _, role := range set {
switch rolePolicy := role.GetPrivateKeyPolicy(); rolePolicy {
case keys.PrivateKeyPolicyHardwareKey:
policy = rolePolicy
case keys.PrivateKeyPolicyHardwareKeyTouch:
// This is the strictest option so we can return now
return keys.PrivateKeyPolicyHardwareKeyTouch
}
policySet = append(policySet, role.GetPrivateKeyPolicy())
}
return policy
return keys.PolicyThatSatisfiesSet(policySet)
}
// AdjustSessionTTL will reduce the requested ttl to the lowest max allowed TTL

View file

@ -39,7 +39,6 @@ import (
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/wrappers"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/sshutils"
"github.com/gravitational/teleport/lib/fixtures"
"github.com/gravitational/teleport/lib/tlsca"
@ -1496,6 +1495,74 @@ func TestCheckAccessToServer(t *testing.T) {
{server: serverWorker, login: "root", hasAccess: true},
{server: serverDB, login: "root", hasAccess: true},
},
}, {
name: "cluster requires hardware key pin, MFA not verified",
roles: []*types.RoleV6{
newRole(func(r *types.RoleV6) {
r.Spec.Allow.Logins = []string{"root"}
}),
},
authSpec: types.AuthPreferenceSpecV2{
// Functionally equivalent to "session".
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_PIN,
},
checks: []check{
{server: serverNoLabels, login: "root", hasAccess: false},
{server: serverWorker, login: "root", hasAccess: false},
{server: serverDB, login: "root", hasAccess: false},
},
},
{
name: "cluster requires hardware key pin, MFA verified",
roles: []*types.RoleV6{
newRole(func(r *types.RoleV6) {
r.Spec.Allow.Logins = []string{"root"}
}),
},
authSpec: types.AuthPreferenceSpecV2{
// Functionally equivalent to "session".
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_PIN,
},
mfaVerified: true,
checks: []check{
{server: serverNoLabels, login: "root", hasAccess: true},
{server: serverWorker, login: "root", hasAccess: true},
{server: serverDB, login: "root", hasAccess: true},
},
}, {
name: "cluster requires hardware key touch and pin, MFA not verified",
roles: []*types.RoleV6{
newRole(func(r *types.RoleV6) {
r.Spec.Allow.Logins = []string{"root"}
}),
},
authSpec: types.AuthPreferenceSpecV2{
// Functionally equivalent to "session".
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN,
},
checks: []check{
{server: serverNoLabels, login: "root", hasAccess: false},
{server: serverWorker, login: "root", hasAccess: false},
{server: serverDB, login: "root", hasAccess: false},
},
},
{
name: "cluster requires hardware key touch and pin, MFA verified",
roles: []*types.RoleV6{
newRole(func(r *types.RoleV6) {
r.Spec.Allow.Logins = []string{"root"}
}),
},
authSpec: types.AuthPreferenceSpecV2{
// Functionally equivalent to "session".
RequireMFAType: types.RequireMFAType_HARDWARE_KEY_TOUCH_AND_PIN,
},
mfaVerified: true,
checks: []check{
{server: serverNoLabels, login: "root", hasAccess: true},
{server: serverWorker, login: "root", hasAccess: true},
{server: serverDB, login: "root", hasAccess: true},
},
},
// Device Trust.
{
@ -7588,79 +7655,6 @@ func TestRoleSet_GetAccessState(t *testing.T) {
}
}
func TestPrivateKeyPolicy(t *testing.T) {
testCases := []struct {
name string
roleMFARequireTypes []types.RequireMFAType
authPrefPrivateKeyPolicy keys.PrivateKeyPolicy
expectPrivateKeyPolicy keys.PrivateKeyPolicy
}{
{
name: "empty role set and auth pref requirement",
},
{
name: "hardware_key not required",
roleMFARequireTypes: []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
},
authPrefPrivateKeyPolicy: keys.PrivateKeyPolicyNone,
expectPrivateKeyPolicy: keys.PrivateKeyPolicyNone,
},
{
name: "auth pref requires hardware_key",
roleMFARequireTypes: []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
},
authPrefPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKey,
expectPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKey,
},
{
name: "role requires hardware_key",
roleMFARequireTypes: []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
},
authPrefPrivateKeyPolicy: keys.PrivateKeyPolicyNone,
expectPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKey,
},
{
name: "auth pref requires hardware_key_touch",
roleMFARequireTypes: []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
},
authPrefPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
expectPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
},
{
name: "role requires hardware_key_touch",
roleMFARequireTypes: []types.RequireMFAType{
types.RequireMFAType_OFF,
types.RequireMFAType_SESSION,
types.RequireMFAType_SESSION_AND_HARDWARE_KEY,
types.RequireMFAType_HARDWARE_KEY_TOUCH,
},
authPrefPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKey,
expectPrivateKeyPolicy: keys.PrivateKeyPolicyHardwareKeyTouch,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var set RoleSet
for _, roleRequirement := range tc.roleMFARequireTypes {
set = append(set, newRole(func(r *types.RoleV6) {
r.Spec.Options.RequireMFAType = roleRequirement
}))
}
require.Equal(t, tc.expectPrivateKeyPolicy, set.PrivateKeyPolicy(tc.authPrefPrivateKeyPolicy))
})
}
}
func TestAzureIdentityMatcher_Match(t *testing.T) {
tests := []struct {
name string

View file

@ -40,13 +40,11 @@ import (
"github.com/gravitational/teleport/lib/services"
)
var (
userSessionLimitHitCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricUserMaxConcurrentSessionsHit,
Help: "Number of times a user exceeded their max concurrent ssh connections",
},
)
var userSessionLimitHitCount = prometheus.NewCounter(
prometheus.CounterOpts{
Name: teleport.MetricUserMaxConcurrentSessionsHit,
Help: "Number of times a user exceeded their max concurrent ssh connections",
},
)
func init() {
@ -219,10 +217,13 @@ func (s *SessionController) AcquireSessionContext(ctx context.Context, identity
// Check that the required private key policy, defined by roles and auth pref,
// is met by this Identity's ssh certificate.
identityPolicy := identity.Certificate.Extensions[teleport.CertExtensionPrivateKeyPolicy]
requiredPolicy := identity.AccessChecker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
if err := requiredPolicy.VerifyPolicy(keys.PrivateKeyPolicy(identityPolicy)); err != nil {
return ctx, trace.Wrap(err)
identityPolicy := keys.PrivateKeyPolicy(identity.Certificate.Extensions[teleport.CertExtensionPrivateKeyPolicy])
requiredPolicy, err := identity.AccessChecker.PrivateKeyPolicy(authPref.GetPrivateKeyPolicy())
if err != nil {
return nil, trace.Wrap(err)
}
if !requiredPolicy.IsSatisfiedBy(identityPolicy) {
return ctx, keys.NewPrivateKeyPolicyError(requiredPolicy)
}
// Don't apply the following checks in non-node contexts.

View file

@ -96,8 +96,8 @@ func (m mockAccessChecker) MaxConnections() int64 {
return m.maxConnections
}
func (m mockAccessChecker) PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) keys.PrivateKeyPolicy {
return m.keyPolicy
func (m mockAccessChecker) PrivateKeyPolicy(defaultPolicy keys.PrivateKeyPolicy) (keys.PrivateKeyPolicy, error) {
return m.keyPolicy, nil
}
func (m mockAccessChecker) RoleNames() []string {

View file

@ -39,7 +39,7 @@ Personal Identity Verification (PIV), described in [FIPS-201](https://csrc.nist.
PIV builds upon the PKCS#11 interface and provides us with additional capabilities including:
* Optional PIN and Touch requirements for accessing keys
* Optional pin and touch requirements for accessing keys
* PIV secrets for granular [administrative access](https://developers.yubico.com/PIV/Introduction/Admin_access.html)
* [Attestation](https://docs.yubico.com/yesdk/users-manual/application-piv/attestation.html) of private key slots
@ -68,6 +68,8 @@ Note: the adjustments above will largely be client-side and therefore should not
### Security
#### Touch enforcement
Currently, Teleport clients generate new RSA private keys to be signed by the Teleport Auth server during login. These keys are then stored on disk alongside the certificates (in `~/.tsh`), where they can be accessed and used to perform actions as the logged in user. These actions include any Teleport Auth server request, such as listing clusters (`tsh ls`), starting an ssh session (`tsh ssh`), or adding/changing cluster resources (`tctl create`). If an attacker manages to exfiltrate a user's `~/.tsh` folder, they could use the contained certificates and key to perform actions as the user.
With the introduction of a hardware private key, the user's key would not be stored on disk in `~/.tsh`. Instead, it would be generated and stored directly on the hardware key, where it can not be exported. Therefore, if an attacker exfiltrates a user's `~/.tsh` folder, the contained certificates would be useless without also having access to the user's hardware key.
@ -77,13 +79,15 @@ So far, just introducing hardware private keys into the login process prevents s
For this, we have two options:
1. Enable [per-session MFA](https://goteleport.com/docs/access-controls/guides/per-session-mfa/), which requires you to pass an MFA check (touch) to start a new Teleport Service session (SSH/Kube/etc.)
2. Require Touch to access hardware private keys, which can be done with PIV-compatible hardware keys. In this case, touch is required for every Teleport request, not just new Teleport Service sessions
2. Require touch to access hardware private keys, which can be done with PIV-compatible hardware keys. In this case, touch is required for every Teleport request, not just new Teleport Service sessions
The first option is a bit simpler as it rides off the coattails of our existing per-session MFA system. On the other hand, the second option provides better security principles, since touch is enforced for every Teleport request rather than just Session requests, and it requires fewer roundtrips to the Auth server.
In this RFD we'll explore both options together, since they are not mutually exclusive, and may provide unique value.
Note: If either of these options are combined with MFA/PIV PIN enforcement, or biometric key usage (like the [Yubikey Bio Series](https://www.yubico.com/products/yubikey-bio-series/)), then even if a user's computer and hardware key are stolen, the user's login session would not provide access to an attacker. To avoid overcomplicating this RFD, we will omit this consideration and leave it as a possible future improvement.
#### PIN enforcement
Hardware key private keys can also be configured to require pin to be used in cryptographical operations. When combined with touch, requiring pin provides a level of authentication security similar to passwordless, as both user presence and a user secret are verified.
### Server changes
@ -96,8 +100,12 @@ We will start with the following private key policies:
* `none` (default): No enforcement on private key usage
* `hardware_key`: A user's private keys must be generated on a hardware key. As a result, the user cannot use their signed certificates unless they have their hardware key connected
* `hardware_key_touch`: A user's private keys must be generated on a hardware key, and must require touch to be accessed. As a result, the user must touch their hardware key on login, and on subsequent requests (touch is cached on the hardware key for 15 seconds)
* `hardware_key_pin`: A user's private keys must be generated on a hardware key, and must require pin to be accessed. As a result, the user must enter their PIV pin on login, and on subsequent requests.
* Unlike touch, pin is not cached explicitly. However, the pin is cached for the duration of a single PIV transaction. PIV transactions take a few seconds to close and can be reclaimed by subsequent PIV connections during the closing period. In this case, when multiple `tsh` commands are run in quick succession, it is as if the pin is cached.
* This policy is intended for rare circumstances where a touch policy can not be configured due to the use of external PIV tools. However, since pin alone does not verify user presence, this option opens the door for remote attacks. When possible, `hardware_key_touch_and_pin` should be used instead of this option.
* `hardware_key_touch_and_pin`: combination of `hardware_key_touch` and `hardware_key_pin`.
In the future, we could choose to enforce more things, such as requiring PIN to be used, or requiring a specific key algorithm.
In the future, we could choose to enforce more things, such as requiring a specific key algorithm.
#### Private Key Policy Enforcement
@ -154,7 +162,7 @@ When the Auth Server receives a login request, it will check the attached attest
After the attestation statement has been verified, we can pull additional properties from the `slot_cert`'s extensions, which includes data like:
* Device information including serial number, model, and version
* Configured Touch (And PIN) Policies
* Configured touch and pin Policies
This data will then be checked against the user's private key policy requirement. If the policy requirement is met, the Auth server will sign the user's certificates with a private key policy extension matching the attestation.
@ -193,7 +201,7 @@ auth_service:
...
authentication:
...
require_session_mfa: off | on | hardware_key | hardware_key_touch
require_session_mfa: off | on | hardware_key | hardware_key_touch | hardware_key_pin | hardware_key_touch_and_pin
```
```yaml
@ -202,7 +210,7 @@ version: v2
metadata:
name: cluster-auth-preference
spec:
require_session_mfa: off | on | hardware_key | hardware_key_touch
require_session_mfa: off | on | hardware_key | hardware_key_touch | hardware_key_pin | hardware_key_touch_and_pin
```
```yaml
@ -212,12 +220,23 @@ metadata:
name: role-name
spec:
role_options:
require_session_mfa: off | on | hardware_key | hardware_key_touch
require_session_mfa: off | on | hardware_key | hardware_key_touch | hardware_key_pin | hardware_key_touch_and_pin
```
* `on`: Enforce per-session MFA. Users are required to pass an MFA challenge with a registered MFA device in order to start new SSH|Kubernetes|DB|Desktop sessions. Non-session requests, and app-session requests are not impacted.
* `hardware_key`: Enforce per-session MFA and private key policy `hardware_key`.
* `hardware_key_touch`: Enforce private key policy `hardware_key_touch`. This replaces per-session MFA with per-request PIV-touch.
* `hardware_key_touch`: Enforce private key policy `hardware_key_touch`. This replaces per-session MFA with per-request PIV touch.
* `hardware_key_pin`: Enforce private key policy `hardware_key_pin`. This replaces per-session MFA with per-request PIV pin.
* `hardware_key_touch_and_pin`: Enforce private key policy `hardware_key_touch_and_pin`. This replaces per-session MFA with per-request PIV touch and pin.
##### PIV PIN counts as MFA
As [mentioned before](#private-key-policy), `hardware_key_pin` does not verify presence. In order to support this use cases, we have 2 options:
1. Require normal MFA in addition to PIV pin when MFA verification is required (per-session MFA, admin actions MFA). This would be functionally similar to the `hardware_key` option, where MFA touch is only required for sessions.
2. Treat `hardware_key_pin` as MFA verified, skipping the presence. This would be functionally the same as `hardware_key_touch`, but only PIN would be prompted instead of touch.
For a simpler UX, we will go with option 2. If in the future we decide to switch approaches, we can deprecate `hardware_key_pin` and replace it with `hardware_key_pin_and_mfa` to implement option 1.
##### Webauthn
@ -233,14 +252,14 @@ auth_service:
require_session_mfa: on | hardware_key
```
However, `hardware_key_touch` used PIV instead of MFA, so it can be configured standalone:
However, touch/pin policies use PIV instead of MFA, so it can be configured without WebAuthn:
```yaml
auth_service:
authentication:
type: local
second_factor: off
require_session_mfa: hardware_key_touch
require_session_mfa: hardware_key_touch | hardware_key_pin | hardware_key_touch_and_pin
```
##### Per-resource enforcement
@ -275,7 +294,7 @@ spec:
...
```
However, the same resource-based approach does not apply to `hardware_key` or `hardware_key_touch`. Since the initial login credentials are used for all requests, regardless of resource, the user's login session must start with the strictest private key policy requirement.
However, the same resource-based approach does not apply to hardware key policy requirement. Since the initial login credentials are used for all requests, regardless of resource, the user's login session must start with the strictest private key policy requirement.
### Client changes
@ -295,15 +314,16 @@ If a user's private key policy requirement is increased during an active login,
On login, a Teleport client will find a private key that meets the private key policy provided (via the key policy guesser or server error). If the key policy is `none`, then a new RSA private key will be generated as usual.
If the key policy is `hardware_key` or `hardware_key_touch`, then a private key will be generated directly on the hardware key. The resulting login certificates will only be operable if:
If a hardware key policy is required, then a private key will be generated directly on the hardware key. The resulting login certificates will only be operable if:
* The hardware key is connected during the operation
* The hardware private key can still be found
* The hardware private key's Touch challenge is passed (if applicable)
* The hardware private key's touch challenge is passed (if applicable)
* The hardware private key's pin challenge is passed (if applicable)
#### PIV slot logic
PIV provides us with up to 24 different slots. Each slot has a different intended purpose, but functionally they are the same. We will use the first two slots (`9a` and `9c`) to store up to two keys at a time (the first with `TouchPolicy=never` and the second with `TouchPolicy=cached`).
PIV provides us with up to 24 different slots. Each slot has a different intended purpose, but functionally they are the same. We will use the first four slots (`9a`, `9c`, `9d`, `9e`) to support each of the 4 hardware key policy requirements (`hardware_key`, `hardware_key_touch`, `hardware_key_pin`, `hardware_key_touch_and_pin` respectively).
Each of these keys will be generated for the first time when a Teleport client is required to meet its respective private key policy. Once a key is generated, it will be reused by any other Teleport client required to meet the same private key policy.
@ -367,7 +387,7 @@ The WebUI will not be able to support PIV login, since it is browser-based and c
It may be possible to work around this limitation by introducing a local proxy to connect to the hardware key, or by supporting a hardware key solution which doesn't need a direct connection, but this is out of scope and will not be explored in this PR.
In cases where WebUI access is needed or desired, cluster admins should only apply `require_session_mfa: hardware_key | hardware_key_touch` selectively to roles which warrant more protection. Teleport Connect will also serve as a great UI alternative.
In cases where WebUI access is needed or desired, cluster admins should only apply hardware key policies selectively to roles which warrant more protection. Teleport Connect will also serve as a great UI alternative.
### UX
@ -413,7 +433,7 @@ Please insert the YubiKey used during login (serial number XXXXXX) to continue..
##### Touch requirement
If a user has private key policy `hardware_key_touch`, then Teleport client requests will require touch (cached for 15 seconds). This will be handled by a touch prompt similar to the one used for MFA. This prompt will occur before prompting for login credentials.
If a user has private key policy `hardware_key_touch` or `hardware_key_touch_and_pin`, then Teleport client requests will require touch (cached for 15 seconds). This will be handled by a touch prompt similar to the one used for MFA. This prompt will occur before prompting for login credentials.
```bash
> tsh login --user=dev
@ -422,6 +442,19 @@ Tap any security key
Tap your YubiKey
```
##### PIN requirement
If a user has private key policy `hardware_key_pin` or `hardware_key_touch_and_pin`, then Teleport client requests will require pin. This will be handled by a password style prompt.
```bash
> tsh login --user=dev
Enter password for Teleport user dev:
Tap any security key
Enter your YubiKey PIV PIN:
```
Note: Since this prompt requires stdin, it may not work in environments that do not support stdin (ex: `ssh` with `tsh proxy ssh` ProxyCommand).
### Additional considerations
#### Database support
@ -448,6 +481,22 @@ Some PIV operations require [administrative access](https://developers.yubico.co
| PIN | 8 chars | `123456` | sign and decrypt data, reset pin |
| PUK | 8 chars | `12345678` | reset PIN when blocked by failed attempts |
In our case, we only need to use the Management Key to generate a key and set a certificate on the YubiKey. To simplify our implementation and limit UX impact, we will assume the user's PIV device to use the default Management Key. User's can use the private `--piv-management-key` flag during login in case they need to use a non-default management key.
To simplify our implementation and limit UX impact, we will assume the user's PIV device to use the default Management Key. User's can use the private `--piv-management-key` flag during login in case they need to use a non-default management key.
In the future, we may want to add support for using non-default management key to better protect the generation and retrieval of private keys on the user's PIV key, as well as PIN management if we decide to new private key policies like `hardware_key_touch_pin`.
However, if pin is required, we must require the user to set a non-default PIN and PUK to prevent these keys from easily being accessed by attackers. To that end, if a user provides `123456` during a PIN prompt, they will be prompted to provide a new PIN and PUK before continuing. Again, the default values will not be accepted.
```bash
> tsh login --user=dev
Enter your YubiKey PIV PIN [blank to use default PIN]:
# \n
The default PIN 123456 is not supported.
Please set a new 6 digit PIN:
Enter your new YubiKey PIV PIN:
Confirm your new YubiKey PIV PIN:
Enter your YubiKey PIV PUK to reset PIN [blank to use default PUK]
# \n
The default PUK 12345678 is not supported
Please set a new 8 digit PUK:
Enter your new YubiKey PIV PUK:
Confirm your new YubiKey PIV PUK:
```

View file

@ -205,7 +205,6 @@ func listDatabasesAllClusters(cf *CLIConf) error {
mu.Unlock()
return nil
})
}
@ -1526,8 +1525,7 @@ func (r *dbLocalProxyRequirement) addLocalProxyWithTunnel(reasons ...string) {
// for a given database.
func getDBLocalProxyRequirement(tc *client.TeleportClient, route tlsca.RouteToDatabase) *dbLocalProxyRequirement {
var out dbLocalProxyRequirement
switch tc.PrivateKeyPolicy {
case keys.PrivateKeyPolicyHardwareKey, keys.PrivateKeyPolicyHardwareKeyTouch:
if tc.PrivateKeyPolicy.IsHardwareKeyPolicy() {
out.addLocalProxyWithTunnel(formatKeyPolicyReason(tc.PrivateKeyPolicy))
}

View file

@ -4202,8 +4202,8 @@ func onStatus(cf *CLIConf) error {
return trace.NotFound("Active profile expired.")
}
if tc.PrivateKeyPolicy == keys.PrivateKeyPolicyHardwareKeyTouch {
log.Debug("Skipping cluster alerts due to Hardware Key Touch requirement.")
if tc.PrivateKeyPolicy.MFAVerified() {
log.Debug("Skipping cluster alerts due to Hardware Key PIN/Touch requirement.")
} else {
if err := common.ShowClusterAlerts(cf.Context, tc, os.Stderr, nil,
types.AlertSeverity_HIGH); err != nil {