Hardening a Linux Server
This activity puts into practice the concepts from the System Security and Hardening lecture. You will apply the Linux hardening checklist to a real server: configure SSH key authentication, close unnecessary ports with a default-deny firewall, install and configure Fail2Ban, and create a least-privilege deploy user. By the end, you will have a server whose attack surface is substantially smaller than the default Ubuntu configuration, and you will be able to describe what each change closes and why it matters.
What You Will Need
Section titled “What You Will Need”- An EC2 instance running Ubuntu 26.04 that you can SSH into
- Your SSH keypair (e.g.,
cs312.pem) that you used to launch the instance - A terminal on your local machine with SSH access to the instance
- The instance’s public IP address
Part 1: Inspect the Current State
Section titled “Part 1: Inspect the Current State”Before hardening anything, document what the server looks like right now. The goal is to establish a baseline: what is listening, who can log in, and what the SSH configuration currently allows. You will compare against this at the end.
-
Connect to your instance:
Connect to Your Instance ssh ubuntu@YOUR_INSTANCE_IPYou should land at a prompt showing your username and hostname, something like
ubuntu@ip-172-31-4-23:~$. Note both; you will use the hostname later. -
Check what services are listening on the network:
Check What Services Are Listening on the Network sudo ss -tlnpssis the modern replacement fornetstat. The flags mean:-tTCP only,-llistening sockets only,-nnumeric addresses (no DNS lookups),-pshow the process name. The-pflag requiressudobecause process ownership information is restricted to root; without it, the Process column will be empty. You will see at minimumsshdon port 22. On a fresh instance you may also seesystemd-resolvedon127.0.0.53(local DNS) and nothing else. Make a note of every entry and its port. -
Check the current SSH configuration:
Inspect SSH Configuration sudo sshd -T | grep -E "passwordauthentication|permitrootlogin|allowusers|pubkeyauthentication"sshd -Tprints the effective sshd configuration after all includes and defaults are resolved. This is more reliable than readingsshd_configdirectly because it shows the actual values in use. On an Ubuntu cloud image on EC2 you will typically seepasswordauthentication noandpermitrootlogin prohibit-password. You will see only three lines, not four:allowusersdoes not appear when noAllowUsersdirective is configured, because the absence means all accounts are implicitly permitted. You will add that directive in Part 2 and see it show up then. Note the three values you do see; you will change at least two of them. -
List human login accounts:
List Human Login Accounts grep -v nologin /etc/passwd | grep -v false | grep -v syncThe
-vflag inverts the match: instead of showing lines that contain the pattern, it shows lines that do not contain it. Each pipe removes another category of system accounts: accounts whose shell is/usr/sbin/nologincannot open an interactive session; those with/bin/falseas the shell immediately exit on login;syncis a special utility account. What remains are accounts with a real shell that a person could actually log in with. On a fresh Ubuntu instance you will typically seerootandubuntu. Note the list. -
Check recent login history and failures:
Check Recent Login History and Failures journalctl -u ssh -n 20 --no-pagerThis shows the 20 most recent entries from the SSH service journal. Look for lines containing
Accepted(successful logins) andFailedorInvalid user(failed attempts). On a new instance you may already see automated scan attempts if the instance has been up for more than a few hours. -
Check the AppArmor profiles already in place:
Check the AppArmor Profiles Already in Place sudo aa-statusaa-statusreports whether AppArmor is loaded and which profiles are in two modes: enforce mode blocks operations the profile does not allow; complain mode only logs policy violations without blocking them. The exact profile list depends on which packages are installed on your image, so do not worry if your output differs from someone else’s. The important observation is that AppArmor is active and already confining at least some software on the system.
Part 2: Set Up SSH Key Authentication
Section titled “Part 2: Set Up SSH Key Authentication”AWS EC2 instances use key pairs for the initial login, which means you already have a key. This section ensures key-based auth is configured correctly and adds a dedicated deploy user whose permissions you will control explicitly.
-
Verify your key-based login is working. Before making any changes, open a second terminal and confirm you can SSH in again with your current key:
Verify Your Key-based Login Is Working ssh -i ~/.ssh/your-key.pem ubuntu@YOUR_INSTANCE_IPKeep this second session open for the rest of Part 2. If a configuration change locks you out, you have a recovery session already in place.
-
Create a dedicated deploy user and grant it full passwordless sudo:
Create a Dedicated Deploy User and Grant It Full sudo adduser --disabled-password --gecos "" deployecho "deploy ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/deployadduser --disabled-passwordcreates the account, home directory, and default shell without prompting for a password. The--gecos ""flag skips the name and contact fields. The second command writes an initial sudoers rule that givesdeployfull passwordless sudo, matching what theubuntuuser has. You will replace this broad rule with a scoped one in Part 5; for now,deployneeds to be able to run any sudo command to complete the firewall and Fail2Ban steps whileubuntuis locked out. -
Add your public key to the deploy user’s authorized keys:
Add Your Public Key to the Deploy User's Authorized Keys sudo mkdir -p /home/deploy/.sshsudo cp /home/ubuntu/.ssh/authorized_keys /home/deploy/.ssh/authorized_keyssudo chown -R deploy:deploy /home/deploy/.sshsudo chmod 700 /home/deploy/.sshsudo chmod 600 /home/deploy/.ssh/authorized_keysauthorized_keysis the file sshd reads to determine which public keys are permitted to authenticate for an account. Each line is one public key. When you connect with SSH, the server checks whether your private key matches any public key in this file; if it does, authentication succeeds without a password. You are copying the same public key that already lets you log in asubuntuto thedeployaccount. The700and600permissions are required by sshd: if the.sshdirectory orauthorized_keysfile is writable or broadly readable, sshd may reject it for security reasons. -
Test the deploy login from your second terminal:
Test the Deploy Login from Your Second Terminal ssh -i ~/.ssh/your-key.pem deploy@YOUR_INSTANCE_IPYou should land at a
deploy@hostnameprompt. If this does not work, do not proceed to the next step; the sshd_config changes below will restrict logins. -
Harden
sshd_config:Harden Sshdconfig sudo vim /etc/ssh/sshd_configFind or add these four directives. If a line exists with a different value, change it. If it is commented out with
#, uncomment and set it:Password Authentication Setting PasswordAuthentication noPermitRootLogin noPubkeyAuthentication yesAllowUsers deploySave and exit.
-
Restart the SSH service and observe the lockout:
Restart the SSH Service and Observe the Lockout sudo systemctl restart sshsudo sshd -T | grep -E "passwordauthentication|permitrootlogin|allowusers|pubkeyauthentication"The output should now show
allowusers deploy. On Ubuntu, the systemd unit is namedssheven though the daemon binary issshdand the configuration file issshd_config. From your second terminal, try connecting as ubuntu:Connect with SSH ssh -i ~/.ssh/your-key.pem ubuntu@YOUR_INSTANCE_IPThe connection will be rejected. Check the journal from your deploy session to see the rejection logged:
View System Logs sudo journalctl -u ssh -n 5 --no-pagerYou will see a line noting that ubuntu was not permitted.
Part 3: Configure the Firewall
Section titled “Part 3: Configure the Firewall”With SSH secured, you will configure UFW (Uncomplicated Firewall) to default-deny all inbound traffic and explicitly allow only the ports this server needs.
-
Set the default policies:
Set the Default Policies sudo ufw default deny incomingsudo ufw default allow outgoingThis does not activate the firewall yet. You are staging the rules first so there is no moment where the firewall is active but SSH is not yet allowed.
-
Allow SSH with rate limiting:
Allow SSH with Rate Limiting sudo ufw limit 22/tcpThe
limitrule allows up to six connection attempts from a single IP within 30 seconds, then blocks that IP temporarily. It is a lighter version of Fail2Ban that works at the network level rather than the application log level. -
Allow web traffic (add only the ports your server actually uses):
Allow HTTP and HTTPS sudo ufw allow 80/tcpsudo ufw allow 443/tcpPort 80 is HTTP (unencrypted web traffic) and port 443 is HTTPS (TLS-encrypted web traffic). If your server does not serve web traffic, skip these. Only open ports for services that are actually running.
-
Enable the firewall and verify:
Enable the Firewall and Verify sudo ufw enablesudo ufw status verboseThe output should show:
Part 3: Configure the Firewall Output Status: activeLogging: on (low)Default: deny (incoming), allow (outgoing), disabled (routed)New profiles: skipTo Action From-- ------ ----22/tcp LIMIT IN Anywhere80/tcp ALLOW IN Anywhere443/tcp ALLOW IN Anywhere22/tcp (v6) LIMIT IN Anywhere (v6)80/tcp (v6) ALLOW IN Anywhere (v6)443/tcp (v6) ALLOW IN Anywhere (v6)If you skipped ports 80 and 443, you will see only the SSH
LIMIT INrules. In either case, if your SSH session stays active, the firewall is correctly configured. -
Confirm the port state from inside the server:
Confirm the Port State from Inside the Server sudo ss -tlnpCompare this output to the list you made in Part 1. Any service listening on
0.0.0.0or::should have a corresponding UFW allow rule. A service bound only to127.0.0.1or::1is local-only and does not need an inbound allow rule.
Part 4: Install and Configure Fail2Ban
Section titled “Part 4: Install and Configure Fail2Ban”UFW’s rate-limiting rule blocks raw connection attempts. Fail2Ban goes a step further: it reads application logs, identifies failed authentication patterns, and dynamically bans the source IP via a firewall rule.
-
Install Fail2Ban:
Install Fail2ban sudo apt updatesudo apt install -y fail2ban -
Create a local configuration file:
Create a Local Configuration File sudo vim /etc/fail2ban/jail.localAdd the following:
Fail2Ban SSH Jail [sshd]enabled = trueport = sshfilter = sshdbackend = systemdmaxretry = 5findtime = 600bantime = 3600banaction = ufwSave and exit. This configures the
sshdjail: if the same IP fails authentication five times within 600 seconds (10 minutes), it is banned for 3600 seconds (one hour) via a UFW block rule. Thebackend = systemddirective tells Fail2Ban to read from the systemd journal rather than from/var/log/auth.logdirectly, which is the correct source on modern Ubuntu. -
Enable and start Fail2Ban:
/var/log/auth.log sudo systemctl enable --now fail2banenableregisters the service to start at every boot.--nowstarts it immediately. Withoutenable, the service would run this session but not survive a reboot. -
Verify the jail is active:
Verify the Jail Is Active sudo fail2ban-client status sshdYou should see output like:
Part 4: Install and Configure Fail2ban Output Status for the jail: sshd|- Filter| |- Currently failed: 0| |- Total failed: 3| `- Journal matches: _SYSTEMD_UNIT=sshd.service + _COMM=sshd`- Actions|- Currently banned: 0|- Total banned: 0`- Banned IP list:The exact counts will vary. On a quiet new instance they may all be zero; on a public IP you may already see failures from background scans. Check whether the ubuntu rejection from Part 2 appears in
Total failed. AllowUsers rejections happen before authentication completes, so depending on the journal pattern Fail2Ban uses, it may or may not have counted it. -
Restore ubuntu SSH access now that Fail2Ban is active:
Restore Ubuntu SSH Access Now That Fail2ban Is Active sudo vim /etc/ssh/sshd_configChange
AllowUsers deployto:AllowUsers Setting AllowUsers deploy ubuntuSave, exit, and restart the SSH service:
Restart Service sudo systemctl restart sshFrom your second terminal, confirm that
ssh -i ~/.ssh/your-key.pem ubuntu@YOUR_INSTANCE_IPworks again. Keep the ubuntu session open; you will use it in Part 6.
Part 5: Least-Privilege Sudoers Configuration
Section titled “Part 5: Least-Privilege Sudoers Configuration”The deploy user currently has full passwordless sudo access via the broad rule you wrote in Part 2. On a production server, application accounts should have only the specific elevated permissions they actually need.
-
Replace the broad sudoers rule with a scoped one:
Replace the Broad Sudoers Rule with a Scoped One sudo visudo -f /etc/sudoers.d/deployvisudovalidates the syntax before saving, preventing a broken sudoers file that locks you out of sudo. The-fflag edits the file in/etc/sudoers.d/directly rather than the main sudoers file. Replace the existingNOPASSWD: ALLline with this single line:Scoped sudoers Rule deploy ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart fail2ban, /usr/bin/fail2ban-client status sshdThe rule format is
user host=(run-as) [flags]: command-list. Reading this one:deployis the user the rule applies to;ALLis the host (this machine);(ALL)is the user to run as (root in practice);NOPASSWD:is the flag that suppresses the password prompt; the command list at the end specifies the exact executables, and in the second case the exact arguments, that are allowed. Listing full paths prevents a compromised user from putting a malicious binary namedsystemctlorfail2ban-clientearlier in$PATH. This grantsdeploypermission to restart Fail2Ban and inspect thesshdjail, and nothing else. Save and exit. -
Verify the restriction works:
From your
deploysession, test that the allowed command succeeds:Verify the Restriction Works sudo fail2ban-client status sshdYou should see the
sshdjail status. Then test that a disallowed command fails:Update Package Lists sudo apt updatesudoshould report thatdeployis not allowed to run the command. The restriction is working. A compromised deploy account still has that user’s normal access, but it no longer has a general-purpose sudo path to root.
Part 6: Verify the Hardened State
Section titled “Part 6: Verify the Hardened State”You have made five distinct changes to this server. Run these verification steps from your ubuntu session; the deploy account’s sudo is now scoped to Fail2Ban only and cannot run the system-level commands below.
-
Confirm the SSH configuration:
Verify Hardened SSH Settings sudo sshd -T | grep -E "passwordauthentication|permitrootlogin|allowusers|pubkeyauthentication"All four lines should show the hardened values:
passwordauthentication no,permitrootlogin no,pubkeyauthentication yes,allowusers deploy ubuntu. -
Confirm the firewall rules:
Confirm the Firewall Rules sudo ufw status verbose -
Confirm Fail2Ban is running:
Confirm Fail2ban Is Running sudo fail2ban-client status sshd -
Check open ports one final time:
Check Open Ports One Final Time sudo ss -tlnpThe only ports visible should be ones with corresponding UFW allow rules.
-
Create a timestamped verification entry:
Create a Timestamped Verification Entry echo "Hardened by $(whoami) on $(hostname) at $(date)" | sudo tee /var/log/cs312-hardened.logcat /var/log/cs312-hardened.log
Going Further
Section titled “Going Further”You have applied the core Linux hardening checklist to a real server. The natural next challenge is to compare your manual hardening work against an automated audit. Install Lynis with sudo apt update && sudo apt install -y lynis, then run sudo lynis audit system and compare its findings against the changes you just made: which recommendations did you already address, and which are still outstanding?
If you want to go deeper into the packet-filtering layer, start with sudo ufw show raw to see the rules UFW is managing on the host. Then read the nftables documentation and compare that lower-level model with UFW’s simplified interface. The useful exercise is to map each rule you added in this activity to the lower-level objects that actually enforce it.