Secure your home server with SSH key auth, UFW firewall, fail2ban & Docker isolation. Practical guide for home users with a printable security audit checklist.
Running a home server gives you powerful control over your data and services โ but it also means you are responsible for your own security. The good news: the threat model for a home server is far simpler than for a corporate network, and a handful of well-applied layers will stop the vast majority of real-world attacks you will ever face.

Before installing any security tool, it helps to be realistic about who is actually coming after your home server โ and who is not.
Realistic threats you will encounter:
What this guide does NOT cover:
This guide is calibrated for a typical home server running Ubuntu 22.04 or Debian 12 with Docker, accessed remotely by one or two people. Each layer addresses a concrete threat.
If you are setting up your first server, start with the beginner's guide to home servers in 2026 before continuing here. For remote access strategy, see the secure remote access comparison.

SSH is the front door to your server. The default configuration โ password authentication on port 22 โ is adequate for a LAN server but a liability for anything internet-exposed. These steps harden it significantly.

On your local Linux or macOS machine, generate an Ed25519 key pair (stronger and shorter than RSA):
ssh-keygen -t ed25519 -C "homeserver-$(date +%Y%m%d)" -f ~/.ssh/homeserver_ed25519
You will be prompted for a passphrase. Use one โ it protects the private key if your laptop is compromised.
This creates two files:
~/.ssh/homeserver_ed25519 โ your private key (never share this)~/.ssh/homeserver_ed25519.pub โ your public key (goes on the server)ssh-copy-id -i ~/.ssh/homeserver_ed25519.pub user@192.168.1.100
If ssh-copy-id is not available, copy it manually:
cat ~/.ssh/homeserver_ed25519.pub | ssh user@192.168.1.100 "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
Verify you can log in with the key before proceeding:
ssh -i ~/.ssh/homeserver_ed25519 user@192.168.1.100
Once key-based login is confirmed, edit the SSH daemon config on the server:
sudo nano /etc/ssh/sshd_config
Set or confirm these values:
PasswordAuthentication no
PubkeyAuthentication yes
AuthorizedKeysFile .ssh/authorized_keys
PermitRootLogin no
X11Forwarding no
Reload SSH to apply changes:
sudo systemctl reload sshd
Keep your current session open while testing from a second terminal โ a misconfiguration could otherwise lock you out.
Changing from port 22 to a high port does not improve security against determined attackers, but it dramatically reduces the log noise from automated scanners. Pick a port above 1024:
sudo nano /etc/ssh/sshd_config
Port 2222
sudo systemctl reload sshd
Update your firewall (Layer 2) to allow the new port before reloading. Connect with:
ssh -p 2222 -i ~/.ssh/homeserver_ed25519 user@192.168.1.100
Restrict SSH access to specific usernames, preventing any accidental or default accounts from being used:
sudo nano /etc/ssh/sshd_config
AllowUsers yourusername
sudo systemctl reload sshd
This whitelist approach means that even if a new user account is created, SSH will not accept logins from it unless explicitly added here.
UFW (Uncomplicated Firewall) wraps iptables in a straightforward interface. The goal: default-deny everything inbound, then explicitly allow only the ports you need.
UFW is included in Ubuntu by default. On Debian:
sudo apt update && sudo apt install ufw -y
sudo ufw default deny incoming
sudo ufw default allow outgoing
This blocks all inbound connections until you explicitly allow them.
If you changed SSH to port 2222:
sudo ufw allow 2222/tcp comment 'SSH custom port'
Or for default port 22:
sudo ufw allow ssh
Add rules only for services you actually expose. Common examples:
# HTTP and HTTPS for a reverse proxy
sudo ufw allow 80/tcp comment 'HTTP'
sudo ufw allow 443/tcp comment 'HTTPS'
# WireGuard VPN (if using)
sudo ufw allow 51820/udp comment 'WireGuard'
# Plex media server (LAN only example)
sudo ufw allow from 192.168.1.0/24 to any port 32400 comment 'Plex LAN'
Prefer source-limiting rules (using from 192.168.1.0/24) for services that only need LAN access. There is no reason to expose a Plex port to the entire internet if you use a VPN for remote access.
sudo ufw enable
sudo ufw status verbose
Sample output:
Status: active
To Action From
-- ------ ----
2222/tcp ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
51820/udp ALLOW IN Anywhere
32400 ALLOW IN 192.168.1.0/24
Check that your critical rules are present before closing your SSH session.
fail2ban monitors log files for repeated failed authentication attempts and temporarily bans the offending IP with iptables rules. It is highly effective against credential-stuffing bots.
sudo apt install fail2ban -y
Do not edit /etc/fail2ban/jail.conf directly โ it will be overwritten on updates. Use a local override instead:
sudo cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local
Edit /etc/fail2ban/jail.local and find the [sshd] section:
sudo nano /etc/fail2ban/jail.local
[sshd]
enabled = true
port = 2222
filter = sshd
logpath = /var/log/auth.log
maxretry = 5
bantime = 1h
findtime = 10m
Adjust port to match your SSH port. maxretry = 5 means 5 failed attempts within 10 minutes triggers a 1-hour ban.
If you are running a reverse proxy, add a jail for HTTP authentication failures. For Nginx:
[nginx-http-auth]
enabled = true
filter = nginx-http-auth
logpath = /var/log/nginx/error.log
maxretry = 10
bantime = 30m
findtime = 5m
For Caddy, since it uses structured JSON logs, create a custom filter at /etc/fail2ban/filter.d/caddy-auth.conf:
[Definition]
failregex = ^.*"remote_addr":"<HOST>".*"status":401.*$
ignoreregex =
Then add a jail:
[caddy-auth]
enabled = true
filter = caddy-auth
logpath = /var/log/caddy/access.log
maxretry = 10
bantime = 30m
sudo systemctl restart fail2ban
sudo systemctl status fail2ban
sudo fail2ban-client status
sudo fail2ban-client status sshd
To manually unban an IP (useful if you accidentally lock yourself out):
sudo fail2ban-client set sshd unbanip 1.2.3.4
Monitor the fail2ban log to see bans in real time:
sudo tail -f /var/log/fail2ban.log
Docker is convenient but introduces its own attack surface. Containers share the host kernel, so a misconfigured container can have significant host access. See the full context in our Docker Compose home server stack guide.
The default Docker behavior often runs container processes as root inside the container. If the container is compromised and has a volume mount or a kernel exploit available, this becomes a host root compromise.
Specify a non-root user in your docker-compose.yml:
services:
app:
image: myapp:latest
user: "1000:1000"
Or in a Dockerfile:
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
Check what user a running container is using:
docker inspect <container_name> | grep -i user
docker exec <container_name> id
For containers that do not need to write to their filesystem:
services:
nginx:
image: nginx:alpine
read_only: true
tmpfs:
- /var/cache/nginx
- /var/run
- /tmp
The tmpfs entries provide writable in-memory directories for files nginx needs at runtime without persisting anything to disk.
Create named networks and assign containers only to the networks they need. Do not connect every container to the default bridge:
networks:
frontend:
backend:
services:
reverse-proxy:
image: caddy:alpine
networks:
- frontend
- backend
database:
image: postgres:16
networks:
- backend
# database is NOT on the frontend network
webapp:
image: myapp:latest
networks:
- backend
This means the database is not reachable from the reverse proxy directly โ only the webapp can connect to it. A compromised frontend container cannot reach your database.
Also, never bind container ports to 0.0.0.0 unless the service genuinely needs to be publicly accessible. Use 127.0.0.1 for services accessed only via the reverse proxy:
ports:
- "127.0.0.1:8080:8080" # Only accessible on localhost
Outdated container images are a leading source of vulnerabilities. Watchtower automates image updates:
services:
watchtower:
image: containrrr/watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 4 * * * # 4 AM daily
- WATCHTOWER_NOTIFICATIONS=email
- WATCHTOWER_NOTIFICATION_EMAIL_FROM=watchtower@yourdomain.com
- WATCHTOWER_NOTIFICATION_EMAIL_TO=you@youremail.com
Note: Watchtower updates images automatically. If you prefer to review changes before applying, run it in monitor-only mode and apply updates manually:
# Monitor mode: just send notifications, don't auto-update
docker run --rm containrrr/watchtower --run-once --monitor-only
Exposing services directly on numbered ports means no TLS, no centralized authentication, and more attack surface. A reverse proxy solves all three.
Caddy is the preferred choice for home servers in 2026 โ it handles Let's Encrypt certificates automatically and has a readable configuration format:
your.domain.com {
reverse_proxy localhost:8096 # Jellyfin example
}
That single block gives you automatic HTTPS with certificate renewal. Caddy handles ACME challenges, certificate storage, and renewal without any additional tooling.
For services you want to protect with multi-factor authentication โ particularly admin panels like Portainer, Grafana, or Proxmox โ add Authelia in front of them:
# In your Caddyfile
grafana.yourdomain.com {
forward_auth localhost:9091 {
uri /api/verify?rd=https://auth.yourdomain.com
copy_headers Remote-User Remote-Groups Remote-Name Remote-Email
}
reverse_proxy localhost:3000
}
Authelia provides:
For the full configuration, run Authelia alongside a Redis instance (for session storage) and an LDAP or file-based user backend. The file backend is sufficient for household use.
Even with a reverse proxy, apply the principle of least exposure: if a service is only accessed from home or via VPN, do not expose it publicly at all. See the VPN section below.
A DNS sinkhole like Pi-hole or AdGuard Home running on your home server serves double duty: it blocks ads and tracking domains, and it blocks known malware, phishing, and C2 (command-and-control) domains across your entire network without installing anything on individual devices.
This is relevant to server security because:
For a detailed comparison and setup guide, see AdGuard Home vs Pi-hole.
Quick DNS security wins regardless of which tool you choose:
# Use DNS-over-TLS or DNS-over-HTTPS upstream resolvers
# to prevent ISP snooping on your DNS queries
# Example upstream for AdGuard Home (in config.yaml)
upstream_dns:
- tls://dns.quad9.net
- tls://cloudflare-dns.com
Set your router's DHCP to hand out your server's IP as the DNS server for all LAN clients.
How you access your home server from outside your LAN determines your largest attack surface. The options, ranked from most to least secure:
Option 1: VPN (Tailscale or WireGuard) โ Most secure
A VPN means your services are not exposed to the public internet at all. Your server only accepts connections from the VPN tunnel. Automated scanners cannot reach your services because there are no open ports to find.
Tailscale is the zero-configuration option: install it on your server and your devices, and your server becomes reachable at a fixed private IP from anywhere without port forwarding. See the WireGuard VPN setup guide for the self-hosted alternative.
Option 2: Cloudflare Tunnel โ Good security, no open ports
Cloudflare Tunnel creates an outbound-only connection from your server to Cloudflare's edge. No inbound firewall rules needed, DDoS protection included, and Cloudflare Access can add authentication. The trade-off is that your traffic routes through Cloudflare.
Option 3: Direct exposure with reverse proxy + Authelia โ Acceptable with careful hardening
If you need to expose services publicly (e.g., a public website), do so through a reverse proxy with HTTPS, fail2ban, and ideally authentication for any admin surfaces. Close every other port.
For a full breakdown of these options, including configuration trade-offs, see the secure remote access comparison.
Never expose these directly to the internet:
Use this checklist before and after changes to your server's configuration.
SSH
PasswordAuthentication no)PermitRootLogin no)AllowUsers restricts access to named accountsFirewall
sudo ufw status)fail2ban
maxretry and bantime valuesDocker
0.0.0.0 unnecessarilyNetwork
Remote Access
Maintenance
Security without visibility is incomplete. You want to know when something unusual happens โ before you discover it the hard way.
Uptime Kuma for availability monitoring:
services:
uptime-kuma:
image: louislam/uptime-kuma:1
volumes:
- ./uptime-kuma-data:/app/data
ports:
- "127.0.0.1:3001:3001"
restart: unless-stopped
Uptime Kuma can monitor TCP ports, HTTP endpoints, and Docker container status. Configure it to send push notifications (Pushover, Telegram, ntfy) when services go down.
fail2ban email alerts: Add notification settings to your jail config to receive emails when bans occur. This gives you early warning of active scanning.
Log monitoring with Loki and Promtail: For a more comprehensive setup, ship your server logs to Loki and visualize them with Grafana. The key logs to monitor:
# Most important security-relevant log files
/var/log/auth.log # SSH logins, sudo usage, authentication
/var/log/fail2ban.log # Ban events
/var/log/ufw.log # Blocked connection attempts (enable with: sudo ufw logging on)
/var/log/caddy/access.log # All HTTP requests through the reverse proxy
Enable UFW logging for connection attempt visibility:
sudo ufw logging on
sudo ufw logging medium
Automated security updates: The single most effective maintenance task for server security is keeping software patched. Enable unattended security upgrades:
sudo apt install unattended-upgrades -y
sudo dpkg-reconfigure -plow unattended-upgrades
Edit /etc/apt/apt.conf.d/50unattended-upgrades to enable email notifications on successful updates.
Backups as a security measure: Ransomware and accidental data deletion are real risks. A solid backup strategy gives you the ability to recover from any security incident. The 3-2-1 backup strategy guide covers off-site backup setup that complements the hardening covered here.
Yes, with appropriate precautions. Millions of people run home servers securely. The key is understanding the threat model: the primary risks are automated bots trying default credentials and unpatched software vulnerabilities โ not targeted attacks. Following the layers in this guide (SSH keys, firewall, fail2ban, reverse proxy) addresses the most common real-world threats. Running a home server behind a VPN with no open ports is genuinely quite safe for personal use.
The most impactful steps in order of effectiveness:
These five steps, properly implemented, stop the overwhelming majority of real-world attacks against home servers.
It depends on what you are exposing and how. A media server or dashboard accessed only by household members should use a VPN โ there is no good reason to expose it publicly. A service that genuinely needs public access (a personal website, a public Nextcloud instance, a public API) should go through a reverse proxy with HTTPS, authentication, and fail2ban.
The key principle: never expose a service to the internet without HTTPS, and never expose admin interfaces to the internet at all. Put everything possible behind a VPN first, and only expose what truly needs to be public. See the secure remote access comparison for a full analysis of your options.
UFW (Uncomplicated Firewall) is the right choice for most home server users on Ubuntu or Debian. It wraps iptables in a human-readable interface, is well-documented, integrates with fail2ban, and handles the needs of a home server with minimal complexity.
For users running Proxmox or more complex network topologies, nftables (the modern replacement for iptables) offers more control. For router-level protection, consider OPNsense or pfSense on a dedicated machine or VM โ they provide network-level protection for everything on your LAN, not just the server.
For most home server setups: UFW on the server, plus your router's built-in NAT (which blocks unsolicited inbound by default), provides a solid two-layer defense.
The most important Docker security practices for home servers:
user: directive in docker-compose or USER in Dockerfiles.0.0.0.0 โ use 127.0.0.1:port:port for services that only need local access.--privileged mode โ only use it when absolutely required (e.g., some VPN containers), and understand what you are granting.:ro for read-only mounts where the container only needs to read files.The Docker Compose home server stack guide shows a full working example with these practices applied.
Home server security is not about building a fortress โ it is about raising the cost of attack above what automated bots and opportunistic scripts are willing to pay. The seven layers covered here do exactly that:
None of these layers requires enterprise expertise or expensive software. They are all free, open-source, and well-supported by the home server community. Apply them in order, work through the audit checklist, and you will have a server that is genuinely secure against the threats you will realistically face.
The most important ongoing habit: keep software updated. A hardened system running outdated, vulnerable software is only as secure as its oldest unpatched component.
Optimization
Learn ZFS pool creation, RAID-Z levels, automated snapshots & ARC tuning for 8โ16GB systems. Real N100 performance data and a ZFS cheat sheet included.
Builds
Complete step-by-step Proxmox tutorial for beginners. From USB installer to running Pi-hole in an LXC container on an Intel N100 mini PC in 30 minutes.

Builds
Complete beginner's guide to building a home server in 2026. Hardware options from $0 (old laptop) to $200 (N100 mini PC), Ubuntu Server installation, Docker setup, and your first services โ Pi-hole, Vaultwarden, Jellyfin.
Use our Power Calculator to see how much you can save.
Try Power Calculator