A Linux Thin Client ============================================================ (2019) We want an inexpensive but highly configurable, reliable, and easily maintained box that acts primarily as a thin client. That is, it primarily functions as an RDP client for a terminal server. A desirable secondary function, if possible, would be for that client to also run a modern, full-featured web browser. After testing several devices within our budget, we opted for a Celeron Intel NUC with 4 GB RAM and a small SSD. This (not really thin) device offers more flexibility and performance than a traditional Wyse or HP thin client. Using Linux removes licensing costs as a concern. Ansible makes fleet management and maintenance relatively painless. Design considerations and user experience ------------------------------------------------------------ - Boot and automatically log into a "friendly" desktop environment that presents limited options to the user. - Click an icon to RDP connect to a central RDS/terminal server - Click an icon to launch a web browser - Lock down the device to limit user customization, or else revert to a standard template after each reboot. - Most things that could "go wrong" should be fixable by a reboot. - Probably, we'll overwrite the user's home directory on each boot with a known-good template. - Do not excessively customize inconsequential elements of the system. Pick a Linux distribution with a good LTS branch, and hew as closely to the stock install as possible. - We picked Ubuntu 18.04 LTS. The transition to Wayland in 20.04 may cause some pain, but other distributions don't offer a better story. Implementation overview ------------------------------------------------------------ - Add two accounts: an "myadmin" and a "myuser". - On boot, automatically log in as myuser. - Enable SSH. - Disable remote root logins. - Make myadmin a sudoer. - Add our public key to `~/myadmin/.ssh/authorized_keys`. - Ideally, creating users, adding keys, and enabling SSH happens during kickstart/preseed. - Drop all inbound network connections except SSH and VNC using the software firewall. Limit SSH and VNC to connections from the IT LAN. Preseed ------------------------------------------------------------ The kickstart/preseed story for Debian-based distributions is challenging in two ways: - There's no comprehensive documentation on the preseed options. The only practical way to generate a preseed file is to manually install an exemplar machine, then extract the options from it for use in subsequent automated installs. - The installer can get the preseed answer file in several ways. To get anything like a fully automated install involves remastering the installation image, which is annoying. Though not _fully_ automated, the least annoying method is to send the client the URL of a preseed file as a DHCP option. Administration, maintenance, and remote support ------------------------------------------------------------ - Use Ansible for most maintenance. - For remote user support, we either run x11vnc all the time, or turn it on when we want to connect to a user's session. The initial configuration happens by a preseed file at install, with the remainder happening by Ansible playbook. However, the rest of this document highlights some parts of that configuration. It's handy sometimes to have the user easily see the IP address of their computer. Use Conky to show it on their desktop. ``` # apt install conky $ mkdir /home/htuser/.config/autostart $ vi /home/htuser/.config/autostart/conky.desktop $ cat /home/htuser/.config/autostart/conky.desktop [Desktop Entry] Type=Application Name=conky Exec=conky --daemonize --pause=20 StartupNotify=false Terminal=false $ vi .conkyrc $ cat .conkyrc conky.config={ background = false, double_buffer = true, gap_y = 20, gap_x = 90, own_window = true, own_window_transparent = true, own_window_argb_visual = true, own_window_type = "desktop", own_window_hints = "undecorated, below, sticky, skip_taskbar, skip_pager", total_run_times = 0, update_interval = 120, use_xft = true, }; conky.text = [[ My IP address is: ${exec ip ro get 8.8.8.8 | head -1 | awk '{ print $7 }'} ]] ``` Running as an unprivileged user and making auto-login work ------------------------------------------------------------ Ubuntu desktop seems to assume in several respects that the user will be a sudoer, or that they'll at least know their password. ``` # apt purge unattended-upgrades # vi /etc/gdm3/custom.conf [daemon] AutomaticLoginEnable=true AutomaticLogin=myuser $ gsettings set org.gnome.desktop.session idle-delay 0 $ gsettings set org.gnome.desktop.screensaver lock-enabled false $ gsettings set org.gnome.desktop.lockdown disable-lock-screen true ``` Secrets ------------------------------------------------------------ We have a couple secrets on the client, like the Hello API key and the VNC password, that we'd like to keep secret. Our adversary is some kid who steals the box, not the NSA. 1. Encrypt swap. 2. Encrypt any scripts containing secrets (gpg symmetric). 3. On boot, create a small tmpfs. 4. Curl the decryption key from the network. 5. Decrypt the secret files into the tmpfs. Encrypt swap: ``` # apt install cryptsetup # fallocate -l 4G /cryptswap # vi /etc/crypttab + cryptswap /cryptswap /dev/urandom swap # cryptdisks_start cryptswap # swapoff -va # vi /etc/fstab - UUID=a9c3c70b-64de-40bc-a42d-335afc0bc1b6 none swap sw 0 0 + /dev/mapper/cryptswap none swap sw 0 0 # swapon -va ``` At boot, after the network comes up, we run a script like: ``` #!/bin/sh set -euf mkdir -p /root/tmp mount -t tmpfs -o size=10M tmpfs /root/tmp curl https://example.com/secret > /root/tmp/passphrase # Maybe we need to de-obfuscate the passphrase here — base64 decode or whatever. gpg --batch --passphrase-file /root/tmp/passphrase --output /root/tmp/myscript --decrypt myscript.asc ``` This assumes we previously encrypted our secret files like: ``` $ gpg -ac myscript ``` Inventory ------------------------------------------------------------ We want a simple, automatically updating way to generate an Ansible inventory. Use https://github.com/pgorman/registarium. Have the client push their info to the API server, and grab a fresh inventory file whenever we need to run a playbook. Firewall ------------------------------------------------------------ Normally, we'd use iptables or nftables directly, but ufw is the default Ubuntu firewall, and it's a useful wrapper to ease the transition for whatever becomes the standard Linux firewall in the future. ``` # systemctl enable ufw # ufw enable # ufw default deny incoming # ufw default allow outgoing # ufw allow from 10.0.0.0/24 to any app OpenSSH # ufw allow from 10.0.0.0/24 to any port 5900 # ufw reload # ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), disabled (routed) New profiles: skip To Action From -- ------ ---- 22/tcp (OpenSSH) ALLOW IN 10.0.0.0/24 5900 ALLOW IN 10.0.0.0/24 ``` Links ------------------------------------------------------------ - https://www.karlrunge.com/x11vnc/