Lab Environment: Ubuntu Server - 172.31.24.10 Nmap (Network Mapper) - 172.31.24.30 Tools Used: nmap, vi, systemctl, apt, ufw


Overview

Security hardening is the process of reducing a system’s attack surface by eliminating unnecessary services, enforcing access controls, and applying the principle of least privilege. In this lab, I walked through hardening a Linux Ubuntu server configured to serve as a web server. The goal was to ensure only the services and ports required for its primary function remain active and accessible.

The three core hardening steps covered are:

  1. Baseline the server - scan open ports to understand the current exposure
  2. Restrict SSH root login - prevent direct root access over SSH
  3. Remove unnecessary services - disable and purge LDAP (slapd)
  4. Enable the firewall - explicitly allow only required traffic

Step 1 - Baseline Scan with Nmap

Before making any changes, it is important to take a snapshot of the server’s current state. This gives you a reference point to compare against after hardening and helps identify what needs to be addressed.

From a separate machine (the NMAP Console at 172.31.24.30), I ran a basic Nmap scan against the target server:

nmap 172.31.24.10

Example Output:

pslearner@ip-172-31-24-30:~$ nmap 172.31.24.10
Starting Nmap 7.80 ( https://nmap.org ) at 2026-05-11 00:02 UTC
Nmap scan report for ip-172-31-24-10.ec2.internal (172.31.24.10)
Host is up (0.00036s latency).
Not shown: 996 closed ports
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
389/tcp open  ldap
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 0.05 seconds

Analysis: Four ports are currently open:

PortServiceExpected?
22SSH✅ Yes - needed for remote administration
80HTTP✅ Yes - web server traffic
443HTTPS✅ Yes - encrypted web traffic
389LDAP❌ No - not required for a web server

Port 389 (LDAP) is the anomaly. LDAP is a directory service protocol - useful in authentication infrastructure, but entirely unnecessary on a standalone web server. Leaving it open increases the attack surface unnecessarily.


Step 2 - Disable Direct Root SSH Login

Administrators commonly manage Linux servers via SSH. However, allowing SSH login directly to the root account is a significant security risk. If an attacker obtains the root password, they have full, unrestricted control over the system. The safer approach is to require admins to log in as a standard user and escalate privileges using sudo only when needed.

The SSH daemon’s behavior is controlled by the /etc/ssh/sshd_config file. I used grep to confirm the current setting, sed to modify it in place, and then verified the change, avoiding the need to manually navigate the file in vi.

# Confirm the current setting
sudo grep -i permitrootlogin /etc/ssh/sshd_config
 
# Replace 'yes' with 'no' in-place
sudo sed -i 's|PermitRootLogin yes|PermitRootLogin no|g' /etc/ssh/sshd_config
 
# Verify the change was applied
sudo grep -i permitrootlogin /etc/ssh/sshd_config

Example Output:

pslearner@ip-172-31-24-10:~$ sudo grep -i permitrootlogin /etc/ssh/sshd_config
PermitRootLogin yes
# the setting of "PermitRootLogin without-password".

pslearner@ip-172-31-24-10:~$ sudo sed -i 's|PermitRootLogin yes|PermitRootLogin no|g' /etc/ssh/sshd_config

pslearner@ip-172-31-24-10:~$ sudo grep -i permitrootlogin /etc/ssh/sshd_config
PermitRootLogin no
# the setting of "PermitRootLogin without-password".

With the configuration file updated, the SSH service must be restarted to apply the changes:

sudo systemctl restart sshd

Why sed instead of manually editing with vi? Using sed -i is faster and scriptable, the same command can be reused in automation playbooks across multiple servers. It’s also more self-documenting: if you review shell history later, the sed command shows you exactly what was changed, whereas a vi entry only tells you the file was opened.


Step 3 - Remove the LDAP Service (slapd)

Disabling a service stops it from running but does not remove it from the system. For services that have no role on this server, the best practice is to fully remove the package to eliminate any residual risk from misconfiguration or future unintended restarts.

3a - Identify the LDAP Service

I used systemctl status piped through grep to locate the LDAP-related service without scrolling through the entire service list:

sudo systemctl status | grep -B 1 -A 1 ldap

Example Output:

             ├─slapd.service
             │ └─2652 /usr/sbin/slapd -h ldap:/// ldapi:/// -g openldap -u openldap -F /etc/ldap/slapd.d
             ├─systemd-resolved.service

The service responsible for LDAP is slapd.service.

3b - Disable and Stop the Service

sudo systemctl disable --now slapd.service

The --now flag both disables the service from starting at boot and stops it immediately. This is equivalent to running systemctl stop and systemctl disable separately.

Example Output:

pslearner@ip-172-31-24-10:~$ sudo systemctl disable --now slapd.service
slapd.service is not a native service, redirecting to systemd-sysv-install.
Executing: /lib/systemd/systemd-sysv-install disable slapd

The message redirecting to systemd-sysv-install is expected, slapd was installed as a SysV init service rather than a native systemd unit, so systemd delegates the disable action to the legacy compatibility layer.

3c - Inspect Package File Locations

Before removing the package, I checked what files and directories slapd had placed on the system:

dpkg -S slapd

This showed all file paths associated with the slapd package, configuration files, binaries, and library dependencies across the system.

3d - Purge the Package

sudo apt purge slapd

apt purge removes both the package binaries and its configuration files, unlike apt remove which leaves config files behind. I confirmed with Y when prompted.

3e - Verify Removal

dpkg -S slapd

Example Output:

dpkg-query: no path found matching pattern *slapd*

This confirms that all remnants of the slapd package have been removed from the system.


Step 4 - Enable the Firewall (UFW)

Even with unnecessary services removed, a host-based firewall adds a critical layer of defense by explicitly defining what traffic is allowed in and out. Ubuntu’s Uncomplicated Firewall (ufw) makes this straightforward.

First, I checked the current firewall status:

sudo ufw status
Status: inactive

The firewall was not running. I then added rules to allow only the traffic this server legitimately needs:

# Allow Apache (covers both HTTP port 80 and HTTPS port 443)
sudo ufw allow "Apache Full"
 
# Allow SSH (so remote administration remains possible)
sudo ufw allow OpenSSH
 
# Enable the firewall
sudo ufw enable
 
# Verify the active rules
sudo ufw status

Example Output:

pslearner@ip-172-31-24-10:~$ sudo ufw allow "Apache Full"
Rules updated
Rules updated (v6)

pslearner@ip-172-31-24-10:~$ sudo ufw allow OpenSSH
Rules updated
Rules updated (v6)

pslearner@ip-172-31-24-10:~$ sudo ufw enable
Firewall is active and enabled on system startup

pslearner@ip-172-31-24-10:~$ sudo ufw status
Status: active

To                         Action      From
--                         ------      ----
Apache Full                ALLOW       Anywhere
OpenSSH                    ALLOW       Anywhere
Apache Full (v6)           ALLOW       Anywhere (v6)
OpenSSH (v6)               ALLOW       Anywhere (v6)

Why Apache Full instead of just port 80 or 443? UFW has named application profiles (stored in /etc/ufw/applications.d/) that group related rules. Apache Full maps to both ports 80 and 443, and OpenSSH maps to port 22. Using named profiles is more readable and easier to audit than raw port numbers.

^NamedApplicationProfiles

The rules apply to both IPv4 and IPv6, ensuring the firewall is consistent across both stacks.


Step 5 - Verify with a Final Nmap Scan

Back on the NMAP Console, I re-ran the scan to confirm that the hardening changes are reflected externally:

nmap 172.31.24.10

Example Output:

pslearner@ip-172-31-24-30:~$ nmap 172.31.24.10
Starting Nmap 7.80 ( https://nmap.org ) at 2026-05-11 01:22 UTC
Nmap scan report for ip-172-31-24-10.ec2.internal (172.31.24.10)
Host is up (0.00059s latency).
Not shown: 997 filtered ports
PORT    STATE SERVICE
22/tcp  open  ssh
80/tcp  open  http
443/tcp open  https

Nmap done: 1 IP address (1 host up) scanned in 4.55 seconds

Port 389 (LDAP) is gone. Only the three ports required for this server’s function remain open. Notice also that the scan now shows 997 filtered ports instead of 996 closed ports, this is the UFW firewall actively filtering traffic rather than the OS simply rejecting connections to closed ports.


Summary

Hardening ActionCommand(s) UsedOutcome
Baseline scannmap 172.31.24.10Identified 4 open ports, 1 unnecessary
Disable root SSHsed -i on sshd_config + systemctl restart sshdRoot login over SSH blocked
Disable LDAP servicesystemctl disable --now slapd.serviceService stopped and disabled
Remove LDAP packageapt purge slapdPackage and config files fully removed
Enable firewallufw allow + ufw enableOnly SSH, HTTP, HTTPS permitted
Verify hardeningnmap 172.31.24.10Confirmed port 389 closed, firewall active

Hardening is not a one-time task, it is an ongoing process. This lab covered foundational steps: removing unnecessary exposure, enforcing least privilege on administrative access, and implementing host-based firewall rules. In a production environment, additional steps would include disabling port 80 in favor of HTTPS-only, configuring fail2ban to block brute-force attempts, and auditing user accounts regularly.