diff --git a/contrib/win32/openssh/win32iocompat.vcxproj b/contrib/win32/openssh/win32iocompat.vcxproj index 5573140daa29..3cceb5598666 100644 --- a/contrib/win32/openssh/win32iocompat.vcxproj +++ b/contrib/win32/openssh/win32iocompat.vcxproj @@ -315,6 +315,7 @@ + diff --git a/contrib/win32/openssh/win32iocompat.vcxproj.filters b/contrib/win32/openssh/win32iocompat.vcxproj.filters index c727e636bca0..2c1349056b8b 100644 --- a/contrib/win32/openssh/win32iocompat.vcxproj.filters +++ b/contrib/win32/openssh/win32iocompat.vcxproj.filters @@ -22,6 +22,7 @@ + diff --git a/contrib/win32/win32compat/misc_internal.h b/contrib/win32/win32compat/misc_internal.h index 5a43a992e820..9881e0faa146 100644 --- a/contrib/win32/win32compat/misc_internal.h +++ b/contrib/win32/win32compat/misc_internal.h @@ -67,6 +67,8 @@ void to_lower_case(char *s); void to_wlower_case(wchar_t *s); HANDLE get_user_token(const char* user, int impersonation); int load_user_profile(HANDLE user_token, char* user); +extern int attach_to_console_session; +HANDLE get_console_session_token(HANDLE authenticated_token); int create_directory_withsddl(wchar_t *path, wchar_t *sddl, BOOL check_permissions); int is_absolute_path(const char *); int file_in_chroot_jail(HANDLE); diff --git a/contrib/win32/win32compat/spawn-ext.c b/contrib/win32/win32compat/spawn-ext.c index 19b569ff892d..54b268ada59b 100644 --- a/contrib/win32/win32compat/spawn-ext.c +++ b/contrib/win32/win32compat/spawn-ext.c @@ -14,7 +14,8 @@ __posix_spawn_asuser(pid_t *pidp, const char *path, const posix_spawn_file_actio int r = -1; /* use token generated from password auth if already present */ HANDLE user_token = NULL; - + int on_console_session = FALSE; + if (password_auth_token) user_token = password_auth_token; else if (sspi_auth_user) @@ -25,7 +26,20 @@ __posix_spawn_asuser(pid_t *pidp, const char *path, const posix_spawn_file_actio errno = EOTHER; return -1; } - if (strcmp(user, "sshd")) + + /* if configured, run inside the user's existing console session */ + if (attach_to_console_session) { + HANDLE console_token = get_console_session_token(user_token); + + if (console_token != NULL) { + CloseHandle(user_token); + user_token = console_token; + on_console_session = TRUE; + } + } + + /* a console user's profile is already loaded by their interactive logon */ + if (!on_console_session && strcmp(user, "sshd")) load_user_profile(user_token, user); r = posix_spawn_internal(pidp, path, file_actions, attrp, argv, envp, user_token, TRUE); diff --git a/contrib/win32/win32compat/w32api_proxies.c b/contrib/win32/win32compat/w32api_proxies.c index 704fd8bebaae..7fc61fef8586 100644 --- a/contrib/win32/win32compat/w32api_proxies.c +++ b/contrib/win32/win32compat/w32api_proxies.c @@ -105,6 +105,17 @@ load_secur32() return s_hm_secur32; } +static HMODULE +load_wtsapi32() +{ + static HMODULE s_hm_wtsapi32 = NULL; + + if (!s_hm_wtsapi32) + s_hm_wtsapi32 = load_module(L"wtsapi32.dll"); + + return s_hm_wtsapi32; +} + static HMODULE load_ntdll() { @@ -259,6 +270,62 @@ ULONG pRtlNtStatusToDosError(NTSTATUS status) return pRtlNtStatusToDosError(status); } +BOOL pWTSQuerySessionInformationW(HANDLE server, DWORD session_id, + WTS_INFO_CLASS info_class, + LPWSTR *buffer, + DWORD *bytes_returned) +{ + HMODULE hm = NULL; + typedef BOOL(WINAPI *WTSQuerySessionInformationWType)(HANDLE, DWORD, WTS_INFO_CLASS, LPWSTR *, DWORD *); + static WTSQuerySessionInformationWType s_pWTSQuerySessionInformationW = NULL; + + if (!s_pWTSQuerySessionInformationW) { + if ((hm = load_wtsapi32()) == NULL) + return FALSE; + + if ((s_pWTSQuerySessionInformationW = (WTSQuerySessionInformationWType) + get_proc_address(hm, "WTSQuerySessionInformationW")) == NULL) + return FALSE; + } + + return s_pWTSQuerySessionInformationW(server, session_id, info_class, buffer, bytes_returned); +} + +BOOL pWTSQueryUserToken(ULONG session_id, PHANDLE token) +{ + HMODULE hm = NULL; + typedef BOOL(WINAPI *WTSQueryUserTokenType)(ULONG, PHANDLE); + static WTSQueryUserTokenType s_pWTSQueryUserToken = NULL; + + if (!s_pWTSQueryUserToken) { + if ((hm = load_wtsapi32()) == NULL) + return FALSE; + + if ((s_pWTSQueryUserToken = (WTSQueryUserTokenType) + get_proc_address(hm, "WTSQueryUserToken")) == NULL) + return FALSE; + } + + return s_pWTSQueryUserToken(session_id, token); +} + +void pWTSFreeMemory(PVOID memory) +{ + HMODULE hm = NULL; + typedef void(WINAPI *WTSFreeMemoryType)(PVOID); + static WTSFreeMemoryType s_pWTSFreeMemory = NULL; + + if (!s_pWTSFreeMemory) { + if ((hm = load_wtsapi32()) == NULL) + return; + + if ((s_pWTSFreeMemory = (WTSFreeMemoryType)get_proc_address(hm, "WTSFreeMemory")) == NULL) + return; + } + + s_pWTSFreeMemory(memory); +} + NTSTATUS pLsaClose(LSA_HANDLE lsa_h) { HMODULE hm = NULL; diff --git a/contrib/win32/win32compat/w32api_proxies.h b/contrib/win32/win32compat/w32api_proxies.h index a47d83b9c3e5..1971d580a6db 100644 --- a/contrib/win32/win32compat/w32api_proxies.h +++ b/contrib/win32/win32compat/w32api_proxies.h @@ -11,6 +11,7 @@ #define SECURITY_WIN32 #include #include +#include BOOL pLogonUserExExW(wchar_t *, wchar_t *, wchar_t *, DWORD, DWORD, PTOKEN_GROUPS, PHANDLE, PSID *, PVOID *, LPDWORD, PQUOTA_LIMITS); BOOLEAN pTranslateNameW(LPCWSTR, EXTENDED_NAME_FORMAT, EXTENDED_NAME_FORMAT, LPWSTR, PULONG); @@ -20,5 +21,7 @@ NTSTATUS pLsaAddAccountRights(LSA_HANDLE, PSID, PLSA_UNICODE_STRING, ULONG); ULONG pRtlNtStatusToDosError(NTSTATUS); NTSTATUS pLsaClose(LSA_HANDLE); NTSTATUS pLsaRemoveAccountRights(LSA_HANDLE, PSID, BOOLEAN, PLSA_UNICODE_STRING, ULONG); - +BOOL pWTSQuerySessionInformationW(HANDLE, DWORD, WTS_INFO_CLASS, LPWSTR *, DWORD *); +BOOL pWTSQueryUserToken(ULONG, PHANDLE); +void pWTSFreeMemory(PVOID); diff --git a/contrib/win32/win32compat/w32fd.c b/contrib/win32/win32compat/w32fd.c index b4c436864dc4..0b2a75f03b0a 100644 --- a/contrib/win32/win32compat/w32fd.c +++ b/contrib/win32/win32compat/w32fd.c @@ -1146,7 +1146,19 @@ spawn_child_internal(const char* cmd, char *const argv[], HANDLE in, HANDLE out, if (as_user) { debug3("spawning %ls as user", t); LPVOID lpEnvironment = NULL; + DWORD token_session = 0, my_session = 0, info_len = 0; + /* lpDesktop is not const, so this cannot be a literal */ + static wchar_t winsta0_default[] = L"WinSta0\\Default"; wchar_t* as_user_name = get_username_from_token(as_user); + + /* a process in another session cannot inherit our window station and desktop */ + if (GetTokenInformation(as_user, TokenSessionId, &token_session, sizeof(token_session), &info_len) && + ProcessIdToSessionId(GetCurrentProcessId(), &my_session) && + token_session != my_session) { + debug3("spawning into session %d (from session %d) on %ls", token_session, my_session, winsta0_default); + si.lpDesktop = winsta0_default; + } + if (as_user_name) { if (wcsncmp(L"sshd", as_user_name, wcslen(L"sshd")) != 0) { /* Ignore any names that begin with the service name `sshd`. */ b = CreateEnvironmentBlock(&lpEnvironment, as_user, TRUE); /* Load a user environment block inheriting the current context, thereby passing session state. */ diff --git a/contrib/win32/win32compat/win32_session.c b/contrib/win32/win32compat/win32_session.c new file mode 100644 index 000000000000..a621eefec5b8 --- /dev/null +++ b/contrib/win32/win32compat/win32_session.c @@ -0,0 +1,205 @@ +/* + * Author: Mitch Gaffigan + * + * Support for running a session inside the user's existing physical console + * session (WTS session) instead of the service session. + * + * Copyright (c) 2026 Mitch Gaffigan + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO + * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include + +#include "w32api_proxies.h" +#include "misc_internal.h" +#include "Debug.h" + +/* set from sshd_config's AttachToConsoleSession, armed only for the post-auth spawn */ +int attach_to_console_session = 0; + +/* union so the SID stays suitably aligned */ +typedef union { + SID sid; + BYTE buf[SECURITY_MAX_SID_SIZE]; +} sid_buf; + +/* returns 1 on success, 0 on failure */ +static int +copy_token_user_sid(HANDLE token, sid_buf *out) +{ + /* union so the sid that follows the TOKEN_USER stays aligned */ + union { + TOKEN_USER token_user; + BYTE buf[sizeof(TOKEN_USER) + SECURITY_MAX_SID_SIZE]; + } u; + DWORD len = 0; + + if (GetTokenInformation(token, TokenUser, &u, sizeof(u), &len) == FALSE) { + error_f("GetTokenInformation(TokenUser) failed with error:%d", GetLastError()); + return 0; + } + + if (CopySid(sizeof(out->buf), &out->sid, u.token_user.User.Sid) == FALSE) { + error_f("CopySid failed with error:%d", GetLastError()); + return 0; + } + + return 1; +} + +static BOOL +tokens_same_user(HANDLE a, HANDLE b) +{ + sid_buf a_sid, b_sid; + + if (!copy_token_user_sid(a, &a_sid) || !copy_token_user_sid(b, &b_sid)) + return FALSE; + + return EqualSid(&a_sid.sid, &b_sid.sid); +} + +/* needs no privilege, so use it to avoid WTSQueryUserToken at the logon screen */ +static int +console_session_is_active(DWORD session_id) +{ + WTS_CONNECTSTATE_CLASS *state = NULL; + DWORD len = 0; + int ret = 0; + + if (pWTSQuerySessionInformationW(WTS_CURRENT_SERVER_HANDLE, session_id, + WTSConnectState, (LPWSTR *)&state, &len) == FALSE) { + debug3_f("WTSQuerySessionInformationW failed for session:%u error:%d", + session_id, GetLastError()); + return 0; + } + + if (state == NULL || len < sizeof(*state)) { + debug3_f("unexpected WTSConnectState result for session:%u", session_id); + goto done; + } + + if (*state != WTSActive) { + debug_f("nobody is logged on to console session:%u", session_id); + goto done; + } + + ret = 1; +done: + if (state) + pWTSFreeMemory(state); + + return ret; +} + +/* + * WTSQueryUserToken returns the filtered token for an administrator on a UAC + * enabled system. sshd sessions are elevated today, so follow the linked token + * to keep that behavior. Returns the token to use, closing the original if it + * was replaced. + */ +static HANDLE +elevate_token(HANDLE token) +{ + TOKEN_ELEVATION_TYPE elevation_type; + TOKEN_LINKED_TOKEN linked; + HANDLE primary = NULL; + DWORD len = 0; + + if (GetTokenInformation(token, TokenElevationType, &elevation_type, + sizeof(elevation_type), &len) == FALSE) { + debug3_f("GetTokenInformation(TokenElevationType) failed with error:%d", + GetLastError()); + return token; + } + + /* standard users and UAC disabled systems have no linked token */ + if (elevation_type != TokenElevationTypeLimited) + return token; + + if (GetTokenInformation(token, TokenLinkedToken, &linked, sizeof(linked), &len) == FALSE) { + debug_f("GetTokenInformation(TokenLinkedToken) failed with error:%d, " + "continuing with the filtered token", GetLastError()); + return token; + } + + /* the linked token is an impersonation token, we need a primary one */ + if (DuplicateTokenEx(linked.LinkedToken, TOKEN_ALL_ACCESS, NULL, + SecurityImpersonation, TokenPrimary, &primary) == FALSE) { + debug_f("DuplicateTokenEx failed with error:%d, " + "continuing with the filtered token", GetLastError()); + CloseHandle(linked.LinkedToken); + return token; + } + + debug3_f("using the linked elevated token"); + CloseHandle(linked.LinkedToken); + CloseHandle(token); + + return primary; +} + +/* + * Returns a primary token for the physical console session, or NULL when there + * is no such session or it belongs to a user other than authenticated_token. + * The caller owns the returned handle. + */ +HANDLE +get_console_session_token(HANDLE authenticated_token) +{ + HANDLE token = NULL; + DWORD console_session_id; + + console_session_id = WTSGetActiveConsoleSessionId(); + if (console_session_id == 0xFFFFFFFF || console_session_id == 0) { + debug_f("no physical console session is attached"); + return NULL; + } + + if (!console_session_is_active(console_session_id)) + return NULL; + + if (pWTSQueryUserToken(console_session_id, &token) == FALSE) { + DWORD err = GetLastError(); + + if (err == ERROR_PRIVILEGE_NOT_HELD) + error_f("WTSQueryUserToken needs SeTcbPrivilege, ensure the sshd " + "service has TCB privileges"); + else + debug_f("WTSQueryUserToken failed for session:%u error:%d", + console_session_id, err); + + return NULL; + } + + if (!tokens_same_user(token, authenticated_token)) { + debug_f("console session:%u belongs to a different user, not attaching", + console_session_id); + CloseHandle(token); + return NULL; + } + + token = elevate_token(token); + + verbose("attaching session to console session %u", console_session_id); + + return token; +} diff --git a/servconf.c b/servconf.c index eeffe77ce36e..a9ec07823062 100644 --- a/servconf.c +++ b/servconf.c @@ -221,6 +221,7 @@ initialize_server_options(ServerOptions *options) options->sshd_session_path = NULL; options->sshd_auth_path = NULL; options->refuse_connection = -1; + options->attach_to_console_session = -1; } /* Returns 1 if a string option is unset or set to "none" or 0 otherwise. */ @@ -513,6 +514,8 @@ fill_default_server_options(ServerOptions *options) #endif // WINDOWS if (options->refuse_connection == -1) options->refuse_connection = 0; + if (options->attach_to_console_session == -1) + options->attach_to_console_session = 0; assemble_algorithms(options); @@ -596,6 +599,7 @@ typedef enum { sExposeAuthInfo, sRDomain, sPubkeyAuthOptions, sSecurityKeyProvider, sRequiredRSASize, sChannelTimeout, sUnusedConnectionTimeout, sSshdSessionPath, sSshdAuthPath, sRefuseConnection, + sAttachToConsoleSession, sDeprecated, sIgnore, sUnsupported } ServerOpCodes; @@ -765,6 +769,11 @@ static struct { { "sshdsessionpath", sSshdSessionPath, SSHCFG_GLOBAL }, { "sshdauthpath", sSshdAuthPath, SSHCFG_GLOBAL }, { "refuseconnection", sRefuseConnection, SSHCFG_ALL }, +#ifdef WINDOWS + { "attachtoconsolesession", sAttachToConsoleSession, SSHCFG_ALL }, +#else + { "attachtoconsolesession", sUnsupported, SSHCFG_ALL }, +#endif // WINDOWS { NULL, sBadOption, 0 } }; @@ -2824,6 +2833,10 @@ process_server_config_line_depth(ServerOptions *options, char *line, multistate_ptr = multistate_flag; goto parse_multistate; + case sAttachToConsoleSession: + intptr = &options->attach_to_console_session; + goto parse_flag; + case sDeprecated: case sIgnore: case sUnsupported: @@ -3044,6 +3057,7 @@ copy_set_server_options(ServerOptions *dst, ServerOptions *src, int preauth) M_CP_INTOPT(required_rsa_size); M_CP_INTOPT(unused_connection_timeout); M_CP_INTOPT(refuse_connection); + M_CP_INTOPT(attach_to_console_session); /* * The bind_mask is a mode_t that may be unsigned, so we can't use @@ -3430,6 +3444,9 @@ dump_config(ServerOptions *o) dump_cfg_fmtint(sFingerprintHash, o->fingerprint_hash); dump_cfg_fmtint(sExposeAuthInfo, o->expose_userauth_info); dump_cfg_fmtint(sRefuseConnection, o->refuse_connection); +#ifdef WINDOWS + dump_cfg_fmtint(sAttachToConsoleSession, o->attach_to_console_session); +#endif // WINDOWS /* string arguments */ dump_cfg_string(sPidFile, o->pid_file); diff --git a/servconf.h b/servconf.h index 9beb90fae3da..adf4342301aa 100644 --- a/servconf.h +++ b/servconf.h @@ -252,6 +252,8 @@ typedef struct { char *sshd_auth_path; int refuse_connection; + + int attach_to_console_session; /* Windows only */ } ServerOptions; /* Information about the incoming connection as used by Match */ diff --git a/sshd-session.c b/sshd-session.c index 623c20eba2df..40274af2a844 100644 --- a/sshd-session.c +++ b/sshd-session.c @@ -126,6 +126,7 @@ /* Privilege separation related spawn fds */ #ifdef WINDOWS #define PRIVSEP_AUTH_MIN_FREE_FD (PRIVSEP_LOG_FD + 1) +extern int attach_to_console_session; #endif /* WINDOWS */ extern char *__progname; @@ -842,6 +843,10 @@ privsep_postauth(struct ssh *ssh, Authctxt *authctxt) fatal("posix_spawn initialization failed"); char** argv = privsep_child_cmdline(); +#ifdef WINDOWS + /* arm for this spawn only, the pre-auth child stays in the service session */ + attach_to_console_session = options.attach_to_console_session; +#endif /* WINDOWS */ if (__posix_spawn_asuser(&pmonitor->m_pid, argv[0], &actions, NULL, argv, NULL, authctxt->pw->pw_name) != 0) fatal("fork of unprivileged child failed"); posix_spawn_file_actions_destroy(&actions); diff --git a/sshd_config.0 b/sshd_config.0 index 2f77b4f4c0b6..1a0934dfdfef 100644 --- a/sshd_config.0 +++ b/sshd_config.0 @@ -87,6 +87,15 @@ DESCRIPTION This keyword may appear multiple times in sshd_config with each instance appending to the list. + AttachToConsoleSession + Windows only. Specifies whether a session should run inside the + authenticated user's existing physical console session rather + than in the service session, so that processes it starts are + visible on that user's desktop. The console session is used only + when the same user that authenticated is logged on to it; + otherwise the session runs in the service session. The default + is no. + AuthenticationMethods Specifies the authentication methods that must be successfully completed for a user to be granted access. This option must be @@ -780,25 +789,25 @@ DESCRIPTION Only a subset of keywords may be used on the lines following a Match keyword. Available keywords are AcceptEnv, AllowAgentForwarding, AllowGroups, AllowStreamLocalForwarding, - AllowTcpForwarding, AllowUsers, AuthenticationMethods, - AuthorizedKeysCommand, AuthorizedKeysCommandUser, - AuthorizedKeysFile, AuthorizedPrincipalsCommand, - AuthorizedPrincipalsCommandUser, AuthorizedPrincipalsFile, - Banner, CASignatureAlgorithms, ChannelTimeout, ChrootDirectory, - ClientAliveCountMax, ClientAliveInterval, DenyGroups, DenyUsers, - DisableForwarding, ExposeAuthInfo, ForceCommand, GatewayPorts, - GSSAPIAuthentication, HostbasedAcceptedAlgorithms, - HostbasedAuthentication, HostbasedUsesNameFromPacketOnly, - IgnoreRhosts, Include, IPQoS, KbdInteractiveAuthentication, - KerberosAuthentication, LogLevel, MaxAuthTries, MaxSessions, - PAMServiceName, PasswordAuthentication, PermitEmptyPasswords, - PermitListen, PermitOpen, PermitRootLogin, PermitTTY, - PermitTunnel, PermitUserRC, PubkeyAcceptedAlgorithms, - PubkeyAuthentication, PubkeyAuthOptions, RefuseConnection, - RekeyLimit, RevokedKeys, RDomain, SetEnv, StreamLocalBindMask, - StreamLocalBindUnlink, TrustedUserCAKeys, - UnusedConnectionTimeout, X11DisplayOffset, X11Forwarding and - X11UseLocalhost. + AllowTcpForwarding, AllowUsers, AttachToConsoleSession, + AuthenticationMethods, AuthorizedKeysCommand, + AuthorizedKeysCommandUser, AuthorizedKeysFile, + AuthorizedPrincipalsCommand, AuthorizedPrincipalsCommandUser, + AuthorizedPrincipalsFile, Banner, CASignatureAlgorithms, + ChannelTimeout, ChrootDirectory, ClientAliveCountMax, + ClientAliveInterval, DenyGroups, DenyUsers, DisableForwarding, + ExposeAuthInfo, ForceCommand, GatewayPorts, GSSAPIAuthentication, + HostbasedAcceptedAlgorithms, HostbasedAuthentication, + HostbasedUsesNameFromPacketOnly, IgnoreRhosts, Include, IPQoS, + KbdInteractiveAuthentication, KerberosAuthentication, LogLevel, + MaxAuthTries, MaxSessions, PAMServiceName, + PasswordAuthentication, PermitEmptyPasswords, PermitListen, + PermitOpen, PermitRootLogin, PermitTTY, PermitTunnel, + PermitUserRC, PubkeyAcceptedAlgorithms, PubkeyAuthentication, + PubkeyAuthOptions, RefuseConnection, RekeyLimit, RevokedKeys, + RDomain, SetEnv, StreamLocalBindMask, StreamLocalBindUnlink, + TrustedUserCAKeys, UnusedConnectionTimeout, X11DisplayOffset, + X11Forwarding and X11UseLocalhost. MaxAuthTries Specifies the maximum number of authentication attempts permitted diff --git a/sshd_config.5 b/sshd_config.5 index c07717375d90..9fc6a69e0fae 100644 --- a/sshd_config.5 +++ b/sshd_config.5 @@ -183,6 +183,15 @@ for more information on patterns. This keyword may appear multiple times in .Nm with each instance appending to the list. +.It Cm AttachToConsoleSession +Windows only. +Specifies whether a session should run inside the authenticated user's +existing physical console session rather than in the service session, +so that processes it starts are visible on that user's desktop. +The console session is used only when the same user that authenticated is +logged on to it; otherwise the session runs in the service session. +The default is +.Cm no . .It Cm AuthenticationMethods Specifies the authentication methods that must be successfully completed for a user to be granted access. @@ -1288,6 +1297,7 @@ Available keywords are .Cm AllowStreamLocalForwarding , .Cm AllowTcpForwarding , .Cm AllowUsers , +.Cm AttachToConsoleSession , .Cm AuthenticationMethods , .Cm AuthorizedKeysCommand , .Cm AuthorizedKeysCommandUser ,