Initial Ubuntu 24.04 Server Setup: Users, SSH, Firewall
Initial Ubuntu 24.04 Server Setup: Users, SSH, Firewall By the end of this guide your fresh Ubuntu 24.04 server will have a non-root user who can run admin commands, SSH locked down so only your key gets in (no root, no passwords), a firewall that drops everything except the ports you actually use, and automatic […]

Initial Ubuntu 24.04 Server Setup: Users, SSH, Firewall
By the end of this guide your fresh Ubuntu 24.04 server will have a non-root user who can run admin commands, SSH locked down so only your key gets in (no root, no passwords), a firewall that drops everything except the ports you actually use, and automatic security patches switched on. It takes about ten minutes, and every command below was run on a clean Ubuntu 24.04.4 server in our lab so you are copying commands that worked, not commands that should work.
The reason this matters is blunt: a server exposed to the public internet starts getting brute-forced within minutes of booting. The default root-and-password configuration is the single biggest thing standing between an attacker and your machine. The steps here close that gap in the right order, so you never lock yourself out doing it.
Ubuntu 24.04 also changed a few things that older tutorials — including the one that still ranks first for this search — get wrong. SSH is now socket-activated through systemd, the cloud images ship their own SSH config drop-ins, and the clean way to harden sshd is no longer to hand-edit the main config file. We cover all three.
Prerequisites
- A server running Ubuntu 24.04 LTS. On a Getting Started with Lineserve Cloud: Launch Your First VPS you get a clean install with a public IPv4 the moment it boots.
- Root access, or a user that can
sudo. Most providers hand you a root login (by password or key) on first boot. We assume you are logging in asrootfor the initial setup, then handing day-to-day work to a normal user. - An SSH key pair on your own laptop. If you do not have one yet, run
ssh-keygen -t ed25519locally and keep the private key safe. You will paste the public key onto the server.
Everything below was captured on a server reporting:
$ lsb_release -d
Description: Ubuntu 24.04.4 LTS
$ uname -r
6.17.0-1019-aws
Step 1: Log in and update the system
Connect as root using whatever method your provider gave you:
ssh root@your_server_ip
Before anything else, refresh the package index and install pending security updates. On a brand-new image there are usually a handful:
apt-get update
apt-get upgrade -y
This is the step the top-ranking guide skips entirely, and it matters. The base image was built weeks or months before you booted it; the upgrade pulls the current kernel, OpenSSH, and OpenSSL builds. Doing it first also means the versions you harden are the versions you keep. On our lab server the relevant packages settled at:
openssh-server 1:9.6p1-3ubuntu13.16
ufw 0.36.2-6
unattended-upgrades 2.9.1+nmu4ubuntu1
If apt-get upgrade pulls a new kernel, plan a reboot at a quiet moment — the running kernel is not swapped until you restart.
Step 2: Create a non-root user with sudo
Working as root all day is how a single fat-fingered command or a compromised session takes down the whole box. Create a normal user and give it administrative rights through sudo instead. Replace deploy with any name you like:
adduser deploy
adduser prompts for a password and a few optional details. Set a strong password even though we will disable password logins shortly — you still need it to confirm sudo actions. Then add the user to the sudo group, which is what grants admin rights on Ubuntu:
usermod -aG sudo deploy
Confirm the group membership. Note that on Ubuntu 24.04 adduser also drops the account into the users group automatically — that is expected, not something you did:
$ id deploy
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),27(sudo),100(users)
The 27(sudo) entry is the one that counts. That user can now run any command with sudo, prompted for its own password each time. That prompt is a feature — it means a hijacked shell cannot silently escalate to root.
Step 3: Set up SSH key access for the new user
Your new user needs to log in by key, because in the next step we switch passwords off entirely. Create the user’s .ssh directory, drop in your public key, and — this is the part people get wrong — set the permissions exactly. SSH refuses to use a key file that other users could read:
mkdir -p /home/deploy/.ssh
nano /home/deploy/.ssh/authorized_keys # paste your PUBLIC key, one line, then save
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys
chown -R deploy:deploy /home/deploy/.ssh
The chown at the end matters because you created these files as root. Without it, the directory belongs to root and deploy cannot read its own key. Verify the result looks exactly like this — 700 on the directory, 600 on the file, owned by deploy:
$ ls -ld /home/deploy/.ssh
drwx------ 2 deploy deploy 4096 Jul 12 14:58 /home/deploy/.ssh
$ ls -l /home/deploy/.ssh/authorized_keys
-rw------- 1 deploy deploy 96 Jul 12 14:58 /home/deploy/.ssh/authorized_keys
Now open a second terminal and test the login before you go any further: ssh deploy@your_server_ip. If that gets you a shell without asking for a password, key auth works. Keep your original root session open until you have confirmed this — it is your safety line if something goes wrong in the next step. If you want a deeper walk-through of generating and managing keys, see How to Access GitHub from a Linux Server Using SSH Keys (Step-by-Step Guide), which covers the same key mechanics for Git access.
Step 4: Harden the SSH daemon
This is where Ubuntu 24.04 differs from every older guide. Two changes matter.
First, do not edit /etc/ssh/sshd_config directly. The main file ends with a line that pulls in a whole directory of overrides:
$ grep Include /etc/ssh/sshd_config
Include /etc/ssh/sshd_config.d/*.conf
Anything in /etc/ssh/sshd_config.d/ wins over the defaults, and it survives package upgrades that might rewrite the main file. Cloud images already use this: our AWS Ubuntu image shipped a 60-cloudimg-settings.conf that had already set PasswordAuthentication no. The clean approach is to add your own drop-in with a high number so it loads last:
nano /etc/ssh/sshd_config.d/99-hardening.conf
Put these four lines in it:
# Managed hardening drop-in — overrides shipped defaults
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
PubkeyAuthentication yes
PermitRootLogin no stops anyone logging in as root over SSH. PasswordAuthentication no and KbdInteractiveAuthentication no together kill every password-based path, so brute-forcing becomes pointless — an attacker needs your private key, which never leaves your laptop. PubkeyAuthentication yes is the default, but stating it makes your intent explicit.
Always validate the syntax before restarting, so a typo cannot leave the daemon unable to start:
$ sshd -t
$ echo $?
0
An exit code of 0 and no output means the config is clean. Now apply it:
systemctl restart ssh
The second Ubuntu 24.04 change is why that restart behaves differently than you might expect. SSH is now socket-activated: systemd holds port 22 open and only spawns an sshd process when a connection actually arrives. You can see it in the service state — the old always-on service is disabled, and a socket unit is active instead:
$ systemctl is-enabled ssh.service
disabled
$ systemctl is-enabled ssh.socket
enabled
Practically, this has two consequences. Your hardening takes effect for the next connection automatically because a fresh sshd reads the config each time. And if you ever want to change the SSH port, editing sshd_config is not enough — you must change the ListenStream in the socket unit too:
$ systemctl cat ssh.socket | grep ListenStream
ListenStream=0.0.0.0:22
ListenStream=[::]:22
Our honest advice: leave the port on 22. Moving it to a random number stops noise in your logs but adds no real security against a targeted attacker, and on 24.04 it is now two files to keep in sync instead of one. Spend that effort on keys and the firewall instead.
Confirm the daemon is actually enforcing your settings with sshd -T, which prints the effective configuration after all drop-ins are merged:
$ sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'
permitrootlogin no
passwordauthentication no
pubkeyauthentication yes
That is the proof that root login and passwords are off. For the wider hardening checklist beyond SSH, our 8 Essential Security Steps for Every Production VPS guide covers the rest of the baseline.
Step 5: Turn on the firewall (UFW)
Ubuntu ships UFW (“Uncomplicated Firewall”), a friendly front-end for the kernel’s packet filter. It is installed but inactive out of the box — check for yourself:
$ ufw status verbose
Status: inactive
Here is the rule that keeps people out of their own servers: allow SSH before you enable the firewall. UFW’s default policy is to deny all incoming traffic. If you enable it first and add the SSH rule second, you drop your own connection the moment it turns on. UFW ships an application profile for OpenSSH, so you do not even need to remember the port:
ufw allow OpenSSH
If your server will run a website, open the web ports too. Only open what you actually need — every open port is attack surface:
ufw allow 80/tcp
ufw allow 443/tcp
Now enable it. Interactively, ufw enable warns that it “may disrupt existing ssh connections” and asks you to confirm; on a fresh server we pass --force to skip the prompt (the SSH rule we just added is what keeps you safe, not the prompt):
$ ufw --force enable
Firewall is active and enabled on system startup
Check the result. Note the default line — deny incoming, allow outgoing — which is exactly what you want. Outgoing stays open so the server can still fetch updates and reach out to services; only unsolicited inbound traffic is blocked:
$ ufw status verbose
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
New profiles: skip
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
80/tcp ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
22/tcp (OpenSSH (v6)) ALLOW IN Anywhere (v6)
80/tcp (v6) ALLOW IN Anywhere (v6)
443/tcp (v6) ALLOW IN Anywhere (v6)
The firewall also survives reboots — “enabled on system startup” means it comes back up with the machine.
Step 6: Confirm automatic security updates
Unpatched servers are how old vulnerabilities keep getting exploited long after a fix exists. The unattended-upgrades package installs security patches on its own. On our AWS image it was already installed and enabled, which you should confirm rather than assume:
$ systemctl is-enabled unattended-upgrades
enabled
The behaviour is controlled by /etc/apt/apt.conf.d/20auto-upgrades. If it is missing, or you want to be certain it is on, write it explicitly:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
You can prove it will actually do something with a dry run, which lists the update channels it watches without changing anything:
$ unattended-upgrade --dry-run --debug
Allowed origins are: o=Ubuntu,a=noble, o=Ubuntu,a=noble-security, ...
No packages found that can be upgraded unattended and no pending auto-removals
By default this only installs security updates, not feature updates, so it will not surprise you with breaking changes. It patches the things that get you hacked and leaves the rest for you to schedule.
Step 7: Set the hostname and timezone
Two small quality-of-life fixes. A clear hostname makes logs and your shell prompt readable across a fleet:
$ hostnamectl set-hostname web-01
$ hostnamectl --static
web-01
And set the timezone so log timestamps match your day. If you and your users are in East Africa, Africa/Nairobi puts everything on EAT (+0300) instead of UTC:
$ timedatectl set-timezone Africa/Nairobi
$ timedatectl | grep 'Time zone'
Time zone: Africa/Nairobi (EAT, +0300)
Use Africa/Dar_es_Salaam for Tanzania or Africa/Lagos (WAT, +0100) for Nigeria. Getting this right early means every log line, cron job, and backup timestamp reads in the time your team actually works in.
Verify it works
Run this quick round-up and check every line against what you expect — it is the whole security posture at a glance:
$ id deploy
uid=1001(deploy) gid=1001(deploy) groups=1001(deploy),27(sudo),100(users)
$ sshd -T | grep -E 'permitrootlogin|passwordauthentication|pubkeyauthentication'
permitrootlogin no
passwordauthentication no
pubkeyauthentication yes
$ ufw status verbose | head -3
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
$ systemctl is-enabled unattended-upgrades
enabled
The real acceptance test is behavioural: from your laptop, ssh root@your_server_ip should now be refused, and ssh deploy@your_server_ip should let you in by key with no password prompt. Confirm both before you close your last root session. Once that passes, your server is ready for whatever comes next — for example, Deploying a Node.js App to Production on a VPS: The Complete Guide picks up exactly where this leaves off.
Troubleshooting
These are the three failures we actually hit or reproduced on the lab server, with the real fix for each.
sshd -t fails with “Missing privilege separation directory: /run/sshd”. The validation command returns exit code 255 and refuses to check your config. This happens when /run/sshd — a runtime directory that normally exists after SSH has started once — is absent. The fix is one command:
mkdir -p /run/sshd
Then re-run sshd -t; it returns 0. The directory is recreated at every boot, so this is only ever a first-run hiccup.
You changed the SSH port but connections still hit (or miss) port 22. On Ubuntu 24.04 the port lives in the socket unit, not just sshd_config. Editing Port 2222 in a drop-in does nothing on its own because systemd — not sshd — owns the listening socket. Either run systemctl edit ssh.socket and override ListenStream, or, simpler, disable socket activation and go back to the classic always-on service with systemctl disable --now ssh.socket before setting the port the old way. For most servers, staying on 22 avoids the whole problem.
Locked out after enabling UFW. If you enabled the firewall before adding the SSH rule, your session is dropped and new connections hang. You cannot fix this over SSH — you need your provider’s out-of-band console. On a Getting Started with Lineserve Cloud: Launch Your First VPS that is the browser console in console.lineserve.net; log in there, run ufw allow OpenSSH, and SSH comes back immediately. The lesson holds: allow SSH first, enable second.
Your Ubuntu 24.04 server is now set up the way a production box should be from minute one — a real sudo user, key-only SSH with root and passwords disabled, a default-deny firewall, and automatic security patches. That is the baseline every service you run on top of it depends on.
If you are still choosing where to run it, our Linux VPS plans start at the S1 (1 vCPU, 2 GB RAM, 20 GB SSD, a dedicated IPv4) — the same 2 GB class of server we tested every command in this guide on — hosted in Nairobi, Dar es Salaam, or Lagos, billed locally in KES, TZS, or NGN with M-Pesa and mobile money, no forex required. Spin one up in a couple of minutes at console.lineserve.net and run through this guide on your own box.
