Server hardening for the Dell OptiPlex 7050 running Ubuntu 24.04.
App-level container requirements live in Adding Apps.
Scanned from LAN via nmap -F <server-ip>:
| Port | Service | Status | Action |
|---|---|---|---|
| 22 | SSH | open | Harden through SSH settings |
| 80 | HTTP | open through Traefik | Redirects to HTTPS |
| 443 | HTTPS | open through Traefik | OK |
| 111 | rpcbind | disabled | Unnecessary for NFS v4.1 |
| 1883 | MQTT | open (Zigbee2MQTT) | Blocked by UFW, Docker-internal only |
| 2283 | Immich | open | Blocked by UFW, access via Traefik only |
| 8123 | Home Assistant | open | Blocked by UFW, access via Traefik only |
| 16992 | Intel AMT HTTP | open | LAN-only remote management |
| 16993 | Intel AMT HTTPS | open | LAN-only remote management |
| 5900 | AMT KVM/VNC | open | LAN-only remote screen access |
| 32400 | Plex | open | Direct Plex access; bypasses Traefik OAuth |
To restrict ports further, change Docker port mappings from 0.0.0.0:PORT:PORT to 127.0.0.1:PORT:PORT.
Active since 2026-03-13.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp # SSH
sudo ufw allow 80/tcp # Traefik HTTP
sudo ufw allow 443/tcp # Traefik HTTPS
sudo ufw allow 32400/tcp # Plex remote access
sudo ufw enableplex.jaw.dev is protected by oauth2-media@file, but direct access to <server-ip>:32400 does not pass through Traefik. Keep 32400/tcp open only if direct Plex client access is required. Close it if Plex must be reachable only through Cloudflare/Traefik/OAuth.
# Management
sudo ufw status verbose
sudo ufw status numbered
sudo ufw allow <port>/tcp
sudo ufw delete <rule-number>Disable password auth, use key-based auth only.
# Copy key from Mac
ssh-copy-id user@<server-ip>Edit /etc/ssh/sshd_config:
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
sudo systemctl restart sshsudo apt install fail2ban
sudo systemctl enable --now fail2banBans IPs after 5 failed SSH attempts for 10 minutes.
| Service | Purpose | Action |
|---|---|---|
| rpcbind | NFS v2/v3 port mapping | Already disabled |
| ModemManager | Cellular modem management | Disable |
| wpa_supplicant | WiFi management | Disable |
| packagekit | GUI package management | Disable |
| udisks2 | GUI disk management | Disable |
| upower | Power management for GUI | Disable |
sudo systemctl disable --now ModemManager wpa_supplicant packagekit udisks2 upowerSeveral services mount /var/run/docker.sock, which is root-equivalent access: Traefik, Backrest, Dozzle, Beszel, Homepage, Walker, docker-cd. Consider docker-socket-proxy to limit API access.
Web traffic is layered:
- Cloudflare handles edge WAF/DDoS/bot filtering.
- UniFi allows only Cloudflare IPs to reach
80/443. - Traefik repeats the Cloudflare-only check and trusts forwarded real-client headers only from Cloudflare.
Keep Cloudflare IP ranges current with:
./scripts/cloudflare.shIf it changes files, review the diff and deploy through the normal git flow.
Traefik routes protected apps through oauth2-proxy:
oauth2-admin@filefor admin-only appsoauth2-media@filefor media apps such as Plex, Jellyfin, Seerr, and ConvertX
The user allowlists live encrypted in apps/oauth2-proxy/.env.sops. docker-cd decrypts them during deploy and Compose renders the runtime files oauth2-proxy reads.
Isolates cameras, sensors, and smart plugs from the main LAN. Devices can't reach the internet or other VLANs, but the server can reach them.
| Network | VLAN ID | Subnet | Internet | Purpose |
|---|---|---|---|---|
| Default | 1 | 192.168.4.0/24 | Yes | Main LAN, server |
| Guest | 2 | 192.168.2.0/24 | Yes | Guest WiFi |
| IoT | 30 | 192.168.30.0/24 | No | Cameras, IoT devices |
Settings → Networks → Create New:
- Name: IoT
- VLAN ID: 30
- IPv4 Address: 192.168.30.1, Netmask /24
- Isolate Network: checked
- Allow Internet Access: unchecked
- mDNS: checked
- DHCP: Server, range 192.168.30.6 - 192.168.30.254
Settings → WiFi → Create New:
- Name: IoT
- Password: set one
- Network: IoT, VLAN 30
- Radio Band: 2.4 GHz only
UniFi auto-creates isolation rules when "Isolate Network" is checked, but two manual LAN In rules are needed so the server can talk to IoT devices:
| Rule | Action | Source | Destination | State | Purpose |
|---|---|---|---|---|---|
| Allow Server to IoT | Accept | 192.168.4.161 | IoT network | Any | Server can reach cameras |
| Allow Established/Related IoT | Accept | IoT network | Any | Established, Related | Return traffic only |
| Isolate IoT | Drop | 192.168.30.0/24 | All VLANs | Any | IoT can't reach main LAN |
| Block IoT internet | Drop | 192.168.30.0/24 | Any | Any | No cloud phoning home |
Why two manual rules? The "Isolate Network" toggle blocks all inter-VLAN traffic, including return traffic from IoT devices back to the server. Without these rules, the server can send packets to the camera but never gets a response.
Why Established/Related instead of a broad allow? A broad "Allow IoT → Server" rule lets a compromised IoT device initiate new connections to the server. Using Established/Related state means IoT devices can only respond to connections the server started — they can never open new connections to anything.
Both manual rules must have a lower ID than the auto-created isolation rules, usually 60001+, so they're evaluated first.
- Open the device app → WiFi settings → connect to
IoTSSID - Set static IP, such as
192.168.30.56for camera - Update
.env.sopswith new IP - Push and redeploy
From your main LAN:
# Server can reach camera
ping 192.168.30.56
nc -zv 192.168.30.56 554 # RTSP
nc -zv 192.168.30.56 2020 # ONVIF
# Camera can't reach server
# No way to test directly, but Tapo app should fail remotely- IoT devices get no internet — TP-Link, Tuya, etc. can't phone home
- IoT devices can't reach your main LAN — compromised camera can't attack your server
- Server
192.168.4.161can reach IoT VLAN — Frigate/HA connects to cameras - mDNS enabled — allows device discovery across VLANs if needed
- Camera credentials still in
.env.sops— only the IP changes when moving VLANs - Delete vendor apps after setup — camera runs standalone on RTSP/ONVIF
AMT runs on the Management Engine chipset independently of the OS. It listens on ports 16992/16993/5900 and provides remote power control and KVM.
- Access:
http://192.168.4.161:16992or HTTPS on 16993 - KVM: VNC client to port 5900
- Risk: AMT has had critical CVEs — keep BIOS firmware updated
- Mitigation: LAN-only access, UFW doesn't affect AMT, firewall at router blocks inbound
- USB Provision disabled, User Consent set to None, Remote IT config disabled
- Disable rpcbind
- Enable UFW firewall
- IoT VLAN: VLAN 30, 192.168.30.0/24
- Intel AMT/vPro enabled for remote power and KVM
- SSH: key-only auth
- Install fail2ban
- Disable unnecessary services
- Bind non-Traefik ports to 127.0.0.1
- Docker socket proxy