Part 1: Ubuntu System Hardening and Execution Baseline

A fresh Ubuntu installation is designed to be permissive, catering equally to desktop, server, and container workloads without assuming a strict security model.
However, the moment a system is exposed to a network (internal/external), its threat model changes. It transitions from an isolated compute environment into a reachable execution target. What follows is the process of moving from this default, permissive state to a controlled execution environment, ending with an operational monitoring model which will be covered in Part Two.
System Exposure and Baseline Inspection
Before altering the system, you need to understand its current exposure.
Network listeners:
sudo ss -tulnp
This command reveals which processes are actively bound to network interfaces. At this stage, the network layer makes no distinction between intended services (like SSH) and accidental exposure (like a default database binding to 0.0.0.0).
Example output:
Installed package drift:
apt list --upgradable
This command shows the gap between what’s installed locally and what the repositories currently provide. That gap is where security debt accumulates.
Example output:
Enabled services:
systemctl list-unit-files --state=enabled
This command lists everything configured to execute automatically during boot. Its the most accurate representation of the system's baseline behaviour.
Example output:
Package State Alignment
Systems degrade securely over time if they're not consistently aligned with upstream security patches.
Update alignment:
sudo apt update && sudo apt upgrade -y
Verification:
apt list --upgradable
The expected steady state is an empty upgrade list. Anything else represents drift that needs to be addressed.
Expected output:
Automating Security Updates
Manual update cycles rely on human consistency, which is inherently flawed. Security patch latency (the window between vulnerability disclosure, package availability, and human execution) is where the majority of exploitation occurs.
Enable unattended updates:
sudo apt install unattended-upgrades -y
Note: you might already have this package installed on your system.
Verify behaviour configuration:
cat /etc/apt/apt.conf.d/20auto-upgrades
This file dictates whether update scheduling is successfully delegated to the system. You should see APT::Periodic::Update-Package-Lists "1"; and APT::Periodic::Unattended-Upgrade "1";.
Example output:
SSH as an Execution Boundary
SSH operates as a remote execution boundary at the operating system level. Once access is established, the session effectively represents direct command execution on the host. When password authentication is enabled, that boundary is governed primarily by password strength and any rate-limiting controls enforced by the service.
Removing password authentication reduces the system to key-based identity verification only, but this change must be executed with a verified fallback path already in place.
Warning: Before disabling password authentication, ensure you have successfully generated and copied your SSH public key to
~/.ssh/authorized_keyson the remote server. Otherwise, you will lock yourself out.
Precondition: Verify SSH Key Access
From your local machine, confirm you can log in using SSH:
ssh user@server_ip
If a password is still required, key authentication hasn't been configured yet.
You can explicitly test key usage:
ssh -i ~/.ssh/id_rsa user@server_ip
Ensure your public key exists on the remote server:
cat ~/.ssh/authorized_keys
If its missing, copy it using:
ssh-copy-id user@server_ip
Make sure permissions are correct:
chmod 700 ~/.ssh
chmod 600 ~/.ssh/authorized_keys
Keep an active SSH session open while making changes, in case rollback is needed.
SSH configuration:
Modifying the /etc/ssh/sshd_config file.
sudo sed -i 's/^#*PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*PermitRootLogin yes/PermitRootLogin no/' /etc/ssh/sshd_config
The commands above will ensure the following directives are set:
PasswordAuthentication no
PermitRootLogin no
Apply and verify the changes:
sudo systemctl restart ssh
sudo sshd -T | grep -i passwordauthentication
Example output:
After this change, SSH access depends entirely on key-based authentication. Password login is no longer accepted by the service configuration.
Example of key-based authentication:
The Firewall as Traffic Definition
A firewall does not inherently secure a vulnerable application, but it enforces strict network boundaries, defining exactly which paths are valid.
Configure UFW (Uncomplicated Firewall):
sudo ufw allow OpenSSH
sudo ufw enable
Example output:
Verification:
sudo ufw status verbose
Example output:
Note: If SSH is not explicitly allowed before enabling the firewall, all active and future connections will be dropped.
Service Minimisation
Every enabled system service increases the memory footprint, dependency surface during boot, and potential attack vectors. There is no distinction between a service you deliberately intended to expose and one installed silently as a dependency unless you actively audit them.
Review your active listeners (sudo ss -tulnp) and disable anything unnecessary:
sudo systemctl disable --now <service_name>
The Operational Monitoring Layer
At this stage, the system is secured but not yet structured for robust observability.
Linux produces vital telemetry, natively split between runtime event streams and persisted file logs (traditionally in /var/log). Historically, managing this was fragmented. Systemd unifies this execution and monitoring model.
Why systemd supersedes legacy execution
Historically, service execution was handled by a mix of SysV init scripts, cron-based scheduling, and ad-hoc supervision tools. This resulted in:
No unified dependency graph.
Inconsistent startup ordering.
Manual supervision required for long-running processes.
Fragmented logging channels.
Systemd is often reductively called a "service manager", but it is actually a unified execution and dependency management framework. Instead of executing disparate scripts, systemd treats the system as a strict dependency graph of managed units.
Core systemd components:
PID 1: The systemd daemon itself, controlling the boot lifecycle.
Unit files: Declarative service definitions.
Journald: The integrated, binary log collection subsystem.
Timers: The scheduled execution model.
Why systemd timers replace cron
Cron operates on a rudimentary premise: "execute command X at time Y".
It has no contextual awareness of whether the previous execution is still running, if the system is overloaded, or if prerequisite services are available.
Systemd timers resolve these operational blind spots:
Contextual execution: Execution is tied strictly to service units, allowing for dependency mapping (e.g., "only run this script if the network is up").
State awareness: You can query execution state reliably via
systemctl.Built-in supervision: Overlapping executions can be prevented automatically.
Unified logging: Standard output (stdout) and errors (stderr) from timed tasks are captured automatically by
journald, ensuring scheduled tasks are monitored exactly like persistent daemons.
The shift from cron to systemd timers reflects a move toward system-aware scheduling that integrates with service state, logging, and lifecycle management. Because systemd inherently understands execution state, manages dependencies, and natively captures logs, it provides the robust execution engine required for the active alerting architecture we will implement in Part Two.





