Skip to content

Instantly share code, notes, and snippets.

@dakdevs
Created April 18, 2026 03:39
Show Gist options
  • Select an option

  • Save dakdevs/f40c259594d2b56f9ae21430aea2e312 to your computer and use it in GitHub Desktop.

Select an option

Save dakdevs/f40c259594d2b56f9ae21430aea2e312 to your computer and use it in GitHub Desktop.
Interactive installer: set up xrdp + Xfce on a remote Ubuntu box for Mac RDP access. Handles console-session conflicts, installs Windows App, idempotent.
#!/usr/bin/env bash
# install-xrdp.sh — set up remote desktop (xrdp + Xfce) on a remote Ubuntu box
#
# Run this on your Mac. It will:
# 1. Check for / install the Mac RDP client (Windows App) via Homebrew or App Store
# 2. Prompt for the SSH host and Linux user to configure
# 3. scp itself over, run via sudo, clean up after
# 4. Install xrdp + xorgxrdp + xfce4 on the remote
# 5. Configure ~/.xsession to launch Xfce with an isolated dbus bus
# (so it coexists with any GNOME session on the physical monitor)
# 6. Open the firewall, start the service, print connection instructions
#
# Idempotent — safe to re-run.
#
# Usage:
# bash install-xrdp.sh # interactive install
# bash install-xrdp.sh --uninstall # interactive uninstall
#
# Advanced (direct-on-target, skip the Mac wrapper):
# sudo bash install-xrdp.sh --exec --user <name>
set -euo pipefail
# ───────────────────────────────────────────────────────────────────────────
# Pretty printing
# ───────────────────────────────────────────────────────────────────────────
bold() { printf '\033[1m%s\033[0m' "$*"; }
blue() { printf '\033[1;34m%s\033[0m' "$*"; }
green() { printf '\033[1;32m%s\033[0m' "$*"; }
yellow(){ printf '\033[1;33m%s\033[0m' "$*"; }
red() { printf '\033[1;31m%s\033[0m' "$*"; }
log() { printf '%s %s\n' "$(blue '[xrdp]')" "$*"; }
info() { printf '%s %s\n' "$(green ' →')" "$*"; }
warn() { printf '%s %s\n' "$(yellow ' !')" "$*"; }
err() { printf '%s %s\n' "$(red ' ✗')" "$*" >&2; }
# ───────────────────────────────────────────────────────────────────────────
# MODE SELECTION
# No --exec flag = "driver" mode, running on your Mac.
# With --exec = "installer" mode, already running on the Ubuntu box.
# ───────────────────────────────────────────────────────────────────────────
MODE="driver"
UNINSTALL=false
TARGET_USER=""
for a in "$@"; do
case "$a" in
--exec) MODE="installer" ;;
--uninstall) UNINSTALL=true ;;
esac
done
# ═══════════════════════════════════════════════════════════════════════════
# DRIVER MODE — runs on your Mac, ships script to remote, runs it there
# ═══════════════════════════════════════════════════════════════════════════
if [[ "$MODE" == "driver" ]]; then
echo
echo "$(bold '╭──────────────────────────────────────────────────────╮')"
echo "$(bold '│') $(blue 'xrdp remote installer') $(bold '│')"
echo "$(bold '│') This will install xrdp on a remote Ubuntu machine $(bold '│')"
echo "$(bold '│') so you can connect to it with Microsoft Remote $(bold '│')"
echo "$(bold '│') Desktop (Windows App) from your Mac. $(bold '│')"
echo "$(bold '╰──────────────────────────────────────────────────────╯')"
echo
# --- Step 0: make sure the Mac client (Windows App) is installed ---
echo "$(bold 'Step 0:') Checking for the Mac RDP client (Windows App / Microsoft Remote Desktop)…"
CLIENT_APP=""
for candidate in "/Applications/Windows App.app" "/Applications/Microsoft Remote Desktop.app"; do
[[ -d "$candidate" ]] && CLIENT_APP="$candidate" && break
done
# Also check via bundle ID in case it's installed somewhere else
if [[ -z "$CLIENT_APP" ]] && command -v mdfind >/dev/null; then
HIT=$(mdfind "kMDItemCFBundleIdentifier == 'com.microsoft.rdc.macos' || kMDItemCFBundleIdentifier == 'com.microsoft.windowsapp.mac'" 2>/dev/null | head -1)
[[ -n "$HIT" ]] && CLIENT_APP="$HIT"
fi
if [[ -n "$CLIENT_APP" ]]; then
info "$(green 'already installed:') $CLIENT_APP"
else
warn "Windows App is not installed on this Mac."
read -rp " Install it now? [Y/n]: " INSTALL_APP
INSTALL_APP="${INSTALL_APP:-Y}"
if [[ "$INSTALL_APP" =~ ^[Yy]$ ]]; then
if command -v brew >/dev/null; then
info "installing via Homebrew (this may take a minute)…"
# Try the new cask name first, fall back to the old one
if brew install --cask windows-app 2>/dev/null; then
info "$(green 'installed via brew cask windows-app')"
elif brew install --cask microsoft-remote-desktop 2>/dev/null; then
info "$(green 'installed via brew cask microsoft-remote-desktop')"
else
warn "Homebrew install failed — opening the Mac App Store instead."
open "macappstore://apps.apple.com/app/windows-app/id1295203466" 2>/dev/null \
|| open "https://apps.apple.com/app/windows-app/id1295203466"
echo
read -rp " Press Enter once you've clicked Install in the App Store… "
fi
else
warn "Homebrew is not installed on this Mac."
echo " Opening the Mac App Store — click $(bold 'Get / Install') there."
open "macappstore://apps.apple.com/app/windows-app/id1295203466" 2>/dev/null \
|| open "https://apps.apple.com/app/windows-app/id1295203466"
echo
read -rp " Press Enter once you've installed it from the App Store… "
fi
else
warn "skipping — you'll need an RDP client later to actually connect."
fi
fi
echo
# --- Ask for the SSH host ---
echo "$(bold 'Step 1:') Which machine am I installing xrdp on?"
echo " Give me the SSH host — the same thing you'd type after 'ssh'."
echo " Examples: $(yellow 'kirsedona') $(yellow '[email protected]') $(yellow 'myhost.lan')"
echo
read -rp " SSH host: " SSH_HOST
SSH_HOST="${SSH_HOST## }"; SSH_HOST="${SSH_HOST%% }"
if [[ -z "$SSH_HOST" ]]; then
err "no host entered — aborting."
exit 1
fi
# --- Sanity check: can we reach it? ---
echo
info "testing SSH connection to $(bold "$SSH_HOST")…"
if ! ssh -o BatchMode=no -o ConnectTimeout=5 "$SSH_HOST" "echo ok" >/dev/null 2>&1; then
warn "couldn't auto-connect non-interactively — that's fine if you use a password or key prompt."
warn "if the next step hangs, it's probably waiting for a password."
else
info "$(green 'connection OK')"
fi
# --- Ask which user's desktop to configure ---
echo
echo "$(bold 'Step 2:') Which Linux user on $(bold "$SSH_HOST") should I set up?"
echo " This is the account whose desktop you'll see when you RDP in."
echo " Press $(yellow Enter) to use whoever you SSH in as (usually right)."
echo
read -rp " Linux user [leave blank for default]: " REMOTE_USER
REMOTE_USER="${REMOTE_USER## }"; REMOTE_USER="${REMOTE_USER%% }"
# --- Confirm ---
echo
echo "$(bold 'About to do this:')"
echo " • copy this script to $(yellow "$SSH_HOST:/tmp/")"
if $UNINSTALL; then
echo " • run it with $(red 'sudo --uninstall') (removes xrdp)"
else
echo " • run it with $(green sudo) to install xrdp + configure a GNOME session"
fi
[[ -n "$REMOTE_USER" ]] && echo " • target user: $(yellow "$REMOTE_USER")" || echo " • target user: $(yellow '(your SSH login)')"
echo " • you'll be asked for your sudo password on the remote box"
echo " • the script cleans itself up afterwards"
echo
read -rp " Proceed? [Y/n]: " CONFIRM
CONFIRM="${CONFIRM:-Y}"
if [[ ! "$CONFIRM" =~ ^[Yy]$ ]]; then
err "cancelled."
exit 1
fi
# --- Copy script to remote ---
SCRIPT_PATH="${BASH_SOURCE[0]:-$0}"
if [[ ! -f "$SCRIPT_PATH" ]]; then
err "can't locate my own script file at '$SCRIPT_PATH' — run as 'bash /path/to/install-xrdp.sh'."
exit 1
fi
REMOTE_SCRIPT="/tmp/install-xrdp-$$.sh"
echo
info "copying script to $(bold "$SSH_HOST:$REMOTE_SCRIPT")…"
scp -q "$SCRIPT_PATH" "$SSH_HOST:$REMOTE_SCRIPT"
info "$(green copied)"
# --- Build remote command ---
REMOTE_ARGS="--exec"
[[ -n "$REMOTE_USER" ]] && REMOTE_ARGS="$REMOTE_ARGS --user $REMOTE_USER"
$UNINSTALL && REMOTE_ARGS="$REMOTE_ARGS --uninstall"
echo
info "running installer on $(bold "$SSH_HOST") (you may be prompted for your sudo password)…"
echo "$(bold '──── remote output ────────────────────────────────────')"
# -t forces a TTY so sudo can prompt for password interactively
ssh -t "$SSH_HOST" "sudo bash $REMOTE_SCRIPT $REMOTE_ARGS; rm -f $REMOTE_SCRIPT"
RC=$?
echo "$(bold '──── end remote output ────────────────────────────────')"
if [[ $RC -ne 0 ]]; then
err "remote installer exited with code $RC"
exit $RC
fi
if $UNINSTALL; then
echo
echo "$(green '✓') xrdp removed from $(bold "$SSH_HOST")."
exit 0
fi
echo
echo "$(green '✓') All done!"
echo
echo "$(bold 'Now connect from your Mac:')"
echo " 1. Install $(yellow 'Windows App') (formerly Microsoft Remote Desktop) from the Mac App Store."
echo " 2. Open it, click the $(yellow '+') button, pick $(yellow 'Add PC')."
echo " 3. $(bold 'PC name:') $(yellow "$SSH_HOST") (or use its Tailscale IP — see output above)"
echo " 4. $(bold 'User account:') click the dropdown, $(yellow 'Add User Account'),"
echo " enter your Linux username + password on $(bold "$SSH_HOST")."
echo " 5. Save, then double-click the PC tile to connect."
echo
echo "$(bold 'Notes:')"
echo " • The RDP session uses $(yellow Xfce), not GNOME. Xfce is lighter, more reliable over RDP,"
echo " and (importantly) coexists cleanly with a GNOME session on the physical monitor."
echo " • Your dotfiles (shell, git, editor, terminal) all carry over — same user, same home dir."
echo " • Look different? It's Xfce styling. Everything still works."
echo
echo "$(bold 'Troubleshooting:')"
echo " • Session closes immediately after login → check logs on the remote box:"
echo " sudo tail -n 80 /var/log/xrdp-sesman.log"
echo " tail -n 40 ~/.xorgxrdp.*.log"
echo " • To uninstall cleanly: bash $(basename "$0") --uninstall"
echo
exit 0
fi
# ═══════════════════════════════════════════════════════════════════════════
# INSTALLER MODE — runs on the Ubuntu box (via ssh from driver mode)
# ═══════════════════════════════════════════════════════════════════════════
# Re-parse args now that we know we're in installer mode
ACTION="install"
TARGET_USER="${SUDO_USER:-}"
ARGS=("$@")
i=0
while [[ $i -lt ${#ARGS[@]} ]]; do
case "${ARGS[$i]}" in
--exec) ;;
--user) TARGET_USER="${ARGS[$((i+1))]}"; i=$((i+1)) ;;
--uninstall) ACTION="uninstall" ;;
-h|--help) sed -n '2,13p' "$0"; exit 0 ;;
*) err "unknown arg: ${ARGS[$i]}"; exit 2 ;;
esac
i=$((i+1))
done
if [[ $EUID -ne 0 ]]; then
err "installer must run as root (use sudo)"
exit 1
fi
if [[ -z "$TARGET_USER" || "$TARGET_USER" == "root" ]]; then
err "could not determine non-root target user; pass --user <name>"
exit 1
fi
if ! id "$TARGET_USER" >/dev/null 2>&1; then
err "user '$TARGET_USER' does not exist on this machine"
exit 1
fi
# ─── Uninstall ─────────────────────────────────────────────────────────────
if [[ "$ACTION" == "uninstall" ]]; then
log "stopping xrdp"
systemctl disable --now xrdp xrdp-sesman 2>/dev/null || true
log "purging packages"
DEBIAN_FRONTEND=noninteractive apt-get purge -y xrdp xorgxrdp pulseaudio-module-xrdp 2>/dev/null || true
apt-get autoremove -y
if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q active; then
ufw delete allow 3389/tcp 2>/dev/null || true
fi
log "done"
exit 0
fi
# ─── Install ───────────────────────────────────────────────────────────────
log "target user: $TARGET_USER"
if ! grep -qi ubuntu /etc/os-release 2>/dev/null; then
warn "this doesn't look like Ubuntu — proceeding anyway, but YMMV"
fi
log "apt update"
DEBIAN_FRONTEND=noninteractive apt-get update -qq
log "installing xrdp + xorgxrdp + dbus-x11 + xfce4"
# We use Xfce for the RDP session (not GNOME) so it can coexist with a GNOME
# session already running on the physical monitor without conflicting on
# single-instance user services like gnome-shell / dbus user bus.
DEBIAN_FRONTEND=noninteractive apt-get install -y \
xrdp xorgxrdp dbus-x11 xfce4 xfce4-goodies
if apt-cache show pulseaudio-module-xrdp >/dev/null 2>&1; then
log "installing pulseaudio-module-xrdp (RDP audio redirection)"
DEBIAN_FRONTEND=noninteractive apt-get install -y pulseaudio-module-xrdp \
|| warn "pulseaudio-module-xrdp failed to install; audio may not work over RDP"
else
warn "pulseaudio-module-xrdp not in this Ubuntu's apt repo; audio may not work over RDP"
fi
log "adding xrdp user to ssl-cert group (so it can read the TLS key)"
if getent group ssl-cert >/dev/null; then
adduser --quiet xrdp ssl-cert || true
fi
POLKIT_RULE=/etc/polkit-1/localauthority/50-local.d/45-allow-colord.pkla
if [[ ! -f "$POLKIT_RULE" ]]; then
log "writing polkit rule to silence colord password prompts in RDP session"
mkdir -p "$(dirname "$POLKIT_RULE")"
cat > "$POLKIT_RULE" <<'EOF'
[Allow Colord all Users]
Identity=unix-user:*
Action=org.freedesktop.color-manager.create-device;org.freedesktop.color-manager.create-profile;org.freedesktop.color-manager.delete-device;org.freedesktop.color-manager.delete-profile;org.freedesktop.color-manager.modify-device;org.freedesktop.color-manager.modify-profile
ResultAny=no
ResultInactive=no
ResultActive=yes
EOF
fi
XSESSION="/home/$TARGET_USER/.xsession"
log "writing $XSESSION → launches Xfce with a fresh dbus bus"
# Unsetting DBUS_SESSION_BUS_ADDRESS + XDG_RUNTIME_DIR and starting via
# dbus-launch gives this RDP session its own bus, so it does NOT collide
# with a GNOME session the same user may have on the physical monitor.
cat > "$XSESSION" <<'EOF'
#!/bin/sh
# xrdp session — launches Xfce with an isolated dbus session bus.
# This lets the same user run a GNOME session on the physical monitor
# and an Xfce session over RDP at the same time, without conflicts.
export XDG_SESSION_TYPE=x11
export XDG_CURRENT_DESKTOP=XFCE
unset DBUS_SESSION_BUS_ADDRESS XDG_RUNTIME_DIR
exec dbus-launch --exit-with-session startxfce4
EOF
chmod +x "$XSESSION"
chown "$TARGET_USER:$TARGET_USER" "$XSESSION"
if command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q "Status: active"; then
log "opening ufw firewall port 3389/tcp"
ufw allow 3389/tcp >/dev/null
fi
log "enabling + (re)starting xrdp service"
systemctl enable --now xrdp >/dev/null
systemctl restart xrdp
IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "<unknown>")
TS_IP=$(command -v tailscale >/dev/null && tailscale ip -4 2>/dev/null | head -1 || true)
HN=$(hostname)
echo
log "$(green 'installation complete.')"
echo
echo " hostname: $HN"
echo " LAN IP: $IP"
[[ -n "$TS_IP" ]] && echo " Tailscale IP: $TS_IP"
echo " port: 3389 (TCP)"
echo " target user: $TARGET_USER"
echo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment