Ansible Playbooks — Automate VPS Hardening, User Creation & Removal
Introduction
In this post I'm sharing 3 Ansible playbooks I use to manage my VPS servers. I won't go into great detail on tasks here — if you're new to Ansible check out my previous post and video first.
The three playbooks covered today:
-
basic-secure.yml— automate VPS hardening -
add-vps-user.yml— semi-automated user creation -
remove-vps-user.yml— completely remove a user and their privileges
📚 Resources:
The Basics
A task is broken up into four parts:
| Part | Description |
|---|---|
| Name | A plain-text description of what the task does |
| Collection | The Ansible content bundle the module belongs to |
| Module | The tool that executes the action |
| Parameters | The specific options passed to the module |
Playbook 1 — basic-secure.yml
This playbook fully hardens a fresh Ubuntu VPS in a single command. It prompts you for a custom admin username, generates a random 16-character password, configures UFW, installs Fail2Ban, and moves SSH to port 2222.
---
- name: Harden Ubuntu VPS Security Configuration
hosts: vpsDemo
gather_facts: true
vars_prompt:
- name: 'custom_admin_user'
prompt: 'Enter the custom username for your main administrator account'
private: false
tasks:
# 1. GENERATE RANDOM PASSWORD
- name: Generate random password
set_fact:
new_admin_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits,hexdigits length=16') }}"
# 2. CREATE SUDO USER
- name: Ensure the custom admin user exists
ansible.builtin.user:
name: '{{ custom_admin_user }}'
password: "{{ new_admin_password | password_hash('sha512') }}"
shell: /bin/bash
state: present
groups: sudo
append: true
- name: Allow the admin user to use sudo without a password prompt
ansible.builtin.copy:
content: "{{ custom_admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
dest: '/etc/sudoers.d/{{ custom_admin_user }}'
mode: '0440'
validate: /usr/sbin/visudo -cf %s
# 3. CONFIGURE UFW FIREWALL
- name: Reset UFW to default settings
community.general.ufw:
state: reset
- name: Set UFW default policies to deny incoming
community.general.ufw:
policy: deny
direction: incoming
- name: Open Port 80 (HTTP)
community.general.ufw:
rule: allow
port: '80'
proto: tcp
- name: Open Port 443 (HTTPS)
community.general.ufw:
rule: allow
port: '443'
proto: tcp
- name: Open Custom SSH Port 2222
community.general.ufw:
rule: allow
port: '2222'
proto: tcp
- name: Enable UFW Firewall
community.general.ufw:
state: enabled
# 4. INSTALL FAIL2BAN
- name: Install Fail2Ban
ansible.builtin.apt:
name: fail2ban
state: present
update_cache: true
- name: Ensure Fail2Ban is running and enabled on boot
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
# 5. HARDEN SSH
- name: Configure SSH to use custom port 2222
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?Port\s'
line: 'Port 2222'
state: present
- name: Disable Root SSH Login
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#?PermitRootLogin\s'
line: 'PermitRootLogin no'
state: present
# 6. FIX SYSTEMD SSH SOCKET
- name: Create systemd override directory for SSH socket
ansible.builtin.file:
path: /etc/systemd/system/ssh.socket.d
state: directory
mode: '0755'
- name: Write dual-stack IPv4/IPv6 socket configuration
ansible.builtin.copy:
dest: /etc/systemd/system/ssh.socket.d/listen.conf
mode: '0644'
content: |
[Socket]
ListenStream=
ListenStream=0.0.0.0:2222
ListenStream=[::]:2222
- name: Reload systemd daemon
ansible.builtin.systemd_service:
daemon_reload: true
- name: Restart SSH socket
ansible.builtin.service:
name: ssh.socket
state: restarted
- name: Restart SSH service
ansible.builtin.service:
name: ssh
state: restarted
# 7. DISPLAY CREDENTIALS
- name: Display your new credentials (SAVE THESE IMMEDIATELY)
ansible.builtin.debug:
msg:
- '========================================================'
- 'NEW SUDO USERNAME: {{ custom_admin_user }}'
- 'NEW PASSWORD: {{ new_admin_password }}'
- 'CUSTOM SSH PORT: 2222'
- '========================================================'
⚠️ Save the displayed credentials immediately — the generated password is only shown once.
Playbook 2 — add-vps-user.yml
Creates a new sudo user with a randomly generated secure password and prints the credentials to your screen.
---
- name: Universal Semi-Automated User Creation Script
hosts: vpsDemo
gather_facts: false
become: true
vars_prompt:
- name: 'custom_admin_user'
prompt: 'Enter the custom username for this new administrator account'
private: false
tasks:
- name: Generate random secure password
set_fact:
new_random_password: "{{ lookup('password', '/dev/null chars=ascii_letters,digits length=16') }}"
- name: Ensure the new user account exists
ansible.builtin.user:
name: '{{ custom_admin_user }}'
password: "{{ new_random_password | password_hash('sha512') }}"
shell: /bin/bash
state: present
groups: sudo
append: true
- name: Allow the new user to use sudo without a password prompt
ansible.builtin.copy:
content: "{{ custom_admin_user }} ALL=(ALL) NOPASSWD:ALL\n"
dest: '/etc/sudoers.d/{{ custom_admin_user }}'
mode: '0440'
validate: /usr/sbin/visudo -cf %s
- name: Display New User Credentials
ansible.builtin.debug:
msg:
- '========================================================'
- 'NEW ADMINISTRATIVE ACCOUNT CREATED SUCCESSFULLY!'
- 'USERNAME: {{ custom_admin_user }}'
- 'PASSWORD: {{ new_random_password }}'
- '========================================================'
Playbook 3 — remove-vps-user.yml
Completely purges a user account, their home directory, mail spool, and sudo privileges from the server.
---
- name: Universal User and Privilege Removal Script
hosts: all
gather_facts: false
become: true
vars_prompt:
- name: 'user_to_delete'
prompt: 'Enter the exact username you want to COMPLETELY delete'
private: false
tasks:
- name: Delete the user's custom sudoers configuration file
ansible.builtin.file:
path: '/etc/sudoers.d/{{ user_to_delete }}'
state: absent
- name: Remove the user account and purge their files
ansible.builtin.user:
name: '{{ user_to_delete }}'
state: absent
remove: true # deletes home directory and mail spool
force: true # kills any active processes owned by the user
ignore_errors: true
- name: Display Removal Confirmation
ansible.builtin.debug:
msg:
- '========================================================'
- "SUCCESS: Account '{{ user_to_delete }}' and their sudo privileges"
- 'have been completely purged from the server.'
- '========================================================'
Conclusion
I hope these playbooks are useful — grab them, adapt them to your environment and save yourself hours of repetitive manual work. I'll be sharing more playbooks as I build them out.
Blessings. 🙏
Looking for developer templates built on TanStack, Directus, and Tailwind CSS?
🛒 northernrangedigital.lemonsqueezy.com
🌐 northernrangedigital.com
Top comments (0)