Automating Securing Ubuntu w/ Ansible

Automating Securing Ubuntu w/ Ansible

In this lightning fast tutorial we’ll go through automating the first 10 minutes on an Ubuntu server (aka securing an Ubuntu server). The playbook includes installing various essential packages, creating users, copying users’ ssh pub keys to the remote servers, installing and  configuring ufw and fail2ban, and hardening the ssh configuration. It requires that you have a remote sudo user on a remote machine with SSH Keys configured. You can see my tutorial here on configuring SSH Keys: https://phoenixignited.tech/setup-ssh-keys/

This tutorial aims to be beginner friendly without being an Intro to Ansible.

LINKS: 

Official Ansible Site: https://www.ansible.com/

Ansible Docs: https://docs.ansible.com/

Installing Ansible

Installing Ansible on Ubuntu is as easy as:

							
							
					sudo apt update
sudo apt install software-properties-common
sudo add-apt-repository --yes --update ppa:ansible/ansible
sudo apt install ansible				
			

Creating Our Ansible Playbook

Next we will setup our environment and create the necessary files:

							
							
					mkdir Ansible-AIO-Ubuntu-Secure && cd Ansible-AIO-Ubuntu-Secure && touch inventory.ini && touch ansibleplay.yaml && touch Hardened-SSHD-CONFIG && touch JailConf				
			

Next we will add our host(s) in the inventory.ini file, these are the remote machines that Ansible will control, open the inventory.ini file with vim, nano, etc. and add: 

							
							
					[myhosts]
Server_Name ansible_host=<server_ip> ansible_port=<port> ansible_user=<username> ansible_connection=ssh ansible_ssh_private_key_file=/path/to/private/ssh/key				
			

Replace all of the variables with your desired values, and repeat for as many hosts as needed.

Next open the ansibleplay.yaml file and enter:

							
							
					---
- name: All in One Ubuntu Server Setup Ansible Playbook
  hosts: myhosts
  become: yes

  vars:
    packages:
      - vim
      - git
      - htop
      - curl
      - wget
      - unzip
      - build-essential
      - net-tools
      - tree
      - fail2ban
      - ufw
    jail_local_src: "./JailConf"  # Path to your jail.local file
    custom_port: 12345  # Replace this with your custom port number
    users:
      - username: alice
        is_admin: yes
        pub_key_path: "/path/to/keys/alice.pub"
      - username: bob
        is_admin: no
        pub_key_path: "/path/to/keys/bob.pub"
      - username: charlie
        is_admin: no
        pub_key_path: "/path/to/keys/charlie.pub"

  tasks:
    ## ENSURE SYSTEM IS DEBIAN BASED ##
    - name: Ensure the system is Debian/Ubuntu based
      debug:
        msg: "This playbook is only supported on Debian/Ubuntu systems."
      when: ansible_facts['os_family'] != "Debian"
      failed_when: ansible_facts['os_family'] != "Debian"

    ## CREATE USER ACCOUNTS ##
    - name: Create user accounts
      user:
        name: "{{ item.username }}"
        state: present
        groups: "{{ 'sudo' if item.is_admin else '' }}"
        shell: /bin/bash
      loop: "{{ users }}"

    - name: Create .ssh directory for each user
      file:
        path: "/home/{{ item.username }}/.ssh"
        state: directory
        owner: "{{ item.username }}"
        group: "{{ item.username }}"
        mode: '0700'
      loop: "{{ users }}"

    - name: Copy SSH public key from file for each user
      ansible.builtin.copy:
        src: "{{ item.pub_key_path }}"
        dest: "/home/{{ item.username }}/.ssh/authorized_keys"
        owner: "{{ item.username }}"
        group: "{{ item.username }}"
        mode: '0600'
      loop: "{{ users }}"

    - name: Ensure sudo group has passwordless sudo (for admins)
      lineinfile:
        path: /etc/sudoers
        regexp: '^%sudo'
        line: '%sudo ALL=(ALL) NOPASSWD: ALL'
        state: present
      when: item.is_admin | bool
      loop: "{{ users }}"
      loop_control:
        label: "{{ item.username }}"

    ## INSTALL COMMON PACKAGES ##
    - name: Update apt cache and upgrade system packages
      apt:
        update_cache: yes
        upgrade: dist

    - name: Install commonly used software packages
      apt:
        name: "{{ packages }}"
        state: present
        update_cache: yes

    ## CONFIGURE FAIL2BAN ##
    - name: Copy custom jail.local configuration file
      ansible.builtin.copy:
        src: "{{ jail_local_src }}"
        dest: /etc/fail2ban/jail.local
        owner: root
        group: root
        mode: '0644'

    - name: Ensure Fail2Ban is running and enabled on boot
      service:
        name: fail2ban
        state: started
        enabled: yes

    ## CONFIGURE UFW ##
    - name: Ensure UFW is enabled
      ufw:
        state: enabled

    - name: Allow custom port through UFW
      ufw:
        rule: allow
        port: "{{ custom_port }}"
        proto: tcp

    - name: Set UFW default policy to deny incoming
      ufw:
        state: enabled
        default: deny
        direction: incoming

    - name: Set UFW default policy to allow outgoing
      ufw:
        state: enabled
        default: allow
        direction: outgoing

    ## HARDEN SSHD_CONFIG ##
    - name: Copy the hardened SSHD_CONFIG file to the remote server
      ansible.builtin.copy:
        src: ./Hardened-SSHD-CONFIG  # Path to your local sshd_config
        dest: /etc/ssh/sshd_config    # Destination path on the remote system
        owner: root
        group: root
        mode: '0600'                  # Secure permissions
      notify:
        - Restart sshd

     ## REBOOT SERVER ##
    - name: Reboot the server
      reboot:
        reboot_timeout: 600  # Wait up to 600 seconds (10 minutes) for the server to reboot

    ## TEST SERVER ACCESS ##
    - name: Test connection after reboot
      wait_for_connection:
        timeout: 300  # Wait up to 300 seconds (5 minutes) for the server to become reachable again
  handlers:
    - name: Restart sshd
      service:
        name: "{{ (ansible_facts.services['sshd'] is defined) | ternary('sshd', 'ssh') }}"
        state: restarted

   
  				
			

This is our playbook, it includes various tasks including ensuring the system is Debian / Ubuntu based, creating user accounts, creating ssh directories for each user, copying ssh pub keys over to the remote server for the users, updating packages, installing and updating packages, installing fail2ban and ufw, copying over a hardened jail config file for fail2ban, configuring ufw, copying a hardened sshd file over to the remote server, and rebooting the server.

Be sure to modify any applicable variables such as usernames, pub key paths, packages to be installed, custom ports, etc. 

Next we will configure our JailConf file that we will copy over to the remote server. It is pretty basic, add any applicable jails etc. Just open the JailConf file that we touched earlier and enter:

							
							
					[DEFAULT]
# Ban IP for 10 minutes after 5 failed login attempts
bantime  = 600
findtime = 600
maxretry = 5

# Enable SSH jail
[sshd]
enabled  = true

# Jail for a custom port (e.g., port 12345 for a custom service)
[customport]
enabled  = true
port     = 12345
filter   = customport
logpath  = /var/log/custom_service.log
maxretry = 3				
			

Now open the Hardened_SSHD_CONFIG file we created and enter:

							
							
					Port <custom_port>
PermitRootLogin no
MaxAuthTries 3
MaxSessions 10
PubkeyAuthentication yes
PasswordAuthentication no
PermitEmptyPasswords no
KbdInteractiveAuthentication no
UsePAM yes
AllowAgentForwarding no
AllowTcpForwarding no
X11Forwarding no
PrintMotd no
ClientAliveInterval 600
ClientAliveCountMax 3
LoginGraceTime 30s
UseDNS no
MaxStartups 10:30:60
AllowUsers <your_username>

# Optional hardening settings
#ChallengeResponseAuthentication no
#KerberosAuthentication no
#GSSAPIAuthentication no
#HostKeyAlgorithms ecdsa-sha2-nistp256,ssh-ed25519,rsa-sha2-512,rsa-sha2-256
#Ciphers aes256-gcm@openssh.com,aes256-ctr
#MACs hmac-sha2-512,hmac-sha2-256
#Banner /etc/issue.net				
			

Once we have created and configured the above files we can run the ansible playbook with:

							
							
					ansible-playbook -i inventory.ini ansibleplay.yaml -K				
			

Ansible will ask you for the BECOME password, this is the sudo password for the user on the remote machine that Ansible is using. 

NOTE: If you change the ssh port in the hardened sshd config file from the current ssh port you are using to connect to the remote server(s), when the server reboots you will loose connection to it. You can reconnect to it either by editing the inventory.ini file and updating the ssh port and re-running the ansible playbook or by simply running ssh -p(new port) username@host_ip.

That’s a wrap! 

Walter Miely is a tech entrepreneur and CEO of Phoenix Ignited Tech You can find him on Linkedin. This material is licensed under the CC BY 4.0 License LEGAL DISCLAIMER: The content provided here is provided AS IS, and part of, or the entirety of this content may be incorrect. Please read the entireLegal Disclaimer here.