How to automate fail2ban with Ansible Part 1
In this article, we will learn about fail2ban and how to use Ansible to automate the management of this tool. We will start by discussing what fail2ban is and how it works, and then move on to using ansible to write a custom module for interacting with fail2ban. We will then create an ansible role with a set of playbooks and variables that will make it easy for others to use the module on any Linux operating system. Finally, we will package the module and role as an ansible collection and share it on Ansible Galaxy. By the end of this tutorial, readers will have a solid understanding of how to use ansible to automate the management of fail2ban and will have the knowledge and tools to create their own custom ansible modules.
What is fail2ban
Fail2ban is a security tool that helps protect servers and other devices from brute-force attacks. It works by scanning log files for failed login attempts and then adding the offending IP addresses to a firewall ban list. This helps to prevent attackers from using automated tools to guess passwords and gain access to systems. Some users may even use a tiny virtual machine instance exposed to the internet as a honeypot with fail2ban to specifically identify offending IPs, which can then be propagated to other instances. Fail2ban is typically used by system administrators and security professionals to protect servers and other critical systems, but it can also be useful for anyone who wants to secure their personal devices. One of the main benefits of fail2ban is that it can help to protect against unauthorized access to systems, which can be a serious security concern. In addition, fail2ban can help to reduce the risk of data breaches and other security incidents, which can be costly and disruptive for businesses and organizations. Overall, fail2ban is a useful tool for anyone looking to secure their systems and protect against unauthorized access.
What is Ansible
Ansible is an open-source software platform for automating and managing IT infrastructure. It is used to configure systems, deploy software, and orchestrate more advanced IT tasks such as continuous deployments or zero downtime rolling updates. One of the strengths of Ansible is its simplicity. It uses an easy-to-read language that allows users to describe the desired state of systems in a clear and concise way.
What is Ansible Module
Ansible has a large number of pre-built modules that can be used to automate a wide range of tasks. These modules can be used to manage systems, deploy applications, and perform other common IT tasks. In addition to the many pre-built modules, Ansible also allows users to write their own custom modules to extend its functionality.
What is Ansible Playbook
Playbooks are a key component of ansible. They are files that contain a series of instructions for ansible to execute. Playbooks are written in YAML and are easy to read and understand, even for users who are not familiar with ansible. This makes playbooks a powerful and flexible tool for automating complex tasks.
What is Ansible Role
Ansible roles are pre-defined playbooks that are designed to be reusable and easily shareable. They allow users to break complex tasks down into smaller, more manageable chunks and to share their work with others. This makes it easy to reuse code and build upon the work of others, which can save time and effort when automating tasks.
What is Ansible Collection
Ansible collections are a distribution format for ansible content. They allow users to package their ansible content, including custom modules, plugins, and roles, into a single, easily shareable package. Collections can be used to share ansible content with others and to distribute it to multiple systems.
What is Ansible Galaxy
Ansible Galaxy is a public hub for sharing ansible content, including collections, roles, and modules. It allows users to search for and download ansible content from a central repository, making it easy to find and use community-contributed content. To push an ansible collection to Ansible Galaxy, you will need to sign up for an account and follow the instructions provided by Ansible for uploading your collection.
Writing Ansible Module to automate fail2ban
Below is the Ansible Module code we will use against fail2ban:
#!/usr/bin/python
from ansible.module_utils.basic import *
import os
import re
DOCUMENTATION = r'''
---
module: fail2ban_config
short_description: Configure Fail2ban settings
description:
- This module allows you to modify settings in the Fail2ban configuration file,
jail.local. It supports setting all the options in the [DEFAULT] section, as
well as creating and modifying options in a [SSH] section.
options:
ignoreip:
description:
- This parameter identifies IP addresses that should be ignored by the
banning system. By default, this is just set to ignore traffic coming
from the machine itself, so that you do not fill up your own logs or
lock yourself out.
required: false
type: str
bantime:
description:
- This parameter sets the length of a ban, in seconds. The default is 10
minutes.
required: false
type: str
findtime:
description:
- This parameter sets the window that Fail2ban will pay attention to when
looking for repeated failed authentication attempts. The default is set
to 10 minutes, which means that the software will count the number of
failed attempts in the last 10 minutes.
required: false
type: str
maxretry:
description:
- This sets the number of failed attempts that will be tolerated within the
findtime window before a ban is instituted.
required: false
type: str
backend:
description:
- This entry specifies how Fail2ban will monitor log files. The setting of
auto means that fail2ban will try pyinotify, then gamin, and then a
polling algorithm based on what is available. inotify is a built-in Linux
kernel feature for tracking when files are accessed, and pyinotify is a
Python interface to inotify, used by Fail2ban.
required: false
type: str
usedns:
description:
- This defines whether reverse DNS is used to help implement bans. Setting
this to “no” will ban IPs themselves instead of their domain hostnames.
The warn setting will attempt to look up a hostname and ban that way, but
will log the activity for review.
required: false
type: str
destemail:
description:
- This is the address that will be sent notification mail if configured
your action to mail alerts.
required: false
type: str
sendername:
description:
- This will be used in the email from field for generated notification
emails
required: false
type: str
banaction:
description:
- This sets the action that will be used when the threshold is reached.
This is actually a path to a file located in /etc/fail2ban/action.d/
called iptables-multiport.conf. This handles the actual iptables
firewall manipulation to ban an IP address. We will look at this later.
required: false
type: str
mta:
description:
- This is the mail transfer agent
protocol:
description:
- This is the type of traffic that will be dropped when an IP ban is
implemented. This is also the type of traffic that is sent to the new
iptables chain.
required: false
type: str
chain:
description:
- This is the chain that will be configured with a jump rule to send
traffic to the fail2ban funnel.
required: false
type: str
ssh_enabled:
description:
- This parameter controls whether the [SSH] section is enabled or not.
required: false
type: bool
ssh_port:
description:
- This parameter sets the port that Fail2ban will monitor for failed
authentication attempts.
required: false
type: str
ssh_filter:
description:
- This parameter sets the name of the filter that will be used to identify
failed authentication attempts in the log file.
required: false
type: str
ssh_logpath:
description:
- This parameter sets the path to the log file that Fail2ban will monitor
for failed authentication attempts.
required: false
type: str
ssh_maxretry:
description:
- This parameter sets the number of failed authentication attempts that
will trigger a ban for the [SSH] section.
required: false
type: str
author:
- ChatGPT (@openai.com)
'''
EXAMPLES = r'''
- name: Ban IPs for 10 minutes after 3 failed SSH authentication attempts
fail2ban_config:
bantime: 10m
findtime: 10m
maxretry: 3
- name: Enable the [SSH] section and set the port to 2222
fail2ban_config:
ssh_enabled: true
ssh_port: 2222
- name: Set the [DEFAULT] bantime to 1 hour and the [SSH] maxretry to 5
fail2ban_config:
bantime: 1h
ssh_maxretry: 5
'''
RETURN = r'''
changed:
description: Whether any changes were made to the configuration file
type: bool
'''
def main():
def update_config(config_lines, section, key, value):
updated = False
if not isinstance(value, bool):
value = str(value)
pattern = r'^[ \t]*%s[ \t]*=[ \t]*.+$' % key
for i, line in enumerate(config_lines):
if line.strip().startswith('[%s]' % section):
in_section = True
elif line.strip().startswith('['):
in_section = False
elif in_section:
match = re.match(pattern, line)
if match:
config_lines[i] = '%s = %s\n' % (key, value
updated = True
if not updated:
config_lines.append('%s = %s\n' % (key, value))
return updated
def create_section(config_lines, section, values):
section_added = False
for i, line in enumerate(config_lines):
if line.strip().startswith('[%s]' % section):
section_added = True
break
if not section_added:
config_lines.append('[%s]\n' % section)
for key, value in values.items():
update_config(config_lines, section, key, value)
return True
return False
def delete_section(config_lines, section):
deleted = False
in_section = False
for i, line in enumerate(config_lines):
if line.strip().startswith('[%s]' % section):
in_section = True
elif line.strip().startswith('['):
in_section = False
elif in_section:
del config_lines[i]
deleted = True
return deleted
def write_config(config_lines):
with open('/etc/fail2ban/jail.local', 'w') as f:
for line in config_lines:
f.write(line)
def read_config():
config_lines = []
with open('/etc/fail2ban/jail.local', 'r') as f:
config_lines = f.readlines()
return config_lines
module = Ansible.module_utils.basic.AnsibleModule(
argument_spec=dict(
ignoreip=dict(type='str', required=False),
bantime=dict(type='str', required=False),
findtime=dict(type='str', required=False),
maxretry=dict(type='str', required=False),
backend=dict(type='str', required=False),
usedns=dict(type='str', required=False),
destemail=dict(type='str', required=False),
sendername=dict(type='str', required=False),
banaction=dict(type='str', required=False),
mta=dict(type='str', required=False),
protocol=dict(type='str', required=False),
chain=dict(type='str', required=False),
ssh_enabled=dict(type='bool', required=False),
ssh_port=dict(type='str', required=False),
ssh_filter=dict(type='str', required=False),
ssh_logpath=dict(type='str', required=False),
ssh_maxretry=dict(type='str', required=False),
),
supports_check_mode=True
)
# Set default values for [DEFAULT] section
default_values = {
'ignoreip': '127.0.0.1/8',
'bantime': '10m',
'findtime': '10m',
'maxretry': '3',
'backend': 'auto',
'usedns': 'warn',
'destemail': 'root@localhost',
'sendername': 'Fail2Ban',
'banaction': 'iptables-multiport',
'mta': 'sendmail',
'protocol': 'tcp',
}
# Set default values for [SSH] section
ssh_values = {
'enabled': 'true',
'port': 'ssh',
'filter': 'sshd',
'logpath': '/var/log/auth.log',
'maxretry': '6',
}
config_lines = read_config()
changed = False
# Update [DEFAULT] section
for key, value in default_values.items():
if module.params[key] is not None:
default_values[key] = module.params[key]
changed |= update_config(config_lines, 'DEFAULT', key, value)
changed |= create_section(config_lines, 'DEFAULT', default_values)
# Update [SSH] section
for key, value in ssh_values.items():
if module.params['ssh_%s' % key] is not None:
ssh_values[key] = module.params['ssh_%s' % key]
if module.params['ssh_enabled']:
changed |= update_config(config_lines, 'SSH', key, value)
changed |= create_section(config_lines, 'SSH', ssh_values)
elif delete_section(config_lines, 'SSH'):
changed = True
if changed and not module.check_mode:
write_config(config_lines)
module.exit_json(changed=changed)
if __name__ == '__main__':
main()
Here is how this ansible module is written and what each part of the code does:
The ansible module is written in Python and utilizes the AnsibleModule utility from Ansible's basic module utilities library. The utility allows the module to accept arguments specified in a dictionary, which in this case includes all of the configurable options for the fail2ban configuration file. The module also supports check mode, which allows it to run a simulation of the changes it would make without actually making them.
Next, default values for the [DEFAULT] section and the [SSH] section of the configuration file are defined in dictionaries. These default values will be used if the corresponding options are not provided as arguments to the module.
The module then reads in the current configuration file and stores it as a list of lines. It then iterates over the default values dictionary and updates the corresponding options in the configuration file if they were provided as arguments to the module. If the [DEFAULT] section does not yet exist in the configuration file, it is created and the default values are added to it.
Similarly, the module iterates over the ssh values dictionary and updates the corresponding options in the [SSH] section of the configuration file if they were provided as arguments to the module. If the ssh_enabled argument is set to true, the [SSH] section is created or updated with the provided ssh values. If the ssh_enabled argument is not provided or is set to false, the [SSH] section is deleted from the configuration file if it exists.
Finally, if the configuration file has been modified, the updated version is written back to the file. If the module was run in check mode, no changes are made. The module then exits, returning whether or not any changes were made.
Writing Ansible Playbook to install fail2ban
Here is an ansible playbook that will install, start, and enable the fail2ban package and service on a Linux system:
---
- hosts: all
become: true
tasks:
- name: Install fail2ban
package:
name: fail2ban
state: present
when: ansible_os_family == 'RedHat'
become: true
- name: Install fail2ban
apt:
name: fail2ban
state: present
when: ansible_os_family == 'Debian'
become: true
- name: Start fail2ban service
service:
name: fail2ban
state: started
become: true
- name: Enable fail2ban service
service:
name: fail2ban
enabled: true
become: true
This playbook uses the package module to install the fail2ban package on RedHat-based systems and the apt module to install it on Debian-based systems. It then uses the service module to start and enable the fail2ban service. The when clauses ensure that the appropriate package manager is used for each type of Linux system. The become directives allow the playbook to run with root privileges, which is necessary to install and manage system packages and services.
Writing Ansible Playbook to configure fail2ban
Here is an ansible playbook that uses the fail2ban ansible module to configure the fail2ban service on a Linux system:
---
- hosts: all
become: true
tasks:
- name: Configure fail2ban
fail2ban:
ignoreip: 127.0.0.1/8
bantime: 10m
findtime: 10m
maxretry: 3
backend: auto
usedns: warn
destemail: root@localhost
sendername: Fail2Ban
banaction: iptables-multiport
mta: sendmail
protocol: tcp
chain: INPUT
become: true
This playbook uses the fail2ban module to configure the fail2ban service with the specified options. The options correspond to the parameters in the [DEFAULT] section of the fail2ban configuration file. The become directive allows the playbook to run with root privileges, which is necessary to modify the fail2ban configuration file.
To configure the [SSH] section of the fail2ban configuration file, you can add an additional task to the playbook as follows:
- name: Configure fail2ban ssh section
fail2ban:
ssh_enabled: true
ssh_port: ssh
ssh_filter: sshd
ssh_logpath: /var/log/auth.log
ssh_maxretry: 6
become: true
This task uses the fail2ban module to enable and configure the [SSH] section of the fail2ban configuration file with the specified options. The options correspond to the parameters in the [SSH] section of the fail2ban configuration file. The become directive allows the playbook to run with root privileges, which is necessary to modify the fail2ban configuration file.
Summary
In this tutorial, we showed how to use Ansible to automate the configuration of the fail2ban service on a Linux system. We started by writing a custom Ansible module that allows users to easily configure the fail2ban service. We then wrote a set of playbooks that use this module to install, start, and enable the fail2ban service, as well as configure it with a set of default values.
While it is certainly possible to automate the configuration of fail2ban using Ansible's template
module, which allows you to generate configuration files based on templates and variables, writing a custom Ansible module has a number of benefits. It allows you to encapsulate all of the logic for configuring the service in a single, reusable piece of code.