Basic server hardening using Ansible

For Ubuntu

Update: There’s a new post that covers this topic and more over here.

Things we want done on a newly spun up server instance running Ubuntu.

TLDR version

  1. Update and upgrade apt packages

  2. Create a new user (deploy) for SSH access and deployments

  3. Setup logging in as deploy user using your local SSH public key

  4. Add deploy user to sudoers

  5. Setup unattended upgrades for the OS

  6. Setup firewall (UFW) for the OS

The code for this post is available at this GitHub repository. The instructions for running the same is also available in it. This post aims to explain the process in a verbose fashion.

Update and upgrade apt packages

This one is fairly self explanatory. We want to ensure that all the packages on the system are updated to their latest version.

- name: Update and upgrade apt packages
  apt:
    upgrade: yes
    update_cache: yes
    cache_valid_time: 86400 # Update the apt cache if its older than cache_valid_time.

Ansible apt module documentation

Create a new user for SSH access and deployments

We want to create a new user with sudo access (done later on in the process). This will be the user we use to login to the instance and perform any administrative actions once the hardening process is done.

- name: Create a deploy user
  user:
    name: deploy
    shell: /bin/bash
    password: $6$kcambP/ZjEfvk0d$0iBhHo9C0jUG2x2GaftkKEvCoffgv2Krb1MQSXlgwH3718pT7GaxZsXkmb0puMa.z3zQAzWxiYtPLVO9gYIXn0

Note that the password for the user must be hashed. There are several ways in which you can create a password hash. I personally prefer using the mkpasswd utility like so

mkpasswd --method=SHA-512 --stdin

You will be prompted to enter a new password. Once you hit return, the hash for the password you entered will be printed on screen.

Setup logging in as deploy user using your local SSH public key

Once we finish the hardening process, we want to be able to login to the instance using our local SSH public key. Like so

ssh [email protected]<instance_ip_address> 

As a prerequisite, setup a new SSH key-pair that you want to be able to use with all of your newly created instances. You can follow the steps oultined in this GitHub help page to do so. Assuming you generated a new key-pair as remote_machines and remote_machines.pub,

- name: Set authorized key taken from file
  authorized_key:
    user: deploy
    state: present
    key: "{{ lookup('file', '/home/<your_username>/.ssh/remote_machines.pub') }}"

Note, replace <your_username> with the appropriate value.

Set permissions on the authorized_keys file (on the remote machine), to allow only the file owner to read the file.

- name: Set file permissions on /home/deploy/.ssh/authorized_keys
  file:
    path: /home/deploy/.ssh/authorized_keys
    owner: deploy
    group: deploy
    mode: 0400

Ansible authorized_key module documentation

Ansible lookups documentation

Ansible file module documentation

Linux file permissions documentation

Add deploy user to sudoers

To allow deploy user, created above, to have sudo privileges, we must add the user to the sudoers list

- name: Add deploy to sudoers
  lineinfile:
    path: /etc/sudoers
    state: present
    regexp: "^%deploy"
    line: "%deploy ALL=(ALL) ALL"
    validate: "/usr/sbin/visudo -cf %s"

For additional security, we remove sudo privileges granted by default to certain groups

- name: Disable admin group in sudoers
  lineinfile:
    path: /etc/sudoers
    state: absent
    regexp: "^%admin"
    line: "#%admin ALL=(ALL) ALL"
    validate: "/usr/sbin/visudo -cf %s"

- name: Disable sudo group in sudoers
  lineinfile:
    path: /etc/sudoers
    state: absent
    regexp: "^%sudo"
    line: "#%sudo ALL=(ALL) ALL"
    validate: "/usr/sbin/visudo -cf %s"

And disallow login as root using password

- name: Disable root login via password
  lineinfile:
    path: /etc/ssh/sshd_config
    state: present
    regexp: "^PermitRootLogin"
    line: "PermitRootLogin prohibit-password"

And allow logins only using valid SSH keys

- name: Disable PasswordAuthentication
  lineinfile:
    path: /etc/ssh/sshd_config
    state: present
    regexp: "^PasswordAuthentication"
    line: "PasswordAuthentication no"

Ansible lineinfile module documentation

Setup unattended upgrades for the OS

We want all security updates to be installed automatically. We use the Automatic Updates package from Ubuntu to do this for us.

- name: Install unattended-upgrades package
  apt:
    name: unattended-upgrades
    update_cache: yes

- name: Enable periodic updates
  copy:
    src: 10periodic
    dest: /etc/apt/apt.conf.d/10periodic
    owner: root
    group: root
    mode: 0644

We’re basically copying a file with our required configuration to the remote system. The contents of the file are as such

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";

By default, only security updates are configured to be automatically applied. We can also control how frequently apt checks for updates. Read more about the available configuration options on the official Ubuntu help page for the same.

Setup firewall (UFW) for the OS

We want to be able to completely control how other remote machines can talk to our instance. On a fresh machine, the only port that needs to opened is port 22, that allows SSH access. When you install other dependencies later on (perhaps Nginx), make sure to open ports required by those applications.

- name: Enable ufw access for OpenSSH
  ufw:
    rule: allow
    name: OpenSSH

- name: Enable ufw
  ufw:
    state: enabled

Note: UFW is disabled by default and we must explicitly enable it.

Ansible UFW module documentation

UFW help page on Ubuntu Community Wiki

Bonus points

You can setup the instance to be able to pull your private Git repositories. The way to go about this is to first create a new SSH key that will have access (granted using the UI on GItHub/GitLab) to your GitHub/GitLab/other projects. And then copy said SSH key-pair to this new instance.

Note, refer to this GitHub help page for creating SSH Keys. The post assumes that the new key-pair created are named as deploy and deploy.pub.

- name: Copy deploy ssh private key
  copy:
    src: deploy
    dest: /home/deploy/.ssh/deploy
    owner: deploy
    group: deploy
    mode: 0600
- name: Copy deploy ssh public key
  copy:
    src: deploy.pub
    dest: /home/deploy/.ssh/deploy.pub
    owner: deploy
    group: deploy
    mode: 0644

You can also configure the ssh program on the instance to use this specific key, when dealing with your GitHub/GitLab projects, by creating a config file for the same.

- name: Copy ssh config file
  copy:
    src: ssh_config
    dest: /home/deploy/.ssh/config
    owner: deploy
    group: deploy
    mode: 0644

The contents of the file specify which key-pair to use and when

Host gitlab.com
 HostName gitlab.com
 PreferredAuthentications publickey
 IdentityFile ~/.ssh/deploy

And that’s it. Moving to an automated workflow is hard in the beginning. Start small, and gradually move on to more complicated automated flows. I will be posting examples for automating deployments of postgres, elasticsearch, docker and for obtaining SSL certificates (Let’s Encrypt) for your machines, in future posts.


Soumyajit Pathak picture

Bookmarked. This is a really nice and clean post. It can be used as a cheatsheet.

Really looking forward to the future posts about automating deployments (especially the elasticsearch one)

Thanks for posting this. :)

Jibi Abraham picture

Thank you for the kind words. I haven't done an elasticsearch one yet. Figured I'd cover the basics first. There's a new post at https://able.bio/jibiabraham/obtaining-ssl-certificates-using-nginx-and-ansible--3585rdl if you're interested in more