Skip to content

SSH and first hardening

Do this on every instance before you expose any service publicly. It's 15 minutes per instance and it's the difference between "a lab server" and "a lab server that stays a lab server."

The threat model

Your instances have public IPs and port 22 open to 0.0.0.0/0. That means:

  • Continuous SSH brute-force attempts. Global scanners test every public IP on port 22, 24/7. Your logs will show thousands per day.
  • Occasional targeted probes. If you accidentally leak your IP or a hostname in a public commit, expect a small increase.
  • Nothing sophisticated unless you're a specific target. For a home lab, defense-in-depth against the noise is the goal, not paranoid hardening against a nation-state.

SSH keys only, no password auth

The Ubuntu image already ships with password auth disabled and root SSH disabled. Verify:

sudo grep -E '^(PasswordAuthentication|PermitRootLogin|ChallengeResponseAuthentication)' /etc/ssh/sshd_config

# Expected:
# PasswordAuthentication no
# PermitRootLogin no

If any is set differently, fix it in /etc/ssh/sshd_config.d/50-cloud-init.conf (respect the include chain) and sudo systemctl restart ssh.

SSH client hardening (on your local machine)

Add an entry to your ~/.ssh/config per instance:

Host web-1
    HostName <public-ip>
    User ubuntu
    IdentityFile ~/.ssh/oci_lab
    IdentitiesOnly yes
    ServerAliveInterval 60

Host web-2
    HostName <public-ip>
    User ubuntu
    IdentityFile ~/.ssh/oci_lab
    IdentitiesOnly yes
    ServerAliveInterval 60

Host arm-1
    HostName <public-ip>
    User ubuntu
    IdentityFile ~/.ssh/oci_lab
    IdentitiesOnly yes
    ServerAliveInterval 60

Now ssh web-1 just works. The IdentitiesOnly yes prevents your SSH agent from spraying every key at the server — if you have many keys, without this you can hit Too many authentication failures before the right key is even tried.

Install fail2ban

fail2ban bans IPs that fail SSH auth repeatedly. On each instance:

sudo apt install -y fail2ban
sudo systemctl enable --now fail2ban

The default jail.conf bans an IP after 5 failed attempts within 10 minutes, for 10 minutes. That's fine for a lab. Verify:

sudo fail2ban-client status sshd

You'll see banned IPs accumulate within a few hours.

To tune (optional), create /etc/fail2ban/jail.local:

[DEFAULT]
bantime = 24h
findtime = 10m
maxretry = 3
ignoreip = 127.0.0.1/8 ::1 <your-home-ip>/32

Then sudo systemctl restart fail2ban.

nftables

Ubuntu 24.04 uses nftables as the kernel firewall framework. The image ships with iptables compatibility rules — perfectly fine to use as-is, but if you want a clean nftables config that you understand every byte of:

sudo apt install -y nftables

Then create /etc/nftables.conf:

#!/usr/sbin/nft -f

flush ruleset

table inet filter {
    chain input {
        type filter hook input priority filter; policy drop;

        # Loopback
        iif lo accept

        # Established / related
        ct state established,related accept

        # ICMP (ping) — useful for debugging
        ip protocol icmp accept
        ip6 nexthdr icmpv6 accept

        # SSH
        tcp dport 22 accept

        # HTTP / HTTPS
        tcp dport { 80, 443 } accept
    }

    chain forward {
        type filter hook forward priority filter; policy drop;
    }

    chain output {
        type filter hook output priority filter; policy accept;
    }
}

Enable and start:

sudo systemctl enable --now nftables
sudo nft list ruleset   # verify

Then disable the iptables service to avoid dual-stack rule confusion:

sudo systemctl disable --now netfilter-persistent 2>/dev/null || true

Test in another SSH session before you drop your active one

Whenever you edit firewall rules, keep your current SSH session open, open a second SSH session in another terminal, verify it works, then rely on it. If you lock yourself out, the OCI web console has a serial console (Instance → Console connection) that will save you, but it's slow.

Unattended-upgrades

Ubuntu 24.04 ships this pre-installed and pre-enabled. Verify:

sudo systemctl status unattended-upgrades
sudo cat /etc/apt/apt.conf.d/20auto-upgrades

You want both APT::Periodic::Update-Package-Lists and APT::Periodic::Unattended-Upgrade set to "1". If not:

sudo dpkg-reconfigure -plow unattended-upgrades

Security updates now install nightly with no action from you. Reboots when a new kernel is available happen automatically at 06:00 local time (see /etc/apt/apt.conf.d/50unattended-upgrades for the Unattended-Upgrade::Automatic-Reboot setting).

Reduce log noise

You'll get very verbose auth.log from failed SSH attempts. That's fine functionally but noisy. Optional:

sudo sed -i 's/^LogLevel INFO/LogLevel VERBOSE/' /etc/ssh/sshd_config
# Or leave it and let fail2ban handle it. VERBOSE mostly helps if you
# ever need to forensically investigate.

Verify from outside

From your local machine:

# Should succeed (key auth)
ssh web-1

# Should fail (password auth disabled)
ssh -o PubkeyAuthentication=no web-1

# Should be reachable
curl -v http://<web-1-ip>

The last one will return a connection error because nothing's listening on port 80 yet — that's expected. What you're verifying is that the OCI security list + your nftables rules aren't blocking the packet.

Repeat for all three instances

Do this on web-1, web-2, and arm-1. It takes about 10 minutes per instance. Skip at your peril.

Next

Head to Caddy and TLS to serve your first HTTPS URL.