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:
The default jail.conf bans an IP after 5 failed attempts within 10
minutes, for 10 minutes. That's fine for a lab. Verify:
You'll see banned IPs accumulate within a few hours.
To tune (optional), create /etc/fail2ban/jail.local:
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:
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:
Then disable the iptables service to avoid dual-stack rule confusion:
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:
You want both APT::Periodic::Update-Package-Lists and
APT::Periodic::Unattended-Upgrade set to "1". If not:
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.