# The Ansible Playbook that will Harden Your VPS

%[https://www.youtube.com/watch?v=vl8IW8F1mxA] 

## 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:**
> 
> *   [Ansible Documentation](https://docs.ansible.com)
>     
> *   [Ansible Collection Index](https://docs.ansible.com/ansible/latest/collections/index.html)
>     

* * *

## 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.

```yaml
---
- 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.

```yaml
---
- 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.

```yaml
---
- 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*](https://northernrangedigital.lemonsqueezy.com/) *🌐* [*northernrangedigital.com*](https://northernrangedigital.com)
