So we have our droplet and our admin
user. The next step is securing our web server before we start deploying app.
🧹 Let’s Tidy Things Up: From Playbooks to Roles
Previously, we created a separate playbook file for each task (like creating the admin user). While this works for small projects, it quickly becomes unmanageable.
Instead, we’ll use Ansible roles, which help organize related tasks, handlers, variables, and files into reusable units.
📘 Official documentation:
👉 Ansible Roles – Reuse and Organize Your Playbooks
📁 Step 1: Set Up the Role Directory
Let’s create a folder structure for our first role:
mkdir -p roles/security/tasks
mkdir -p roles/security/handlers
We’ll be configuring the handlers
directory in a moment, but first let’s define the security tasks.
🔐 Step 2: Define the Security Tasks
Create the file:roles/security/tasks/main.yml
# roles/security/tasks/main.yml
---
- name: Update and upgrade APT packages
apt: update_cache=yes upgrade=dist
- name: Install essential packages
apt:
name:
- curl
- unzip
- zip
- software-properties-common
state: present
# SSH Security
- name: Disable root SSH login
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PermitRootLogin'
line: 'PermitRootLogin no'
state: present
- name: Disable password authentication
lineinfile:
path: /etc/ssh/sshd_config
regexp: '^PasswordAuthentication'
line: 'PasswordAuthentication no'
state: present
notify:
- Restart ssh
# UFW
- name: Install ufw
apt: name=ufw state=present update_cache=yes
- name: Configure ufw
ufw: rule=allow name=OpenSSH
- name: Enable UFW
ufw: state=enabled
# Fail2ban
- name: Install fail2ban
apt: name=fail2ban state=present
notify:
- Enable fail2ban
# Unattended Upgrades
- name: Install unattended-upgrades
apt: name=unattended-upgrades state=present
- name: Enable unattended security updates
copy:
dest: /etc/apt/apt.conf.d/20auto-upgrades
content: |
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
🔍 Let’s Break It Down
APT upgrade: Ensures the system is up-to-date.
Essential packages: Useful tools that we’ll need later.
SSH hardening:
- Disables root login and password-based login.
- Only users with SSH keys (like our
admin
user) can connect.
UFW (Uncomplicated Firewall):
- Blocks all traffic by default, then allows only SSH.
Fail2ban:
- Helps block malicious login attempts like brute force SSH attacks.
Unattended upgrades:
Automatically installs security updates in the background.
🔁 Step 3: Add Service Handlers
We need handlers to restart or enable services after certain changes.
Create the file:roles/security/handlers/main.yml
# roles/security/handlers/main.yml
---
- name: Restart ssh
service: name=ssh state=restarted
- name: Enable fail2ban
service: name=fail2ban enabled=yes state=started
📜 Step 4: Add the Role to Your Playbook
Create the main playbook file:main.yml
# main.yml
- hosts: all
gather_facts: false
become: true
vars:
username: admin
remote_user: "{{ username }}"
roles:
- role: roles/basic-setup
🚀 Step 5: Run the Playbook
play main.yml
Then, test your setup:
ssh root@<droplet ip>
Should result in an error:
root@142.93.170.130: Permission denied (publickey).
✅ That means root login is now disabled—just what we want.
🧪 Step 6: Verify the Setup
🔥 UFW (Firewall)
SSH into the droplet with your admin
user and run:
sudo ufw status
You should see output like:
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
🔒 Fail2ban
To verify fail2ban is running:
sudo systemctl status fail2ban
Look for something like:
Active: active (running)
For deeper inspection:
sudo fail2ban-client status
This shows which jails are active (e.g., sshd
).
Or wait a little while and see bans comming in at:
sudo cat /var/log/fail2ban.log | grep -i ' ban'
.
🔁 Unattended Upgrades
To check that it’s working, run:
sudo cat /etc/apt/apt.conf.d/20auto-upgrades
You should see:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
You can also confirm the service is enabled with:
sudo systemctl status unattended-upgrades
✅ What We’ve Accomplished
In this article, we:
- Organized our Ansible config using a role
- Hardened the server by:
- Disabling root SSH and password login
- Setting up a basic firewall
- Installing fail2ban for brute-force protection
- Enabling automatic security updates
- Created a reusable playbook for future provisioning
🔜 Coming Up Next…
In the next part of the series, we’ll install Nginx to serve our app and act as a reverse proxy. Stay tuned 👋
Leave a Reply