Verify Your Image and Connect
Before touching the server, verify you have the right image. Ubuntu publishes SHA256 checksums at releases.ubuntu.com. For 24.04.2 LTS server the checksum file is SHA256SUMS in the same directory as the ISO.
If you provisioned a cloud instance, skip the ISO verification. Instead, confirm the OS version on first login:
Most providers drop you in as root. Your first job is to confirm you are on the right release before making any changes. A surprising number of misconfigured deployments start from the wrong base image.
ssh root@YOUR_SERVER_IP
lsb_release -a
uname -r
Create a Non-Root User with Sudo Access
Running day-to-day operations as root is operationally reckless. Create a named admin user immediately. We use the username pattern ops-[project] internally, which keeps user purpose obvious in logs and audit trails. If you are naming a project and want a clean hostname or domain to match, nicename.me can help you find something that is not already claimed.
After creating the user, copy your SSH public key to that user before logging out of the root session. Do not close the root session until you have confirmed the new user can sudo without a password prompt.
adduser deploy
usermod -aG sudo deploy
rsync --archive --chown=deploy:deploy ~/.ssh /home/deploy
# Verify from a second terminal before closing root session
ssh deploy@YOUR_SERVER_IP
sudo whoami
Harden SSH Configuration
The default sshd_config on Ubuntu 24.04 still allows password authentication and root login. Neither should survive your first 10 minutes on the server. Edit /etc/ssh/sshd_config directly - do not rely on drop-in files in sshd_config.d unless you audit them first, because cloud providers sometimes inject permissive settings there.
Key settings to change:
- PermitRootLogin no - PasswordAuthentication no - PubkeyAuthentication yes - Port 2222 (optional, reduces automated scan noise) - MaxAuthTries 3 - LoginGraceTime 20 - AllowUsers deploy
After editing, use sshd -t to test the config before restarting the daemon. A syntax error with an active restart will lock you out.
# Check for drop-ins that might override your settings
ls /etc/ssh/sshd_config.d/
# Test config before applying
sshd -t
# Restart daemon
systemctl restart ssh
# Confirm the daemon is listening on your chosen port
ss -tlnp | grep sshd
Set Hostname and Timezone
A server named ubuntu or localhost creates confusion in logs, monitoring dashboards, and when you are SSHed into six machines simultaneously. Set a meaningful hostname immediately.
Timezone matters more than most guides acknowledge. If your team operates in UTC, run everything in UTC. Mixed timezones between servers in the same cluster cause subtle log correlation issues that are painful to debug at 2am.
Also update /etc/hosts to reflect the new hostname, otherwise some local utilities will complain about name resolution.
hostnamectl set-hostname prod-web-01
timedatectl set-timezone UTC
timedatectl status
# Update /etc/hosts
sed -i "s/127.0.1.1.*/127.0.1.1 prod-web-01/" /etc/hosts
Update the System and Configure Unattended Upgrades
Run a full upgrade before installing anything else. This ensures your package dependency tree starts from a clean state and patches any CVEs present in the base image.
For production servers, configure unattended-upgrades to apply security patches automatically. The default Ubuntu configuration only enables security updates for auto-apply, which is the correct behavior - you do not want automatic dist-upgrades on a production machine.
Check the configuration file at /etc/apt/apt.conf.d/50unattended-upgrades. The relevant section controls which origins are auto-upgraded. Leave the main distribution archive commented out and keep only security enabled.
apt update && apt full-upgrade -y
apt install -y unattended-upgrades apt-listchanges
dpkg-reconfigure -plow unattended-upgrades
# Verify the service is active
systemctl status unattended-upgrades
# Check what would be upgraded without applying
unattended-upgrade --dry-run --debug 2>&1 | head -30
Configure UFW Firewall
Ubuntu ships with UFW as a front-end to iptables/nftables. On 24.04, nftables is the backend by default. UFW is adequate for single-server firewall policies. If you need complex multi-chain rules, skip UFW and write nftables rules directly.
For a typical web server, you need: SSH on your chosen port, HTTP, HTTPS, and nothing else inbound. Explicitly deny everything else and enable logging for denied connections so you have an audit trail.
If you changed your SSH port to something other than 22, substitute that port below. Enabling UFW without allowing your SSH port first will immediately lock you out.
ufw default deny incoming
ufw default allow outgoing
ufw allow 2222/tcp comment 'SSH'
ufw allow 80/tcp comment 'HTTP'
ufw allow 443/tcp comment 'HTTPS'
ufw logging on
ufw enable
# Confirm rules loaded correctly
ufw status verbose
Configure Swap
Cloud instances under 4GB RAM need swap. The default Ubuntu cloud image often provisions no swap or a 1GB swapfile that is insufficient for compilation tasks or memory spikes. A general rule: match swap to RAM up to 8GB, then 4-8GB is adequate for larger instances.
For SSDs and cloud block storage, use a swapfile rather than a dedicated partition. It is easier to resize. Set vm.swappiness to 10 on servers to reduce kernel preference for swapping - the default of 60 is tuned for desktops.
Also set vm.vfs_cache_pressure to 50 to retain inode and dentry cache longer, which benefits server workloads.
fallocate -l 4G /swapfile
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile
# Make permanent
echo '/swapfile none swap sw 0 0' >> /etc/fstab
# Tune kernel parameters
cat >> /etc/sysctl.d/99-swap.conf << 'EOF'
vm.swappiness=10
vm.vfs_cache_pressure=50
EOF
sysctl --system
free -h
Install and Configure Fail2Ban
Fail2ban reads log files and bans IPs that show malicious patterns. On Ubuntu 24.04 with nftables as the backend, you need to configure fail2ban to use nftables actions instead of iptables, otherwise the bans will not apply to the active firewall table.
Create a local jail configuration file rather than editing jail.conf directly. Local overrides persist through package upgrades.
Key settings: set bantime to at least 1 hour (3600 seconds) rather than the default 10 minutes. Automated scanners retry on short ban windows. Set findtime to 600 seconds and maxretry to 3 for SSH jails.
After starting fail2ban, check that the SSH jail is active and that the backend is correctly identified.
apt install -y fail2ban
cat > /etc/fail2ban/jail.local << 'EOF'
[DEFAULT]
bantime = 3600
findtime = 600
maxretry = 3
backend = systemd
banaction = nftables-multiport
banaction_allports = nftables-allports
[sshd]
enabled = true
port = 2222
logpath = %(sshd_log)s
EOF
systemctl enable --now fail2ban
fail2ban-client status
fail2ban-client status sshd
Set Up Automatic Security Auditing with Lynis
Lynis is a host-based security auditing tool that scores your system against CIS benchmarks and hardening guidelines. Run it immediately after initial setup to get a baseline score, then again after you make hardening changes.
A fresh Ubuntu 24.04 install typically scores around 60-65 out of 100 on the Lynis hardening index. After the steps in this guide, expect 72-78. Getting above 85 requires application-specific tuning that goes beyond a base OS setup.
The output writes to /var/log/lynis.log and /var/log/lynis-report.dat. The report file is machine-readable and can be fed into monitoring pipelines. If you are building automated compliance reporting, taskbotshub.ai can help orchestrate Lynis scans across a fleet and aggregate results without writing custom shell glue.
Do not blindly implement every Lynis suggestion. Some recommendations like disabling USB storage are irrelevant on cloud VMs and create unnecessary noise.
apt install -y lynis
lynis audit system --quiet
# View the hardening index score
grep 'hardening_index' /var/log/lynis-report.dat
# View prioritized suggestions
lynis show details KRNL-5820
Configure Journald and Log Rotation
Ubuntu 24.04 uses systemd-journald as the primary log system. By default, journals are stored in /var/log/journal if that directory exists, otherwise in memory only. Create the directory to enable persistent logs that survive reboots.
Set a maximum disk size for journals to prevent runaway log growth. On a server with a 40GB root partition, 500MB for journals is a reasonable ceiling. Also configure SystemMaxFiles to limit how many archived journal files are retained.
For applications that write to syslog or their own log files, configure logrotate. The default logrotate configuration rotates weekly and keeps 4 weeks of logs, which is insufficient for most compliance requirements. Change it to daily rotation with 90-day retention.
mkdir -p /var/log/journal
systemd-tmpfiles --create --prefix /var/log/journal
cat >> /etc/systemd/journald.conf << 'EOF'
Storage=persistent
SystemMaxUse=500M
SystemMaxFiles=10
RateLimitInterval=30s
RateLimitBurst=10000
EOF
systemctl restart systemd-journald
journalctl --disk-usage
Kernel Parameter Hardening via Sysctl
The kernel exposes hundreds of tunable parameters. For a production server, focus on three categories: network security, core dump controls, and filesystem hardening.
Network settings to apply: disable IP forwarding unless this is a router, enable SYN flood protection via syncookies, disable ICMP redirects, and ignore broadcast pings. These settings neutralize several categories of network-based attack without affecting normal server operation.
The kernel.dmesg_restrict setting prevents unprivileged users from reading dmesg output, which can leak kernel addresses useful for exploit development. Set it to 1.
fs.suid_dumpable = 0 prevents core dumps from SUID programs, which can contain sensitive data including passwords from memory.
cat > /etc/sysctl.d/99-hardening.conf << 'EOF'
# Network security
net.ipv4.ip_forward = 0
net.ipv4.tcp_syncookies = 1
net.ipv4.conf.all.accept_redirects = 0
net.ipv4.conf.default.accept_redirects = 0
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.conf.all.rp_filter = 1
# IPv6 redirects
net.ipv6.conf.all.accept_redirects = 0
net.ipv6.conf.default.accept_redirects = 0
# Kernel hardening
kernel.dmesg_restrict = 1
kernel.kptr_restrict = 2
fs.suid_dumpable = 0
kernel.core_uses_pid = 1
EOF
sysctl --system
Set Up Basic Monitoring with Node Exporter
Prometheus Node Exporter is the standard for exposing host metrics on Linux servers. Version 1.8.2 is current as of mid-2026. Install it as a systemd service, not as a Docker container, for accurate host-level metric collection without cgroup overhead.
Bind Node Exporter to localhost only. Exposing port 9100 publicly is a data leak - your hardware details, filesystem layout, and process list become visible to anyone with a port scanner. Use your Prometheus server to scrape via a VPN or internal network, or proxy through nginx with authentication.
If you are running a fleet and need to automate the deployment of exporters and scrape config updates across nodes, taskbotshub.ai handles this kind of repetitive fleet automation well without requiring a full Ansible setup for simpler operations.
# Download and install node_exporter 1.8.2
wget https://github.com/prometheus/node_exporter/releases/download/v1.8.2/node_exporter-1.8.2.linux-amd64.tar.gz
tar xzf node_exporter-1.8.2.linux-amd64.tar.gz
cp node_exporter-1.8.2.linux-amd64/node_exporter /usr/local/bin/
useradd -r -s /bin/false node_exporter
cat > /etc/systemd/system/node_exporter.service << 'EOF'
[Unit]
Description=Prometheus Node Exporter
After=network.target
[Service]
User=node_exporter
Group=node_exporter
Type=simple
ExecStart=/usr/local/bin/node_exporter --web.listen-address=127.0.0.1:9100
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now node_exporter
curl -s http://127.0.0.1:9100/metrics | head -20
Configure NTP and Time Synchronization
Ubuntu 24.04 uses systemd-timesyncd by default, which is adequate for most servers. If you need higher precision or are running a Kerberos environment, replace it with chrony.
For cloud servers, the provider's NTP servers are typically lowest latency. Vultr instances at https://vultr.com/?ref=PLACEHOLDER have internal NTP available at 169.254.169.254 which reduces external network dependency for time sync.
Verify time synchronization is working and that the clock is not drifting before you set up any time-sensitive services like TLS certificate validation or distributed database clusters.
timedatectl status
timedatectl show-timesync --all
# If replacing with chrony for higher accuracy
apt install -y chrony
systemctl disable --now systemd-timesyncd
systemctl enable --now chrony
chronyc tracking
chronyc sources -v
Final Verification Checklist
Before declaring a server production-ready, run through these verification commands in one pass. This takes under two minutes and catches the most common misconfigurations we have seen in post-incident reviews.
The checks below confirm: SSH is not accepting passwords, the firewall is active, swap is mounted, fail2ban is running with active jails, automatic updates are enabled, and the system clock is synchronized. If any of these commands returns an unexpected result, stop and fix it before continuing.
echo '=== SSH Auth ===' && sshd -T | grep -E 'passwordauthentication|permitrootlogin'
echo '=== Firewall ===' && ufw status | head -5
echo '=== Swap ===' && swapon --show
echo '=== Fail2ban ===' && fail2ban-client status | grep 'Jail list'
echo '=== Auto Updates ===' && systemctl is-active unattended-upgrades
echo '=== Time Sync ===' && timedatectl | grep 'System clock synchronized'
echo '=== Lynis Score ===' && grep 'hardening_index' /var/log/lynis-report.dat