Securing Linux Servers using Ansible

Securing Linux Servers using Ansible

Managing a single Linux machine can already present substantial challenges, requiring a solid understanding of command-line interfaces, system configurations, and troubleshooting procedures.

But managing two or more such systems, as a system administrator or homelabber, multiplies the complexity. This task necessitates precise tracking and orchestration of different system states, configurations, and network settings.

Establishing consistent update schedules, managing inter-system communications, and ensuring data security across all devices becomes crucial, highlighting the importance of advanced system administration skills and tools.

Having a robust knowledge base and leveraging automation tools can substantially aid in efficiently managing multiple Linux machines.

Introducing Ansible

Ansible is an automation tool that can streamline the management of multiple systems. By leveraging a push-based model, Ansible allows administrators to automate tasks such as configuration management, and application deployment.

The cornerstone of automation with Ansible is the playbooks.

You use playbooks to define the desired system state and which hosts to apply it. Its usefulness lies in the fact that Ansible is idempotent, which means that repeating the same operation multiple times results in the same outcome. For example, if you create a playbook to install neofetch on a host of systems, it will first check if neofetch is already present and only make changes if the current state does not match the desired state.

Building blocks of Ansible

Inventory

The inventory file in Ansible serves as a blueprint for the hosts you aim to manage. It's essentially a text file that outlines the hosts and groups of hosts upon which commands, modules, and playbooks will operate. The inventory file enables Ansible to connect and interact with your nodes, making it a pivotal component of Ansible's configuration.

For instance, imagine you have two servers you want to manage: server1 and server2. Your inventory file might look like this:


[webservers]

server1 ansible_host=192.168.0.1

server2 ansible_host=192.168.0.2

In this example, webservers is a group containing two hosts, server1 and server2. The ansible_host setting is used to define the IP addresses of the hosts.

You can also specify other parameters like the ansible_user and ansible_ssh_private_key_file for each host.

Playbooks

Ansible Playbooks are the vehicle for bringing your system to a desired state.

Playbooks are written in the YAML format and provide a user-friendly way to script complex tasks.

They can be used for various tasks, such as provisioning servers, deploying applications, and orchestrating complex workflows. They are an excellent tool for managing repetitive tasks, ensuring that all tasks are executed in the same order and manner each time, reducing the potential for human error.

To install the packages net-tools and neofetch using an Ansible playbook, you could create a playbook file that looks like the following:

- name: Deploy Application  
  hosts: webservers
  tasks:  
    - name: Copy application files  
        copy:  
        src: /path/to/app  
        dest: /var/www/app  
        owner: www-data  
        group: www-data  
        mode: 0755  

    - name: Restart Apache  
        service:  
        name: apache2  
        state: restarted

In this example, the playbook hosts: webservers instructs Ansible to run this playbook on the webservers group defined in your inventory file.

The become: yes directive is used to gain superuser privileges for installing packages.

The tasks: list outlines the steps to be taken on the target hosts. Each task uses the apt module to ensure that the specified package is installed.

The update_cache: yes  option ensures the package database is updated before the package is installed.

Templates

Ansible template files serve an essential role in configuration management and multi-machine deployment. They enable the customization of files for each system while maintaining a single source file. Built upon the Jinja2 templating engine, these files use the `.j2` extension and allow variables to be inserted, which Ansible will replace with actual values at runtime. This functionality makes them an invaluable tool for tasks where configuration files can change based on specific system properties or roles, providing a high degree of flexibility and reusability.

Here's an example template greeting.j2.

Hello, {{ name }}! Welcome to Ansible. Your age is {{ age }}.

Vars

Ansible variables, often termed as vars for short, provide a powerful way to control the behavior of playbooks, and accordingly, the systems being configured.

These variables can be defined in numerous places, including in a dedicated vars.yml file, directly in the playbook, in an inventory file, or even as command-line arguments during playbook execution.

More importantly, the use of Ansible variables helps to enhance the reusability of playbooks, as they allow users to define and manipulate values that can change based on specific system properties or roles.

name: John
age: 30

Handlers

In Ansible, handlers are just like regular tasks in an Ansible playbook but are only run if notified by another task. They are triggered by a notifying directive and are run once, at the end of the block of tasks in a playbook, irrespective of how many tasks notify them.

For example, if we have a setup where we want to restart a service whenever a configuration file changes, we can use a handler for this. The task that modifies the configuration file will notify the handler to restart the service. But no matter how many tasks notify the handler, it will only run once, after all the tasks have been executed. Here's a basic example:

---
name: a simple playbook
hosts: localhost
  tasks:
    name: Install the software
      command: echo "installing software"
      notify: restart service
    handlers:
      name: restart service
      command: echo "restarting service"

In this example, after the Install the software the task is run, it notifies the restart service handler.

After all the tasks are complete, the restart service handler executes. Handlers are perfect for managing services related to a configuration file, ensuring that changes are correctly applied.

Roles

Ansible roles could be thought of as the Swiss army knife of Ansible playbooks. They are designed to be fully self-contained, reusable components containing variables, tasks, templates, files, and modules that can be seamlessly integrated into playbooks. Think of them as tiny playbooks themselves, each designed to perform a specific task.

Let's consider them in terms of setting up a server. You could have individual roles for each component of the server, for example, one for the web server setup, another for the database, and so on. When it's time to set up a new server, you just bring together all these roles in your playbook, like pieces of a jigsaw puzzle. This approach keeps your main playbook neat and tidy, and promotes the reuse of roles across different playbooks, leading to more efficient use of your time and resources.

Hardening Linux Servers

When it comes to server security, it's imperative to adopt a robust, proactive approach. One effective way to achieve this is through the automation capabilities provided by Ansible. It allows you to harden your Linux server security in a streamlined, efficient manner.

Moreover, Ansible's idempotent nature ensures that these security configurations are maintained over time. This means that no matter how many times a playbook is run, the result will always be the state defined in the playbook. This is especially helpful in maintaining the integrity of your security configurations, as any deviation from the desired state can potentially open up vulnerabilities.

In addition, Ansible's clear syntax and documentation make it a user-friendly choice for system administrators. It's also compatible with a wide range of Linux distributions, ensuring you can deploy consistent security configurations across diverse environments.

Installing public keys on hosts

After you have created your ssh keys, you need to configure ssh for all required hosts to allow Ansible to communicate and configure all machines.

install_public_keys.yml

The first task of this playbook is to install the ssh key into all your machines. The second task modifies the system config to allow `sudo` privileges without prompting the user for a password. This is necessary to set up Ansible as we don't want to enter passwords for every machine when we call on Playbooks.

- hosts: all
  become: true
  tasks:
    - name: install public keys
      ansible.posix.authorized_key:
        user: mustafa
        state: present
        key: "{{ lookup('file', '/home/mustafa/.ssh/id_rsa.pub') }}"
    - name: change sudoers file
      lineinfile:
        path: /etc/sudoers
        state: present
        regexp: '^%sudo'
        line: '%sudo ALL=(ALL) NOPASSWD: ALL'
        validate: /usr/sbin/visudo -cf %s
set_ssh_permissions.yml

Setting permissions for ~/.ssh and ~/.ssh/authorized_keys is a crucial step in securing your SSH setup.

The ~/.ssh directory should have 0700 permissions, ensuring that only the owner has the rights to read, write, and execute files within the directory.

The ~/.ssh/authorized_keys file should have 0600 permissions, allowing only the owner to read and write to the file. These permissions prevent unauthorized users from accessing or altering your SSH keys, protecting your system from potential security breaches.

- name: Change permissions for SSH directory and authorized_keys
  hosts: all
  become: true
  tasks:
    - name: Change permissions for ~/.ssh directory
      file:
        path: /home/mustafa/.ssh
        mode: "0700"

    - name: Change permissions for ~/.ssh/authorized_keys file
      file:
        path: /home/mustafa/.ssh/authorized_keys
        mode: "0600"
configure_ssh_settings.yml

Another crucial step is disabling root login and password authentication.

---
- name: Secure SSH configuration
  hosts: proxmox
  become: yes

  tasks:
    - name: Update sshd_config to disable SSH password authentication and restrict root login
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: "{{ item.regexp }}"
        line: "{{ item.line }}"
      loop:
        - { regexp: '^(#)?PasswordAuthentication', line: 'PasswordAuthentication no' }
        - { regexp: '^(#)?PermitRootLogin', line: 'PermitRootLogin no' }
        
    - name: Update sshd_config to restrict SSH service to IPv4
      lineinfile:
        path: /etc/ssh/sshd_config
        regexp: '^(#)?AddressFamily'
        line: 'AddressFamily inet'

    - name: Restart SSH service
      service:
        name: sshd
        state: restarted

setup fail2ban

Fail2Ban is an essential tool to consider in your cybersecurity arsenal. It operates by monitoring system logs for any malicious activity. Upon detection of repeated failed login attempts, which are often indicative of brute force attacks, Fail2Ban promptly blocks the offender's IP address. This significantly mitigates the risk of unauthorized system access. However, it's worth noting that while Fail2Ban provides an additional layer of security, it should not be the sole measure for system protection and needs to be incorporated into a more comprehensive security strategy.

Here's a quick guide on setting up fail2ban using Ansible:

Ansible fail2ban guide

Hardening a Linux server is a continuous and multilayered process, encompassing a wide array of security measures.

This list is by no means exhaustive of all the measures you might need to take for proper security. It's only a starting point to getting you started with thinking about all the doors you need to close to make sure that your server is protected from bad actors.


Here are some references that go into much more detail on the topic.