Article

Table of Contents
Theme:
Was this article helpful?
Try Vultr Today with

$50 Free on Us!

Want to contribute?

You could earn up to $300 by adding new articles!

How to Configure a New Ubuntu Server Using Ansible

Author: George Wilder

Last Updated: Mon, Jun 28, 2021
DevOps Ubuntu

Introduction

This guide will show you how to automate the initial Ubuntu server configuration using Ansible. Ansible is a software tool that automates the configuration of one or more remote nodes from a local control node. The local node can be another Linux system, a Mac, or a Windows PC. If you use a Windows PC, you can install Ubuntu using the Windows Subsystem for Linux.

We will use a single Ansible playbook that will do the following:

  • Upgrade installed apt packages.
  • Install new software packages.
  • Set a fully qualified domain name (FQDN).
  • Set the timezone.
  • Set the SSH port number (allows for setting a non-standard port number).
  • Set sudo password timeout (can change the default 15-minute timeout).
  • Create a regular user with sudo privileges.
  • Create a 2-line prompt for root and the new regular user.
  • Install SSH Keys for the new regular user.
  • Disable password authentication for root.
  • Disable tunneled clear-text passwords.
  • Configure a firewall using ufw.
  • Configure brute force mitigation using fail2ban.
  • Reboot and restart services as needed.

Prerequisites

  • A Vultr server with a freshly installed Ubuntu instance (20.04 or later).
  • A local Mac, Windows (with Ubuntu installed via the WSL), or Ubuntu system.
  • If using a Mac, Homebrew should be installed.
  • A previously generated SSH Key for the Vultr host; the SSH public key should be installed for the root user.

1. Install Ansible on the Local System

If you are using a Mac with Homebrew installed:

brew install ansible

If you are using Ubuntu:

apt install ansible

This will install Ansible along with all the required dependencies.

Create a Simple Ansible Configuration

Create the .ansible.cfg configuration file in the local_user home directory. This will tell Ansible how to locate the hosts inventory file.

If you are using a Mac, add the following content:

[defaults]
inventory  = /Users/local_user/ansible/hosts.ini

If you are using Ubuntu:

[defaults]
inventory  = /home/local_user/ansible/hosts.ini

Create the hosts.ini hosts inventory file at:

mkdir ~/ansible
cd ~/ansible

Add the following content:

[vultr:vars]
user=remote_user
user_hashed_passwd="{{ remote_user_hashed_pw }}"
ssh_pub_key="{{ lookup('file', '~/.ssh/host_ed25519.pub')  }}"
ansible_become=yes
ansible_become_method=sudo 
ansible_become_pass="{{ remote_user_passwd }}"

[vultr]
host.example.com:22

The user is the regular user to be created. The user_hashed_passwd is the regular user password that will be stored in the Linux hashed format. We will describe how to create it. ssh_pub_key points to the SSH public key for the Vultr host. The ansible_become lines provide the ability for the newly created user to execute sudo commands (in future ansible playbooks). The last line defines the fully qualified domain name (with the SSH port number). When using the standard SSH port 22, the :22 port declaration is not required, but it shows how to define the SSH port number if a non-standard port number was being used.

The remote user password (and the hashed version) is stored in the same directory as the Ansible playbook. It will be stored in an Ansible Vault for extra password security.

Using the Ansible Vault

Create the directory for the Ansible password vault and setup playbook:

$ mkdir -p ~/ansible/ubuntu
$ cd ~/ansible/ubuntu

You will need the hashed version of the user password. The easiest way to generate the hashed version of the user password is to log into your Vultr Ubuntu server and execute the following command:

# mkpasswd --method=sha-512
Password:

This generates a very long string that begins with $6.

I copy and paste my password to avoid any typos when I enter the password.

Create the passwd.yml Ansible password vault file:

$ ansible-vault create passwd.yml
New Vault password: 
Confirm New Vault password:

Enter a password for the password vault file (different from your user password). This command will start up your default text editor. You should add the following content:

remote_user_passwd: myRegualarUserPassword
remote_user_hashed_pw: '$6..........'

Save and exit your editor. This creates an encrypted file that only Ansible can read. You can view the contents of the file with:

$ ansible-vault view passwd.yml
Vault password:

You can edit the file with:

$ ansible-vault edit passwd.yml                
Vault password: 

You can add multiple users to this same file.

2. Create an SSH Config File for the Vultr Host

This provides the capability to do:

$ ssh host
Welcome to Ubuntu 21.04 (GNU/Linux 5.11.0-18-generic x86_64)
o o o
remote_user@host:~
$

. . . logging in as the regular user; or:

$ ssh root@host
Welcome to Ubuntu 21.04 (GNU/Linux 5.11.0-18-generic x86_64)
o o o
root@host:~
#

. . . logging in as the root user.

In both cases, I am specifying a short-cut name for the host name. This assumes that you have configured ssh-key-based authentication for root and ran the Ansible playbook to create the regular user.

To do this, create (or update) the config file in ~/.ssh:

Host *
  AddKeysToAgent yes
  UseKeychain yes
  IdentitiesOnly yes

Host host.example.com host
  Hostname host.example.com
  Port 22
  User remote_user
  IdentityFile ~/.ssh/host_ed25519

UserKeychain is specific to macOS. It stores the SSH public key in the macOS key chain.

host.example.com is the FQDN that needs to be defined in your DNS or /etc/hosts file on your local system. Port 22 is optional, but required if you define a non-standard SSH port.

3. Test Your SSH/Ansible Configuration

First, verify that Ansible is installed correctly on a Mac:

$ ansible --version
ansible [core 2.11.0] 
  config file = /Users/local_user/.ansible.cfg
  o o o

or on Ubuntu:

$ ansible --version
ansible 2.9.6
  config file = /home/local_user/.ansible.cfg
  o o o

These are currently the latest versions of Ansible on a Mac/Homebrew and Ubuntu 20.04. The important thing to note is the config file line.

Run this command to test your configuration:

$ cd ~/ansible/ubuntu
$ ansible -m ping --ask-vault-pass --extra-vars '@passwd.yml' vultr -u root
Vault password: 
host.example.com | SUCCESS => {
    "ansible_facts": {
        "discovered_interpreter_python": "/usr/bin/python3"
    },
    "changed": false,
    "ping": "pong"
}

If you see the above output, then everything is working fine. If not, go back and double-check all your configuration settings.

4. Running the Ansible Ubuntu Server Configuration Playbook

We are finally ready to run the Ansible playbook, which I will describe shortly. Be sure that you are in the ~/ansible/ubuntu directory. This is the command that you would run:

$ ansible-playbook --ask-vault-pass --extra-vars '@passwd.yml' setup-pb.yml -l vultr -u root
Vault password:

Enter your vault password. The playbook will execute a number of tasks with a PLAY RECAP at the end. You can rerun the playbook multiple times, especially if you changed something. It will only execute tasks when needed.

This is the setup-pb.yml playbook:

# Initial server setup
#
---
- hosts: all
  become: true
  vars:
    ssh_port: "22"
    my_client_ip: XX.XX.XX.XX
    tmzone: America/Chicago
    sudo_timeout: 20
    f2b_jail_local: |
      [DEFAULT]
      ignoreip = 127.0.0.1/8 ::1 {{ my_client_ip }}

      [sshd]
      enabled = true
      port = {{ ssh_port }}
      maxretry = 3
      findtime = 15m
      bantime = 1h

  tasks:
    # Update and install the base software
    - name: Update apt package cache
      apt:
        update_cache: yes
        cache_valid_time: 600

    - name: Upgrade installed apt packages 
      apt:
        upgrade: dist
      register: upgrade

    - name: Ensure that these software packages are installed
      apt:
        pkg:
          - fail2ban
          - pwgen
          - needrestart
          - sudo
        state: latest

    - name: Check if a reboot is needed for Debian-based systems
      stat:
        path: /var/run/reboot-required
      register: reboot_required

    # Host Setup
    - name: Set static hostname
      hostname:
        name: "{{ inventory_hostname_short }}"

    - name: Add FQDN to /etc/hosts
      lineinfile:
        dest: /etc/hosts
        regexp: '^127\.0\.1\.1'
        line: '127.0.1.1    {{ inventory_hostname }} {{ inventory_hostname_short }}'
        state: present

    - name: set timezone
      timezone:
        name: "{{ tmzone }}"

    - name: Set ssh port port number
      lineinfile:
        dest: /etc/ssh/sshd_config
        regexp: 'Port '
        line: 'Port {{ ssh_port }}'
        state: present
      notify:
        - restart sshd

    # Set sudo password timeout (default is 15 minutes)
    - name: Set sudo password timeout.
      lineinfile:
        path: /etc/sudoers
        state: present
        regexp: '^Defaults\tenv_reset'
        line: 'Defaults env_reset, timestamp_timeout={{ sudo_timeout }}'
        validate: '/usr/sbin/visudo -cf %s'

    - name: Create/update regular user with sudo privileges
      user:
        name: "{{ user }}"
        password: "{{ user_hashed_passwd }}"
        state: present
        groups: sudo
        append: true
        shell: /bin/bash

    - name: Set user PS1 to a two-line prompt
      lineinfile:
        dest: "/home/{{ user }}/.bashrc"
        insertafter: EOF
        line: "PS1='${debian_chroot:+($debian_chroot)}\\[\\033[01;32m\\]\\u@\\h\\[\\033[00m\\]:\\[\\033[01;34m\\]\\w\\[\\033[00m\\]\\n\\$ '"
        state: present

    - name: Set root PS1 to a two-line prompt
      lineinfile:
        path: '/root/.bashrc'
        state: present
        insertafter: EOF
        line: PS1='${debian_chroot:+($debian_chroot)}\u@\h:\w\n\$ '

    - name: Ensure authorized keys for remote user is installed
      authorized_key:
        user: "{{ user }}"
        state: present
        key: "{{ ssh_pub_key }}"

    - name: Ensure authorized key for root user is installed
      authorized_key:
        user: root
        state: present
        key: "{{ ssh_pub_key }}"

    - name: Disable password authentication for root
      lineinfile:
        path: /etc/ssh/sshd_config
        state: present
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin prohibit-password'
      notify:
        - restart sshd

    - name: Disable tunneled clear-text passwords
      lineinfile:
        path: /etc/ssh/sshd_config
        state: present
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
      notify:
        - restart sshd

    # Configure a firewall
    - name: Allow ssh port '{{ ssh_port }}'
      ufw:
        rule: allow
        port: '{{ ssh_port }}'
        proto: tcp
        state: enabled

    - name: Turn UFW logging off
      ufw:
        logging: "off"

    - name: configure fail2ban for ssh
      copy:
        dest: /etc/fail2ban/jail.local
        content: "{{ f2b_jail_local }}"
      notify:
        - restart fail2ban

    # simple shell script to display fail2ban-client status info; usage:
    #   f2bst
    #   f2bst sshd
    - name: Configure f2bst
      copy:
        dest: /usr/local/bin/f2bst
        content: |
          #!/usr/bin/sh
          fail2ban-client status $*
        owner: root
        group: root
        mode: 0750

    - name: run needrestart
      command: needrestart -r a
      when: upgrade.changed

    - name: Reboot the server if needed
      reboot:
        msg: "Reboot initiated by Ansible because of reboot required file."
        connect_timeout: 5
        reboot_timeout: 600
        pre_reboot_delay: 0
        post_reboot_delay: 30
        test_command: whoami
      when: reboot_required.stat.exists

    - name: Remove old packages from the cache
      apt:
        autoclean: yes

    - name: Remove dependencies that are no longer needed
      apt:
        autoremove: yes
        purge: yes

  handlers:
    - name: restart sshd
      service:
        name: sshd
        state: restarted
      when: reboot_required.stat.exists == false

    - name: restart fail2ban
      service:
        name: fail2ban
        state: restarted
      when: reboot_required.stat.exists == false

You can read the Ansible Documentation to learn more about Ansible.

You should only have to update the vars: section to change the settings for your specific situation. Most likely, you will want to set the client IP and timezone. Setting the client IP prevents one from being accidentally locked out by fail2ban.

Conclusion

In this guide, we have introduced Ansible for automating the initial Ubuntu server setup. This is very useful for deploying or redeploying a server after testing an application.

Want to contribute?

You could earn up to $300 by adding new articles