Speaking of security, there is a very nice article about how to secure your Ubuntu server in 10 minutes. In this blog post, I’ll try to use the hints from that article, and create a playbook that should apply everything within 30 seconds.
From the official Ansible website:
“Ansible is a radically simple IT automation engine that automates cloud provisioning, configuration management, application deployment, intra-service orchestration, and many other IT needs.
Designed for multi-tier deployments since day one, Ansible models your IT infrastructure by describing how all of your systems inter-relate, rather than just managing one system at a time.
It uses no agents and no additional custom security infrastructure, so it's easy to deploy - and most importantly, it uses a very simple language (YAML, in the form of Ansible Playbooks) that allows you to describe your automation jobs in a way that approaches plain English.”
In other words - by having a simple YAML file describing a “playbook”, we can easily deploy changes to our server without drilling deep into the system. Ansible will do this for us. It contains a lot of different modules which should be enough to - say - quickly secure a “fresh” machine.
I assume you already have a running machine and you want to provision it using Ansible. It is not the time or place to cover how to start a server, but you can just grab a simple t2.micro EC2 Ubuntu instance from Amazon for that purpose (t2.micro is free for 1 year for newcomers!) - so we’ll only focus on the Ansible part here.
The official website contains multiple instructions on how to install Ansible on various systems and distributions. Remember that you only have to install Ansible on your local laptop, not on the server!
I’m using a Mac, and it is also possible to install Ansible via brew, so that’s what I’ll just do now:
Ansible needs you to have Python installed on a remote machine, so log in to your remote machine, and execute:
All right, now you’re ready to rustle up your first playbook!
Ansible allows you to create a complex yet clean configuration that can be applied to multiple machines simultaneously. This configuration includes playbooks, roles, inventory files, group vars, host vars, tags and other neat solutions. We will only focus here on playbook and inventory because these are the bare necessities when it comes to using Ansible. If you want to use other aspects of Ansible, go right ahead :) there is a very nice documentation on how to use all of these things here.
Ok, first off - let’s create a simple directory on your local machine where we will store all the necessary files. Let’s call it “ansible”.
Next, we need two files: inventory and playbook.yml:
Now, edit the inventory file and input the following content:
Note - replace the IP_ADDRESS with the actual IP address of your remote machine. This file defines which machines Ansible should provision the changes. Here, we created a “linux” group and defined one machine which Ansible will try to log in using the “ubuntu” user. The user may, of course, be different, depending on your setup, so please feel free to make any changes that may apply. The only requirement here is that the user must have sudo privileges (you may also use “root”).
To check if the inventory file is correct, we can ask Ansible to gather the “facts” about the machine using the following command:
Ansible will take the hosts from the “linux” group using the “inventory” file and run the “setup” module. If everything is correct, you should receive a long green-coloured json-like output describing your machine - from the IP address, through the architecture, date, devices and so on. If you come up with an error at this point, you’ll need to fix the configuration accordingly.
Next, edit the playbook.yml file and paste:
This is the minimum definition of a playbook. It doesn’t contain any tasks so far (we’ll add them soon), but by using these lines, Ansible already knows the following:
Right, so let’s create our first user using Ansible playbook. Indentation is important in the yaml file so make sure that the “tasks” line is on the same level as “become”. Add the following code to “playbook.yml”:
This is a simple task which will create an “admin” account to your system and assign a /bin/bash shell for it. Right now, you can’t log into this account because it has neither a password nor a ssh-key attached. Let’s give it a key. Pop these additional lines into the playbook:
Of course, replace SSH_KEY with your own key. Now, let’s run the playbook and see the output. You can check out the playbook using the following command:
The expected output should look like this:
Two changes have been applied:
If everything goes as planned, you should be able to ssh to your machine using the “admin” account.
Ok, now what happens when we run the same playbook again? Well, the output should look like this:
So, this time there were no changes. Ansible already created an account and added the key in the previous playbook run, so there was nothing to do the second time around. Ansible is smart enough to know whether any changes have to be made or not.
Ok, let’s dust ourselves off and write a playbook based on the aforementioned “10-minute” article. First, let’s remove the “admin” account since it will no longer be necessary. Remove these two tasks from the playbook and create this one:
Then run the playbook. This will remove the admin account, and we can proceed to write tasks from the article.
Let’s take the article step-by-step and switch the result over to Ansible tasks
1. First Things First
Let’s bring it down a bit. First - we are performing a default update & upgrade thing to install the latest patches and software. Then, we have to create a random password and assign a root user. At this point, we’re using two tools: pwgen and whois. Pwgen allows you to generate a random password string, while the whois package contains the mkpasswd utility which can create an encrypted password as is required by Ansible to set a password for a specific user. So, what happens here is this:
2. Add Your User
This is something we’ve already done, so:
Here we create a “deploy” user, assign /bin/bash shell and add this user to the “sudo” group (the sudo group should already exist in /etc/sudoers in Ubuntu by default, but if it doesn’t, refer to the article above on how to add it). The next steps are similar to the “root” user - we create a random password and assign it to the “deploy” user. The password will be used when switching to the “root”.
3. Enforce ssh Key Logins
Here we will manipulate the ssh config file using the lineinfile module:
I’m adding only the PermitRootLogin and PasswordAuthentication options. If you want to include both AllowUsers and AddressFamily, you can do this in a similar way.
Please note that we also use here the “notify” command. It means that if a specific task has made any changes, we’ll want to “notify” a specific handler about it. In this case, the handler is named “Restart ssh”. But what does that do? Well, we have to configure it by ourselves. Handlers are defined on the same level as “tasks” in the playbook, and they can use the same modules as tasks but are executed only once – at the end of all tasks – regardless of how many times they were notified. This is a useful feature that can be used here, for example instead of restarting the SSH server each time we change something in it. Instead, we’ll just do it once, after all the tasks are completed. Here is the code for defining this handler (it should be positioned just right after the basic definition of a playbook, before the tasks):
We will simply restart the ssh service here. :)
4. Setting up a Firewall
Ufw is installed in Ubuntu by default, so we don’t have to install it using Ansible. Instead, we just have to configure it. Ansible has an ufw module by default, so we can use it to manipulate a firewall:
In this scenario, I allow port 22 globally, but if you want to have it restricted as described in the article, you could use this line for port 22:
Of course, replacing IP with your IP Address. :)
5. Automated Security Updates
Once again, we’ll just use the mix of apt and lineinfile modules:
The default configuration for
already includes the configuration for upgrading security packages only, so we don’t have to alter that file.
This one is easy – just install the fail2ban package:
7. Factor Authentication
Ansible does not support pure interactive action, so we cannot set it up completely via Ansible, although we can install the package, so:
- apt: name=libpam-google-authenticator state=present
When all the tasks are complete, just log into the remote machine and run the “google-authenticator” command to set it up (using the desired user). However, this is not enough to enable the PAM / ChallengeResponseAuthentication for SSH. For more information about that, please refer to this article.
Here we need to install a package and set-up cron to send daily mail about the logs:
That’s basically all there is to it. The whole playbook should look something like this:
So, in around 70 lines we have a ready-to-go Ansible playbook which can be run on a fresh machine.
I encourage you to look further into Ansible. There are some topics that I didn’t go into like how to use roles, variables and many other modules. You can write your own scripts or use the ones written by the Ansible community using the Ansible Galaxy. Check it out - it’s a very nice tool!