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.
Prerequisites
- A newly-provisioned or rebuilt server running any of our OS options—CentOS, Debian, or Ubuntu.
- Ansible installed on your local machine— see these instructions for more details
- An Ansible hosts file set up with the IP(s) of your server(s)—see Step 2 of our previous tutorial
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:
provision.yml
roles
common/
tasks/
main.yml
ssh
tasks/
main.yml
packages
tasks/
main.yml
iptables
tasks/
main.yml
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
vars:
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/id_rsa.pub
roles:
- 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
group:
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
lineinfile:
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
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
user:
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)
apt:
upgrade: dist
when: ansible_os_family == "Debian" or ansible_os_family == "Ubuntu"
- name: Upgrading all packages (CentOS)
yum:
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
package:
name: "{{item}}"
state: installed
with_items:
- 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
authorized_key:
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
lineinfile:
dest: /etc/ssh/sshd_config
regexp: "{{item.regexp}}"
line: "{{item.line}}"
state: present
with_items:
- 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
systemd:
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.
A note about tutorials: We encourage our users to try out tutorials, but they aren't fully supported by our team—we can't always provide support when things go wrong. Be sure to check which OS and version it was tested with before you proceed.
If you want a fully managed experience, with dedicated support for any application you might want to run, contact us for more information.