Ubuntu Server 24.04 LTS Install: The Minimal Setup That’s Actually Secure

Was this helpful?

You want a small Ubuntu Server install because bloat is a liability. You also want it secure because the internet is a tire fire and your server is the nearest pile of rubber.

The trap is thinking “minimal” automatically means “safe.” A tiny system with sloppy SSH, lax updates, and a disk layout you didn’t mean to create is still a tiny system waiting to be owned—or to page you at 03:17 because /var filled up and took journald with it.

What “minimal” should mean in production

Minimal is not “the smallest ISO.” Minimal is a set of deliberate constraints:

  • Small attack surface: fewer network-facing daemons, fewer parsers, fewer dependencies.
  • Predictable upgrades: security updates come in automatically; feature upgrades are planned.
  • Auditable state: you can explain why a package exists and what opened that port.
  • Recoverable storage: your disk layout can survive log spikes, core dumps, and human error.
  • Operational ergonomics: journald works, time sync works, DNS works, and you can diagnose issues without guessing.

“Secure” is not a checkbox either. It’s a posture: sane defaults, tight remote access, least privilege, timely patching, and enough observability to catch things before customers do.

One of my favorite production truths is a paraphrased idea often attributed to the SRE world: paraphrased idea: Hope is not a strategy; reliability comes from engineered feedback loops — attributed to the operations/reliability community around Google SRE thinking.

Joke #1: Security is like flossing: everyone agrees it’s good, and most people only do it right after something starts bleeding.

Facts and historical context you can use at 2 a.m.

Short, concrete context helps you make better choices under pressure:

  1. Ubuntu LTS cadence: LTS releases land every two years; operationally, that rhythm became a corporate standard because it matches budget cycles and change control.
  2. systemd didn’t “win” overnight: it replaced a patchwork of init scripts with a unified model (units, dependencies, watchdogs). That unification is why modern Linux diagnosis is so fast—if you learn the tools.
  3. OpenSSH’s defaults tightened over time: older cryptography got removed, not “deprecated forever.” If your ancient client breaks, it’s a clue, not Ubuntu being mean.
  4. UFW exists because humans need guardrails: iptables/nftables are powerful, but a simple policy model prevents the “I accidentally allowed the world to talk to Redis” incident.
  5. journald replaced scattershot logs: structured metadata (unit, PID, boot ID) is why “what happened since last reboot?” is one command now.
  6. ext4 became boring on purpose: it earned its reputation by failing predictably. Many production teams choose it not because it’s exciting, but because it doesn’t surprise them.
  7. LUKS became mainstream with laptops, then servers: disk encryption moved from “paranoid” to “compliance baseline” once breach reporting laws and cloud snapshots made data-at-rest real.
  8. TPM-backed secrets aren’t just for desktops: modern servers increasingly expect hardware roots of trust; Ubuntu’s tooling has followed that trend.
  9. Unattended security updates became normal after worm eras: operational culture shifted when “patch Tuesday” turned into “patch now or rebuild later.”

Installer decisions that you can’t easily undo

1) Pick the right ISO and install mode

Use the official Ubuntu Server 24.04 LTS installer. If you’re installing on physical hardware, prefer the standard server ISO. If you’re provisioning in cloud, your “installer” is mostly cloud-init plus image selection.

Minimal setup means: no GUI, no “helpful” random server roles at install time, no extra snaps unless you know why.

2) Network: DHCP first, static later (unless you’re a router)

If this server is not your network infrastructure, start with DHCP so you can finish the install and get remote access. Then set a static lease in DHCP or move to a static netplan config once you know the interface name and the right DNS.

The failure mode: you guess a static IP, fat-finger the gateway, and now your only management path is a crash cart. That’s not “secure,” it’s just lonely.

3) Users: create one admin user, no shared accounts

Create a single initial user with sudo. Do not enable direct root SSH login. Treat shared “admin” accounts as an audit failure waiting to happen.

4) SSH: keys only, and do it early

If the installer offers to import SSH keys (for example, from a hosted identity), do it. If not, you’ll add your public key right after first boot. Password login over SSH is a liability you don’t need.

5) Updates: security updates automatic, reboots deliberate

Enable security updates automatically. You’re not proving toughness by manually approving every OpenSSL CVE fix. You are just creating a backlog of risk.

6) Storage: decide your blast radius

Your storage choices determine how the machine fails. A single root filesystem is simple until logs spike, containers expand, or someone drops a backup in /var/tmp.

For minimal-but-secure, you’re aiming for:

  • A clean EFI setup (on modern hardware).
  • A separate /boot only if you need it (e.g., some encrypted-root setups).
  • Encrypted root if the threat model includes stolen disks, decommissioned drives, remote hands, or compliance.
  • Separate space for /var or at least sensible log retention. If you run containers, treat /var/lib as its own growth zone.

Storage layout: boring choices, fewer outages

Let’s be blunt: storage is where “minimal” most often becomes “fragile.” Your CPU will forgive you. Your disk will not.

Recommended baseline layouts

Option A (simple, strong default): ext4 + LUKS on a single disk

Good for: general servers, VMs, single-disk nodes, anything you want to recover easily.

  • EFI System Partition (ESP): ~512 MiB
  • /boot (optional): 1 GiB (unencrypted if needed)
  • LUKS container holding LVM or plain ext4 for /

Why: ext4 is stable, LUKS gives you at-rest protection, and recovery doesn’t require a PhD.

Option B (growth control): ext4 + separate /var

Good for: anything with logs, databases, containers, CI runners.

  • / (root): modest size (e.g., 20–40 GiB)
  • /var: larger, because it will grow
  • /home: small or none (servers shouldn’t be personal storage)

Why: root stays healthy even when /var gets messy. When /var fills, you can still log in and fix it.

Option C (specialized): ZFS root

Good for: teams that already know ZFS operationally. Not good for “I heard ZFS is cool.” ZFS is a system, not a filesystem.

If you choose ZFS, commit to monitoring ARC pressure, scrub schedules, and bootloader interactions. If you don’t want that homework, use ext4 and move on.

Mount options that reduce damage

Some mount options are cheap wins:

  • nodev on partitions that shouldn’t contain device files (often /home, /var/tmp).
  • nosuid where you don’t want setuid binaries to work (often /tmp, /var/tmp).
  • noexec can help on /tmp, but beware: it can break installers and tooling. Use it intentionally.

These don’t “make you secure” alone. They reduce the blast radius of a bad day.

Post-install baseline: accounts, SSH, firewall, updates

Packages: install less, remove more

Start with what the server image gives you. Add only what your job needs. If you need a tool for a one-time rescue, install it, use it, and consider removing it afterward. A production server is not your laptop.

SSH hardening: key auth, minimal surface, strong defaults

Target state:

  • Key-based authentication
  • No root login via SSH
  • Restricted users or groups allowed to SSH
  • Reasonable idle timeouts
  • Logging that’s useful without being noisy

Firewall: default deny inbound, allow only what you serve

If this is a server, it should not accept inbound traffic unless you explicitly intend it. That’s the whole point of a firewall.

Automatic security updates

Enable unattended upgrades for security patches. Then decide how to handle reboots. For many fleets: automatic security updates plus scheduled reboot windows is the sweet spot.

Time sync: because logs without time are fan fiction

systemd-timesyncd (or chrony if you prefer) ensures your TLS, logs, and incident timelines aren’t garbage.

Joke #2: Nothing ages you faster than debugging “random” failures that were actually time drift.

Practical tasks (commands, outputs, decisions)

Below are real tasks you will perform on a fresh Ubuntu Server 24.04 install. Each includes: the command, what typical output means, and the decision you make next. Run them as your admin user with sudo where needed.

Task 1: Confirm what you actually installed

cr0x@server:~$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04 LTS
Release:        24.04
Codename:       noble

What it means: You’re on the right LTS release and codename. No guessing, no “I thought I installed 24.04.”

Decision: If this is not 24.04 LTS, stop and fix your image pipeline before you harden the wrong thing.

Task 2: Verify kernel and boot mode (EFI vs legacy)

cr0x@server:~$ uname -r
6.8.0-31-generic
cr0x@server:~$ test -d /sys/firmware/efi && echo "UEFI boot" || echo "Legacy boot"
UEFI boot

What it means: Kernel line confirms what you’re running. UEFI presence matters for disk partitioning and bootloader behavior.

Decision: If you expected UEFI but got legacy, fix firmware settings now. Mixed environments cause weird boot recovery later.

Task 3: See your disks, partitions, and filesystems clearly

cr0x@server:~$ lsblk -e7 -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINTS,MODEL
NAME        SIZE TYPE FSTYPE      MOUNTPOINTS MODEL
nvme0n1   476.9G disk                       Samsung SSD 980
├─nvme0n1p1  512M part vfat        /boot/efi
├─nvme0n1p2    1G part ext4        /boot
└─nvme0n1p3 475.4G part crypto_LUKS          
cr0x@server:~$ sudo cryptsetup status luks-root
/dev/mapper/luks-root is active and is in use.
  type:    LUKS2
  cipher:  aes-xts-plain64
  keysize: 512 bits
  device:  /dev/nvme0n1p3
  sector size:  512
  offset:  32768 sectors
  size:    997638144 sectors
  mode:    read/write

What it means: You can see the actual device map. LUKS2 active indicates encryption is working.

Decision: If encryption was required and you don’t see crypto_LUKS, you’re out of compliance. Reinstall or migrate correctly; don’t “fix later.”

Task 4: Check filesystem usage before it becomes a surprise

cr0x@server:~$ df -hT
Filesystem     Type   Size  Used Avail Use% Mounted on
/dev/mapper/vg0-root ext4    40G  3.2G   35G   9% /
/dev/nvme0n1p2 ext4  1007M  220M  720M  24% /boot
/dev/nvme0n1p1 vfat   511M  6.1M  505M   2% /boot/efi
tmpfs          tmpfs  3.1G     0  3.1G   0% /run/user/1000

What it means: You’re verifying that root isn’t tiny and that /boot has room for kernel updates.

Decision: If /boot is under ~300–400 MiB free, you’ll eventually fail kernel upgrades. Fix partition sizing or prune old kernels intentionally.

Task 5: Confirm what is listening on the network

cr0x@server:~$ sudo ss -tulpen
Netid State  Recv-Q Send-Q Local Address:Port  Peer Address:Port Process
tcp   LISTEN 0      4096   0.0.0.0:22         0.0.0.0:*     users:(("sshd",pid=1020,fd=3)) uid:0 ino:27360 sk:3 cgroup:/system.slice/ssh.service

What it means: Only SSH is listening. That’s a clean baseline.

Decision: If you see unexpected listeners (database, web server, RPC services), uninstall/disable them or justify them. “I didn’t know it was running” is not a defense.

Task 6: Establish firewall defaults (UFW)

cr0x@server:~$ sudo ufw status verbose
Status: inactive
cr0x@server:~$ sudo ufw default deny incoming
Default incoming policy changed to 'deny'
(be sure to update your rules accordingly)
cr0x@server:~$ sudo ufw default allow outgoing
Default outgoing policy changed to 'allow'
(be sure to update your rules accordingly)
cr0x@server:~$ sudo ufw allow 22/tcp
Rule added
Rule added (v6)
cr0x@server:~$ sudo ufw enable
Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
Firewall is active and enabled on system startup
cr0x@server:~$ sudo ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip

To                         Action      From
22/tcp                     ALLOW IN    Anywhere
22/tcp (v6)                ALLOW IN    Anywhere (v6)

What it means: You’ve locked inbound traffic down to SSH only.

Decision: Before enabling UFW remotely, ensure SSH is allowed and you have console access if you mess up. Production rule: never firewall yourself off a box you can’t physically reach.

Task 7: Harden SSH configuration and validate it safely

cr0x@server:~$ sudo cp -a /etc/ssh/sshd_config /etc/ssh/sshd_config.bak
cr0x@server:~$ sudoedit /etc/ssh/sshd_config

Example minimal hardening lines to set (adjust usernames/groups):

cr0x@server:~$ sudo grep -nE '^(PermitRootLogin|PasswordAuthentication|KbdInteractiveAuthentication|AllowUsers|AllowGroups|X11Forwarding|ClientAliveInterval|ClientAliveCountMax)' /etc/ssh/sshd_config
33:PermitRootLogin no
57:PasswordAuthentication no
58:KbdInteractiveAuthentication no
89:X11Forwarding no
101:ClientAliveInterval 300
102:ClientAliveCountMax 2
cr0x@server:~$ sudo sshd -t
cr0x@server:~$ sudo systemctl reload ssh

What it means: sshd -t returning no output is success. Reload applies changes without dropping existing sessions.

Decision: If sshd -t prints errors, do not reload. Fix the config first. Syntax errors are a classic self-own.

Task 8: Confirm SSH authentication methods from logs

cr0x@server:~$ sudo journalctl -u ssh -n 20 --no-pager
Aug 01 10:12:44 server sshd[1442]: Accepted publickey for cr0x from 10.0.0.50 port 50192 ssh2: ED25519 SHA256:Qm...
Aug 01 10:13:02 server sshd[1461]: Connection closed by authenticating user ubuntu 10.0.0.51 port 55322 [preauth]

What it means: You see successful key-based logins. You can also spot suspicious attempts or unexpected usernames.

Decision: If you still see password-based acceptance, your SSH config isn’t applied or you’re editing the wrong file. Fix it now.

Task 9: Enable unattended security upgrades and verify behavior

cr0x@server:~$ sudo apt update
Hit:1 http://archive.ubuntu.com/ubuntu noble InRelease
Hit:2 http://security.ubuntu.com/ubuntu noble-security InRelease
Reading package lists... Done
Building dependency tree... Done
All packages are up to date.
cr0x@server:~$ sudo apt install -y unattended-upgrades
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  unattended-upgrades
0 upgraded, 1 newly installed, 0 to remove and 0 not upgraded.
cr0x@server:~$ sudo systemctl status unattended-upgrades --no-pager
● unattended-upgrades.service - Unattended Upgrades Shutdown
     Loaded: loaded (/usr/lib/systemd/system/unattended-upgrades.service; enabled)
     Active: inactive (dead)

What it means: The service runs on timers and shutdown hooks; “inactive” can be normal.

Decision: Ensure the timer is enabled, and confirm only security repos are selected if you want minimal change.

Task 10: Check apt timers and last run status

cr0x@server:~$ systemctl list-timers --all | grep -E 'apt|unattended'
Fri 2026-02-06 06:12:21 UTC  10h left   Thu 2026-02-05 06:13:02 UTC  13h ago  apt-daily.timer             apt-daily.service
Fri 2026-02-06 06:45:10 UTC  10h left   Thu 2026-02-05 06:45:22 UTC  13h ago  apt-daily-upgrade.timer     apt-daily-upgrade.service
cr0x@server:~$ sudo tail -n 30 /var/log/unattended-upgrades/unattended-upgrades.log
2026-02-05 06:45:23,121 INFO Starting unattended upgrades script
2026-02-05 06:45:23,210 INFO No packages found that can be upgraded unattended

What it means: The timers are scheduled and ran. Logs show if upgrades occurred.

Decision: If timers are missing, you may be on a stripped image or timers disabled by policy—fix that intentionally, not accidentally.

Task 11: Confirm time sync status

cr0x@server:~$ timedatectl
               Local time: Thu 2026-02-05 20:10:17 UTC
           Universal time: Thu 2026-02-05 20:10:17 UTC
                 RTC time: Thu 2026-02-05 20:10:17
                Time zone: Etc/UTC (UTC, +0000)
System clock synchronized: yes
              NTP service: active
          RTC in local TZ: no

What it means: Clock is synchronized and NTP is active. UTC is a sane server default.

Decision: If sync is “no,” fix time before debugging anything else. TLS and cluster systems behave badly with skew.

Task 12: Check DNS and routing quickly (network sanity)

cr0x@server:~$ ip -br a
lo               UNKNOWN        127.0.0.1/8 ::1/128
enp1s0           UP             10.0.10.25/24 fe80::a00:27ff:fe4e:66a1/64
cr0x@server:~$ ip r
default via 10.0.10.1 dev enp1s0
10.0.10.0/24 dev enp1s0 proto kernel scope link src 10.0.10.25
cr0x@server:~$ resolvectl status
Global
       Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported
resolv.conf mode: stub
Current DNS Server: 10.0.10.2
       DNS Servers: 10.0.10.2 10.0.10.3

What it means: Interface is up, default route exists, DNS servers are set.

Decision: If DNS is wrong, fix netplan or DHCP options; don’t “work around it” with random /etc/hosts hacks unless it’s a deliberate service mapping.

Task 13: Confirm what packages are installed (spot accidental bloat)

cr0x@server:~$ apt list --installed | head -n 15
Listing... Done
adduser/noble,now 3.137ubuntu1 all [installed]
apt/noble-updates,now 2.7.14 amd64 [installed]
base-files/noble,now 13ubuntu10 amd64 [installed]
bash/noble,now 5.2.21-2ubuntu4 amd64 [installed]
bsdutils/noble,now 1:2.39.3-9ubuntu6 amd64 [installed]

What it means: You can quickly see the package universe you’re maintaining.

Decision: If you see server roles you didn’t intend (database, mail stack), remove them or rebuild. Hidden daemons become “mystery ports.”

Task 14: Confirm AppArmor is active

cr0x@server:~$ sudo aa-status
apparmor module is loaded.
45 profiles are loaded.
45 profiles are in enforce mode.
0 profiles are in complain mode.
0 processes are unconfined but have a profile defined.

What it means: Mandatory access control is present and enforcing for known profiles.

Decision: If AppArmor is disabled, you’re choosing a weaker baseline. Re-enable unless you have a proven incompatibility and a mitigation plan.

Task 15: Verify user privileges and remove risky defaults

cr0x@server:~$ id
uid=1000(cr0x) gid=1000(cr0x) groups=1000(cr0x),27(sudo)
cr0x@server:~$ sudo -l
Matching Defaults entries for cr0x on server:
    env_reset, mail_badpass, secure_path=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

User cr0x may run the following commands on server:
    (ALL : ALL) ALL

What it means: Your admin user can escalate, and sudo policy is default.

Decision: For corporate environments, prefer least privilege via sudoers group policies. For a single-purpose server, keep admins limited and traceable.

Task 16: Spot disk health issues before they become “random crashes”

cr0x@server:~$ sudo apt install -y smartmontools
Reading package lists... Done
Building dependency tree... Done
The following NEW packages will be installed:
  smartmontools
cr0x@server:~$ sudo smartctl -a /dev/nvme0n1 | head -n 20
smartctl 7.4 2023-08-01 r5530 [x86_64-linux-6.8.0-31-generic] (local build)
=== START OF INFORMATION SECTION ===
Model Number:                       Samsung SSD 980
Serial Number:                      S64...
Firmware Version:                   2B4QFXO7
NVMe Version:                       1.4
Total NVM Capacity:                 500,107,862,016 [500 GB]

What it means: You can confirm the device, firmware, and later check error counters.

Decision: If smart data shows media errors or critical warnings, replace the disk. Don’t argue with physics.

Fast diagnosis playbook (find the bottleneck fast)

This is the “server feels slow” playbook that works in real operations. The goal is to identify the limiting resource in minutes, not to collect trivia.

First: confirm it’s not networking or DNS

  • Check routes and link: ip -br a, ip r
  • Check DNS: resolvectl status
  • Check socket backlog/listeners: ss -s, ss -tulpen

If DNS is broken, everything looks slow. Package installs, TLS handshakes, API calls—pain everywhere.

Second: find the hot process and what it’s waiting on

  • Top-level view: top or htop (if installed)
  • System-wide pressure: uptime (load average), vmstat 1
  • Per-process IO: pidstat -d 1 (from sysstat) if you need it

Look for: CPU pegged (compute), high load with low CPU (often IO wait), memory pressure (swap), or a thundering herd of processes.

Third: determine if storage is the choke point

  • IO wait and block devices: iostat -xz 1 (sysstat)
  • Filesystems full or near full: df -hT
  • Disk errors: dmesg -T | tail, journalctl -k -n 200

Classic sign: load average is high, CPU idle is decent, and response times are awful. That’s often storage latency.

Fourth: confirm memory isn’t being quietly crushed

  • Memory overview: free -h
  • OOM evidence: journalctl -k | grep -i oom

If the kernel is killing processes, treat that as a capacity or leak issue, not “random crashes.”

Fifth: check systemd for failing units and flapping services

  • Failed services: systemctl --failed
  • Recent errors: journalctl -p err..alert -b

Flapping daemons can create load, log storms, port exhaustion, and false symptoms. systemd will tell you if you ask directly.

Common mistakes: symptoms → root cause → fix

1) “I enabled UFW and got locked out”

Symptoms: SSH drops, reconnect fails, console still works.

Root cause: Firewall enabled before allowing SSH, or you allowed the wrong interface/port.

Fix: From console: sudo ufw disable. Then re-apply in the right order: allow SSH first, confirm, then enable. In environments with nonstandard SSH ports, explicitly allow that port and verify with ss -tulpen before enabling.

2) “Kernel upgrades fail with /boot full”

Symptoms: apt upgrade errors, DKMS failures, repeated old kernel packages.

Root cause: /boot partition too small or old kernels not cleaned.

Fix: Remove old kernels carefully: dpkg -l 'linux-image-*', then purge old versions (not the running kernel). Longer-term: allocate a larger /boot or avoid a separate /boot unless needed.

3) “SSH keys don’t work; password login still prompts”

Symptoms: Key auth fails, server asks for password, logs show Failed publickey.

Root cause: Wrong permissions on ~/.ssh or authorized_keys, wrong user, or edited a non-active sshd config include.

Fix: Ensure ~/.ssh is 700, authorized_keys is 600, owned by the user. Validate config with sshd -T and sshd -t. Then reload ssh.

4) “Apt updates randomly hang”

Symptoms: apt update stalls, especially on DNS lookups or mirror access.

Root cause: Broken DNS, captive proxy, or IPv6 path issues.

Fix: Check resolvectl status and routing. Test name resolution with getent hosts archive.ubuntu.com. If IPv6 is broken, fix network properly instead of disabling IPv6 blindly (disabling can break modern environments too).

5) “Disk fills overnight and the machine acts haunted”

Symptoms: Services fail to start, SSH slow, logs stop, journald complains.

Root cause: /var growth (logs, container layers, crash dumps) on a single root filesystem with no quotas or separation.

Fix: Identify offenders (du -xh --max-depth=1 /var). Add log retention, separate /var next rebuild, and consider filesystem reserved space tuning. If you run containers, treat /var/lib as a dedicated capacity domain.

6) “The box is slow, but CPU is idle”

Symptoms: Load average high, CPU idle significant, response times poor.

Root cause: Storage latency, IO wait, or a process stuck on IO (e.g., synchronous fsync from a database).

Fix: Use iostat -xz 1 and check disk utilization and await. Look for errors in journalctl -k. If it’s a VM, check host storage and noisy neighbors too.

7) “Unattended upgrades broke my service”

Symptoms: Service fails after nightly upgrades; compatibility issues.

Root cause: You allowed more than security updates (or the security update included a behavior change).

Fix: Pin unattended upgrades to security pockets only, and stage updates in a canary environment. If a service is extremely sensitive, manage that package with explicit change control—but keep the rest patched.

Three corporate mini-stories from the trenches

Mini-story 1: The incident caused by a wrong assumption

The company had a “minimal Ubuntu” standard image. It booted fast, ran one application, and passed the security scan. Everyone congratulated themselves and moved on, which is how you know the next page is coming.

A team deployed a new batch of servers and enabled full-disk encryption. They assumed the installer’s layout would keep root safe. They didn’t separate /var, didn’t set journald limits, and didn’t validate log volume assumptions. The application had a bug that increased log verbosity under a transient error condition. You can guess what happened next.

Within hours, disks filled. Once / was full, the failure cascaded: journald couldn’t write, services couldn’t create temp files, package managers couldn’t run, and even SSH logins became erratic because authentication writes to disk too. The on-call engineer had the classic experience of trying to fix a system that couldn’t record the fact it was broken.

The postmortem wasn’t about the app bug; those happen. The real wrong assumption was “minimal image means minimal risk.” What they needed was a layout that isolates growth (separate /var) and a logging policy that treats disk as a finite resource, not a suggestion.

The fix was boring: partitioning guidance, log limits, and a pre-deploy test that intentionally spams logs to prove the server stays recoverable. That test is now the reason their “minimal” image deserves the name.

Mini-story 2: The optimization that backfired

A performance-minded group decided that “encryption is overhead” and removed LUKS from a fleet of servers running on fast NVMe. They measured a benchmark that improved slightly. The slide deck was convincing. It always is.

Months later, a decommission workflow sent drives to a third-party recycler. One batch didn’t get properly wiped; a process gap, not malicious intent. But intent doesn’t matter when a drive with customer data leaves the building. The incident response was brutal: legal, compliance, customer communications, and the kind of internal meetings where every sentence becomes evidence.

Technically, the outcome wasn’t a hacker breach. It was worse in a different way: it was preventable. Encryption would have turned that event into a non-event, assuming keys were managed correctly and not taped to the chassis.

The backfire wasn’t just “we lost security.” It was operational drag. They had to implement emergency wiping controls, re-audit asset handling, and retrofit encryption in an environment now full of data. Retrofitting at scale is always harder than doing it at install.

The lesson landed: optimize where it matters and where you can prove safety. If the optimization saves 2% and creates a compliance cliff, it’s not an optimization. It’s a future incident with better marketing.

Mini-story 3: The boring but correct practice that saved the day

Another org had an unglamorous policy: every server build must pass a “listen test” and a “patch test.” The listen test is simply: list all listening ports and explain each one. The patch test is: prove security updates are enabled and scheduled, and show the last successful run.

People rolled their eyes at it. It sounded like busywork. It also took about three minutes when automated properly, which is less time than arguing about it.

One day a new base image accidentally included a telemetry agent that opened a local web UI bound to 0.0.0.0. No one intended to expose it; it was just packaged that way. In many environments, that would have reached production and waited for a scanner to find it.

The listen test failed immediately. The engineer could point to ss -tulpen, identify the process, and block the release. They removed the agent, rebuilt the image, and moved on. No incident. No PR statement. No late-night patch scramble.

That’s the quiet win: a dull habit that catches sharp problems. The best outages are the ones you never have to write up.

Checklists / step-by-step plan

Phase 0: Before you boot the installer (10 minutes that prevent regret)

  • Decide: is disk encryption required? If yes, plan for key entry/recovery in your environment (console, remote KVM, or TPM integration if applicable).
  • Decide: do you need separate /var (recommended for most servers)? If yes, size it based on logs + application data + container growth.
  • Decide: how will you access the server if networking is broken? IPMI/iDRAC/iLO, virtual console, or physical access.
  • Prepare SSH keys for the initial admin user.
  • Write down: hostname, intended IP strategy (DHCP/static), DNS servers, NTP expectations.

Phase 1: Installer choices (keep it minimal, keep it recoverable)

  1. Install Ubuntu Server 24.04 LTS (no GUI).
  2. Create one admin user (sudo enabled).
  3. Import or add SSH key support during install if offered.
  4. Select storage:
    • Use LUKS if required.
    • Prefer ext4 unless you have a ZFS operating model already.
    • Strongly consider separate /var for servers with any write-heavy workload.
  5. Skip extra server roles unless this is a dedicated purpose install and you know exactly why you need them.

Phase 2: First boot baseline (the secure minimum)

  1. Update package lists: sudo apt update.
  2. Install security automation: sudo apt install unattended-upgrades; confirm timers.
  3. Harden SSH: disable password auth, disable root login, reload ssh, confirm key login works.
  4. Enable UFW with default deny inbound; allow only required ports.
  5. Confirm time sync: timedatectl shows synchronized.
  6. Confirm listeners: sudo ss -tulpen is exactly what you intended.
  7. Record state: capture outputs of lsblk, df -hT, systemctl --failed.

Phase 3: Hardening without self-sabotage

  • Apply mount options for /tmp and /var/tmp thoughtfully. Test your software; don’t break installers silently.
  • Limit journald retention so logs don’t eat disks: cap by size and time based on your environment.
  • Use systemd service hardening for your app units (NoNewPrivileges, ProtectSystem, PrivateTmp). Do it incrementally and test.
  • Decide reboot policy: scheduled maintenance window or automated reboots when required. Either way, be explicit.

Phase 4: Operability (because “secure” and “diagnosable” are friends)

  • Set up basic monitoring hooks: disk usage, SMART/NVMe health, failed systemd units, update status.
  • Ensure logs are accessible remotely (centralized logging) if you run more than one server.
  • Run a failure drill: fill /var (safely, in staging), verify the server remains recoverable and alerts fire.

FAQ

Do I really need a firewall if only SSH is listening?

Yes. “Only SSH is listening” is a snapshot, not a guarantee. The firewall is the safety net for future package installs, accidental services, and human mistakes.

Is disabling SSH password authentication always the right move?

For internet-reachable servers: yes, almost always. For isolated lab networks: still recommended. If you must keep passwords (compliance or legacy), enforce MFA via a supported method and rate-limit aggressively. But recognize you’re paying in risk.

Should I change the SSH port?

It reduces drive-by noise, not real attackers. If you do it, do it for log hygiene, not as “security.” Real security is keys, access control, and patching.

Do I need Fail2ban?

Not as a first-line requirement if you’ve disabled password auth and restricted SSH users. It can still be useful for reducing noise and slowing brute force attempts, but it’s not a substitute for correct authentication policy.

Is full-disk encryption worth it on servers?

Yes when the threat model includes lost drives, remote hands, colocation risk, decommission errors, or snapshots leaving your control. The operational cost is key management and boot-time unlocking. Plan that part, don’t improvise it during an incident.

ext4 or XFS for a minimal secure setup?

ext4 is the conservative default and easy to recover. XFS is fine for large filesystems and certain workloads, but ext4 wins the “boring and predictable” contest for general servers.

How do I keep logs from filling the disk?

Use two levers: journald limits and logrotate (for traditional logs). Also isolate growth: separate /var or at least monitor it. If you run chatty apps, fix the app logging too.

Should I install a bunch of troubleshooting tools upfront?

No. Install a minimal baseline plus what you need for your operational model (often: smartmontools, maybe sysstat). Everything else can be installed temporarily. Every extra package is extra code you patch forever.

What’s the minimum set of “proof” I should capture after install?

At minimum: OS version, disk layout, filesystem usage, listening ports, firewall rules, update timers status, time sync status, and failed systemd units. Those prove both security posture and operability.

How do I know if my system is secure enough?

Ask it concrete questions: Can anyone password-brute-force SSH? Are inbound ports restricted? Are security updates automatic? Is AppArmor enforcing? Can disk fill take down the whole machine? If you can answer those with commands and configs, you’re doing real security.

Next steps you should actually do

Here’s the practical close-out list—the part that separates “installed” from “production-ready”:

  1. Lock SSH down and test it from a second session before you disconnect.
  2. Turn on UFW with default deny inbound and explicit allows only.
  3. Enable unattended security updates and confirm timers ran at least once.
  4. Verify storage layout with lsblk and df -hT; ensure /boot and /var won’t surprise you.
  5. Set journald retention to a sane cap for your disk size and logging needs.
  6. Document the “listen list”: what ports are open and why. Treat any unknown listener as a release blocker.
  7. Run the fast diagnosis playbook once on a healthy system so you know what “normal” looks like.
  8. Decide your reboot strategy before the first kernel security update forces the discussion at the worst possible moment.

Minimal is a discipline. Secure is a habit. Ubuntu Server 24.04 LTS gives you good primitives—but you still have to drive the car.

← Previous
Docker Desktop vs Docker in WSL: Which One Is Actually Faster?
Next →
Proxmox: The ‘Ballooning’ Setting That Creates Fake Memory Pressure

Leave a comment