Deploy and Harden a Linux Server

Launch an Ubuntu EC2 instance, harden SSH, configure UFW, and automate health monitoring.

Introduction

30 Second Summary

Every website, app, and online service you use runs on a server somewhere. The people who keep those servers secure and running smoothly are system administrators.

In this project, you will launch your own Linux server on AWS, connect to it remotely, lock it down with production-grade security, and set up automated health monitoring.

What You'll Build

You'll SSH into a hardened server in the cloud, see a live web page served by Nginx in your browser, and watch your custom monitoring script report real system stats on a timer.

By the end of this project, you'll have:

  • A live Ubuntu Server on EC2 you can SSH into from your Mac, locked down by a UFW firewall that blocks everything except SSH and HTTP traffic.
  • Production-style SSH hardening with root login disabled, password authentication off, and login attempt limits enforced.
  • An Nginx web server proving your server is publicly reachable, plus a Bash health monitoring script running on a systemd timer that logs CPU load, memory, disk space, and active services.
  • Secret Mission: Install and configure Fail2Ban to automatically ban IPs that repeatedly fail SSH login attempts.

Are there any prerequisites?

You'll need a paid AWS account (the project costs about $0.05 total) and basic Linux familiarity with commands like cd, ls, and nano. No additional software installs are required on your Mac.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters. You're deploying and hardening a Linux server on AWS EC2, following the core sysadmin workflow: provision, connect, secure, serve, monitor.

Launch Your EC2 Instance and Connect via SSH

Every sysadmin workflow starts with a server. Right now, you don't have one.

AWS EC2 lets you spin up a virtual Linux machine in minutes, accessible from anywhere in the world. You will launch an Ubuntu server in the cloud and connect to it remotely via SSH. This is the same way administrators manage production servers across the globe.

In this step, get ready to:
  • Launch a t3.micro EC2 instance running Ubuntu Server 24.04 LTS.
  • Create an ED25519 SSH key pair for secure authentication.
  • Connect to the instance from your Mac terminal via SSH.
Open the EC2 console and configure your instance

First, you need to reach the EC2 Launch Instance wizard in the AWS Management Console.

  • Open your browser and navigate to the Amazon EC2 console at https://console.aws.amazon.com/ec2/.
  • Sign in with your AWS account credentials if prompted.
  • In the navigation bar at the top of the screen, check the region selector (top-right area). If it does not already say US East (N. Virginia), click it and select US East (N. Virginia).
  • Click Launch instance from the EC2 console dashboard.

What is EC2?

Amazon EC2 (Elastic Compute Cloud) is a service that provides virtual servers in the AWS cloud. Each virtual server is called an instance. You choose the operating system, hardware size, and region for your instance.

Think of it as renting a computer that lives in a data center. You connect to it over the internet instead of sitting in front of it.

Now fill in the Launch Instance wizard with your server's configuration.

  • Under Name and tags, enter sysadmin-lab in the Name field.
  • Under Application and OS Images (Amazon Machine Image), click Quick Start.
  • Select Ubuntu as the operating system.
  • From the Amazon Machine Image (AMI) dropdown, select the Ubuntu Server 24.04 LTS AMI.
  • Under Instance type, select t3.micro.

What is an AMI?

An AMI (Amazon Machine Image) is a template that contains the operating system and software needed to launch an instance. Selecting the Ubuntu Server 24.04 LTS AMI means your instance will boot into a fresh Ubuntu Linux environment.

The t3.micro instance type gives you 2 vCPUs and 1 GiB of memory. It costs $0.0104 per hour, so this entire project will cost roughly 5 cents.

Create your SSH key pair

SSH key pairs let you prove your identity when connecting to the server. The private key stays on your Mac. AWS holds the public key on the instance.

  • Scroll down to the Key pair (login) section in the wizard.
  • Click Create new key pair.
  • Enter sysadmin-key as the Key pair name.
  • For Key pair type, select ED25519.
  • For Private key file format, select .pem.
  • Click Create key pair. Your browser will automatically download sysadmin-key.pem to your Downloads folder.

Before you can use this key file, you need to restrict its permissions. Open your Mac terminal to do this.

  • Press Cmd+Space to open Spotlight Search.
  • Type Terminal and press Enter to open it.
  • Lock down the key file permissions by running this command:
chmod 400 ~/Downloads/sysadmin-key.pem

What does chmod 400 do?

The chmod 400 command sets the file to read-only for you and no access for anyone else. SSH refuses to use a key file if other users on the system can read it. This is a security requirement, not optional.

Seeing 'No such file or directory'?

Your browser may have saved the file with a slightly different name or in a different location. Check your Downloads folder for the file.

  • Run ls ~/Downloads/sysadmin* to see what was downloaded.
  • If the file is named sysadmin-key.pem.txt, rename it with mv ~/Downloads/sysadmin-key.pem.txt ~/Downloads/sysadmin-key.pem.

Help me find my downloaded .pem key file

Configure networking, launch, and connect via SSH

Back in the AWS console, the last section before launch is the network and security group configuration. A security group acts as a virtual firewall that controls which traffic can reach your instance.

  • Switch back to your browser tab with the EC2 Launch Instance wizard.
  • Under Network settings, keep the default VPC selected.
  • Under Firewall (security groups), select Create security group.
  • Check Allow SSH traffic from and select My IP from the dropdown. This restricts SSH access to only your current IP address.
  • Click Launch instance.

Why 'My IP' instead of 'Anywhere'?

Selecting My IP means only your home or office network can attempt an SSH connection. If you chose Anywhere, every IP address on the internet could try to connect. Restricting to your IP is a basic security practice for any server you manage.

AWS will show a success notification with a link to your instance. Now you need to wait for it to finish booting and grab its public IP address.

  • Click the instance ID link in the success notification to open the Instances page.
  • Wait for the Instance state column to show Running. This usually takes 30-60 seconds. You can click the refresh icon to update the status.
  • Once running, select your sysadmin-lab instance by clicking its checkbox.
  • In the details panel below, find the Public IPv4 address field and copy the IP address.

Before you run the SSH command, take a moment to predict: what do you think you will see when the connection succeeds?

  • Switch back to your Terminal window and connect to your instance by running this command:
ssh -i ~/Downloads/sysadmin-key.pem ubuntu@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]

What is this command doing?

  • ssh starts a secure shell connection to a remote server.
  • -i ~/Downloads/sysadmin-key.pem tells SSH which private key to use for authentication.
  • ubuntu is the default username for Ubuntu AMIs on EC2.
  • The IP address after @ is your instance's public address on the internet.

The first time you connect, SSH will ask you to confirm the server's fingerprint. Type yes and press Enter.

You should see a welcome message starting with Welcome to Ubuntu 24.04 followed by system information and documentation links. Your terminal prompt will change to something like ubuntu@ip-xxx-xxx-xxx-xxx:~$. You are now logged into your cloud server.

Connection timed out or refused?

  • If you see Operation timed out: your security group may not have your current IP. Go back to the EC2 console, find your instance's security group, click Edit inbound rules, and re-add SSH with source My IP.
  • If you see Permission denied (publickey): make sure you ran chmod 400 on the .pem file and that you are using the correct key file path.
  • If the instance state is not Running yet, wait another minute and try again.

Help me troubleshoot my SSH connection to EC2

Your server is live and you are connected. Next up, you will install a web server and prove your instance is reachable from the public internet.

Deploy Nginx and See Your Server Live on the Internet

Your EC2 instance is running and you can reach it over SSH. But right now, it does nothing visible to the outside world.

In this step, you'll install Nginx, a web server that responds to browser requests. You'll also open port 80 in your security group so anyone on the internet can reach it.

In this step, get ready to:
  • Install Nginx on your Ubuntu instance and confirm it's running.
  • Add an HTTP inbound rule to your security group.
  • Visit your server's public IP in a browser and see a live web page.
Install Nginx on your instance

Your server needs software to listen for incoming web requests and send back a page. Nginx is one of the most widely used web servers in production environments.

  • In your SSH session on the EC2 instance, update the package list and install Nginx by running this command:
sudo apt update && sudo apt install -y nginx

You'll see packages downloading and installing. This takes about 10-20 seconds.

  • Confirm Nginx is running by checking its systemd service status:
sudo systemctl status nginx

You should see a green dot and the word active (running) in the output. Press q to exit the status view.

Not seeing active (running)?

If the status shows inactive or failed, try starting Nginx manually with sudo systemctl start nginx and check the status again.

If the install itself failed, run sudo apt update first, then retry the install command. Network connectivity issues on the instance can cause package downloads to fail.

Help me troubleshoot why Nginx isn't running on my Ubuntu EC2 instance.

Open port 80 in your security group

Nginx is listening on port 80, but your security group currently only allows SSH traffic on port 22. You need to add an HTTP rule so browser requests can reach your server.

  • Switch back to the AWS Management Console in your browser.
  • In the EC2 dashboard, click Instances in the left sidebar.
  • Click on your sysadmin-lab instance to open its details.
  • Click the Security tab in the lower panel.
  • Click the security group link (it starts with sg-).
  • Click Edit inbound rules.
  • Click Add rule.
  • Set Type to HTTP.
  • Set Source to Anywhere-IPv4 (0.0.0.0/0).
  • Click Save rules.

Why Anywhere-IPv4?

For SSH, you restricted access to your IP only. For a web server, you want anyone on the internet to be able to visit the page. That's why the source is 0.0.0.0/0 (all IPv4 addresses).

Verify Nginx is live from your browser

Your server is now running Nginx and your security group allows HTTP traffic. Before you test this, take a moment to predict what you'll see when you visit your server's IP address.

  • Copy your instance's Public IPv4 address from the EC2 instance details page in the AWS Console.
  • Open a new browser tab and navigate to your server's address:
http://[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]

You should see the default Nginx welcome page with the heading "Welcome to nginx!" displayed in your browser.

This proves your server is publicly reachable. A machine you launched minutes ago is now serving a web page to anyone on the internet.

Page not loading?

Make sure you're using http:// and not https:// in your browser. HTTPS requires a certificate you haven't set up yet.

Double-check the security group rule. Go back to the inbound rules and confirm the HTTP rule shows port 80 with source 0.0.0.0/0. If the rule is missing, click Edit inbound rules and add it again.

Verify you copied the correct IP address. The public IP is shown on the instance details page under Public IPv4 address.

Help me figure out why I can't see the Nginx welcome page in my browser.

Your server is alive on the internet. Next up, you'll lock it down so only the right people can access it.

Harden SSH and Lock Down Access

In the previous step, you deployed Nginx and proved your server is publicly reachable. That is a great milestone, but it also means your server is now a target.

Right now, your server's SSH configuration uses default settings. Root login is technically possible. Password-based authentication could be enabled. Automated bots scan every public IP address looking for exactly this kind of opening. Hardening SSH is the single most impactful security step you can take on any Linux server.

In this step, get ready to:
  • Back up and edit the SSH configuration to disable dangerous defaults.
  • Restart the SSH service and confirm your own access still works.
  • Attempt a root login and verify it is blocked.
Back up and edit the SSH configuration

Before changing any system configuration file, you always create a backup first. If something breaks, you can restore the original in seconds.

  • In your SSH session on the EC2 instance, create a backup of the SSH config by running this command:
sudo cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup
  • Confirm the backup exists by running:
ls /etc/ssh/sshd_config.backup

You should see the file path printed back to you, confirming the backup was created.

Why back up before editing?

If a typo in the SSH config prevents you from reconnecting, you can boot into the instance via the AWS Console's serial connection and restore the backup with sudo cp /etc/ssh/sshd_config.backup /etc/ssh/sshd_config. This is your safety net.

  • Open the SSH configuration file in the nano text editor by running:
sudo nano /etc/ssh/sshd_config

You are now inside the config file. You need to find and change five settings. Some may already exist as commented-out lines (starting with #). Others may be set to different values.

  • Use Ctrl+W to search inside nano. Search for PermitRootLogin and change or add the line so it reads:
  • PermitRootLogin no
  • Search for PasswordAuthentication and change or add the line so it reads:
  • PasswordAuthentication no
  • Search for MaxAuthTries and change or add the line so it reads:
  • MaxAuthTries 3
  • Search for LoginGraceTime and change or add the line so it reads:
  • LoginGraceTime 30
  • Search for X11Forwarding and change or add the line so it reads:
  • X11Forwarding no
  • Save the file by pressing Ctrl+O, then press Enter to confirm the filename.
  • Exit nano by pressing Ctrl+X.

What do these settings do?

  • PermitRootLogin no blocks anyone from SSH-ing in as the root user, even with a valid key.
  • PasswordAuthentication no forces key-based authentication only. No one can brute-force a password.
  • MaxAuthTries 3 disconnects a client after 3 failed login attempts per session.
  • LoginGraceTime 30 gives the client 30 seconds to authenticate before the server drops the connection.
  • X11Forwarding no disables graphical display forwarding, which is an unnecessary attack surface on a headless server.

Removing the # comment character

If you find a line like #PermitRootLogin prohibit-password, remove the # at the start and change the value to no. A line starting with # is a comment and has no effect.

Test the configuration and restart SSH

Before restarting SSH with your new config, you test the syntax first. A typo in this file could lock you out of your own server.

  • Test the SSH config syntax by running:
sudo sshd -t

No output means the config is valid. If you see an error, go back and fix the typo in the config file.

Seeing a syntax error?

The error message will include a line number. Run sudo nano +LINE_NUMBER /etc/ssh/sshd_config (replacing LINE_NUMBER with the number shown) to jump straight to the problem. Common causes: a missing space between the directive name and its value, or leftover # characters.

If the file is too broken to fix, restore your backup and start over:

sudo cp /etc/ssh/sshd_config.backup /etc/ssh/sshd_config

Help me fix an sshd_config syntax error

  • Restart the SSH service to apply your changes by running:
sudo systemctl restart ssh

Do NOT close your current SSH session

Keep this terminal window open. If something went wrong, this session is your only way back in. You will test the new config from a separate terminal tab.

  • Open a new terminal tab on your Mac by pressing Cmd+T.
  • Test that your normal access still works by connecting from this new tab:
ssh -i ~/Downloads/sysadmin-key.pem ubuntu@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]

You should see the Ubuntu welcome message, confirming your key-based access as the ubuntu user still works with the hardened config.

Can't connect from the new tab?

Switch back to your original SSH session (which is still connected). Restore the backup config and restart SSH:

sudo cp /etc/ssh/sshd_config.backup /etc/ssh/sshd_config
sudo systemctl restart ssh

Then try connecting from the new tab again. If it works now, re-edit the config file carefully and repeat the process.

Help me troubleshoot my SSH connection after hardening

Verify root login is blocked

The whole point of setting PermitRootLogin no is to block the root user from connecting. Time to prove it actually works.

  • Before you run the next command, predict what you think will happen. Will SSH let you connect as root, or will it reject you?
  • Attempt to log in as root from your Mac terminal by running:
ssh -i ~/Downloads/sysadmin-key.pem root@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]

You should see Permission denied (publickey). Root access is blocked. Your hardening worked.

Why does it say publickey and not root disabled?

SSH does not reveal the reason it rejected a login. It simply reports the authentication method that was tried and failed. This is intentional. Telling an attacker exactly why they were rejected would help them refine their approach.

  • Finally, confirm your ubuntu user can still connect. If you closed the second tab, open a new one and run:
ssh -i ~/Downloads/sysadmin-key.pem ubuntu@[[EC2_PUBLIC_IP="your EC2 public IPv4 address"]]

You should see the Ubuntu welcome message. Your access is intact, and root is locked out.

Your SSH access is now locked down to key-only authentication with root blocked. Next up, you will add a second layer of defense by configuring a host-based firewall to control exactly which ports your server exposes.

Configure UFW Firewall

Your server's SSH configuration is locked down, but all traffic filtering currently lives in the AWS security group. If someone accidentally opens a port there, your server has zero local defense.

A host-based firewall adds a second wall directly on the instance. In this step, you'll configure UFW (Uncomplicated Firewall) to deny all incoming traffic by default, then punch holes for only SSH and HTTP.

In this step, get ready to:
  • Set UFW default policies to deny incoming traffic.
  • Allow only SSH and HTTP through the firewall.
  • Verify the firewall is active and Nginx still loads.
Set default policies and allow SSH

The order of operations here is critical. You must allow SSH traffic before enabling the firewall. If you enable a deny-all firewall without an SSH exception, you permanently lock yourself out of the instance.

  • In your SSH session on the EC2 instance, set the default policies and allow SSH by running these commands one at a time:
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow OpenSSH

What do these commands do?

  • sudo ufw default deny incoming blocks all inbound traffic unless you create an explicit exception. This is the deny-by-default stance that production servers use.
  • sudo ufw default allow outgoing lets your server make outbound connections freely (for package updates, DNS lookups, etc.).
  • sudo ufw allow OpenSSH adds a rule for port 22 so your SSH session survives once the firewall goes active.

Each command prints a confirmation. You should see messages like Default incoming policy changed to 'deny' after the first command.

Why both a security group and UFW?

Security groups filter traffic at the AWS network level. UFW filters traffic on the server itself. Running both is called defense-in-depth.

If a teammate accidentally opens port 3306 on the security group, UFW still blocks it at the OS. Two independent layers mean a single misconfiguration cannot expose your server.

Allow HTTP and enable the firewall

Your Nginx web server needs port 80 open for HTTP traffic. Once that rule is in place, you can safely activate the firewall.

  • Allow HTTP traffic on port 80 by running:
sudo ufw allow 80/tcp
  • Enable the firewall by running:
sudo ufw --force enable

What does the --force flag do?

Without --force, UFW prompts you to confirm because enabling could disrupt existing connections. The flag skips that prompt since you have already added your SSH rule.

You should see Firewall is active and enabled on system startup confirming UFW is now enforcing your rules.

Lost your SSH connection?

If your terminal disconnects after enabling UFW, the SSH rule was not added correctly. You will need to terminate this instance in the AWS Console and launch a new one from Step 1. This is why the order matters.

help me recover SSH access after UFW locked me out

Verify your firewall rules

Before you run the next command, think about what you expect to see. You set two defaults (deny incoming, allow outgoing) and added two allow rules (SSH and HTTP). What should the status output show?

  • Check the full firewall status by running:
sudo ufw status verbose

You should see output showing default incoming set to deny, default outgoing set to allow, with two rules listed: 22/tcp (OpenSSH) ALLOW IN Anywhere and 80/tcp ALLOW IN Anywhere.

  • Switch to your browser and refresh http://your EC2 public IP to confirm the Nginx welcome page still loads.

Seeing the Nginx welcome page proves that port 80 traffic passes through both the security group and UFW successfully.

Nginx page not loading?

  • Run sudo ufw status verbose and confirm you see a rule for 80/tcp ALLOW IN. If it is missing, run sudo ufw allow 80/tcp again.
  • Check that the AWS security group still has the HTTP inbound rule from Step 2. Navigate to your instance in the EC2 Console, click the Security tab, and verify port 80 is open to 0.0.0.0/0.
  • Confirm Nginx is still running with sudo systemctl status nginx. If it stopped, restart it with sudo systemctl start nginx.

help me troubleshoot why my Nginx page is not loading after enabling UFW

Your server now has two independent layers of network protection. Next up, you'll build a monitoring script that automatically watches over this hardened server's health.

Create a System Health Monitoring Script with Systemd Timer

Your server is locked down. Hardened SSH blocks root logins, and UFW denies everything except the ports you explicitly allowed.

Securing a server is only half the job. You also need visibility into what is happening on it. Manually SSHing in to check CPU, memory, and disk every time is not sustainable. In this step, you will write a Bash script that gathers key health metrics and automate it with a systemd timer so the data logs itself without you lifting a finger.

In this step, get ready to:
  • Create a Bash script that reports hostname, CPU load, memory, disk, and active services.
  • Set up a systemd timer to run the script every 10 minutes automatically.
  • Verify the automated output in the system journal.
Create the health monitoring script

Your monitoring script needs to pull data from multiple system sources and format it into a readable report. Linux exposes most of this information through files like /proc/loadavg and built-in commands like free and df.

  • Create the script file by running:
sudo nano /usr/local/bin/health-check.sh
  • Paste the following script content into nano:
#!/bin/bash
echo "=============================="
echo "SYSTEM HEALTH CHECK"
echo "=============================="
echo "Hostname: $(hostname)"
echo "Date: $(date)"
echo "Uptime: $(uptime -p)"
echo ""
echo "--- CPU Load ---"
echo "Load Averages: $(cat /proc/loadavg | awk '{print $1, $2, $3}')"
echo ""
echo "--- Memory Usage ---"
free -h | grep -E "^(Mem|Swap)"
echo ""
echo "--- Disk Usage ---"
df -h / | tail -1 | awk '{print "Used: "$3" / "$2" ("$5" full)"}'
echo ""
echo "--- Active Services ---"
echo "Active units: $(systemctl list-units --state=active --no-pager --no-legend | wc -l)"
echo "=============================="

What does this script do?

  • $(hostname) and $(date) identify which server generated the report and when.
  • /proc/loadavg gives CPU load averages for 1, 5, and 15 minutes. Higher numbers mean the CPU is busier.
  • free -h shows human-readable memory and swap usage.
  • df -h / reports how full the root filesystem is.
  • systemctl list-units --state=active counts all active services so you can spot unexpected stops.
  • Save and exit nano by pressing Ctrl+O, then Enter, then Ctrl+X.
  • Make the script executable by running:
sudo chmod +x /usr/local/bin/health-check.sh

Before testing, what do you think the script will print for your server's hostname and uptime? Think about what values a freshly launched EC2 instance would show.

  • Test the script manually by running:
sudo /usr/local/bin/health-check.sh

You should see a formatted report showing your instance's hostname (something like ip-10-x-x-x), the current date, uptime, CPU load averages, memory and swap usage, disk utilization, and a count of active systemd units.

Seeing 'Permission denied'?

  • Make sure you ran chmod +x on the script. Without execute permission, Bash cannot run it.
  • Check that you used sudo when running the script. It needs elevated privileges to read all system info.
  • Double check the file path is exactly /usr/local/bin/health-check.sh. A typo means Bash cannot find the file.

My health check script gives a permission denied error when I run it.

✔️ Awesome, I've got everything!

Great. Double check you saved the file and move on to the next substep.

ⓧ I'd like to double check the full code

Here is the complete health-check.sh file. Compare it against yours:

#!/bin/bash
echo "=============================="
echo "SYSTEM HEALTH CHECK"
echo "=============================="
echo "Hostname: $(hostname)"
echo "Date: $(date)"
echo "Uptime: $(uptime -p)"
echo ""
echo "--- CPU Load ---"
echo "Load Averages: $(cat /proc/loadavg | awk '{print $1, $2, $3}')"
echo ""
echo "--- Memory Usage ---"
free -h | grep -E "^(Mem|Swap)"
echo ""
echo "--- Disk Usage ---"
df -h / | tail -1 | awk '{print "Used: "$3" / "$2" ("$5" full)"}'
echo ""
echo "--- Active Services ---"
echo "Active units: $(systemctl list-units --state=active --no-pager --no-legend | wc -l)"
echo "=============================="
Create the systemd service and timer units

The script works, but you would have to SSH in every time to run it. A systemd timer triggers the script automatically on a schedule without any manual intervention.

Systemd needs two files: a service unit that defines what to run, and a timer unit that defines when to run it.

  • Create the service unit file by running:
sudo nano /etc/systemd/system/health-check.service
  • Paste the following content:
[Unit]
Description=System Health Check

[Service]
Type=oneshot
ExecStart=/usr/local/bin/health-check.sh

What does this service unit do?

  • Type=oneshot tells systemd to run the script once and exit, rather than keeping it alive as a long-running daemon.
  • ExecStart points to the full path of the Bash script you just created.
  • Save and exit nano (Ctrl+O, Enter, Ctrl+X).
  • Create the timer unit file by running:
sudo nano /etc/systemd/system/health-check.timer
  • Paste the following content:
[Unit]
Description=Run System Health Check every 10 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=10min

[Install]
WantedBy=timers.target

What does this timer unit do?

  • OnBootSec=1min runs the health check 1 minute after the server boots up.
  • OnUnitActiveSec=10min repeats it every 10 minutes after each run completes.
  • WantedBy=timers.target tells systemd to activate this timer when the system reaches its timer management stage during boot.
  • Save and exit nano (Ctrl+O, Enter, Ctrl+X).
  • Verify both unit files exist by running:
ls /etc/systemd/system/health-check.*

You should see /etc/systemd/system/health-check.service and /etc/systemd/system/health-check.timer listed.

Files not showing up?

  • Confirm you saved in nano with Ctrl+O (Write Out) before pressing Ctrl+X (Exit). If you pressed Ctrl+X first without saving, the file was not written.
  • Check the path is exactly /etc/systemd/system/. A typo in the directory means systemd will not find the units.

My systemd unit files are missing after I thought I saved them in nano.

✔️ Awesome, I've got everything!

Great. Both unit files are in place. Move on to enable the timer.

ⓧ I'd like to double check the full code

Here is the complete health-check.service:

[Unit]
Description=System Health Check

[Service]
Type=oneshot
ExecStart=/usr/local/bin/health-check.sh

And here is the complete health-check.timer:

[Unit]
Description=Run System Health Check every 10 minutes

[Timer]
OnBootSec=1min
OnUnitActiveSec=10min

[Install]
WantedBy=timers.target
Enable the timer and verify automated output

With the files in place, you need to tell systemd to load the new units and start the timer.

  • Reload the systemd daemon and enable the timer by running:
sudo systemctl daemon-reload && sudo systemctl enable --now health-check.timer

What does this command do?

  • daemon-reload tells systemd to re-read all unit files, picking up your new service and timer.
  • enable --now both enables the timer for future boots AND starts it immediately.
  • Confirm the timer is active by running:
sudo systemctl list-timers | grep health

You should see a line showing health-check.timer with NEXT and LAST columns indicating when it will fire next and that it activates health-check.service.

Now trigger a manual run so you can see output immediately without waiting 10 minutes.

  • Start the service manually by running:
sudo systemctl start health-check.service

Before checking the logs, what do you expect to see in the journal output? Think back to what the script printed when you ran it directly.

  • View the health check output in the system journal by running:
sudo journalctl -u health-check.service --no-pager -n 20

You should see the same health report from earlier, now captured by systemd's journal. The output includes hostname, date, uptime, CPU load averages, memory usage, disk usage, and active service count.

Seeing 'No entries' in journalctl?

  • Run sudo systemctl status health-check.service to check if the service ran successfully. Look for an exit code of 0.
  • If you see a failed status, check that the script path in your service file matches exactly: /usr/local/bin/health-check.sh.
  • Make sure you ran daemon-reload before starting the service. Without it, systemd does not know about your new unit files.

My health check timer ran but journalctl shows no entries.

Why systemd timers instead of cron?

Systemd timers log output directly to the journal, making them easy to inspect with journalctl. Cron jobs write to separate log files or disappear into email.

Timers also integrate with systemd's dependency management. You can make a timer wait for network or a specific mount before running.

Your server now monitors itself. Every 10 minutes, systemd runs your health check script and logs the results to the journal. Next up, tackle the Secret Mission to add Fail2Ban for automated intrusion prevention.

Secret mission

Install and Configure Fail2Ban

Your SSH port is open and protected by key-only auth, but automated bots will still hammer it with thousands of login attempts. In this secret mission, you will install Fail2Ban to automatically ban IPs that repeatedly fail SSH login, and configure it to enforce bans through UFW so every block appears in a single auditable view.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project uses a t3.micro EC2 instance that costs approximately $0.0104 per hour if left running.

Cost warning

Your EC2 instance costs approximately $7.50/month if left running, plus ~$0.64/month for the EBS volume. If you are done with this project, terminate the instance to stop all charges.

Resources you used:

  • EC2 t3.micro instance (sysadmin-lab).
  • EBS volume (8GB gp3, attached to your instance).
  • Security Group (SSH and HTTP inbound rules).
  • Key Pair (sysadmin-key in AWS and sysadmin-key.pem on your Mac).

Keep everything running

No action needed. Choose this if you are still actively building on your server or plan to use it for further Linux practice.

  • Your instance will continue running at approximately $0.0104/hr (~$7.50/month).
  • The EBS volume adds ~$0.64/month.
  • The Security Group and Key Pair remain in your AWS account at no cost.

Pause - I'll come back to this later

Stop the instance to free up compute charges while keeping your configuration intact. You will only pay for EBS storage (~$0.64/month).

  • In the EC2 console, click **Instances** in the navigation pane.
  • Select your sysadmin-lab instance.
  • Click **Instance state**, then **Stop instance**.
  • Confirm by clicking **Stop**.

Your public IP will change when you restart the instance. To resume, select the instance, click **Instance state**, then **Start instance**, and use the new public IP to SSH in.

Delete - I don't want to use this again

Remove all project resources and stop all charges permanently.

Terminate the EC2 instance:

  • In the EC2 console, click **Instances** in the navigation pane.
  • Select your sysadmin-lab instance.
  • Click **Instance state**, then **Terminate instance**.
  • Confirm by clicking **Terminate**. This permanently deletes the server and its attached EBS volume.

Delete the Security Group:

  • Wait a minute for the instance to fully terminate.
  • In the EC2 console navigation pane, click **Security Groups**.
  • Select the security group you created for this project.
  • Click **Actions**, then **Delete security groups**.
  • Type Delete in the confirmation field and click **Delete**.

Delete the Key Pair from AWS:

  • In the EC2 console navigation pane, click **Key Pairs**.
  • Select sysadmin-key and click **Actions**, then **Delete**.
  • Type Delete in the confirmation field and click **Delete**.

Delete the local .pem file from your Mac:

  • Remove the key file by running this command in your Mac terminal:
rm ~/Downloads/sysadmin-key.pem

All project resources are now removed and no further charges will apply.

Nice Work!

Nice Work!

You just deployed, secured, and automated a Linux server in the cloud from scratch. That is the core sysadmin workflow that every infrastructure job posting asks for.

You've learned how to:

  • Launch an EC2 instance and connect securely via SSH key-based authentication from your Mac.
  • Harden SSH and configure a UFW firewall with a deny-by-default policy, locking your server down to production standards.
  • Deploy Nginx and automate system health monitoring with a Bash script running on a systemd timer.
  • Secret Mission: Install Fail2Ban with UFW integration to automatically ban brute-force attackers.

Ready to quiz yourself?