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 modulePlaybook 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'
- '========================================================'
Enter fullscreen mode Exit fullscreen mode
⚠️ 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 }}'
- '========================================================'
Enter fullscreen mode Exit fullscreen mode
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.'
- '========================================================'
Enter fullscreen mode Exit fullscreen mode
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
답글 남기기