Building “The Shire”: A Secure Jump Server for my Home Lab
If you follow me on LinkedIn you know that I started to do a Homelab Makeover series and since every great adventure begins in The Shire, and every secure home network should start with a properly hardened jump server.
As part of my home lab overhaul project, I’ve created “The Shire” a dedicated jump server that serves as the entry point into my network from the outside world.
In this guide, I’ll walk through setting up a jump server similar to mine, using Ansible for automation and implementing multiple layers of security.
While not every home lab needs this level of protection, the peace of mind that comes with knowing your network is properly secured is worth the effort I think.
Why Use a Jump Server?
A jump server acts as a secure, controlled gateway between external networks and your internal systems.
By forcing all remote access through this single hardened entry point, you:
- Create a security chokepoint that’s easier to monitor.
- Reduce the attack surface of your internal network.
- Establish clear separation between public-facing and internal services.
- Create a simple path for remote access to multiple internal resources.
For those with extensive IoT devices, network appliances, or self-hosted services, a jump server offers an additional layer of protection against the constant port scanning and automated attacks that target home networks.
Hardware Considerations
For “The Shire,” I retired my trusty Raspberry Pi 3 that was previously running Pi-Hole and Cloudflared, and upgraded to a more capable device.
While a Pi can certainly handle jump server duties, I wanted something with:
- Better CPU performance for OpenVPN encryption/decryption
- More RAM for potential future services
- Faster network throughput
- Greater reliability
Any reasonably modern small form factor PC, NUC, UpBoard, or even a virtualized instance on more powerful hardware can serve this purpose well.
The key is that it should be dedicated to this gateway function and not run unnecessary services.
Prerequisites
If you want to follow along, you’ll need:
- A Linux-based server (I’m using the latest Ubuntu Server)
- SSH access to your server
- Ansible installed on your local machine
- A static IP address (or DHCP reservation) for your jump server.
- Port forwarding configured on your router for SSH and OpenVPN to the Server
Initial Server Setup
Before running the Ansible playbook, I performed some basic setup on my Ubuntu server:
- Installed Ubuntu Server with a minimal installation profile.
- Created a non-root user with sudo privileges.
- Updated the system with
sudo apt update && sudo apt upgrade -y - Installed Python 3 for Ansible compatibility:
sudo apt install python3 -y - Generated an SSH key pair on my local machine and copied the public key to the server
Ansible Playbook for Jump Server Configuration
Rather than manually configuring everything, I’ve created an Ansible playbook that automates the hardening process. This ensures that if I ever need to rebuild the server, I can do so consistently and quickly.
Here’s the full playbook I’m using:
---
- name: Jump Server Configuration Playbook
hosts: "<server name or ip>"
become: yes
vars:
ssh_port: 8888
openvpn_port: 1194
openvpn_protocol: udp
admin_email: <email address>
tasks:
- name: Update package cache
apt:
update_cache: yes
cache_valid_time: 3600
- name: Install required packages
apt:
name:
- fail2ban
- aide
- ufw
- unattended-upgrades
- logwatch
- chkrootkit
state: present
# SSH Hardening
- name: SSH configuration
lineinfile:
dest: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
with_items:
- { regexp: '^#?Port', line: 'Port {{ ssh_port }}' }
- { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' }
- { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' }
- { regexp: '^#?UsePAM', line: 'UsePAM no' }
notify: Restart SSH
# Fail2ban Configuration
- name: Configure fail2ban
copy:
dest: /etc/fail2ban/jail.local
content: |
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
[sshd]
enabled = true
port = {{ ssh_port }}
filter = sshd
logpath = /var/log/auth.log
maxretry = 3
notify: Restart fail2ban
# UFW Configuration
- name: Set default UFW policies
ufw:
state: enabled
policy: deny
direction: incoming
notify: Reload UFW
- name: Allow SSH through UFW
ufw:
rule: allow
port: "{{ ssh_port }}"
proto: tcp
notify: Reload UFW
- name: Allow OpenVPN through UFW
ufw:
rule: allow
port: "{{ openvpn_port }}"
proto: "{{ openvpn_protocol }}"
notify: Reload UFW
# AIDE Configuration
- name: Initialize AIDE database
command: aideinit
args:
creates: /var/lib/aide/aide.db.new
ignore_errors: yes
- name: Copy AIDE initial database
command: cp /var/lib/aide/aide.db.new /var/lib/aide/aide.db
args:
creates: /var/lib/aide/aide.db
ignore_errors: yes
# Unattended Upgrades Configuration
- name: Configure unattended-upgrades
blockinfile:
path: /etc/apt/apt.conf.d/50unattended-upgrades
block: |
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}";
"${distro_id}:${distro_codename}-security";
"${distro_id}:${distro_codename}-updates";
};
Unattended-Upgrade::Package-Blacklist {};
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
Unattended-Upgrade::MinimalSteps "true";
Unattended-Upgrade::InstallOnShutdown "false";
Unattended-Upgrade::Mail "{{ admin_email }}";
Unattended-Upgrade::MailOnlyOnError "false";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
- name: Enable unattended-upgrades
copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
# Logwatch Configuration
- name: Configure logwatch
copy:
dest: /etc/cron.daily/00logwatch
mode: 0755
content: |
#!/bin/bash
/usr/sbin/logwatch --output mail --mailto {{ admin_email }} --detail high
# Download OpenVPN Installer Script
- name: Download OpenVPN installer script
get_url:
url: https://git.io/vpn
dest: /root/openvpn-install.sh
mode: 0700
# Note: The OpenVPN installation requires user interaction
# You'll need to run this manually or create an expect script
- name: Notify about OpenVPN manual installation
debug:
msg: "OpenVPN installer script downloaded to /root/openvpn-install.sh. Run it manually to complete setup."
handlers:
- name: Restart SSH
service:
name: sshd
state: restarted
- name: Restart fail2ban
service:
name: fail2ban
state: restarted
- name: Reload UFW
ufw:
state: reloaded
Breaking Down the Playbook
Let’s examine what this playbook does, section by section:
Basic Security Packages
The playbook starts by installing essential security packages:
- fail2ban: Protects against brute force attacks by temporarily banning IPs after failed login attempts
- aide: Advanced Intrusion Detection Environment, monitors file integrity
- ufw: Uncomplicated Firewall, for network filtering
- unattended-upgrades: Automatically installs security updates
- logwatch: Analyzes logs and emails summaries
- chkrootkit: Tool to scan for rootkits
SSH Hardening
SSH is the primary method for administrative access, so it’s crucial to harden it:
- Moving SSH to a non-standard port (8888) reduces automated attacks targeting port 22
- Disabling password authentication forces the use of SSH keys, which are much more secure
- Disabling root login prevents direct root access
- Disabling X11 forwarding prevents potential GUI-based attacks
- Disabling PAM simplifies authentication and removes potential vulnerabilities
Fail2Ban Configuration
Fail2Ban adds an extra layer of protection against brute force attempts by temporarily banning IP addresses after multiple failed authentication attempts. The configuration:
- Sets a ban time of 1 hour (3600 seconds)
- Considers failed attempts within a 10-minute window (600 seconds)
- Bans an IP after 3 failed attempts
- Specifically monitors SSH on our custom port
Firewall Configuration
The UFW firewall is configured with a default deny policy, blocking all incoming traffic except:
- SSH on our custom port (8888)
- OpenVPN on UDP port 1194
This minimizes the attack surface by only allowing the necessary services.
System Integrity Monitoring
AIDE creates a database of file checksums and attributes, which can be used to detect unauthorized changes to system files. The playbook:
- Initializes the AIDE database
- Copies the initial database to the active location
Unattended Upgrades
Security updates are critical, and the playbook configures automatic installation to minimize the window of exposure to known vulnerabilities. It also:
- Sets up email notifications for updates
- Configures automatic cleanup of unused dependencies
- Disables automatic reboots (you can enable this if desired)
Log Monitoring
Logwatch is configured to send daily log summaries via email, providing visibility into system activities and potential security issues. The configuration:
- Sets a high detail level for comprehensive monitoring
- Emails the reports to the specified admin email address
OpenVPN Setup
Rather than automating the entire OpenVPN installation (which requires interactive input), the playbook downloads the installation script to the server, making it easy to complete the setup manually.
How to Use the Playbook
- Copy the YAML above to a file named
jumpbox-ansible.yaml - Update the
hostsandvarssections with your server IP, SSH port, OpenVPN port, and email address - Create an inventory file named
inventory.iniin the same directory:
[jump_server]
192.168.1.1 ansible_port=22 ansible_user=your_server_username
[all:vars]
ansible_python_interpreter=/usr/bin/python3
- Run the playbook:
ansible-playbook -i inventory.ini jumpbox-ansible.yaml --ask-become-pass
- When prompted, enter your sudo password
Completing OpenVPN Setup
After the playbook runs, you’ll need to manually complete the OpenVPN installation:
- SSH into your server (now on port 8888):
ssh -p 8888 yourusername@server-ip - Run the OpenVPN installation script:
sudo bash /root/openvpn-install.sh - Follow the prompts to complete the installation
- Transfer the generated
.ovpnfile to your devices for VPN access
Adding Dynamic DNS (Optional)
If your home IP address changes frequently, you might want to set up dynamic DNS updates. While not included in the Ansible playbook, you can add this capability by:
- Creating a simple script to update your DNS provider (Cloudflare, No-IP, DuckDNS, etc.)
- Setting up a systemd timer to run the script periodically
Here’s a basic example for updating Cloudflare DNS using a simple bash script and curl:
#!/bin/bash
# Cloudflare API credentials
AUTH_EMAIL="your-email@example.com"
AUTH_KEY="your-global-api-key"
ZONE_ID="your-zone-id"
RECORD_ID="your-dns-record-id"
RECORD_NAME="vpn.yourdomain.com"
# Get the current public IP
IP=$(curl -s https://api.ipify.org)
# Update the DNS record
curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
-H "X-Auth-Email: $AUTH_EMAIL" \
-H "X-Auth-Key: $AUTH_KEY" \
-H "Content-Type: application/json" \
--data "{\"type\":\"A\",\"name\":\"$RECORD_NAME\",\"content\":\"$IP\",\"ttl\":120,\"proxied\":false}"
Save this as /usr/local/bin/update-cloudflare-dns.sh, make it executable with chmod +x, and then create a systemd timer to run it hourly for example.
Testing Your Jump Server
To ensure everything is working properly:
- Test SSH access: From outside your network, attempt to SSH to your public IP on the custom port
- Test VPN access: Connect using your OpenVPN client
- Test internal access: Once connected to the VPN, verify you can access internal resources
- Verify security: Use a tool like nmap to scan your public IP and ensure only the configured ports are open
Bonus: Making “The Shire” Even More Secure
Beyond what’s in the playbook, consider these additional security measures:
2FA for SSH
You can add two-factor authentication to SSH for an additional layer of security:
sudo apt install libpam-google-authenticator
google-authenticator # Follow the prompts
Edit /etc/pam.d/sshd to add:
auth required pam_google_authenticator.so
Edit /etc/ssh/sshd_config to enable challenge-response authentication:
ChallengeResponseAuthentication yes
AuthenticationMethods publickey,keyboard-interactive
This configuration requires both an SSH key and a one-time password for authentication.
IP Allowlisting
If you access your network from a limited set of known IPs, you can configure UFW to only allow connections from those IPs:
sudo ufw allow from 1.2.3.4/24 to any port 8888 proto tcp
Next Steps
With “The Shire” established as my secure gateway, I’ve laid a solid foundation for my home lab makeover. In the next part of this series, I’ll explore setting up “The Tower of Sauron” – our monitoring and metrics server using Grafana, Prometheus, and Loki.
Conclusion
A properly configured jump server significantly improves your home network’s security posture. By centralizing access through a single, hardened point, you gain better control and visibility while reducing your attack surface.
“The Shire” may seem humble in the grand scheme of my Middle-earth project, but like its namesake, it serves as the perfect starting point for greater adventures. In our case, these adventures will be more secure and better managed than the organic sprawl of networks past.
This guide is part of my Middle-earth Home Lab series, documenting my journey from digital sprawl to properly architected infrastructure. Stay tuned for more installations!