{"id":5539,"date":"2018-10-31T00:00:00","date_gmt":"2018-10-31T00:00:00","guid":{"rendered":"http:\/\/ssdnodes.billabailey.com\/2017\/08\/31\/tutorial-a-more-secure-ansible-playbook-part-1\/"},"modified":"2025-05-18T13:05:56","modified_gmt":"2025-05-18T13:05:56","slug":"secure-ansible-playbook","status":"publish","type":"post","link":"https:\/\/www.ssdnodes.com\/blog\/secure-ansible-playbook\/","title":{"rendered":"Ansible security playbook for your VPS (part 1)"},"content":{"rendered":"<p>In our last <a href=\"https:\/\/www.ssdnodes.com\/blog\/tutorial-getting-started-with-ansible-and-configuration-management\/\">Ansible tutorial<\/a>, we covered the basics of using <a href=\"https:\/\/www.ssdnodes.com\/blog\/curious-about-configuration-management-for-your-vps\/\" target=\"_blank\" rel=\"noopener noreferrer\">Ansible for configuration management<\/a>, which can help you get new servers set up faster and more reliably.<\/p>\n<p>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.<\/p>\n<p>The goals for this Ansible security playbook are:<\/p>\n<ul>\n<li>Set up a non-root user with sudo access<\/li>\n<li>Upgrade all installed packages<\/li>\n<li>Install a few basic packages to make initial management easier, like <code>nano<\/code>. These can be easily customized according to your needs<\/li>\n<li>Copy your SSH key to the VPS to enable password-less logins<\/li>\n<li>Harden SSH with some basic security measures, such as disabling root and password-based logins<\/li>\n<li>Install <code>iptables<\/code> if needed, and set up some basic restrictions to improve security<\/li>\n<li>Install <code>fail2ban<\/code> to help prevent brute force attacks<\/li>\n<\/ul>\n<p>(The final two steps will be outlined in <a href=\"https:\/\/www.ssdnodes.com\/blog\/secure-ansible-playbook-2\/\" target=\"_blank\" rel=\"noopener noreferrer\">part 2<\/a> of this Ansible security tutorial).<\/p>\n<p>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.<\/p>\n<p>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.<\/p>\n<p>And this playbook isn't comprehensive when it comes to security for your VPS\u2014once you provision the server using this playbook, you may want to research some additional steps, such as <a href=\"https:\/\/www.ssdnodes.com\/blog\/tutorial-vps-security-audits-using-lynis\/\">using Lynis to audit your security<\/a>.<\/p>\n<h2>Prerequisites<\/h2>\n<ol>\n<li>A newly-provisioned or rebuilt server running any of our OS options\u2014CentOS, Debian, or Ubuntu.<\/li>\n<li>Ansible installed on your local machine\u2014 see these instructions for more details<\/li>\n<li>An Ansible hosts file set up with the IP(s) of your server(s)\u2014see Step 2 of our <a href=\"https:\/\/www.ssdnodes.com\/blog\/tutorial-getting-started-with-ansible-and-configuration-management\/\">previous tutorial<\/a><\/li>\n<\/ol>\n<div class=\"cta-inline\"><\/div>\n<h2>Step 1. Setting up the playbook structure<\/h2>\n<p>Ansible playbooks can be structured in a number of different ways, but the <a href=\"http:\/\/docs.ansible.com\/ansible\/latest\/playbooks_best_practices.html#directory-layout\" target=\"_blank\" rel=\"noopener\">developers do have their recommendations<\/a>.<\/p>\n<p>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.<\/p>\n<p>Here's the general structure we're following:<\/p>\n<pre><code class=\"hljs\">provision.yml\n\nroles\n  common\/\n    tasks\/\n      main.yml\n  ssh\n    tasks\/\n      main.yml\n  packages\n    tasks\/\n      main.yml\n  iptables\n    tasks\/\n      main.yml\n<\/code><\/pre>\n<p>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.<\/p>\n<h2>Step 2. Creating provision.yml<\/h2>\n<p>The <code>provision.yml<\/code> file is the core of our playbook\u2014it'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.<\/p>\n<pre><code class=\"language-yml\">---\n- name: Provision a new server with hardened SSH and basic iptables.\n\n  # Specify the hosts you want to target\n  hosts: HOSTNAME\n\n  # Specify the user you want to connect to the server.\n  # With a new installation, you will connect with <code>root<\/code>. If you want to\n  # re-run this playbook at a later date, you should change <code>remote_user<\/code> to\n  # the user you specified under <code>vars\/username<\/code> below and uncomment the\n  # <code>become: true<\/code> line. You should then run the playbook using the\n  # <code>--ask-become-pass<\/code> flag, like so:\n  # <code>ansible-playbook -k provision.yml --ask-become-pass<\/code>.\n  remote_user: root\n  # become: true\n\n  vars:\n    username: USER\n    # Before first using the playbook, run the below command to create a hashed\n    # password that Ansible will assign to your new user.\n    # python -c 'import crypt; print crypt.crypt(\"&lt;b&gt;password&lt;\/b&gt;\", \"$1$&lt;b&gt;SALT&lt;\/b&gt;$\")'\n    password: PASSWORD\n    public_key: ~\/.ssh\/id_rsa.pub\n\n  roles:\n    - user\n    - packages\n    - ssh\n    - iptables\n<\/code><\/pre>\n<p>There are a number of variables that you will need to change according to your needs.<\/p>\n<h2>Step 3. Creating user\/tasks\/main.yml<\/h2>\n<p>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:<\/p>\n<pre><code class=\"hljs cs\">- name: Ensure wheel <span class=\"hljs-keyword\">group<\/span> <span class=\"hljs-keyword\">is<\/span> present\n  <span class=\"hljs-keyword\">group<\/span>:\n    name: wheel\n    state: present\n<\/code><\/pre>\n<p>This Ansible task is a simple one: it checks to see if the <code>wheel<\/code> group exists on your server. If it doesn't for some reason\u2014it should on all our OS options\u2014the playbook will fail, and then you can fix it with the <code>groupadd<\/code> command.<\/p>\n<p>The next step is a critical one, so let's take a look:<\/p>\n<pre><code class=\"hljs perl\">- name: Ensure wheel group has sudo privileges\n  lineinfile:\n    dest: <span class=\"hljs-regexp\">\/etc\/sudoers<\/span>\n    <span class=\"hljs-keyword\">state<\/span>: present\n    regexp: <span class=\"hljs-string\">\"^%wheel\"<\/span>\n    line: <span class=\"hljs-string\">\"%wheel ALL=(ALL:ALL) ALL\"<\/span>\n    validate: <span class=\"hljs-string\">\"\/usr\/sbin\/visudo -cf %s\"<\/span>\n<\/code><\/pre>\n<p>This is an example of using regex to replace one line within a file with a different string of text.<\/p>\n<p>We're looking inside of the <code>\/etc\/sudoers<\/code> file, and requesting a line that begins (<code>^<\/code>) with <code>%wheel<\/code>. When that line is found, we replace the entire line with <code>%wheel ALL=(ALL:ALL) ALL<\/code>, which allows users in the <code>wheel<\/code> group to execute commands using <code>sudo<\/code>.<\/p>\n<p>When it comes to editing <code>\/etc\/sudoers<\/code>, the final <code>validate<\/code> line is <strong>critical<\/strong>, as you would rather the playbook fail due to an improper file than break your administrator capabilities.<\/p>\n<p>We want to make sure the <code>sudo<\/code> package is installed as well.<\/p>\n<pre><code class=\"hljs sql\">- name: <span class=\"hljs-keyword\">Install<\/span> the <span class=\"hljs-string\"><code>sudo<\/code><\/span> <span class=\"hljs-keyword\">package<\/span>\n  <span class=\"hljs-keyword\">package<\/span>:\n    <span class=\"hljs-keyword\">name<\/span>: sudo\n    state: latest\n<\/code><\/pre>\n<p>Installing any package, whether it's for CentOS, Ubuntu, or Debian, works this exact same way. That's the beauty of Ansible\u2014you can create one task that works anywhere due to the built-in logic.<\/p>\n<p>Finally, we create the non-root user account that was specified in the variables in <code>provision.yml<\/code>.<\/p>\n<pre><code class=\"hljs sql\">- name: <span class=\"hljs-keyword\">Create<\/span> the non-root <span class=\"hljs-keyword\">user<\/span> <span class=\"hljs-keyword\">account<\/span>\n  <span class=\"hljs-keyword\">user<\/span>:\n    <span class=\"hljs-keyword\">name<\/span>: <span class=\"hljs-string\">\"\"<\/span>\n    <span class=\"hljs-keyword\">password<\/span>: <span class=\"hljs-string\">\"\"<\/span>\n    shell: \/<span class=\"hljs-keyword\">bin<\/span>\/bash\n    update_password: on_create\n    <span class=\"hljs-keyword\">groups<\/span>: wheel\n    append: yes\n<\/code><\/pre>\n<p>This tasks sets up the user with the hashed password you created, and sets the shell to <code>\/bin\/bash<\/code>. Because we're putting this user in the <code>wheel<\/code> group, we'll be able to use <code>sudo<\/code> straightaway.<\/p>\n<h2>Step 4. Creating packages\/tasks\/main.yml<\/h2>\n<p>The <code>packages<\/code> 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.<\/p>\n<pre><code class=\"hljs perl\">- name: Upgrading all packages (Ubuntu\/Debian)\n  apt:\n    upgrade: dist\n  <span class=\"hljs-keyword\">when<\/span>: ansible_os_family == <span class=\"hljs-string\">\"Debian\"<\/span> <span class=\"hljs-keyword\">or<\/span> ansible_os_family == <span class=\"hljs-string\">\"Ubuntu\"<\/span>\n\n- name: Upgrading all packages (CentOS)\n  yum:\n    name: <span class=\"hljs-string\">'*'<\/span>\n    <span class=\"hljs-keyword\">state<\/span>: latest\n  <span class=\"hljs-keyword\">when<\/span>: ansible_os_family == <span class=\"hljs-string\">\"RedHat\"<\/span>\n<\/code><\/pre>\n<p>The key to these two tasks is the <code>when<\/code> option\u2014this allows you specify when to run certain commands depending on the OS you've chosen. This is necessary, because <code>yum<\/code> won't work on Ubuntu, and <code>apt<\/code> won't work on CentOS.<\/p>\n<p>In either case, we're simply asking the respective package manager to update every installed package.<\/p>\n<p>We can also install additional packages:<\/p>\n<pre><code class=\"hljs sql\">- name: <span class=\"hljs-keyword\">Install<\/span> a few more packages\n  <span class=\"hljs-keyword\">package<\/span>:\n    <span class=\"hljs-keyword\">name<\/span>: <span class=\"hljs-string\">\"{{item}}\"<\/span>\n    state: installed\n  with_items:\n   - vim\n   - htop\n<\/code><\/pre>\n<p>Essentially, we're asking the <code>package<\/code> task to look through the list of items under <code>with_items<\/code> and install each of them in sequence. If you want some of your own packages, just customize that list to your heart's content.<\/p>\n<h2>Step 5. Creating ssh\/tasks\/main.yml<\/h2>\n<p>Next up, we want to enable logging into the newly-created user with SSH keys rather than passwords\u2014a simple-but-effective VPS security measure.<\/p>\n<p>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.<\/p>\n<pre><code class=\"hljs perl\">- name: Add <span class=\"hljs-keyword\">local<\/span> public key <span class=\"hljs-keyword\">for<\/span> key-based SSH authentication\n  authorized_key:\n    user: <span class=\"hljs-string\">\"\"<\/span>\n    <span class=\"hljs-keyword\">state<\/span>: present\n    key: <span class=\"hljs-string\">\"\"<\/span>\n<\/code><\/pre>\n<p>This command looks for an SSH key on <strong>the local machine<\/strong> at the location specified in the <code>vars<\/code> section in <code>provision.yml<\/code> and then copies it to the server. Much easier than using <code>ssh-copy-id<\/code>, eh?<\/p>\n<p>Next, let's make SSH a little more secure.<\/p>\n<pre><code class=\"hljs javascript\">- name: Harden sshd configuration\n  lineinfile:\n    dest: <span class=\"hljs-regexp\">\/etc\/<\/span>ssh\/sshd_config\n    regexp: <span class=\"hljs-string\">\"{{item.regexp}}\"<\/span>\n    line: <span class=\"hljs-string\">\"{{item.line}}\"<\/span>\n    state: present\n  with_items:\n    - regexp: <span class=\"hljs-string\">\"^#?PermitRootLogin\"<\/span>\n      line: <span class=\"hljs-string\">\"PermitRootLogin no\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^^#?PasswordAuthentication\"<\/span>\n      line: <span class=\"hljs-string\">\"PasswordAuthentication no\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?AllowAgentForwarding\"<\/span>\n      line: <span class=\"hljs-string\">\"AllowAgentForwarding no\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?AllowTcpForwarding\"<\/span>\n      line: <span class=\"hljs-string\">\"AllowTcpForwarding no\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?MaxAuthTries\"<\/span>\n      line: <span class=\"hljs-string\">\"MaxAuthTries 2\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?MaxSessions\"<\/span>\n      line: <span class=\"hljs-string\">\"MaxSessions 2\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?TCPKeepAlive\"<\/span>\n      line: <span class=\"hljs-string\">\"TCPKeepAlive no\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?UseDNS\"<\/span>\n      line: <span class=\"hljs-string\">\"UseDNS no\"<\/span>\n    - regexp: <span class=\"hljs-string\">\"^#?AllowAgentForwarding\"<\/span>\n      line: <span class=\"hljs-string\">\"AllowAgentForwarding no\"<\/span>\n<\/code><\/pre>\n<p>The <code>lineinfile<\/code> and <code>regexp<\/code> should look familiar to you at this point\u2014as with making changes to <code>\/etc\/sudoers<\/code>, we're looking at <code>\/etc\/ssh\/sshd_config<\/code> 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 <code>^#?<\/code> regex allows us to replace lines whether or not they're commented out, and thus begin with a <code>#<\/code>.<\/p>\n<p>Finally, let's have the SSD daemon to make sure our changes are applied.<\/p>\n<pre><code class=\"hljs coffeescript\">- name: Restart sshd\n  systemd:\n    state: restarted\n    daemon_reload: <span class=\"hljs-literal\">yes<\/span>\n    name: sshd\n<\/code><\/pre>\n<p>This <code>systemd<\/code> task allows us to run the equivalent of <code>systemd restart sshd<\/code>.<\/p>\n<h2>Final thoughts<\/h2>\n<p>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 <code>iptables<\/code> configuration, and install <code>fail2ban<\/code>.<\/p>\n<p>But, in the meantime, you can run this playbook now <strong>and<\/strong> later.<\/p>\n<p>That's the great thing about a correctly-configured Ansible playbook\u2014 they are <strong>idempotent<\/strong>, 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 <code>packages<\/code> role, and then run the playbook again without error.<\/p>\n<p>Once you get everything up and running, how do you actually run this playbook? It's pretty straightforward.<\/p>\n<p><strong>Generate a hashed password<\/strong>. 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 <code>password<\/code> with your chosen password: <code>python -c 'import crypt; print crypt.crypt(&quot;password&quot;, &quot;$1$AnsibleSalt$&quot;)'<\/code>.<\/p>\n<p><strong>Copy your new hash into <code>provision.yml<\/code><\/strong>.<\/p>\n<p><strong>Run Ansible<\/strong>. Running the playbook itself is straightforward, with one simple command: <code>$ ansible-playbook -k provision.yml<\/code><\/p>\n<p>If you need to re-run the playbook after the first run, you'll need to make some changes to <code>provision.yml<\/code>\u2014look at the code above (and in the version you've created) for some basic instructions.<\/p>\n<p>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.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this step-by-step Ansible tutorial, let&#8217;s build a playbook that creates a new user, installs some packages, and hardens SSH from attack.<\/p>\n","protected":false},"author":20,"featured_media":23,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"footnotes":""},"categories":[18,30],"tags":[209],"class_list":["post-5539","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-devops","category-tutorials","tag-ansible"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/5539","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/comments?post=5539"}],"version-history":[{"count":4,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/5539\/revisions"}],"predecessor-version":[{"id":13011,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/posts\/5539\/revisions\/13011"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media\/23"}],"wp:attachment":[{"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/media?parent=5539"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/categories?post=5539"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ssdnodes.com\/wp-json\/wp\/v2\/tags?post=5539"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}