Ideas, experiments, and ongoing thoughts...

Building "The Shire": A Secure Jump Server for Your Home Lab

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:

  1. Installed Ubuntu Server with a minimal installation profile.
  2. Created a non-root user with sudo privileges.
  3. Updated the system with sudo apt update && sudo apt upgrade -y
  4. Installed Python 3 for Ansible compatibility: sudo apt install python3 -y
  5. 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

  1. Copy the YAML above to a file named jumpbox-ansible.yaml
  2. Update the hosts and vars sections with your server IP, SSH port, OpenVPN port, and email address
  3. Create an inventory file named inventory.ini in 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
  1. Run the playbook:
ansible-playbook -i inventory.ini jumpbox-ansible.yaml --ask-become-pass
  1. When prompted, enter your sudo password

Completing OpenVPN Setup

After the playbook runs, you’ll need to manually complete the OpenVPN installation:

  1. SSH into your server (now on port 8888): ssh -p 8888 yourusername@server-ip
  2. Run the OpenVPN installation script: sudo bash /root/openvpn-install.sh
  3. Follow the prompts to complete the installation
  4. Transfer the generated .ovpn file 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:

  1. Creating a simple script to update your DNS provider (Cloudflare, No-IP, DuckDNS, etc.)
  2. 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:

  1. Test SSH access: From outside your network, attempt to SSH to your public IP on the custom port
  2. Test VPN access: Connect using your OpenVPN client
  3. Test internal access: Once connected to the VPN, verify you can access internal resources
  4. 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!

← Back to Articles