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 ,