Ansible security playbook for your VPS (part 1)

In our last Ansible tutorial, we covered the basics of using Ansible for configuration management, which can help you get new servers set up faster and more reliably.

But the Ansible security playbook that we created there was pretty basic, so I thought we would show create a new playbook that supports more security out of the box without sacrificing normal access to the server.

The goals for this Ansible security playbook are:

  • Set up a non-root user with sudo access
  • Upgrade all installed packages
  • Install a few basic packages to make initial management easier, like nano. These can be easily customized according to your needs
  • Copy your SSH key to the VPS to enable password-less logins
  • Harden SSH with some basic security measures, such as disabling root and password-based logins
  • Install iptables if needed, and set up some basic restrictions to improve security
  • Install fail2ban to help prevent brute force attacks

(The final two steps will be outlined in part 2 of this Ansible security tutorial).

The goal here isn't to have you copy the code here and recreate your own playbook-- it's to teach you how to do it for yourself.

We'll walk you through the various components step-by-step so that you can use this playbook as a foundation for your own customizations.

And this playbook isn't comprehensive when it comes to security for your VPS—once you provision the server using this playbook, you may want to research some additional steps, such as using Lynis to audit your security.


  1. A newly-provisioned or rebuilt server running any of our OS options—CentOS, Debian, or Ubuntu.
  2. Ansible installed on your local machine—see these instructions for more details
  3. An Ansible hosts file set up with the IP(s) of your server(s)—see Step 2 of our previous tutorial

What's the BEST DEAL in cloud hosting?

Develop at hyperspeed with a Performance VPS from SSD Nodes. We DOUBLED the amount of blazing-fast NVMe storage on our most popular plan and beefed up the CPU offering on these plans. There's nothing else like it on the market, at least not at these prices.

Score a 16GB Performance VPS with 160GB of NVMe storage for just $99/year for a limited time!

Get limited-time deals!⚡

Step 1. Setting up the playbook structure

Ansible playbooks can be structured in a number of different ways, but the developers do have their recommendations.

This Ansible script is still relatively simple compared to what's possible with the system, so our structure is going to be far simpler as well.

Here's the general structure we're following:



If you want, you can go ahead and create the directories now, just to give you a better sense as to how the playbook separates its logic into different areas.

Step 2. Creating provision.yml

The provision.yml file is the core of our playbook—it's where we define which servers we're going to be working with, a few global variables, and tell Ansible where to look for its tasks.

- name: Provision a new server with hardened SSH and basic iptables.

  # Specify the hosts you want to target
  hosts: HOSTNAME

  # Specify the user you want to connect to the server.
  # With a new installation, you will connect with `root`. If you want to
  # re-run this playbook at a later date, you should change `remote_user` to
  # the user you specified under `vars/username` below and uncomment the
  # `become: true` line. You should then run the playbook using the
  # `--ask-become-pass` flag, like so:
  # `ansible-playbook -k provision.yml --ask-become-pass`.
  remote_user: root
  # become: true

    username: USER
    # Before first using the playbook, run the below command to create a hashed
    # password that Ansible will assign to your new user.
    # python -c 'import crypt; print crypt.crypt("<b>password</b>", "$1$<b>SALT</b>$")'
    password: PASSWORD
    public_key: ~/.ssh/

    - user
    - packages
    - ssh
    - iptables

There are a number of variables that you will need to change according to your needs.

Step 3. Creating user/tasks/main.yml

Our first major step is setting up the right environment for a new non-root user, and then creating that user. Here's the first component:

- name: Ensure wheel group is present
    name: wheel
    state: present

This Ansible task is a simple one: it checks to see if the wheel group exists on your server. If it doesn't for some reason—it should on all our OS options—the playbook will fail, and then you can fix it with the groupadd command.

The next step is a critical one, so let's take a look:

- name: Ensure wheel group has sudo privileges
    dest: /etc/sudoers
    state: present
    regexp: "^%wheel"
    line: "%wheel ALL=(ALL:ALL) ALL"
    validate: "/usr/sbin/visudo -cf %s"

This is an example of using regex to replace one line within a file with a different string of text.

We're looking inside of the /etc/sudoers file, and requesting a line that begins (^) with %wheel. When that line is found, we replace the entire line with %wheel ALL=(ALL:ALL) ALL, which allows users in the wheel group to execute commands using sudo.

When it comes to editing /etc/sudoers, the final validate line is critical, as you would rather the playbook fail due to an improper file than break your administrator capabilities.

We want to make sure the sudo package is installed as well.

- name: Install the `sudo` package
    name: sudo
    state: latest

Installing any package, whether it's for CentOS, Ubuntu, or Debian, works this exact same way. That's the beauty of Ansible—you can create one task that works anywhere due to the built-in logic.

Finally, we create the non-root user account that was specified in the variables in provision.yml.

- name: Create the non-root user account
    name: ""
    password: ""
    shell: /bin/bash
    update_password: on_create
    groups: wheel
    append: yes

This tasks sets up the user with the hashed password you created, and sets the shell to /bin/bash. Because we're putting this user in the wheel group, we'll be able to use sudo straightaway.

Step 4. Creating packages/tasks/main.yml

The packages task is really simple: we just want to update all packages so that we have the latest in security fixes, and then install any number of extra packages according to our specific needs.

- name: Upgrading all packages (Ubuntu/Debian)
    upgrade: dist
  when: ansible_os_family == "Debian" or ansible_os_family == "Ubuntu"

- name: Upgrading all packages (CentOS)
    name: '*'
    state: latest
  when: ansible_os_family == "RedHat"

The key to these two tasks is the when option—this allows you specify when to run certain commands depending on the OS you've chosen. This is necessary, because yum won't work on Ubuntu, and apt won't work on CentOS.

In either case, we're simply asking the respective package manager to update every installed package.

We can also install additional packages:

- name: Install a few more packages
    name: "{{item}}"
    state: installed
   - vim
   - htop

Essentially, we're asking the package task to look through the list of items under with_items and install each of them in sequence. If you want some of your own packages, just customize that list to your heart's content.

Step 5. Creating ssh/tasks/main.yml

Next up, we want to enable logging into the newly-created user with SSH keys rather than passwords—a simple-but-effective VPS security measure.

Beyond that, we want to use Ansible to make some configuration changes to the SSH daemon that will harden it against some basic attacks. It's not foolproof, but it's a big step above the defaults.

- name: Add local public key for key-based SSH authentication
    user: ""
    state: present
    key: ""

This command looks for an SSH key on the local machine at the location specified in the vars section in provision.yml and then copies it to the server. Much easier than using ssh-copy-id, eh?

Next, let's make SSH a little more secure.

- name: Harden sshd configuration
    dest: /etc/ssh/sshd_config
    regexp: "{{item.regexp}}"
    line: "{{item.line}}"
    state: present
    - regexp: "^#?PermitRootLogin"
      line: "PermitRootLogin no"
    - regexp: "^^#?PasswordAuthentication"
      line: "PasswordAuthentication no"
    - regexp: "^#?AllowAgentForwarding"
      line: "AllowAgentForwarding no"
    - regexp: "^#?AllowTcpForwarding"
      line: "AllowTcpForwarding no"
    - regexp: "^#?MaxAuthTries"
      line: "MaxAuthTries 2"
    - regexp: "^#?MaxSessions"
      line: "MaxSessions 2"
    - regexp: "^#?TCPKeepAlive"
      line: "TCPKeepAlive no"
    - regexp: "^#?UseDNS"
      line: "UseDNS no"
    - regexp: "^#?AllowAgentForwarding"
      line: "AllowAgentForwarding no"

The lineinfile and regexp should look familiar to you at this point—as with making changes to /etc/sudoers, we're looking at /etc/ssh/sshd_config and replacing a number of existing lines with new ones. If the lines don't currently exist, Ansible will create new lines at the bottom of the file containing our revisions. The ^#? regex allows us to replace lines whether or not they're commented out, and thus begin with a #.

Finally, let's have the SSD daemon to make sure our changes are applied.

- name: Restart sshd
    state: restarted
    daemon_reload: yes
    name: sshd

This systemd task allows us to run the equivalent of systemd restart sshd.

Final thoughts

As mentioned above, we're hitting pause here for a moment and will return a week from now with the second half of this Ansible security playbook tutorial, which will walk through a basic iptables configuration, and install fail2ban.

But, in the meantime, you can run this playbook now and later.

That's the great thing about a correctly-configured Ansible playbook— they are idempotent, which means they can be run again and again without changing the result beyond the initial installation. You can run the playbook once, make a small change such as adding another package to be installed under the packages role, and then run the playbook again without error.

Once you get everything up and running, how do you actually run this playbook? It's pretty straightforward.

Generate a hashed password. You first need to convert the password you want for your non-root user into a hashed password. This command should work on Linux and OS X, and be sure to replace password with your chosen password: python -c 'import crypt; print crypt.crypt("password", "$1$AnsibleSalt$")'.

Copy your new hash into provision.yml.

Run Ansible. Running the playbook itself is straightforward, with one simple command: $ ansible-playbook -k provision.yml

If you need to re-run the playbook after the first run, you'll need to make some changes to provision.yml—look at the code above (and in the version you've created) for some basic instructions.

Stay tuned for next week's conclusion, along with the full code you need to run this playbook on your own servers and get to work faster than ever.