| Mount | Device | FS | Used | Size | Usage |
|---|
| PID | Name | CPU% | RSS |
|---|
| PID | Name | CPU% | RSS |
|---|
What was scanned, validated, blocked, and shipped.
A real RDP session to this machine's Xfce desktop, rendered here — no client to install and no SSH tunnel to keep open. You land on the same xrdp login card a desktop client shows, with your username already filled in; type your Linux password to sign in.
Negotiating the RDP session with xrdp.
What you copy inside the session lands here. Anything you put here is sent to the session's clipboard, ready to paste.
Shared with the session as the Browser drive. Inside Xfce it is
under ~/thinclient_drives.
The session above is not a video stream and not a screenshot loop — it is a real RDP
connection. guacd speaks RDP to the same xrdp on
127.0.0.1:3389 a desktop client would reach, and this dashboard relays the
Guacamole protocol over a WebSocket to the page. Drawing is still done server-side, so
it behaves like RDP rather than like a recording of one.
~/thinclient_drives.Ctrl+W and friends, so they reach the desktop
rather than your browser.Ctrl+T opens a tab instead of a terminal.Three steps, and the first is the one that trips people up. The tunnel has to stay open the whole time you are connected — that terminal window is the connection.
On your laptop, in a terminal. Leave the window open.
$ ssh -L 13389:localhost:3389 azureuser@…
-L 3389:…
fails with bind: Permission denied. Any free local port works —
13389, 3390, 33890. If 13389 is also refused,
Hyper-V or WSL has reserved that range; run
netsh interface ipv4 show excludedportrange protocol=tcp and pick a
port outside it.
Connect to localhost:13389 — not to the VM's address. Accept the
certificate warning; the cert is self-signed, which is expected.
| Platform | Client |
|---|---|
| Windows | Remote Desktop Connection (mstsc) — already installed |
| macOS | Windows App, free on the App Store |
| Linux | remmina or xfreerdp |
| iPad / Android | Windows App — but it needs the tunnel too, so a laptop is the practical client |
At the login card: leave Session on Xorg, username
azureuser, and your Linux password — the same
one SSH just accepted. If you have never set one, run
sudo passwd azureuser over SSH first.
Nothing here is a virtual machine inside a virtual machine. xrdp accepts the RDP
connection, asks sesman to start a real X server for you, and that X
server launches Xfce. Five links:
Because xorgxrdp is a driver inside a genuine X server, the desktop renders
at whatever resolution your client asks for, and drawing is done server-side rather than
streamed as video.
The login dialog is not part of your desktop. It is drawn by xrdp itself, in C, using a
small widget toolkit that ships inside xrdp — and it runs before any X server
exists. At that moment there is no GTK, no theme engine, no font config, so the
Materia-dark theme and Papirus icons the desktop uses cannot reach it, by construction.
Hand-drawn two-pixel bevels are why it reads as Windows 95; the bitmap font
/usr/share/xrdp/sans-10.fv1 has one fixed size and no antialiasing; and the
350×430 dialog has no HiDPI awareness, so on a 3840×2160 client it renders postage-stamp
sized.
Only /etc/xrdp/xrdp.ini and two image files changed — no patched binaries.
Two constraints are worth knowing before you retheme: this xrdp is not linked against
imlib2, so images must be BMP (a PNG is silently ignored), and xrdp paints label
text and the title-bar caption from the same two palette slots it uses for the input-field
fill, so a dark card cannot also have a readable caption. The title bar is therefore set
to the card colour and disappears into it; the wordmark carries the identity instead.
The theme lives in a script rather than in hand-edited config, so it is repeatable. Edit
the THEME dictionary and re-run it; every run also writes a timestamped
.bak of the ini beside it.
# redraw the images after editing the palette $ python3 ~/vm-config/xrdp-theme/make_theme.py $ sudo install -m 644 ~/vm-config/xrdp-theme/{background,logo}.bmp /usr/share/xrdp/ # apply the ini keys, then restart $ sudo python3 ~/vm-config/xrdp-theme/apply_theme.py $ sudo systemctl restart xrdp # back to the stock xrdp look $ sudo python3 ~/vm-config/xrdp-theme/apply_theme.py --revert
xrdp gives Xorg whatever resolution your client asks for, and Xfce has no automatic
scaling. At 96 dpi a 3840×2160 client renders the whole desktop at half the size it should
be. That is not a theme problem; it is a missing setting. ~/bin/hidpi handles
it, and an Xfce autostart entry runs it at every login so the scale follows whichever
machine you connect from.
$ hidpi # what is it now? $ hidpi 150 # pin a scale — stops auto-detecting $ hidpi auto # back to matching the client
| Client width | Scale applied |
|---|
Detection happens once, at login. If you resize the RDP window mid-session, run
hidpi auto again. Applications already open keep the old scale until
restarted — logging out and back in is the clean way.
RDP left open to the internet is scanned and brute-forced continuously. This setup never exposes it: xrdp binds the loopback interface only, so the SSH tunnel is the sole route in and no Azure network security group rule was needed. Your RDP password is your Linux password, and the tunnel is already authenticated by SSH — anyone who cannot SSH in cannot reach the login screen at all.
| Task | Command |
|---|---|
| Service state | systemctl status xrdp xrdp-sesman |
| Restart the listener | sudo systemctl restart xrdp |
| Who is connected | ss -tnp | grep :3389 |
| Live desktop sessions | pgrep -a Xorg |
| Connection log | sudo tail -f /var/log/xrdp.log |
| Login / session log | sudo tail -f /var/log/xrdp-sesman.log |
| Your X server log | tail -f ~/.xorgxrdp.10.log |
| Interface scale | hidpi · hidpi auto · hidpi 200 |
The pre-change backup holds the packaged xrdp.ini, sesman.ini and
startwm.sh exactly as Ubuntu shipped them. Each apply_theme.py
run additionally drops a timestamped xrdp.ini.bak-… next to the live file, so
you can always diff against the state before the last change.
| Symptom | Cause and fix |
|---|---|
bind: Permission denied on the tunnel |
Windows owns local port 3389. Tunnel through a different local port: -L 13389:localhost:3389. |
| RDP client cannot connect at all | The tunnel closed. That SSH window has to stay open. Confirm with ss -tlnp | grep 3389 on the VM. |
| Login box appears, then closes instantly | Wrong password — it is the Linux one, not an Azure credential. Confirm in /var/log/xrdp-sesman.log; reset with sudo passwd azureuser. |
| Blank or black screen after login | The session started but Xfce did not. Check ~/.xorgxrdp.10.log and that startwm.sh still ends in exec startxfce4. |
| Everything is tiny | 4K client at 96 dpi. Run hidpi auto, then log out and back in. |
| Login screen back to teal | A package update replaced xrdp.ini. Re-run sudo python3 ~/vm-config/xrdp-theme/apply_theme.py. |
| Browser session says guacd unavailable | The proxy is not running. sudo systemctl status guacd, then sudo systemctl start guacd. |
| Typing does nothing in the browser session | The stage takes keys only while focused — click the desktop once. The strip along the bottom says so while it is unfocused. |
Ctrl+W closes the browser tab |
Expected outside fullscreen. Use the Fullscreen button; it asks the browser for the keyboard so those combinations reach the desktop. |
| Browser and desktop client show different desktops | Session policy. It is set to UBI so both loopback connections share one session, but sesman only reads that at startup — see the note in section 01. |
| Reconnected into a stale session | Expected — sessions persist by design. Log out from inside Xfce to end one properly. |
Plan windows and conversation tokens for every model on this VM. The Chat sidebar still shows Claude and Codex while you work; this is the comparison across all of them.
Stored chats and Engineer sessions. Claude cost is the SDK’s dollar figure; Codex bills against plan quota, so its cost column stays empty on purpose. Titles only — the transcript stays in Chat.
The agents working this repository cannot talk to each other, so everything between them is files and commits. This is that exchange — but it opens with what is still owed rather than with what was written, because an unanswered review only costs something once it has been sitting for a while. Any item opens and reads in full, and it updates itself as they work: nothing here needs a refresh.
Reports and reviews written for this VM are filed here, so you never have to go looking for a link again.
Store an HTML document on this VM so it stays with the dashboard.
A command line is split on &&, ||,
; and |, and every part must clear these rules on its own.
Ask rules win over allow rules, and command substitution or output redirection always
asks — so an allow rule for ls can never approve ls && rm -rf /.
access.log — the same lines the
sysmon-auth fail2ban jail counts.
sysmon-auth jail: 4 failed dashboard
logins in 10 minutes. Releasing one lets that address try again immediately.sshd journal and fail2ban log as it happens.
| Address | Network | Attempts | Accounts tried | Active | Status |
|---|
| Time | Client | Method | Path | Status | Took | Size | Agent |
|---|