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:
- Baseline the server - scan open ports to understand the current exposure
- Restrict SSH root login - prevent direct root access over SSH
- Remove unnecessary services - disable and purge LDAP (
slapd) - 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.10Example 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:
| Port | Service | Expected? |
|---|---|---|
| 22 | SSH | ✅ Yes - needed for remote administration |
| 80 | HTTP | ✅ Yes - web server traffic |
| 443 | HTTPS | ✅ Yes - encrypted web traffic |
| 389 | LDAP | ❌ 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_configExample 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 sshdWhy
sedinstead of manually editing withvi? Usingsed -iis 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, thesedcommand shows you exactly what was changed, whereas avientry 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 ldapExample 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.serviceThe --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-installis expected,slapdwas 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 slapdThis showed all file paths associated with the slapd package, configuration files, binaries, and library dependencies across the system.
Output
slapd: /usr/share/man/man5/slapd-mdb.5.gz slapd: /usr/share/man/man5/slapd-ldif.5.gz slapd: /usr/share/doc/slapd slapd: /usr/share/man/man5/slapd-sock.5.gz slapd: /usr/share/doc/slapd/examples/slapd.backup slapd: /usr/share/man/man5/slapd-monitor.5.gz slapd: /usr/share/man/man5/slapd-relay.5.gz slapd: /usr/share/man/man5/slapd-ldap.5.gz slapd: /usr/share/apport/package-hooks/slapd.py slapd: /usr/sbin/slapd slapd: /usr/share/man/man5/slapd-dnssrv.5.gz slapd: /usr/share/man/man5/slapd-passwd.5.gz slapd: /usr/share/man/man5/slapd.access.5.gz slapd: /usr/sbin/slapdn slapd: /usr/share/doc/slapd/examples slapd: /usr/share/doc/slapd/changelog.Debian.gz slapd: /usr/share/lintian/overrides/slapd slapd: /usr/share/doc/slapd/examples/slapd.conf slapd: /usr/share/man/man5/slapd-asyncmeta.5.gz slapd: /lib/systemd/system/slapd.service.d slapd: /usr/share/man/man5/slapd-sql.5.gz slapd: /etc/apparmor.d/usr.sbin.slapd slapd: /usr/share/man/man5/slapd.conf.5.gz slapd: /usr/share/doc/slapd/NEWS.Debian.gz slapd: /usr/share/doc/slapd/TODO.Debian slapd: /usr/share/slapd/ldiftopasswd slapd: /etc/default/slapd slapd: /usr/share/man/man5/slapd-ndb.5.gz slapd: /usr/share/doc/slapd/README.Debian.gz slapd: /lib/systemd/system/slapd.service.d/slapd-remain-after-exit.conf slapd: /usr/share/man/man5/slapd-meta.5.gz slapd: /etc/init.d/slapd slapd: /usr/share/man/man5/slapd.backends.5.gz slapd: /usr/share/slapd/slapd.init.ldif slapd: /usr/share/man/man5/slapd.overlays.5.gz slapd: /usr/share/man/man5/slapd-null.5.gz slapd: /usr/share/man/man5/slapd-config.5.gz slapd: /usr/share/man/man8/slapd.8.gz slapd: /usr/share/doc/slapd/copyright slapd: /usr/share/slapd slapd: /etc/ufw/applications.d/slapd slapd: /usr/share/man/man5/slapd.plugin.5.gz slapd: /usr/share/man/man5/slapd-perl.5.gz slapd: /usr/share/man/man8/slapdn.8.gz
3d - Purge the Package
sudo apt purge slapdapt 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 slapdExample 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 statusStatus: 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 statusExample 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 Fullinstead of just port 80 or 443? UFW has named application profiles (stored in/etc/ufw/applications.d/) that group related rules.Apache Fullmaps to both ports 80 and 443, andOpenSSHmaps 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.10Example 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 Action | Command(s) Used | Outcome |
|---|---|---|
| Baseline scan | nmap 172.31.24.10 | Identified 4 open ports, 1 unnecessary |
| Disable root SSH | sed -i on sshd_config + systemctl restart sshd | Root login over SSH blocked |
| Disable LDAP service | systemctl disable --now slapd.service | Service stopped and disabled |
| Remove LDAP package | apt purge slapd | Package and config files fully removed |
| Enable firewall | ufw allow + ufw enable | Only SSH, HTTP, HTTPS permitted |
| Verify hardening | nmap 172.31.24.10 | Confirmed 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.