diff --git a/lib/client/api.go b/lib/client/api.go index 6fef13ad9a50b..8a6c74afd35bb 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -4791,7 +4791,7 @@ func (tc *TeleportClient) RootClusterCACertPool(ctx context.Context) (*x509.Cert } // HeadlessApprove handles approval of a headless authentication request. -func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthenticationID string) error { +func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthenticationID string, confirm bool) error { ctx, span := tc.Tracer.Start( ctx, "teleportClient/HeadlessApprove", @@ -4829,15 +4829,18 @@ func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthentic return trace.Errorf("cannot approve a headless authentication from a non-pending state: %v", headlessAuthn.State.Stringify()) } - confirmationPrompt := fmt.Sprintf("Headless login attempt from IP address %q requires approval.\nContact your administrator if you didn't initiate this login attempt.\nApprove?", headlessAuthn.ClientIpAddress) - ok, err := prompt.Confirmation(ctx, tc.Stdout, prompt.Stdin(), confirmationPrompt) - if err != nil { - return trace.Wrap(err) - } + fmt.Fprintf(tc.Stdout, "Headless login attempt from IP address %q requires approval.\nContact your administrator if you didn't initiate this login attempt.\n", headlessAuthn.ClientIpAddress) - if !ok { - err = rootClient.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED, nil) - return trace.Wrap(err) + if confirm { + ok, err := prompt.Confirmation(ctx, tc.Stdout, prompt.Stdin(), "Approve?") + if err != nil { + return trace.Wrap(err) + } + + if !ok { + err = rootClient.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_DENIED, nil) + return trace.Wrap(err) + } } chall, err := rootClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ diff --git a/tool/tsh/tsh.go b/tool/tsh/tsh.go index 75673961c42b2..bcec9a516d64c 100644 --- a/tool/tsh/tsh.go +++ b/tool/tsh/tsh.go @@ -451,6 +451,10 @@ type CLIConf struct { // HeadlessAuthenticationID is the ID of a headless authentication. HeadlessAuthenticationID string + + // headlessSkipConfirm determines whether to provide a y/N + // confirmation prompt before prompting for MFA. + headlessSkipConfirm bool } // Stdout returns the stdout writer. @@ -520,13 +524,14 @@ func main() { } const ( - authEnvVar = "TELEPORT_AUTH" - clusterEnvVar = "TELEPORT_CLUSTER" - kubeClusterEnvVar = "TELEPORT_KUBE_CLUSTER" - loginEnvVar = "TELEPORT_LOGIN" - bindAddrEnvVar = "TELEPORT_LOGIN_BIND_ADDR" - proxyEnvVar = "TELEPORT_PROXY" - headlessEnvVar = "TELEPORT_HEADLESS" + authEnvVar = "TELEPORT_AUTH" + clusterEnvVar = "TELEPORT_CLUSTER" + kubeClusterEnvVar = "TELEPORT_KUBE_CLUSTER" + loginEnvVar = "TELEPORT_LOGIN" + bindAddrEnvVar = "TELEPORT_LOGIN_BIND_ADDR" + proxyEnvVar = "TELEPORT_PROXY" + headlessEnvVar = "TELEPORT_HEADLESS" + headlessSkipConfirmEnvVar = "TELEPORT_HEADLESS_SKIP_CONFIRM" // TELEPORT_SITE uses the older deprecated "site" terminology to refer to a // cluster. All new code should use TELEPORT_CLUSTER instead. siteEnvVar = "TELEPORT_SITE" @@ -970,8 +975,9 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error { // Headless login approval headless := app.Command("headless", "Headless authentication commands.").Interspersed(true) - approve := headless.Command("approve", "Approve a headless authentication request.").Interspersed(true) - approve.Arg("request id", "Headless authentication request ID").StringVar(&cf.HeadlessAuthenticationID) + headlessApprove := headless.Command("approve", "Approve a headless authentication request.").Interspersed(true) + headlessApprove.Arg("request id", "Headless authentication request ID").StringVar(&cf.HeadlessAuthenticationID) + headlessApprove.Flag("skip-confirm", "Skip confirmation and prompt for MFA immediately").Envar(headlessSkipConfirmEnvVar).BoolVar(&cf.headlessSkipConfirm) reqDrop := req.Command("drop", "Drop one more access requests from current identity.") reqDrop.Arg("request-id", "IDs of requests to drop (default drops all requests)").Default("*").StringsVar(&cf.RequestIDs) @@ -1326,7 +1332,7 @@ func Run(ctx context.Context, args []string, opts ...cliOption) error { case kubectl.FullCommand(): idx := slices.Index(args, kubectl.FullCommand()) err = onKubectlCommand(&cf, args, args[idx:]) - case approve.FullCommand(): + case headlessApprove.FullCommand(): err = onHeadlessApprove(&cf) default: // Handle commands that might not be available. @@ -4831,7 +4837,7 @@ func onHeadlessApprove(cf *CLIConf) error { tc.Stdin = os.Stdin err = client.RetryWithRelogin(cf.Context, tc, func() error { - return tc.HeadlessApprove(cf.Context, cf.HeadlessAuthenticationID) + return tc.HeadlessApprove(cf.Context, cf.HeadlessAuthenticationID, !cf.headlessSkipConfirm) }) return trace.Wrap(err) }