Skip to main content

5 ways to harden your Linux server with Ansible

Automation allows you to apply compliance and security policies consistently across your servers, verify compliance, and remediate servers.

Image by Steve Buissinne from Pixabay

In his article 5 ways to harden a new system with Ansible, Enable Sysadmin Sudoer Anthony Critelli walks through developing an Ansible playbook to secure a new Linux server. He shows how to use Ansible to patch the system, lock remote access, disable unused software and services, and do other useful tasks.

In this article, I'll show you how to use Ansible to further govern your Linux server with additional policies and recommendations.

[ Cheat sheet: Get a list of Linux utilities and commands for managing servers and networks. ]

While these recommendations are helpful, keep in mind that they don't guarantee your server is perfectly safe. It's unlikely that any servers exposed on the internet are completely secure, but these additional policies help improve their security, making it harder for anyone to attack or exploit your server.

Finally, make sure that these recommendations are compatible with your server's purpose before applying them.

Encoding these recommendations and policies into an Ansible playbook and automating their application allows you to apply them consistently across your servers, verify compliance, and remediate servers periodically. I've included a complete sample playbook at the end of this article with all recommendations below.

1. Ensure your firewall is up and running

Ensuring your local firewall is up and running is one of the most basic things you can do to restrict access to your Linux server. Some Linux distributions, such as Red Hat Enterprise Linux (RHEL), enable the firewall by default during installation. However, it's a good idea to verify that the firewall is enabled in case you installed a server from, for instance, a corporate template that doesn't enable it by default.

To ensure the firewall is installed, up, and running using Ansible, use the ansible.builtin.dnf and ansible.builtin.service modules with the appropriate firewall service for your distribution. For Fedora-based distributions, the firewall service is firewalld. Enable firewalld by adding both tasks like this:

- name: Ensure firewall package is installed
  ansible.builtin.dnf:
    name: firewalld
    state: present

- name: Ensure firewall service is up and running
  ansible.builtin.service:
    name: firewalld
    state: started
    enabled: yes

Then, ensure the firewall allows access only to services you need, disabling other services. This task varies depending on your system's purpose. For example, disable services cockpit and dhcpv6-client if you don't use them by using the module ansible.posix.firewalld with a loop:

- name: Block non-required services
  ansible.posix.firewalld:
    service: "{{ item }}"
    state: disabled
    permanent: yes
    immediate: yes
  loop:
    - cockpit
    - dhcpv6-client

Finally, enable required services using the same module. For example, ensure that you can access your server using SSH:

- name: Enable required services
  ansible.posix.firewalld:
    service: "ssh"
    state: enabled
    permanent: yes
    immediate: yes

You can also use a loop if you need to enable more services or even configure both settings as a single task by looping over a dictionary and combining the services with the desired state.

Next, ensure SELinux is enabled.

2. Ensure SELinux is enabled and enforcing

Linux distributions in the Red Hat family, such as RHEL and Fedora, implement a mandatory access control (MAC) security solution known as SELinux (Security Enhanced Linux). SELinux is one of the best ways to improve your system's security by ensuring that processes only have access to specific resources, such as certain files or designated network ports.

I won't cover SELinux in detail here. It's a large topic, so I encourage you to learn more about it. For more information, consult the SELinux documentation.

[ Get the SELinux cheat sheet. ]

To enable SELinux and set it to enforcing mode to allow active system protection, use the ansible.posix.selinux module:

- name: Ensure SELinux is enabled and enforcing
  ansible.posix.selinux:
    policy: targeted
    state: enforcing
  register: selinux_status

As with the firewall, SELinux should be enabled by default with RHEL and Fedora, but this is a good check and action in case it's not. When you change the SELinux status, a reboot is required. In this case, the selinux module returns the variable reboot_required set to true. Check this value with the debug module and use a changed_when condition to notify a handler to reboot the machine at the end:

- name: Verify if reboot needed
  ansible.builtin.debug:
    msg: "Reboot needed: {{ selinux_status.reboot_required }}"
  changed_when: "{{ selinux_status.reboot_required | bool }}"
  notify: reboot_host

Then, at the end of your playbook, define the reboot_host handle using the ansible.builtin.reboot module to restart the machine:

handlers:
- name: reboot_host
  ansible.builtin.reboot:
    reboot_timeout: 360

Note that if you enable SELinux for an existing server with many files, the system relabels them when the machine restarts. This process can take a long time, so plan changes accordingly. The best option is to enable SELinux when deploying a new server to ensure it benefits from this protection from the start.

Next, enable some secure kernel parameters.

3. Enable kernel security parameters

The Linux kernel is highly flexible and customizable to meet diverse requirements, workloads, and uses. You can benefit from this flexibility to increase your server security by changing some kernel parameters. Linux exposes most of these parameters in the virtual filesystem /proc. You can change them by setting the values directly into the corresponding /proc key, but it's harder to manage. The command-line utility sysctl makes this task pretty easy to implement and manage. You can also use the Ansible module ansible.posix.sysctl to manage kernel parameters.

The Linux kernel makes several parameters available to change. These parameters impact the system's functionality and security in different ways. Ensure that you change the ones compatible with your server's purpose and utilization.

Start by updating basic system parameters such as:

  • Randomize virtual address space, which makes it harder for an attacker to use known memory addresses for exploitation.
  • Disable dmesg access to unprivileged users.
  • Disable kernel profiling by unprivileged users.

Set these parameters using the ansible.posix.sysctl module with a loop, like this:

- name: Harden kernel parameters
  ansible.posix.sysctl:
    name: "{{ item.name }}"
    value: '{{ item.value }}'
    sysctl_set: yes
    state: present
    reload: yes
    sysctl_file: /etc/sysctl.d/90-kernel.conf
  loop:
  - name: kernel.randomize_va_space
    value: 2
  - name: kernel.dmesg_restrict
    value: 1
  - name: kernel.perf_event_paranoid
    value: 2

Next, enable some basic network-related protections:

  • Disable TCP SYN cookies to prevent SYN flood attacks.
  • Log Martian packets to allow inspection of Internet Protocol (IP) packets from reserved addresses.
  • Disable acceptance of source-routed packets on IPv4 and IPv6 to prevent accepting routing changes that could bypass network security.
- name: Harden network parameters
  ansible.posix.sysctl:
    name: "{{ item.name }}"
    value: '{{ item.value }}'
    sysctl_set: yes
    state: present
    reload: yes
    sysctl_file: /etc/sysctl.d/90-net.conf
  loop:
   - name: net.ipv4.tcp_syncookies
     value: 1
   - name: net.ipv4.conf.default.log_martians
     value: 1
   - name: net.ipv4.conf.all.log_martians
     value: 1
   - name: net.ipv4.conf.all.accept_source_route
     value: 0
   - name: net.ipv4.conf.default.accept_source_route
     value: 0
   - name: net.ipv6.conf.all.accept_source_route
     value: 0
   - name: net.ipv6.conf.default.accept_source_route
     value: 0

Finally, unless you use bridged network connections for virtual machines or containers, disable IP forwarding:

- name: Disable ip forwarding
  ansible.posix.sysctl:
    name: "{{ item.name }}"
    value: '{{ item.value }}'
    sysctl_set: yes
    state: present
    reload: yes
    sysctl_file: /etc/sysctl.d/90-ip.conf
  loop:
  - name: net.ipv4.ip_forward
    value: 0
  - name: net.ipv6.conf.all.forwarding
    value: 0

These options improve your server security by preventing attackers from using the network to circumvent other protections. Note that the default value for some of these parameters can be useful in other situations, such as network routers. They should not be needed in most servers.

Once you set some basic network security options, improve on them by blocking some ICMP requests.

[ Learn more about server and configuration management by downloading Ansible for DevOps. ]

4. Disable ICMP

Internet Control Message Protocol (ICMP) is a network protocol mainly used to manage and monitor networks. ICMP has many useful applications, but attackers can use some of its functionality to exploit your system.

Disabling ICMP may impact network monitoring tools that rely on it. Consult with your system's management team before making changes. If this is your private server or your server is exposed on the internet, you should disable some ICMP capabilities to prevent common attack vectors. These include:

  • Ignore ICMP broadcast echo requests to make it harder to map your host.
  • Ignore ICMP echo "ping" requests to make it harder to find your server.
  • Disable ICMP redirects on IPv4/IPv6 interfaces to prevent "man-in-the-middle" attacks.
  • Disable sending ICMP redirects to prevent leaking network routing information that could be further exploited.

Enable these options using the module ansible.posix.sysctl with a loop:

- name: Disable ICMP echo and redirects
  ansible.posix.sysctl:
    name: "{{ item.name }}"
    value: '{{ item.value }}'
    sysctl_set: yes
    state: present
    reload: yes
    sysctl_file: /etc/sysctl.d/90-icmp.conf
  loop:
  - name: net.ipv4.icmp_echo_ignore_broadcasts
    value: 1
  - name: net.ipv4.icmp_echo_ignore_all
    value: 1
  - name: net.ipv4.conf.default.accept_redirects
    value: 0
  - name: net.ipv4.conf.all.accept_redirects
    value: 0
  - name: net.ipv6.conf.all.accept_redirects
    value: 0
  - name: net.ipv6.conf.default.accept_redirects
    value: 0
  - name: net.ipv4.conf.default.send_redirects
    value: 0
  - name: net.ipv4.conf.all.send_redirects
    value: 0

As with the network options, the default values of these parameters are required for routers but not servers. You can change them to improve security.

Next, make it easier to understand your system by enabling system auditing.

[ Boost security, flexibility, and scale at the edge with Red Hat Enterprise Linux. ]

5. Enable system auditing

The final step to improve security in your servers is enabling the audit services. Unlike the actions from previous sections in this article, enabling system auditing does not offer active system protection. Nonetheless, it improves your system security by capturing information you can use to track security-related incidents and better understand server activities to implement additional policies or enhance security measures.

Ensure system audit is working by using Ansible modules ansible.builtin.dnf and ansible.builtin.service:

- name: Ensure audit package is installed
  ansible.builtin.dnf:
    name: audit
    state: present

- name: Ensure auditd service is up and running
  ansible.builtin.service:
    name: auditd
    state: started
    enabled: yes

Then, implement a basic set of rules that include, at minimum, the option -e 2 to make the rules configuration immutable. This setting requires a reboot to implement the changes, which prevents someone from intentionally or inadvertently stopping or tampering with the audit system:

$ vi audit.rules
## First rule - delete all
-D

## Increase the buffers to survive stress events.
## Make this bigger for busy systems
-b 8192

## This determine how long to wait in burst of events
--backlog_wait_time 60000

## Set failure mode to syslog
-f 1

## Make rules configuration immutable (requires reboot to change)
-e 2

Finally, use Ansible's ansible.builtin.copy module to copy the rules file to its proper place. The audit service doesn't like to be restarted because it could not log potential issues. It's recommended to reboot the machine to make these changes effective. You can do that by notifying the same reboot_host handler you added for the SELinux task:

- name: Add a basic audit config
  ansible.builtin.copy:
    src: audit.rules
    dest: /etc/audit/rules.d/audit.rules
    owner: root
    group: root
    mode: 0600
  notify: reboot_host

The default audit rules provide a good foundation for system auditing, but you can add more rules to record and track other relevant events. For more information, consult the Auditing chapter in the RHEL security manual.

Security is a journey

The topics in this article are good starting points for improving your server's security. You may not make your server completely secure, but you're making it safer. Ensuring your firewall is running, SELinux is enforced, and network access is tightened are basic security measures. In many cases, taking care of the basics greatly protects your systems.

Don't stop here! After applying these policies and recommendations, continue to observe and monitor your system and adjust accordingly. Use the audit system to understand potential threats and utilization patterns to continuously improve your policies and update your systems.

Security is a journey, not a destination.

Complete playbook

For reference, this is the complete playbook using all the recommendations from this article:

- name: Linux hardening
  hosts: linux_servers
  gather_facts: yes

  tasks:
    - name: Ensure firewall package is installed
      ansible.builtin.dnf:
        name: firewalld
        state: present

    - name: Ensure firewall service is up and running
      ansible.builtin.service:
        name: firewalld
        state: started
        enabled: yes

    - name: Block non-required services
      ansible.posix.firewalld:
        service: "{{ item }}"
        state: disabled
        permanent: yes
        immediate: yes
      loop:
        - cockpit
        - dhcpv6-client

    - name: Enable required services
      ansible.posix.firewalld:
        service: "ssh"
        state: enabled
        permanent: yes
        immediate: yes

    - name: Ensure SELinux is enabled and enforcing
      ansible.posix.selinux:
        policy: targeted
        state: enforcing
      register: selinux_status

    - name: Verify if reboot needed
      ansible.builtin.debug:
        msg: "Reboot needed: {{ selinux_status.reboot_required }}"
      changed_when: "{{ selinux_status.reboot_required | bool }}"
      notify: reboot_host

    - name: Harden kernel parameters
      ansible.posix.sysctl:
        name: "{{ item.name }}"
        value: '{{ item.value }}'
        sysctl_set: yes
        state: present
        reload: yes
        sysctl_file: /etc/sysctl.d/90-kernel.conf
      loop:
      - name: kernel.randomize_va_space
        value: 2
      - name: kernel.dmesg_restrict
        value: 1
      - name: kernel.perf_event_paranoid
        value: 2

    - name: Harden network parameters
      ansible.posix.sysctl:
        name: "{{ item.name }}"
        value: '{{ item.value }}'
        sysctl_set: yes
        state: present
        reload: yes
        sysctl_file: /etc/sysctl.d/90-net.conf
      loop:
       - name: net.ipv4.tcp_syncookies
         value: 1
       - name: net.ipv4.conf.default.log_martians
         value: 1
       - name: net.ipv4.conf.all.log_martians
         value: 1
       - name: net.ipv4.conf.all.accept_source_route
         value: 0
       - name: net.ipv4.conf.default.accept_source_route
         value: 0
       - name: net.ipv6.conf.all.accept_source_route
         value: 0
       - name: net.ipv6.conf.default.accept_source_route
         value: 0

    - name: Disable ip forwarding
      ansible.posix.sysctl:
        name: "{{ item.name }}"
        value: '{{ item.value }}'
        sysctl_set: yes
        state: present
        reload: yes
        sysctl_file: /etc/sysctl.d/90-ip.conf
      loop:
      - name: net.ipv4.ip_forward
        value: 0
      - name: net.ipv6.conf.all.forwarding
        value: 0

    - name: Disable ICMP echo and redirects
      ansible.posix.sysctl:
        name: "{{ item.name }}"
        value: '{{ item.value }}'
        sysctl_set: yes
        state: present
        reload: yes
        sysctl_file: /etc/sysctl.d/90-icmp.conf
      loop:
      - name: net.ipv4.icmp_echo_ignore_broadcasts
        value: 1
      - name: net.ipv4.icmp_echo_ignore_all
        value: 1
      - name: net.ipv4.conf.default.accept_redirects
        value: 0
      - name: net.ipv4.conf.all.accept_redirects
        value: 0
      - name: net.ipv6.conf.all.accept_redirects
        value: 0
      - name: net.ipv6.conf.default.accept_redirects
        value: 0
      - name: net.ipv4.conf.default.send_redirects
        value: 0
      - name: net.ipv4.conf.all.send_redirects
        value: 0

    - name: Ensure audit package is installed
      ansible.builtin.dnf:
        name: audit
        state: present

    - name: Ensure auditd service is up and running
      ansible.builtin.service:
        name: auditd
        state: started
        enabled: yes

    - name: Add a basic audit config
      ansible.builtin.copy:
        src: audit.rules
        dest: /etc/audit/rules.d/audit.rules
        owner: root
        group: root
        mode: 0600
      notify: reboot_host

  handlers:
    - name: reboot_host
      ansible.builtin.reboot:
        reboot_timeout: 360


[ Check out this guide to boosting hybrid cloud security and protecting your business. ]

Check out these related articles on Enable Sysadmin

Author’s photo

Ricardo Gerardi

Ricardo Gerardi is Technical Community Advocate for Enable Sysadmin and Enable Architect. He was previously a senior consultant at Red Hat Canada, where he specialized in IT automation with Ansible and OpenShift.  More about me

Red Hat Summit 2022: On Demand

Get the latest on Ansible, Red Hat Enterprise Linux, OpenShift, and more from our virtual event on demand.

Related Content

OUR BEST CONTENT, DELIVERED TO YOUR INBOX

Privacy Statement