* Allow connections to nodes when Auth is offline
`tsh ssh user@foo` currently fails if Auth is unreachable. Prior to
connecting to the target, `tsh` sends a `proto.AuthService/IsMFARequired`
request to determine if per-session MFA is required. If necessary, the MFA
ceremony then occurs before `tsh` attempts to connect to the node.
However, nodes ultimately makes the decision on whether the user has
access. To both increase availability of nodes and reduce initial
connection latency, `tsh` can first attempt to connect to a node and
fallback to checking if per-session MFA is required and then performing
the ceremony if necessary followed by attempting another connection to the
node. The major tradeoff is now when per-session MFA is required, `tsh`
will connect to the target **twice**. The added latency is likely to be
minimized by the amount of time it takes to perform the MFA ceremony.
## Connection flows prior to this change:
### Auth online and per-session MFA required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Auth: IsMFARequired
Auth-->>-tsh: yes
tsh->>+Auth: perform mfa ceremony
Auth-->>+tsh: issue challenge
tsh-->>+Auth: challenge response
Auth-->>+tsh: issue certificates
tsh->>+Node: Connect
Node-->>+tsh: Success
```
### Auth online and per-session MFA not required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Auth: IsMFARequired
Auth-->>-tsh: no
tsh->>+Node: Connect
Node-->>+tsh: Success
```
### Auth offline and per-session MFA not required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Auth: IsMFARequired
Auth-->>-tsh: Error
```
### Auth offline and per-session MFA required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Auth: IsMFARequired
Auth-->>-tsh: Error
```
## Connection flows after this change:
### Auth online and per-session MFA required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Node: Connect
Node-->>+tsh: Access Denied
tsh->>+Auth: IsMFARequired
Auth-->>-tsh: yes
tsh->>+Auth: perform mfa ceremony
Auth-->>+tsh: issue challenge
tsh-->>+Auth: challenge response
Auth-->>+tsh: issue certificates
tsh->>+Node: Connect
Node-->>+tsh: Success
```
### Auth online and per-session MFA not required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Node: Connect
Node-->>+tsh: Success
```
### Auth offline and per-session MFA not required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Node: Connect
Node-->>+tsh: Success
```
### Auth offline and per-session MFA required
```mermaid
sequenceDiagram
participant tsh
participant Auth
participant Node
tsh->>+Node: Connect
Node-->>+tsh: Access Denied
tsh->>+Auth: IsMFARequired
Auth-->>-tsh: Error
```
* Add local proxy middleware for db cert checking
* Use tls conversion util instead of inline
* Add middleware to local proxy config
* Add middleware configuration in tsh
* Use route to database check and set defaults func
* Dont trigger normal db login flow if using local proxy tunnel
* Split out adding client creds into helper func for testing
* Add integration test for local proxy tunnel db cert middleware
* Add unit test for local proxy middleware
* Update comment
* Make middleware on new conn block
* godoc
* Make any cert check error trigger cert renewal in local proxy middleware
* Move dbcertchecker into lib/client
* Remove unneeded mutex in local proxy and unused func in lib/utils
* Make local proxy middleware integration test more robust
* Print message before mfa prompt in proxy tunnel
* Add before prompt option to test
* Remove unneeded comment
* Change local proxy messages to be more clear
* Pass local proxy opts by reference
* Pass certs in opts instead of cert/key file path
This is so we can check if the error is recoverable while preparing
local proxy options. A tunneled local proxy can ignore the error because
it does not rely on cert files - it can just renew its certs if
necessary.
* Move db route checking back to tsh
* Fix lint err
* Fix typo and print the hint to same writer as the mfa prompt
Add a credential picker to the tsh FIDO2/WebAuthn backend.
The PR pulls a recent patch in our go-libfido2 fork that makes it correctly
return multiple assertions from the authenticator. This allows us to implement
the credential picker for FIDO2, simplify our implementation and provide the
exact same UX that browsers use (always 1-touch for bio, touch->PIN->touch
otherwise).
I've dropped concepts like "optimistic assertions" and "eager PIN prompts" in
favor of a simple, uniform implementation.
Issue #13901.
* Prompt for credentials in LoginPrompt
* Update go-libfido2
* Implement FIDO2 credential picker
* Drop optimistic assertions, only set user if explicit
* Add license to fido2_prompt_test.go
Reinstates some logic that was removed on #12475 and changes `optsOverride` to a
function, so there is less ambiguity in dealing with booleans / default values.
* Pass proxy address to PromptMFAChallenge calls
* Add coverage for TeleportClient.PromptMFAChallenge
Avoid "input swallowing" bugs by disabling stdin hijacking by default.
Only `tsh login` is allowed to hijack stdin, as it is expected to exit right
after authentication. Any MFA authentication attempts resulting from
non-`tsh login` invocations default to the user's strongest auth method.
Defaulting to the strongest auth method can cause problems in constrained
environments for users that have both Webauthn and OTP registered. For example,
someone using `tsh` under WSL (Windows Subsystem for Linux) or a remote machine
could be locked into Webauthn MFA, which they can't use because their
environment lacks USB access or they don't have physical access to it. In order
to solve this problem I've slightly modified the meaning of the `--mfa-mode`
flag so `otp` can be specified.
The `TELEPORT_MFA_MODE` environment variable may be set to avoid constant flag
passing.
Fixes#12675 and #13021.
* Expand --mfa-mode and disable stdin hijack by default
* Use TELEPORT_ instead of TSH_ for FIDO2 env var
* Use t.Setenv in tests
Adding a "marker" to Keychain labels lets us detect unexpected entries (which I
did see happen) and gives us flexibility to change formats in the future.
A marker in the "label" field seems to be enough to achieve the purposes above,
so I stopped at that. Added a couple minor UX tweaks as well.
#9160
* Use a marker for Keychain labels
* Tweak: Rename "Key Handle" to "Credential ID" for consistency
* Tweak: Do not prompt MFA/OTP if doing Touch ID
* Use trace errors
Implement touch ID credential management via tsh touchid ls and tsh touchid rm.
Departs slightly from RFD command names in order to better match the tsh mfa.
See https://github.com/gravitational/teleport/blob/master/rfd/0054-passwordless-macos.md.
#9160
* Implement touch ID credential listing
* Add the `tsh touchid ls` command
* Implement touch ID credential deletion
* Add the `tsh touchid rm` command
* Delegate MFA prompts to WebAuthn
* Undo changes to tsh.go command switch
* Prompt newline. Trace errors.
* Update e/ to 6abb96b
* Var initialization. Guard against NULL. Return all credentials.
* Address review comments: simplifications and style
Add the --mfa-mode flag and wire touch ID into tsh authn/registration.
In the default MFA mode (auto), touch ID should be eager if there is a
credential present in the system. Users may change this behavior using other MFA
modes.
See https://github.com/gravitational/teleport/blob/master/rfd/0054-passwordless-macos.md.
#9160
* Add AuthenticatorAttachment/MFAMode to tsh
* Add support for touch ID in wancli.Login
* Warn users about passwordless in U2F mode
* Add support for touch ID in `tsh mfa add`
Fixes a potential stdin hijacking bug by making relogin attempts default to a
single MFA method (the strongest available).
The problematic scenario is as follows:
1. User has both OTP and security keys registered
2. "Relogin" is triggered via a tsh command (say,
`tsh logout; tsh ssh --proxy=example.com llama@myserver`)
3. User is prompted to pick either OTP or security key ("Tap any security key or
enter a code from a OTP device")
4. An stdin read is fired in the background to read the OTP code (via
prompt.Stdin)
5. User picks the security method, thus the stdin read is "abandoned"
In most cases this is fine, as the program ends right after. The issue is when a
relogin is triggered by a long living tsh invocation (again, `tsh ssh ...`): in
this case the stdin hijack causes input to be swallowed.
Forcing a single MFA option avoids the potential stdin hijack, fixing the
problem for all relogin invocations. `tsh login` behavior remains the same.
Note that we have to default to cluster's most secure method _without_ checking
the user devices. The user is not logged in yet, thus the backend cannot reveal
any information about that user.
Fixes#11709.
* Add UseStrongestAuth flag to PromptMFAChallenge
* Add TeleportClient.UseStrongestAuth and set it true for relogin
* Proper testing
* Address review comments
Introduce the concept of an "optimistic assertion", which allows us to skip
credential listing (saving a touch) and go directly for an authenticator
assertion.
The downside of an optimistic assertion is that the authenticator picks the
credential, meaning that we can't guarantee or choose the user. This should be
fine for most people, as they are unlikely to have multiple Teleport users in a
single cluster. If the --user flag is explicitly provided we'll honor it and do
the two-touch ceremony instead.
Optimistic assertions are only applied for biometric authenticators; we already
do single-touch for PINs if possible.
This is a bit of an experimental change. It should improve the experience in
most scenarios, but we may elect to rollback if the underlying assumption proves
itself to be poor.
Note that we now depend on gravitational/go-libfido2, as the upstream
go-libfido2 doesn't yet return the credential ID and user ID in assertions.
#9160
* Alias keys-pub/go-libfido2 to gravitational/go-libfido2
* Add LoginOpts to wancli.Login
* Allow optimistic assertions for bio devices
* Use optimistic assertions if the user is not provided
* Pull optimistic assertion check to deviceCallback
* Tweak OptimisticAssertion godoc
I couldn't actually repro the failure, but that seems like a plausible cause, so
let's see if it helps.
#9160
* Attempt to fix flakiness in TestTeleportClient_Login_local
* s/otpWait/otpCancelAndWait/g
During passwordless logins, if the only candidate device is PIN protected, we
can ask for the PIN right away, saving a touch.
The timing for the the initial touch prompt is now dictated by
wancli.Login/wancli.Register, as the touch is not necessarily the first
operation to happen anymore.
#9160
* Move initial prompt to wancli callbacks
* Eagerly ask for PIN in certain situations
* Rename func to "shouldDoEagerPINPrompt"
Introduce the prompt.Password function and use it consistently whenever we read
a password, OTP or PIN.
The PR makes it easy to support PINs during MFA authentication, so now we do so.
It also adds the capability to mock prompt.Stdin() for tests, adding a uniform
way to fake user input in tests without having to swap functions.
Complements #10953 and #9160.
* Prompt for OTPs as passwords (take 1), read PINs on MFA authentication
* Add the prompt.Password method
* Add Stdin mocking capabilities to the prompt package
* Delegate password reads to prompt.Stdin().ReadPassword()
* Remove stdinHijack from PromptMFAChallenge
* Change api_login_test.go to FakeReader
* Change tsh_test.go to FakeReader
* Retire client.ReadPassword in favor of prompt.Password
Seamlessly change the public API of lib/auth/webauthncli to use the
libfido2-backed implementation, as long as the binary was compiled with the
libfido2 build tag.
A few adjustments are necessary to "wancli" methods to allow users to provide
prompt callbacks and to return the credential user (not applicable here, but
will be in following PRs).
Additional changes are made to tsh mfa add in order to avoid stdin hijacking by
ContextReader, since we now may require PIN reads for authenticators.
#9160
* Move U2F logic to u2f_* files
* Split U2F API from general l/a/webauthncli API
* Move FIDO2 public API to fido2_common.go, introduce IsFIDO2Available
* Introduce prompt.SyncReader
Sync reads allow prompt calls to be mixed with term.ReadPassword calls.
* Wire FIDO2 into MFA login
* Wire FIDO2 into MFA registration
Alias the "u2f" second factor mode to "webauthn", effectively sunsetting U2F in
favor of WebAuthn.
The change effectively disables "U2F mode" server-side, making Teleport use
WebAuthn instead. This is in line with our compatibility promise, as Teleport
8.x clients are already WebAuthn-capable (and thus have no problems talking to
the cluster).
I have cleaned up a good chunk of U2F references in lib/web and lib/client, plus
a few other places. Changes on lib/auth are just the necessary to get the tests
back to good standing. There is more work to be done, but this seems enough for
a single PR.
#10375
* Remove "Disabled" field from types.Webauthn
* Update generated protos
* Treat second_factor "u2f" as "webauthn"
* Remove references to Webauthn.Disabled
* Remove U2F from lib/web/
* Remove U2F from lib/client/
* Remove U2F from lib/auth/ (partially)
* Fix issues after rebase on master
* Fix typo
`tsh` doesn't support MFA logins on Windows; we make that explicit by warning
users when necessary, instead of directing them to a hopeless workflow.
Example attempt:
```shell
$ tsh login
> Enter password for Teleport user codingllama: <password>
> ERROR: hardware device MFA not supported by your platform, please register an OTP device
>
> exit status 1
```
This is a collection of a few small changes related to user presentation of
WebAuthn/MFA in tsh. The intent is to make tsh language match ongoing Web UI
changes.
* Make use of preferred MFA in `tsh mfa add`
* Tweak prompt error message
Old:
ERROR: "U2F\n" is not a valid option, please specify one of [TOTP, WEBAUTHN]
New:
ERROR: "U2F" is not a valid option, please specify one of [TOTP, WEBAUTHN]
* Directly mention WebAuthn when prompting for challenges
* Fix typo on godoc
* Print devices sorted by name on `tsh mfa ls`
* Address origin validation TODOs
For registration and a few other use cases the original error is relayed
back to the client, so there is already a good indicator that it failed
due to origin woes.
For login we purposefully obsfucate errors. To address that I've added a
few debug-level server-side log statements; it seems best to not make
further changes in this case.
* Amend preferred device type logic
* Adjust PromptMFAChallenge message
One endpoint is now responsible for creating/returning
MFA register challenges:
- Deprecates RotateUserTokenSecrets
- Deprecates GetSignupU2FRegisterRequest
Created a new rpc `CreateAuthenticateChallenge` that
replaces/deprecates `GetMFAAuthenticateChallenge`
Allows 3 ways to invoke this request:
1. with username + password
2. with token ID (username retrieved from token)
3. with empty request (username retrieved from context)
Implement the device polling loop for Webauthn devices and apply it to
tsh login.
A separate package, lib/auth/webauthncli, is introduced to hold client-focused
Webauthn logic. The separation highlights the distinction between server- and
client-side code and isolates client-side dependencies from the existing
lib/auth/webauthn.
Includes improved test coverage for TeleportClient.Login (local logins only) and
refactoring of existing code to support Webauthn challenges (PromptMFAChallenge
logic has less branching and U2F device loop extracted and refactored into
lib/auth/webauthncli).
In terms of device compatibility, this is identical to the existing U2F
solution- we are using the same underlying libraries and adapting the CTAP1
responses to Webauthn. This is easy to do and easy to reason about. I'm planning
for a native Webauthn solution, such as libfido2, but that is larger change that
doesn't seem to get us all the way to where we want for now (looking at you,
Touch ID).
* Add transformations and validations to lib/auth/webauthn
* Implement client-side Webauthn login
* Use RunOnU2FDevices in the lib/auth/u2f package
This one is optional: it cuts a bit of the codebase today, but one could
argue that it is safer to keep U2F untouched (up to the point where it
gets removed).
* Use CollectedClientData definition in lib/auth/mocku2f
* Fix data race on auth.Server clock usage
* Add direct test for TeleportClient.Login
* Reply to Webauthn challenges on `tsh login`
Implement context-based cancellation in `/lib/utils/prompt`, for MFA
prompts.
This fixes the following scenario:
```sh
User has both OTP and U2F devices registered.
$ tsh mfa ls
Name Type Added at Last used
----- ---- ----------------------------- -----------------------------
otp TOTP Wed, 21 Apr 2021 19:41:44 UTC Wed, 21 Apr 2021 19:44:32 UTC
usb-a U2F Wed, 21 Apr 2021 19:44:34 UTC Wed, 21 Apr 2021 19:44:34 UTC
Add a new OTP device, using existing U2F device:
$ tsh mfa add
Choose device type [TOTP, U2F]: totp
Enter device name: otp2
Tap any *registered* security key or enter a code from a *registered* OTP device: <tap> # <- First OTP prompt here
Open your TOTP app and create a new manual entry with these fields:
Name: awly@localhost:3080
Issuer: Teleport
Algorithm: SHA1
Number of digits: 6
Period: 30s
Secret: 3UD42X2NN7EEZ6LUPG6NFMNOLDY6AJTS
Once created, enter an OTP code generated by the app: 607738 # <- Second OTP prompt here
MFA device "otp2" added.
```
Before this PR, the first OTP prompt (for `*registered* device`) would
hang in the background. The OTP code from the newly-registered device is
prompted later, but any text written ends up going to the first prompt.
After this PR, the first prompt is canceled and the code from a new
device goes to the second prompt as intended.
Note: this is implemented using pure Go code (background goroutine
consuming `os.Stdin`) rather than syscalls (e.g. `poll` or `select`)
for portability.
* mfa: per-session MFA certs for SSH and Kubernetes
This is client-side support for requesting single-use certs with an MFA
check.
The client doesn't know whether they need MFA check when accessing a
resource, this is decided during an RBAC check on the server. So a
client will always try to get a single-use cert, and the server will
respond with NotNeeded if MFA is not required. This is an extra
round-trip for every session which causes ~20% slowdown in SSH logins:
```
$ hyperfine '/tmp/tsh-old ssh talos date' '/tmp/tsh-new ssh talos date'
Benchmark #1: /tmp/tsh-old ssh talos date
Time (mean ± σ): 49.9 ms ± 1.0 ms [User: 15.1 ms, System: 7.4 ms]
Range (min … max): 48.4 ms … 54.1 ms 59 runs
Benchmark #2: /tmp/tsh-new ssh talos date
Time (mean ± σ): 60.2 ms ± 1.6 ms [User: 19.1 ms, System: 8.3 ms]
Range (min … max): 59.0 ms … 69.7 ms 50 runs
Warning: Statistical outliers were detected. Consider re-running this benchmark on a quiet PC without any interferences from other programs. It might help to use the '--warmup' or '--prepare' options.
Summary
'/tmp/tsh-old ssh talos date' ran
1.21 ± 0.04 times faster than '/tmp/tsh-new ssh talos date'
```
Another few other internal changes:
- client.LocalKeyAgent will now always have a non-nil LocalKeyStore.
Previously, it would be nil (e.g. in a web UI handler or when using an
identity file) which easily causes panics. I added a noLocalKeyStore
type instead that returns errors from all methods.
- requesting a user cert with a TTL < 1min will now succeed and return a
1min cert instead of failing
* Capture access approvals on MFA-issued certs
* Address review feedback
* Address review feedback
* mfa: accept unknown nodes during short-term MFA cert creation
An unknown node could be an OpenSSH node set up via
https://goteleport.com/teleport/docs/openssh-teleport/
In this case, we shouldn't prevent the user from connecting.
There's a small risk of authz bypass - an attacker might know a
different name/IP for a registered node which Teleport doesn't know
about. But a Teleport node will still check RBAC and reject the
connection.
* Validate username against unmapped user identity
IssueUserCertsWithMFA is called on the leaf auth server in case of
trusted clusters. Username in the request object will be that of the
original unmapped caller.
* mfa: add IsMFARequired RPC
This RPC is ran before every connection to check whether MFA is
required. If a connection is against the leaf cluster, this request is
forwarded from root to leaf for evaluation.
* Fix integration tests
* Correctly treat "Username" as login name in IsMFARequired
Also, move the logic into auth.Server out of ServerWithRoles.
* Fix TestHA
* Address review feedback
* mfa: add new second_factor options "on" and "optional"
"on" means that 2FA is required for all users, either TOTP or U2F.
"optional" means that 2FA is supported for all users, but not required.
Only users with MFA devices registered will be prompted for 2FA on
login.
The login with both supported methods is using the same API as the U2F
login. It just now supports TOTP in addition. The API endpoints are
still named after "u2f", I'll rename those in a future PR (in a
backwards-compatible way).
* Apply suggestions from code review
Co-authored-by: Gus Luxton <gus@gravitational.com>
Co-authored-by: a-palchikov <deemok@gmail.com>
* Address reivew feedback
Co-authored-by: Gus Luxton <gus@gravitational.com>
Co-authored-by: a-palchikov <deemok@gmail.com>