Created
April 18, 2026 03:39
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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