(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.
Add two accounts: an “myadmin” and a “myuser”.
On boot, automatically log in as myuser.
Enable SSH.
~/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.
The kickstart/preseed story for Debian-based distributions is challenging in two ways:
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 }'}
]]
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
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.
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
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.
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