Skip to content

Control an Ubuntu Laptop from Windows 11: SSH, Hermes and GNOME Remote Desktop

  • ubuntu
  • windows
  • openssh
  • gnome-remote-desktop
  • hermes-agent
  • powershell

This tutorial turns a Windows 11 laptop into the control seat for a physical Ubuntu 26.04 laptop on the same home network. You get passwordless SSH, Hermes Agent running its terminal commands on Ubuntu, and Ubuntu’s built-in GNOME Remote Desktop in two modes: the real screen, and a separate login session, each one click away.

A title card for a workspace setup guide in yellow lettering over a blurred red and blue photo, with the labels SSH, remote control, Termius, Hermes, Claude Code, Kali, Ubuntu and Windows

I use this setup on my own machines, and I repeat it every time I add a laptop. The steps below are the ones I follow, with my own addresses, names and keys replaced by example values. It is the companion of Control a Kali Linux Laptop from Windows 11: same Windows laptop, and the Kali laptop sits on the same network.

What this tutorial covers:

  • the LAN and a static IP;
  • SSH key access;
  • Hermes Mode A (agent on Windows, commands on Ubuntu);
  • Ubuntu’s built-in GNOME Remote Desktop, in two modes: Desktop Sharing (the real screen) and Remote Login (a separate session);
  • one-click desktop shortcuts with an Ubuntu icon.

Every command in this tutorial uses the example values below, so you can copy it as it is. These are not your values. Replace each one with your own before you run a command.

What Value in this tutorial How to find yours
Home network 192.168.50.0/24 ipconfig on Windows
Router (gateway and DNS) 192.168.50.1 ipconfig → Default Gateway
Windows laptop address 192.168.50.10 ipconfig → Wi-Fi → IPv4 Address
Ubuntu laptop address (static) 192.168.50.21 You choose it in Phase 1
Kali laptop address (other tutorial) 192.168.50.20 Only if you also follow the Kali tutorial
Wi-Fi connection name on Ubuntu HomeWiFi nmcli connection show --active on Ubuntu
Ubuntu Wi-Fi interface wlp2s0 ip -4 -br addr on Ubuntu
Ubuntu account labuser whoami on Ubuntu
Ubuntu hostname lab-Ubuntu hostname on Ubuntu
Windows account folder C:\Users\Lab User echo $env:USERPROFILE in PowerShell
SSH alias, key file and Hermes profile ubuntu-lab, ubuntu_lab, ubuntu-lab You choose them in Phases 4 and 5
Folder for the RDP files and the icon C:\RemoteAccess You choose it in Appendix B

The example Windows account folder contains a space on purpose. Many real account folders have one, and a space breaks a path that is not in quotes.

A short Your values note marks each command that holds a value from this table.

  • The SSH alias is ubuntu-lab (hyphen).
  • The key file is ubuntu_lab (underscore).
  • The machine’s hostname is lab-Ubuntu.
  • The login user is labuser.
  • The Hermes profile is ubuntu-lab.

All the hostname spellings work with SSH because they share one Host block (Phase 4.3).

Port Service on Ubuntu Purpose Windows uses
TCP 22 OpenSSH (ssh.socket → ssh.service) SSH, Hermes Mode A ssh ubuntu-lab, Hermes profile ubuntu-lab
TCP 3390 GNOME Remote Desktop, user daemon: Desktop Sharing The real screen Shortcut Ubuntu Screen (C:\RemoteAccess\Ubuntu-Screen.rdp)
TCP 3389 GNOME Remote Desktop, system daemon: Remote Login A separate login session Shortcut Ubuntu Login (C:\RemoteAccess\Ubuntu-Login.rdp)

Both modes default to 3389. When both are enabled, Desktop Sharing moves to 3390 and Remote Login keeps 3389 (Ubuntu documentation).

0.4 What you need, and the versions this was tested with

Section titled “0.4 What you need, and the versions this was tested with”
  • A Windows 11 laptop with Hermes Desktop installed and signed in to a model provider, and PowerShell 7.3 or later.
  • A physical Ubuntu 26.04 laptop with the default GNOME desktop, and an administrator account (a member of the sudo group).
  • Both laptops on the same home Wi-Fi network, which you control.
Machine Software Tested version
Ubuntu Ubuntu Desktop 26.04.1 LTS, GNOME 50 on Wayland
Ubuntu openssh-server 1:10.2p1-2ubuntu3.8
Ubuntu GNOME gnome-shell 50.1, gdm3 50.1, gnome-control-center 50.3
Ubuntu gnome-remote-desktop 50.2 (grdctl command-line tool)
Ubuntu Firewall ufw installed, not enabled (ENABLED=no)
Ubuntu tmux, Hermes not installed (not needed for Mode A; tmux optional)
Windows OpenSSH client OpenSSH_for_Windows_9.5p2
Windows Hermes Desktop Store package NousResearchInc.HermesAgent, agent v0.22.5
Windows Remote Desktop client built-in mstsc.exe

The example screens are a 4K Ubuntu panel at 200 % scale (1920×1080 logical) and a 4K Windows laptop at 200 %. The RDP settings in Phase 6.5 refer to these.

Administrator-account warning: labuser is an administrator, so the agent in profile ubuntu-lab can do anything labuser can.

  • Never configure passwordless unrestricted sudo.
  • Never put the sudo password or the RDP passwords in prompts, .env, or scripts.
  • Keep Hermes command approvals on.
flowchart LR
    subgraph W["Windows 11 (192.168.50.10)"]
      HD["Hermes Desktop<br/>gateway: This device"]
      PU["Profile ubuntu-lab<br/>(agent on Windows)"]
      RS["Shortcut: Ubuntu Screen<br/>mstsc → :3390"]
      RL["Shortcut: Ubuntu Login<br/>mstsc → :3389"]
    end
    subgraph U["Ubuntu laptop (192.168.50.21, user labuser)"]
      SSHD["OpenSSH :22"]
      DS["Desktop Sharing :3390<br/>(user daemon, real screen)"]
      RLD["Remote Login :3389<br/>(system daemon → GDM → session)"]
    end
    HD --> PU
    PU -- "Mode A: commands over SSH (key ubuntu_lab)" --> SSHD
    RS -- "RDP login labuser + RDP password" --> DS
    RL -- "RDP login labuser + RDP password, then Linux password at GDM" --> RLD

The Hermes agent and model run on Windows. The profile ubuntu-lab sends only its terminal commands to Ubuntu over SSH. Nothing Hermes-related is installed on Ubuntu, and there is no Desktop SSH gateway for Ubuntu.

To add Mode B later, follow Phase 6 Option B of the Kali tutorial. Ubuntu’s shell is bash, so the ~/.zshenv step there does not apply.

Aspect Desktop Sharing (“Ubuntu Screen”) Remote Login (“Ubuntu Login”)
Shows The real screen of the logged-in user (mirror-primary) A new session for your account, started remotely
Requires You are logged in on the laptop You are not logged in locally. Otherwise Ubuntu offers to end the local session
Resolution Ubuntu’s own (for example 3840×2160), scaled by Windows to fit (smart sizing) Follows the Windows window (dynamic resolution); sharp
Daemon systemctl --user service gnome-remote-desktop (runs as your account) system service gnome-remote-desktop + gnome-remote-desktop-handover
Where its login is stored your GNOME login keyring root-only GKeyFile under /var/lib/gnome-remote-desktop (on a laptop without a TPM chip)
Port (both enabled) 3390 3389

Phase 1: Confirm the LAN and give Ubuntu its own static IP

Section titled “Phase 1: Confirm the LAN and give Ubuntu its own static IP”
ipconfig # Wireless LAN adapter Wi-Fi: 192.168.50.10 / 255.255.255.0 / 192.168.50.1
Get-NetConnectionProfile # Wi-Fi -> Private

On the Ubuntu laptop, in a terminal (no sudo):

echo "host=$(hostname) user=$(whoami) shell=$SHELL session=$XDG_SESSION_TYPE desktop=$XDG_CURRENT_DESKTOP"
grep PRETTY_NAME /etc/os-release
ip -4 -br addr | grep -v '^lo'
ip route | grep default
nmcli -t -f NAME,DEVICE connection show --active

Example output, before the static address is set:

host=lab-Ubuntu user=labuser shell=/bin/bash session=wayland desktop=ubuntu:GNOME
PRETTY_NAME="Ubuntu 26.04.1 LTS"
wlp2s0 UP 192.168.50.20/24
default via 192.168.50.1 dev wlp2s0 proto dhcp src 192.168.50.20 metric 600
HomeWiFi:wlp2s0

An address conflict to watch for. In the example above, the router’s DHCP gave Ubuntu 192.168.50.20, which is the Kali laptop’s static address from the other tutorial. This happens when Kali is switched off: the router considers the address free, because Kali’s static address is set on Kali only and is not reserved in the router.

Running both laptops like that makes SSH and RDP reach whichever one answers first. It also confuses Windows: lab-ubuntu.local resolves to the Kali address, and port 22 is refused.

Rule: every laptop gets its own static address. In this tutorial, Ubuntu gets 192.168.50.21. Before you choose an address, confirm from Windows that nothing answers there:

foreach ($ip in '192.168.50.21','192.168.50.22','192.168.50.23') {
"{0}: ping={1}" -f $ip, (Test-Connection $ip -Count 1 -TimeoutSeconds 1 -Quiet) }
# 192.168.50.21: ping=False (and no ARP entry), so it looks unused. Not absolute proof.

If the router’s DHCP pool includes the address you choose, a future device could still receive it. If that ever happens, pick an address outside the pool.

1.3 Set the static address on Ubuntu (NetworkManager)

Section titled “1.3 Set the static address on Ubuntu (NetworkManager)”

At the Ubuntu laptop’s own keyboard (the network drops briefly):

CON=$(nmcli -t -f NAME,DEVICE connection show --active | grep -v ':lo$' | head -1 | cut -d: -f1); echo "connection: $CON" # HomeWiFi
sudo nmcli connection modify "$CON" ipv4.method manual ipv4.addresses 192.168.50.21/24 ipv4.gateway 192.168.50.1 ipv4.dns 192.168.50.1
sudo nmcli connection up "$CON"
ip -4 -br addr | grep -v '^lo' # wlp2s0 UP 192.168.50.21/24

GUI alternative: Settings → Wi-Fi → (gear on your network) → IPv4 → Manual, with address 192.168.50.21, netmask 255.255.255.0, gateway 192.168.50.1, DNS 192.168.50.1.

nmcli -g ipv4.method,ipv4.addresses,ipv4.gateway,ipv4.dns,connection.autoconnect connection show HomeWiFi
# manual / 192.168.50.21/24 / 192.168.50.1 / 192.168.50.1 / yes
ip route | head -2 # default via 192.168.50.1 dev wlp2s0 proto static metric 600
ping -c 3 192.168.50.1 && getent hosts ubuntu.com

Reboot once and confirm the address persists. From Windows: ping 192.168.50.21.

Phase 2: Verify the existing Ubuntu account

Section titled “Phase 2: Verify the existing Ubuntu account”

Do not create another user.

whoami; id; sudo -v

Expected:

  • labuser, uid 1000.
  • Groups: labuser adm cdrom sudo dip plugdev users lpadmin lxd. adm lets labuser read system logs, including Remote Login’s, without sudo.
  • Shell /bin/bash, so non-interactive SSH sees ~/.local/bin normally (unlike Kali’s zsh).
  • sudo asks for the password (sudo -n true → “interactive authentication is required”). That is why you run every sudo step in this tutorial yourself, never through an automated tool.

Phase 3: Install and start the SSH server on Ubuntu

Section titled “Phase 3: Install and start the SSH server on Ubuntu”

Ubuntu Desktop ships without an SSH server, so Windows sees port 22 refused at first.

sudo apt update && sudo apt install -y openssh-server && sudo systemctl enable --now ssh
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
# 256 SHA256:<your Ubuntu host key fingerprint> root@lab-Ubuntu (ED25519) <- write this down

The install pulls in openssh-sftp-server, ssh-import-id, and ncurses-term, creates the sshd user, and enables socket activation (ssh.socket), so systemd starts sshd on the first connection. Verify:

systemctl is-active ssh.socket ssh.service # active / active
systemctl is-enabled ssh # enabled
ss -ltn | grep ':22 ' # LISTEN 0.0.0.0:22 and [::]:22
grep -vE '^\s*(#|$)' /etc/ssh/sshd_config # stock: Include sshd_config.d, KbdInteractiveAuthentication no, UsePAM yes, X11Forwarding yes, ...

The stock config allows both key and password login. You need the password exactly once, to install the key (Phase 4.4).

Firewall: ufw is installed but not enabled (/etc/ufw/ufw.conf: ENABLED=no), so ports 22, 3389, and 3390 need no rules. If you enable UFW later, allow the Windows laptop first:

sudo ufw allow from 192.168.50.10 to any port 22 proto tcp
sudo ufw allow from 192.168.50.10 to any port 3389:3390 proto tcp

OpenSSH 10 penalty: like Kali, Ubuntu’s sshd temporarily blocks a source address after repeated failed logins (PerSourcePenalties). Never leave a misconfigured client retrying (Kali tutorial, Phase 3.1).

Phase 4: Windows key, host key, and the ubuntu-lab alias

Section titled “Phase 4: Windows key, host key, and the ubuntu-lab alias”

4.1 Create a dedicated key (PowerShell 7, normal user)

Section titled “4.1 Create a dedicated key (PowerShell 7, normal user)”
$key = "$env:USERPROFILE\.ssh\ubuntu_lab"
if (Test-Path $key) { 'ubuntu_lab already exists - leaving it untouched' } else { ssh-keygen -q -t ed25519 -N '' -C 'lab user@winpc ubuntu_lab' -f $key }
ssh-keygen -lf "$key.pub" # 256 SHA256:... lab user@winpc ubuntu_lab (ED25519)
if (ssh-keygen -y -P '' -f $key 2>$null) { 'no passphrase (required for Hermes)' }

A separate key per machine (kali_lab, ubuntu_lab) means you can revoke one without touching the other. Never overwrite an existing key (answer n to Overwrite (y/n)?).

4.2 Record Ubuntu’s host key, verified out-of-band

Section titled “4.2 Record Ubuntu’s host key, verified out-of-band”

Accept the host key only after comparing it with the fingerprint printed on the Ubuntu screen in Phase 3. The Windows ssh-keyscan cannot fetch it:

choose_kex: unsupported KEX method sntrup761x25519-sha512@openssh.com

This is a limitation of OpenSSH for Windows 9.5 (LibreSSL build). Capture the key with ssh itself, attempting no login, into a temporary file. Then compare it and append it only if it matches:

$expected = 'SHA256:PASTE-THE-FINGERPRINT-FROM-PHASE-3'
$tmp = Join-Path $env:TEMP 'ubuntu_known_hosts'
ssh -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o "UserKnownHostsFile=$tmp" -o HostKeyAlgorithms=ssh-ed25519 `
-o PubkeyAuthentication=no -o PasswordAuthentication=no -o KbdInteractiveAuthentication=no labuser@192.168.50.21 exit 2>&1 | Out-Null
$line = Get-Content $tmp | Where-Object { $_ -match '^192\.168\.50\.21 ssh-ed25519 ' } | Select-Object -First 1
$fp = ((ssh-keygen -lf $tmp) -split ' ')[1]; [IO.File]::Delete($tmp)
if ($fp -eq $expected) { Add-Content "$env:USERPROFILE\.ssh\known_hosts" $line; 'MATCH - added' } else { 'MISMATCH - stop' }

The simpler alternative is to answer yes at the first interactive ssh labuser@192.168.50.21, but only after comparing the fingerprint it shows.

4.3 Add the alias block (%USERPROFILE%\.ssh\config)

Section titled “4.3 Add the alias block (%USERPROFILE%\.ssh\config)”

Append this, keeping any Kali and other blocks:

# Ubuntu laptop (lab-Ubuntu) on the home LAN, static 192.168.50.21
Host ubuntu-lab lab-ubuntu lab-Ubuntu 192.168.50.21
HostName 192.168.50.21
User labuser
Port 22
IdentityFile ~/.ssh/ubuntu_lab
IdentitiesOnly yes

Why every name is listed:

  • ubuntu-lab is the short alias.
  • lab-Ubuntu / lab-ubuntu is what you naturally type (the hostname). Without these entries, Windows resolves that name over mDNS (Ubuntu’s local name broadcasting) to an IPv6 link-local address (fe80::…%11) and asks you to trust a “new” host.
  • The raw IP covers tools that connect by address.

Because all names share one HostName, one known_hosts entry serves them all. Alias matching is case-sensitive, so both spellings of the hostname are listed.

foreach ($n in 'ubuntu-lab','lab-ubuntu','lab-Ubuntu','192.168.50.21') {
"$n -> " + ((ssh -G $n | Select-String '^(user|hostname|identityfile) ' | ForEach-Object Line) -join ' | ') }
# each: user labuser | hostname 192.168.50.21 | identityfile ~/.ssh/ubuntu_lab

4.4 Install the public key on Ubuntu (asks the Linux password once)

Section titled “4.4 Install the public key on Ubuntu (asks the Linux password once)”

Run this in your own PowerShell window:

$pub = Get-Content "$env:USERPROFILE\.ssh\ubuntu_lab.pub"
ssh ubuntu-lab "mkdir -p ~/.ssh && chmod 700 ~/.ssh && echo '$pub' >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys && echo KEY-INSTALLED"

Type your Ubuntu account’s Linux password, then expect KEY-INSTALLED.

Do not pipe the key into ssh (Get-Content key.pub | ssh ...). On the first connection, the “Are you sure you want to continue connecting (yes/no)?” question reads from that same input stream. It gets the key text instead of your yes and aborts with Host key verification failed. Passing the key inside the remote command keeps the input free.

Running the install twice appends the key twice. Remove duplicates while keeping order:

awk '!seen[$0]++' ~/.ssh/authorized_keys > ~/.ssh/ak.tmp && mv ~/.ssh/ak.tmp ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys
wc -l < ~/.ssh/authorized_keys # 1
ssh -o BatchMode=yes ubuntu-lab "hostname && whoami && pwd" # lab-Ubuntu / labuser / /home/labuser (no prompts)
ssh -o BatchMode=yes lab-Ubuntu "hostname" # lab-Ubuntu
ssh ubuntu-lab # interactive: "Welcome to Ubuntu 26.04.1 LTS ..."

BatchMode=yes must pass, because Hermes connects the same way.

Phase 5: Hermes Mode A with profile ubuntu-lab

Section titled “Phase 5: Hermes Mode A with profile ubuntu-lab”

If you followed the Kali tutorial, clone its working profile:

hermes profile create ubuntu-lab --clone-from kali-lab --no-alias `
--description "Agent on Windows; terminal commands run on the Ubuntu laptop over SSH"

Expected output:

  • Cloned config, .env, SOUL.md, and skills from kali-lab, plus the installed plugins.
  • ⚠ This profile has no API keys yet: harmless when your model provider uses a sign-in (such as Nous Portal) instead of an API key. The model is the same as in kali-lab.

Without a Kali profile, clone your default profile instead, then switch its terminal backend to SSH:

hermes profile create ubuntu-lab --clone-from default --no-alias `
--description "Agent on Windows; terminal commands run on the Ubuntu laptop over SSH"
hermes -p ubuntu-lab config set terminal.backend ssh

--no-alias skips the wrapper command; use hermes -p ubuntu-lab .... Confirm the backend:

hermes -p ubuntu-lab config get terminal.backend # ssh

A profile cloned from kali-lab still targets Kali. This script replaces any TERMINAL_SSH_* lines and keeps everything else (PowerShell 7):

$envFile = "$env:LOCALAPPDATA\hermes\profiles\ubuntu-lab\.env"
if (-not (Test-Path $envFile)) { New-Item -ItemType File -Force $envFile | Out-Null }
$lines = @(Get-Content $envFile | Where-Object { $_ -notmatch '^\s*TERMINAL_SSH_' -and $_ -notmatch '^# SSH terminal backend:' })
$lines += @("# SSH terminal backend: this profile's commands run on the Ubuntu laptop as 'labuser'",
'TERMINAL_SSH_HOST=192.168.50.21', 'TERMINAL_SSH_USER=labuser', 'TERMINAL_SSH_PORT=22',
'TERMINAL_SSH_KEY="C:/Users/Lab User/.ssh/ubuntu_lab"')
Set-Content -Path $envFile -Value $lines -Encoding utf8NoBOM
Select-String -Path $envFile -Pattern '^TERMINAL_SSH_' | ForEach-Object Line

The quotes around the key path are required when your Windows account folder contains a space, as Lab User does. Use forward slashes and the private key. A Kali profile stays untouched: hermes -p kali-lab ... still targets the Kali laptop.

hermes gateway restart # "Gateway started via direct spawn"; profile list shows ubuntu-lab "running"
hermes profile list
hermes -p ubuntu-lab chat -Q -q "Use your terminal tool to run: hostname && whoami && pwd && grep PRETTY_NAME /etc/os-release. Reply with only the output."

Expected output:

lab-Ubuntu
labuser
/home/labuser
PRETTY_NAME="Ubuntu 26.04.1 LTS"

In Hermes Desktop:

  1. In the Sessions sidebar, select the gateway This device.
  2. In the profile rail, select ubuntu-lab.
  3. Ask the same question, and expect the same output.

The profiles side by side:

Profile Commands run on
default Windows
kali-lab Kali (192.168.50.20), from the Kali tutorial
ubuntu-lab Ubuntu (192.168.50.21)

Desktop window backend: %APPDATA%\Hermes\connection.json holds the connection mode that Desktop’s window boots into. If it points over SSH at another laptop, for example Kali’s Hermes from the Kali tutorial (its section B.5), Desktop fails at launch whenever that laptop is off, with Desktop boot failed: Could not reach ... over SSH. For everyday use of ubuntu-lab, set Settings → Gateways → connection mode to Local / This device. Kali’s own Hermes stays reachable through its Kali Lab gateway entry.

Phase 6: GNOME Remote Desktop (Desktop Sharing + Remote Login)

Section titled “Phase 6: GNOME Remote Desktop (Desktop Sharing + Remote Login)”
  • Both modes speak RDP, so Windows uses its built-in Remote Desktop Connection (mstsc). No extra software is needed.
  • Each mode has its own RDP username and password, set in Settings → System → Remote Desktop. They are not your Linux password and not your Windows/Microsoft account. This tutorial uses the username labuser for both modes; use the same password for both (6.6).
  • Remote Login adds a second step: after the RDP login, Ubuntu’s own login screen (GDM) appears, and you sign in there with the Linux password.
  • Ports: Desktop Sharing 3390 and Remote Login 3389 when both are on.

On the Ubuntu laptop:

  1. Open Settings → System → Remote Desktop → Desktop Sharing.
  2. Turn on Desktop Sharing and Remote Control (Remote Control lets you use mouse and keyboard, not just watch).
  3. Under Login Details, set the username (labuser in this tutorial) and a password.

Verify (as labuser, in a local terminal or over SSH):

export XDG_RUNTIME_DIR=/run/user/$(id -u) DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/$(id -u)/bus # only needed over SSH
grdctl status # RDP Status: enabled, View-only: no, Negotiate port: yes, Username/Password: (hidden)
gsettings get org.gnome.desktop.remote-desktop.rdp screen-share-mode # 'mirror-primary'
systemctl --user is-active gnome-remote-desktop # active
ss -ltn | grep -E ':(3389|3390)\b'

Expected:

  • A TLS certificate at ~/.local/share/gnome-remote-desktop/certificates/rdp-tls.crt.
  • Port 3390 once Remote Login holds 3389.

grdctl status keeps showing the configured Port: 3389. The actual port is in ss (Negotiate port: yes lets it move).

If logins fail with SEC_E_NO_CREDENTIALS in the log (Phase 10), the running daemon has no login loaded. Fix:

  1. Switch Desktop Sharing off and on and re-type the password under Login Details.

  2. Run systemctl --user restart gnome-remote-desktop.

  3. Check it’s loaded (prints set/empty only, never the value):

    grdctl status --show-credentials | sed -E 's/^(\s*(Username|Password):)\s+\S.*$/\1 <set>/'

--show-credentials goes after status; grdctl --show-credentials status only prints usage.

On the Ubuntu laptop, open Settings → System → Remote Desktop → Remote Login → Unlock (admin password), switch Remote Login on, and fill Login Details (username labuser + password).

Settings can display the Remote Login details without delivering them to the service. The system daemon then keeps logging [RDP] Credentials are not set, denying client, and Windows shows error 0x904 / 0x7. The reliable way is to store them with grdctl --system and restart.

In an SSH or local terminal on Ubuntu:

sudo grdctl --system rdp set-credentials # prompts: "Username:" then "Password:" (nothing on the command line)
sudo systemctl daemon-reload # clears "unit file ... changed on disk" warnings
sudo systemctl restart gnome-remote-desktop
sudo grdctl --system status | grep -E 'Status|Port|Username' # Status: enabled / Port: 3389 / Username: (hidden)

Notes:

  • Init TPM credentials failed because No TPM device found, using GKeyFile as fallback is informational. On a laptop without a TPM chip, the login is stored in a root-only key file under /var/lib/gnome-remote-desktop.
  • If you must pass the values as arguments, start the line with a space. Ubuntu’s default HISTCONTROL=ignoreboth then keeps it out of bash history:
    sudo grdctl --system rdp set-credentials labuser 'NEW_PASSWORD'
    Never type such a command in Windows PowerShell (PSReadLine saves history), and never put the password in a screenshot or chat. If that happens, set a new password.
  • Each successful connection is logged by the system daemon as [RDP] Sending server redirection, followed by gnome-remote-desktop-handover.service starting in the session. Read it with journalctl -u gnome-remote-desktop (no sudo needed for a member of adm).

Ubuntu Screen (Desktop Sharing): RDP login → Ubuntu’s live screen, scaled to the window. If the screen is locked, you see the lock screen; unlock it with the Linux password.

Ubuntu Login (Remote Login):

  1. RDP login.
  2. Ubuntu’s login screen (GDM) appears inside the window.
  3. Sign in to your account (GDM shows its display name) with the Linux password.
  4. If you are logged in on the laptop, Ubuntu offers to end that local session and continue remotely. Accepting it also closes any open Ubuntu Screen window, because that local session is the one being shared.

Use Ubuntu Screen while you are logged in at the laptop, and Ubuntu Login when nobody is.

6.5 Windows connection files and desktop shortcuts

Section titled “6.5 Windows connection files and desktop shortcuts”

Two RDP connection files hold the settings. Desktop shortcuts with an Ubuntu icon open them.

C:\RemoteAccess\Ubuntu-Screen.rdp (Desktop Sharing):

full address:s:192.168.50.21:3390
username:s:labuser
screen mode id:i:1
smart sizing:i:1
dynamic resolution:i:0
prompt for credentials on client:i:1
authentication level:i:2
  • smart sizing:i:1 scales Ubuntu’s 4K screen to fit any window size; it’s a mirror, so its resolution can’t change.
  • username:s:labuser stops Windows from pre-filling your Microsoft account.
  • authentication level:i:2 warns on the self-signed certificate instead of refusing it.

C:\RemoteAccess\Ubuntu-Login.rdp (Remote Login):

full address:s:192.168.50.21:3389
username:s:labuser
screen mode id:i:2
use multimon:i:0
dynamic resolution:i:1
smart sizing:i:0
desktopscalefactor:i:200
prompt for credentials on client:i:1
authentication level:i:2
  • screen mode id:i:2 opens full screen on one monitor.
  • dynamic resolution:i:1 makes Ubuntu re-fit its desktop whenever the window size changes.
  • desktopscalefactor:i:200 asks Ubuntu to draw at 200 %, to match a Windows display at 200 %.

Without the size settings, the Remote Login window opens tiny (about 430×340), with content at 100 % scale. If text is still small with the settings above, set Settings → Displays → Scale → 200 % inside the remote session.

Create both files and shortcuts with the commands in Appendix B.

Windows saves remembered RDP logins per host, not per port. The target is TERMSRV/192.168.50.21, the same for 3389 and 3390; a Kali laptop from the other tutorial would have its own entry, TERMSRV/192.168.50.20. So both Ubuntu shortcuts share one remembered login.

Give Desktop Sharing and Remote Login the same username and password, and tick Remember me once. Otherwise leave Remember me unticked, or each shortcut will overwrite the other’s saved password.

List or remove saved entries (target names only):

(cmdkey /list) | Select-String 'Target:.*TERMSRV'
cmdkey /delete:TERMSRV/192.168.50.21

Remote access stops if the laptop suspends. Check these settings (as labuser):

gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-ac-type # 'nothing'
gsettings get org.gnome.settings-daemon.plugins.power sleep-inactive-battery-type # 'nothing'
gsettings get org.gnome.desktop.session idle-delay # uint32 0 (screen never blanks)
gsettings get org.gnome.desktop.screensaver lock-enabled # true (locks when blanked)

In the GUI: Settings → Power → Automatic Suspend: Off. If you change these, Desktop Sharing still works on a locked screen (you unlock it remotely). A suspended laptop needs to be woken locally.

Optional, for long jobs over SSH (tmux keeps them running if SSH drops):

ssh -t ubuntu-lab "sudo apt install -y tmux"
  • LAN only. Do not port-forward 22, 3389, or 3390 on the router. Use a VPN for outside access.
  • Keys: ubuntu_lab stays on Windows; Ubuntu holds one authorized_keys line (mode 600). Revoke Windows’ access by deleting that line.
  • Host key: stop if Ubuntu’s SSH fingerprint ever changes from the one you recorded in Phase 3.
  • RDP passwords:
    • They are stored in your account’s keyring (Desktop Sharing) and in a root-only file (Remote Login).
    • Use strong, unique values.
    • Change them with Settings (Desktop Sharing) or sudo grdctl --system rdp set-credentials (Remote Login).
    • Never share them in screenshots or chats; rotate them if that happens.
  • Certificates: both RDP modes use self-signed TLS certificates. Accept the prompt only on your home network, and tick “Don’t ask me again” for this host once verified.
  • Hermes:
    • Profile ubuntu-lab acts as labuser (an administrator).
    • Keep approvals on, and never store the sudo password.
    • Keep %LOCALAPPDATA%\hermes\profiles\ubuntu-lab\.env private.
    • The SSH backend copies some Hermes cache folders (documents, images, attachments) to Ubuntu during sessions; treat Ubuntu as trusted.
# Network and ports
Test-NetConnection 192.168.50.21 -Port 22 # True
Test-NetConnection 192.168.50.21 -Port 3389 # True (Remote Login)
Test-NetConnection 192.168.50.21 -Port 3390 # True (Desktop Sharing)
# SSH (no prompts)
ssh -o BatchMode=yes ubuntu-lab "hostname && whoami && grep PRETTY_NAME /etc/os-release"
# Hermes Mode A
hermes -p ubuntu-lab config get terminal.backend # ssh
hermes -p ubuntu-lab chat -Q -q "Use your terminal tool to run: hostname && whoami. Reply with only the output."

Then:

  1. Ubuntu Screen: while you are logged in at the laptop, double-click it and sign in as labuser with the RDP password. You see the live screen.
  2. Ubuntu Login: while you are logged out at the laptop (or accept “end session”), double-click it and sign in as labuser with the RDP password, then with the Linux password at GDM. You get a separate session.
Symptom / exact message Cause Fix
lab-ubuntu.local → 192.168.50.20; port 22 refused at that address Router DHCP gave Ubuntu Kali’s static address (Kali off; the address not reserved in the router) Give Ubuntu its own static address (Phase 1.3)
TcpTestSucceeded : False on port 22 / connection refused Ubuntu Desktop has no SSH server by default sudo apt install -y openssh-server && sudo systemctl enable --now ssh
Are you sure you want to continue connecting (yes/no/[fingerprint])? then Host key verification failed. while piping the key The yes/no prompt read the piped key text Pass the key inside the command (4.4), or record the host key first (4.2)
choose_kex: unsupported KEX method sntrup761x25519-sha512@openssh.com from ssh-keyscan Windows OpenSSH 9.5 ssh-keyscan limitation Capture the host key with ssh and a temporary known_hosts file (4.2)
ssh lab-Ubuntu: The authenticity of host 'lab-ubuntu (fe80::...%11)' can't be established The hostname isn’t an alias; mDNS resolved it to IPv6 link-local Add lab-ubuntu lab-Ubuntu to the Host line (4.3)
authorized_keys has the key twice Install command run twice awk '!seen[$0]++' dedupe (4.4)
ssh-keygen creates a key with a passphrase, or fails on -N '' Windows PowerShell 5.1 drops the empty argument Run the command in PowerShell 7.3 or later (0.1)
hermes profile create: “This profile has no API keys yet” Expected for sign-in (OAuth) providers such as Nous Portal Ignore; test with the one-shot chat (5.3)
Desktop: Desktop boot failed: Could not reach 192.168.50.20 over SSH Desktop’s window backend is SSH → Kali, and Kali is off Connection mode Local / This device (5.3)
Desktop shortcut “not visible” Explorer has not refreshed Click the desktop and press F5 (the file is in your Desktop folder)
Windows Security: “Your credentials did not work”, user pre-filled MicrosoftAccount\… The Remote Desktop app defaults to your Windows account More choices → Use a different account → labuser + RDP password; the username:s:labuser line pre-sets it
Desktop Sharing log: AcceptSecurityContext status SEC_E_NO_CREDENTIALS / client authentication failure The running user daemon had no RDP login loaded Re-enter Login Details (toggle off/on), systemctl --user restart gnome-remote-desktop (6.2)
grdctl --show-credentials status prints only Usage: ... Option in the wrong position grdctl status --show-credentials
0x904, extended 0x7 “This computer can’t connect to the remote computer” on 3389 after enabling Remote Login Remote Login took 3389 and Desktop Sharing moved to 3390; the shortcut still targeted 3389 Ubuntu Screen → 192.168.50.21:3390 (6.5)
0x904 / 0x7 on Ubuntu Login; system log [RDP] Credentials are not set, denying client Settings showed Remote Login details but never stored them for the system daemon sudo grdctl --system rdp set-credentials + restart (6.3)
Init TPM credentials failed because No TPM device found, using GKeyFile as fallback No TPM chip; informational None
Warning: The unit file ... of gnome-remote-desktop.service changed on disk. Run 'systemctl daemon-reload' Unit changed when Remote Login was enabled sudo systemctl daemon-reload && sudo systemctl restart gnome-remote-desktop
Ubuntu Login opens as a tiny window with small content No size or scale in the RDP file Full screen + dynamic resolution + desktopscalefactor:i:200 (6.5)
One shortcut’s saved password breaks the other Windows saves RDP logins per host (TERMSRV/192.168.50.21), not per port Same login for both modes, or don’t tick Remember me (6.6)
Symptom Check
Can’t reach Ubuntu at all Laptop on and awake? ping 192.168.50.21; nmcli -g ipv4.addresses connection show HomeWiFi
SSH works, Hermes fails hermes -p ubuntu-lab config get terminal.backend; the profile’s .env (quoted key path); SSH penalty after failed logins
Hermes ran commands on the wrong machine Selected profile: ubuntu-lab on gateway This device
Desktop Sharing shows the lock screen Normal when locked; unlock with the Linux password
Ubuntu Screen fails, Ubuntu Login works (or vice versa) ss -ltn | grep -E ':(3389|3390)' on Ubuntu; check which port each .rdp uses
Machine Log Command
Ubuntu Desktop Sharing (user daemon) journalctl --user -u gnome-remote-desktop --since "-30 min"
Ubuntu Remote Login (system daemon) journalctl -u gnome-remote-desktop --since "-30 min" (no sudo for a member of adm)
Ubuntu Handover / GDM journalctl --since "-30 min" | grep -iE 'handover|gdm'
Ubuntu SSH journalctl -u ssh -n 50 --no-pager
Windows RDP client Event Viewer → Applications and Services Logs → Microsoft → Windows → TerminalServices-RDPClient → Operational
Windows Hermes Desktop %LOCALAPPDATA%\hermes\logs\desktop.log

Filter the noisy FreeRDP lines when reading GNOME Remote Desktop logs:

journalctl -u gnome-remote-desktop --since "-30 min" --no-pager | grep -vE 'winpr|freerdp'
journalctl --user -u gnome-remote-desktop --since "-30 min" --no-pager | grep -oE 'SEC_E_[A-Z_]+|Credentials are not set|Sending server redirection' | sort | uniq -c

Windows RDP client events from the last 30 minutes:

Get-WinEvent -FilterHashtable @{ LogName='Microsoft-Windows-TerminalServices-RDPClient/Operational'; StartTime=(Get-Date).AddMinutes(-30) } |
Sort-Object TimeCreated | Select-Object TimeCreated, Id, @{n='Msg';e={($_.Message -replace '\s+',' ').Substring(0,[Math]::Min(140,($_.Message -replace '\s+',' ').Length))}}
Where What
Ubuntu NetworkManager HomeWiFi ipv4.method manual, 192.168.50.21/24, gw/DNS 192.168.50.1, autoconnect yes
Ubuntu ~/.ssh/authorized_keys 1 line: ssh-ed25519 … lab user@winpc ubuntu_lab; ~/.ssh 700, the file 600
Ubuntu GNOME Remote Desktop (user) RDP enabled, mirror-primary, view-only no, negotiate port yes → listening 3390, username labuser
Ubuntu GNOME Remote Desktop (system) enabled, port 3389, username labuser, credentials in GKeyFile (no TPM)
Ubuntu power suspend nothing (AC and battery), idle-delay 0
Windows ~\.ssh\config Host ubuntu-lab lab-ubuntu lab-Ubuntu 192.168.50.21 block (4.3)
Windows ~\.ssh\known_hosts 192.168.50.21 ssh-ed25519 … (the fingerprint you recorded in Phase 3)
Windows Hermes profile ubuntu-lab terminal.backend: ssh; .env: TERMINAL_SSH_HOST=192.168.50.21, USER=labuser, PORT=22, KEY="C:/Users/Lab User/.ssh/ubuntu_lab"
Windows RDP files C:\RemoteAccess\Ubuntu-Screen.rdp (3390), C:\RemoteAccess\Ubuntu-Login.rdp (3389), contents in 6.5
Windows shortcuts Ubuntu Screen.lnk and Ubuntu Login.lnk in your Desktop folder → mstsc.exe "<rdp>", icon C:\RemoteAccess\ubuntu.ico

Appendix B: Create the RDP files and desktop shortcuts (Ubuntu icon)

Section titled “Appendix B: Create the RDP files and desktop shortcuts (Ubuntu icon)”

Any .ico file works for the icon. Download an Ubuntu icon from a source whose license lets you use it, and save it as C:\RemoteAccess\ubuntu.ico.

# 0. A folder for the RDP files and the icon
New-Item -ItemType Directory -Force 'C:\RemoteAccess' | Out-Null
# 1. RDP connection files (Unicode encoding, as mstsc writes them)
@'
full address:s:192.168.50.21:3390
username:s:labuser
screen mode id:i:1
smart sizing:i:1
dynamic resolution:i:0
prompt for credentials on client:i:1
authentication level:i:2
'@ | Set-Content 'C:\RemoteAccess\Ubuntu-Screen.rdp' -Encoding Unicode
@'
full address:s:192.168.50.21:3389
username:s:labuser
screen mode id:i:2
use multimon:i:0
dynamic resolution:i:1
smart sizing:i:0
desktopscalefactor:i:200
prompt for credentials on client:i:1
authentication level:i:2
'@ | Set-Content 'C:\RemoteAccess\Ubuntu-Login.rdp' -Encoding Unicode
# 2. Desktop shortcuts with the Ubuntu icon
$desk = [Environment]::GetFolderPath('Desktop'); $sh = New-Object -ComObject WScript.Shell
foreach ($s in @(
@{ Name = 'Ubuntu Screen'; Rdp = 'C:\RemoteAccess\Ubuntu-Screen.rdp'; Desc = "Ubuntu laptop's real screen (GNOME Desktop Sharing, RDP port 3390)" },
@{ Name = 'Ubuntu Login'; Rdp = 'C:\RemoteAccess\Ubuntu-Login.rdp'; Desc = 'Ubuntu laptop: separate session (GNOME Remote Login, RDP port 3389)' })) {
$l = $sh.CreateShortcut("$desk\$($s.Name).lnk")
$l.TargetPath = "$env:SystemRoot\System32\mstsc.exe"; $l.Arguments = "`"$($s.Rdp)`""; $l.WorkingDirectory = 'C:\RemoteAccess'
$l.IconLocation = 'C:\RemoteAccess\ubuntu.ico,0'; $l.Description = $s.Desc; $l.Save()
}
# 3. Make Explorer show them immediately (same as pressing F5 on the desktop)
Add-Type -Namespace Shell32 -Name Notify -MemberDefinition '[DllImport("shell32.dll")] public static extern void SHChangeNotify(int e, uint f, System.IntPtr a, System.IntPtr b);'
[Shell32.Notify]::SHChangeNotify(0x08000000, 0, [IntPtr]::Zero, [IntPtr]::Zero)
  1. Open Remote Desktop Connection (mstsc), then Show Options:
    • General: Computer 192.168.50.21:3390, User name labuser.
    • Save As C:\RemoteAccess\Ubuntu-Screen.rdp.
    • Smart sizing isn’t in this dialog. Open the saved file in Notepad and add the line smart sizing:i:1.
  2. Repeat for 192.168.50.21:3389 with Display → Full screen, saved as C:\RemoteAccess\Ubuntu-Login.rdp. Add the dynamic resolution and desktopscalefactor lines in Notepad.
  3. Right-click the desktop → New → Shortcut → location C:\Windows\System32\mstsc.exe "C:\RemoteAccess\Ubuntu-Screen.rdp" → name Ubuntu Screen.
  4. Properties → Change Icon… → Browse → C:\RemoteAccess\ubuntu.ico. Repeat for Ubuntu Login with C:\RemoteAccess\Ubuntu-Login.rdp.
  5. If the icons don’t appear, press F5 on the desktop. If an icon stays stale, run ie4uinit.exe -show.
Item Kali (Kali tutorial) Ubuntu (this tutorial)
Address 192.168.50.20 static 192.168.50.21 static
SSH alias / key kali-lab / kali_lab ubuntu-lab (+ lab-ubuntu, lab-Ubuntu) / ubuntu_lab
Shell zsh (needs ~/.zshenv for Mode B) bash
Hermes Mode A (kali-lab) + Mode B (Kali Lab gateway) Mode A only (ubuntu-lab)
Separate desktop XRDP + Xfce on 3389, login with the Linux password; coexists with a local login (private D-Bus fix) GNOME Remote Login on 3389, RDP password, then the Linux password at GDM; ends a local login if one exists
Real screen x11vnc + TigerVNC via SSH tunnel (Open-KaliScreen.ps1) GNOME Desktop Sharing on 3390 via mstsc
Scaling fix x11vnc -scale fit (TigerVNC is DPI-unaware) smart sizing (Screen); dynamic resolution + desktopscalefactor (Login)
Desktop shortcuts Kali Screen Ubuntu Screen, Ubuntu Login

The setup is complete when three things work:

  1. ssh ubuntu-lab works without prompts.
  2. Profile ubuntu-lab returns lab-Ubuntu / labuser.
  3. Both Ubuntu Screen and Ubuntu Login connect with the RDP login.