Pocket Ansible and protection against brute force attacks
Introduction
Hello! In my professional work, I often work with systems located on different networks, isolated both from each other and from the Internet.
Often these networks contain Linux hosts with a variety of functionality, but, as a rule, have a number of common configurations. For example, setting up connection points to shared network folders, security, repository mirrors and other aspects – all this requires a significant investment of time, especially given the large number of such devices.
To manage many Linux hosts (and Windows, too, by the way), there is an excellent tool that I really love – Ansible. However, to use it, you need a server from which playbooks will be launched. This implies the need to set up a Linux production machine or virtual machine.
It is not always convenient or practical to carry a laptop with you, and I prefer to use Windows on my work device, although I am familiar with the Linux terminal. I believe that it is worth borrowing the best from each system: Windows for the desktop, and Linux for servers and open-source solutions (this is my personal opinion, and I am sure that many will not agree with it, but there is no arguing about tastes).
How to simplify your life? One option is to use OrangePi, and if you don't have a working laptop, you can control it all from your mobile phone. Fortunately, launching customized playbooks is not difficult, even on a small mobile device screen.
Content:
Configuring and creating Ansible roles for Debian hosts
Setting up an Ansible server on Orange PI and pairing it with a mobile phone
Why is this necessary?
Let’s summarize the above and determine how such a project can be useful:
Conducting an information security audit
Working with different networks isolated from each other
Working on a network without the Internet (if there is a local repository mirror)
Mobility and ease of use
What we have:
Test virtual host
Orange Pi 3 LTS
Android mobile phone
Setting up Ansible
In this article, we will touch on the topic of information security and prepare roles that centrally close all ports on the specified hosts, except those that we need. We will also protect our machines from brute force (password brute force) using fail2ban. Usually I configure sudo and authorize using RSA keys, but in this article we will not consider this so as not to make the article too lengthy. If you are interested in this question, write in the comments and we will consider it in the future.
Let's install Ansible
sudo apt install ansible
We will prepare the roles in advance in a test environment, and then transfer them via git to our mobile server for further work. Let's start with the environment, which is extremely important for Ansible. For it to work correctly, the correct directory structure must be created. There may be several configurations, but my working version without unnecessary details looks like this:
.
├── ansible.cfg
├── inventory
│ ├── group_vars
│ ├── hosts
│ ├── host_vars
│ │ └── ansible-test
│ └── secrets.yml
├── playbooks
│ └── test
│ └── test.yml
├── requirements.txt
├── requirements.yml
├── roles
│ └── secure
│ ├── fail2ban
│ └── ufw
└── Vagrantfile
requirements.txt and requirements.yml
are intended for loading dependencies: requirements.yml is used for dependencies with ansible-galaxy, and requirements.txt is used for libraries in the Python virtual environment. I also recommend using Ansible-lint – an excellent library for checking roles. I strongly advise you to study this tool if you want to write clean roles and avoid possible errors in future use.
ansible.cfg
In this file we indicate the paths to our roles, keys, authorization methods, as well as the method of storing the cache, which reduces the time for testing new roles.
[defaults]
skip_ansible_lint = True
host_key_checking = False
inventory = inventory/hosts
roles_path = roles/secure
private_key_file = ~/.ssh/id_rsa
become_method = sudo
become_user = root
# кеширование фактов
gathering = smart
# 1 час
fact_caching_timeout = 3600
# кеш в jsonfact_caching = jsonfile
fact_caching_connection = /tmp/ansible_fact_cache
inventory
The directory in which we determine what we will work with.
group_vars/all.yml useful for specifying variables common to all roles. These can be server addresses, service names, proxies and, in general, everything that is periodically needed in our roles and that we would not want to duplicate.
Files describing the host configuration are stored in the directory host_varsin file secrets.yml Conveniently store passwords and other confidential data. I recommend storing these facts in encrypted form using Ansible Vault. It is also important to add these files to .gitignoreto store it locally on the device and not send it to a remote repository.
.gitignore
# Игнорировать все файлы в директории inventory/host_vars/
inventory/host_vars/*
When encrypting, we enter a password, which we will later use when launching playbooks:
ansible-vault encrypt inventory/secrets.yml # шифровать
ansible-vault edit inventory/secrets.yml # редактировать
ansible-vault decrypt inventory/secrets.yml # расшифровать
hosts
In this file we will indicate the hosts, as well as the groups to which these hosts belong.
[test]
ansible-test
[localhost]
127.0.0.1 ansible_connection=local
You can also specify the addresses and passwords of our hosts in this file, but from a security point of view this is not the best solution, so we will indicate them in separate files corresponding to the host names in the host_vars directory
host_vars/ansible-test
ansible_become: true
ansible_host: 192.168.2.16
ansible_user: vagrant
ansible_ssh_pass: vagrant
ansible_become_pass: vagrant
Don't forget to encrypt the file using ansible-vault
ansible-vault encrypt inventory/host_vars/ansible-test
Vagrantfile
We configure this file taking into account our hypervisor; setting up vagrant is not related to the topic of our article, but in short, this configuration file will allow you to describe all aspects of the desired virtual machine and launch it with one command.
We run our Vagrantfile on the hypervisor or create a test virtual machine ourselves. I prefer automation, so I trust everything to pre-written code.
In order to use Ansible password authentication instead of the default key authentication, we need to install the appropriate utility:
sudo apt install sshpass
Now we are ready to check how correctly our inventory is configured. Don't forget to specify the password request for Ansible Vault:
ansible all -m ping --ask-vault-pass
Setting up roles
Role of the UFW
There are two approaches to working with Ansible, which depend on the scale of your tasks. If you want to perform a simple action, such as installing packages, copying files, or transferring data, you will only need to use a single Ansible file specifying hosts, variables, and playbooks. It is easy to carry, simple in design and convenient enough to perform simple actions. However, as the scale grows, working with such files becomes more difficult, and this is where roles come to the rescue.
Roles allow you to structure your playbooks and simplify the configuration management process by breaking it down into smaller, more understandable parts. Each role is a collection of tasks, handlers, variables, and other files grouped according to your choice. This makes code easier to reuse and more readable. The main rule is that each role must perform a specific completed action and be as universal as possible. In complex roles, for example, I add option variables for greater variability and versatility, so as not to create many roles, but to combine several adjacent ones into one.
In our example, we will create two roles: the first is ufw, which will configure the firewall, and the second is fail2ban, which will provide protection against brute force attacks.
To create the entire role directory structure, use the command
ansible-galaxy role init roles/secure/ufw
let's go to the role directory
cd roles/secure/ufw
Let's study the structure
tree
.
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
The role is simple, here we only need to fix 2 files, the file with tasks tasks/main.yml and a file with default variables defaults/main.yml
tasks/main.yml
---
# tasks file for roles/common/ufw
- name: Ensure UFW is installed
ansible.builtin.apt:
name: ufw
state: present
become: true
- name: Set default outgoing policy to allow
community.general.ufw:
default: allow
direction: outgoing
become: true
- name: Allow SSH connections
community.general.ufw:
rule: allow
port: 22
proto: tcp
become: true
- name: Ensure UFW is enabled and set to start on boot
community.general.ufw:
state: enabled
become: true
- name: Add custom rules
community.general.ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
loop: "{{ ufw__rules }}"
become: true
In the first task we make sure that ufw is installed. If it is not installed, Ansible will install it. A wonderful feature of Ansible called idempotency will prevent us from performing the same action multiple times, so we don't have to worry about possible errors when we run the code again.
OdHowever, it is important to remember that this rule applies to tasks launched by Ansible modules, and does not apply to commands executed through terminal modules!
For example, such a module will not have the idempotency property
- name: Установить пакет ufw через команду
command: apt-get install -y ufw
And this will be
- name: Ensure UFW is installed
ansible.builtin.apt:
name: ufw
state: present
become: true
A list of all modules and options can be viewed on the official website Ansible
- name: Set default outgoing policy to allow
community.general.ufw:
default: allow
direction: outgoing
become: true
This task sets the default outbound connection policy to Allow. This means that all outgoing connections (i.e. connections initiated from your server to external resources) will be allowed unless specified otherwise in various firewall rules.
- name: Allow SSH connections
community.general.ufw:
rule: allow
port: 22
proto: tcp
become: true
In this example, we allow port 22 to work via ssh and include ufw in autostart, simultaneously launching it with the configured rules.
- name: Add custom rules
community.general.ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
loop: "{{ ufw__rules }}"
become: true
Here it’s more interesting, we use a loop to resolve the specified ports from the variable ufw__ruleswhich we will specify by default and can be reassigned when launching the playbook.
Let's set the default variables:
defaults/main.yml
---
# defaults file for roles/common/ufw
ufw__rules: # настройка UFW
- { rule: 'allow', port: '80', proto: 'tcp' }
- { rule: 'allow', port: '443', proto: 'tcp' }
The role is ready, all that remains is to test it. To do this, let’s create a playbook and fill it in:
mkdir -p playbooks/test
touch playbooks/test/test.yml
playbooks/test/test.yml
- name: Testing ufw
hosts: test
roles:
- ufw
vars:
# - ufw
ufw__rules:
- { rule: 'allow', port: '53', proto: 'udp' }
In this example, we open a port for the DNS service to demonstrate how to ignore default variables from the UFW role.
Launching a playbook – Testing ufw
After writing the playbook, let's launch it using the following command:
ansible-playbook playbooks/test/test.yml --ask-vault-pass
From the output of the command we can conclude that everything was successful and all ports except the necessary ones are closed.
The role of fail2ban
To create the entire role directory structure, use the command
ansible-galaxy role init roles/secure/fail2ban
let's go to the role directory
cd roles/secure/fail2ban
tasks/main.yml
---
# tasks file for roles/secure/fail2ban
# sshd logs
- name: Update_apt_cache
ansible.builtin.apt:
update_cache: yes
- name: Install rsyslog
ansible.builtin.apt:
name: rsyslog
state: present
- name: Install iptables
ansible.builtin.apt:
name: iptables
state: present
- name: Ensure the log directory exists
ansible.builtin.file:
path: /var/log/sshd/
state: directory
mode: '0755'
- name: Create SSH log file
ansible.builtin.file:
path: /var/log/sshd/sshd.log
state: touch
owner: sshd
group: adm
mode: '0640'
- name: Configure rsyslog for SSH logging
ansible.builtin.copy:
dest: /etc/rsyslog.d/50-ssh.conf
content: |
if $programname == 'sshd' then /var/log/sshd/sshd.log
& stop
owner: root
group: root
mode: '0644'
- name: Restart rsyslog service
ansible.builtin.systemd:
name: rsyslog
state: restarted
- name: Enable log sshd
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#SyslogFacility AUTH'
line: 'SyslogFacility AUTH'
state: present
- name: Level log sshd
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: '^#LogLevel INFO'
line: 'LogLevel INFO'
state: present
# fail2ban
- name: Install Fail2Ban
ansible.builtin.apt:
name: fail2ban
state: present
- name: Restart sshd service to apply changes
ansible.builtin.systemd:
name: sshd
state: restarted
- name: Config fail2ban
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: '0644'
notify:
- Restart_service
- name: Start and autostart fail2ban
ansible.builtin.service:
name: fail2ban
state: started
enabled: true
# test
- name: Pause
ansible.builtin.pause:
seconds: 3
tags: test
- name: Check service status
ansible.builtin.service:
name: fail2ban
state: started
register: service_status
tags: test
- name: Info
ansible.builtin.assert:
that:
- service_status.status.ActiveState == 'active'
fail_msg: "[error] - Служба не запущена"
success_msg: "[info] - Служба запущена"
In this file, tasks are divided into 3 parts:
Installing and configuring rsyslog and iptables which reads fail2ban
Configures fail2ban, namely transfers the jinja template of the configuration file
Testing the success of the operation by checking the service status
Let's study the configuration file template
templates/jail.local.j2
[DEFAULT]
bantime = {{ fail2ban__bantime }}
findtime = {{ fail2ban__findtime }}
maxretry = {{ fail2ban__maxretry }}
allowipv6 = true
[sshd]
enabled = true
port = ssh
filter = sshd
action = iptables-multiport[name=sshd, port=ssh, protocol=tcp]
logpath = /var/log/sshd/sshd.log
We pass 3 variables to this template, which we will define by default.
defaults/main.yml
---
# defaults file for roles/secure/fail2ban
fail2ban__bantime: 600 # время бана
fail2ban__findtime: 600 # частота бана
fail2ban__maxretry: 5 # число неудачных попыток
Don’t forget about the handler which we call in the task under the name Restart_service
handlers/main.yml
---
# handlers file for roles/secure/fail2ban
- name: Restart_service
ansible.builtin.systemd:
name: fail2ban
state: restarted
Adding our role to the playbook
playbooks/test/test.yml
- name: Testing ufw + fail2ban
hosts: test
roles:
- ufw
- fail2ban
vars:
# - ufw
ufw__rules:
- { rule: 'allow', port: '53', proto: 'udp' }
# - fail2ban
fail2ban__bantime: 600 # время бана
fail2ban__findtime: 600 # частота бана
fail2ban__maxretry: 3 # число неудачных попыток
I like to override key variables in playbooks for more flexible role management.
Launching the playbook – Testing ufw + fail2ban
ansible-playbook playbooks/test/test.yml --ask-vault-pass
Let's check the operation of fail2ban by deliberately making a mistake in ssh authorization when connecting to our test server
1. Fail2ban detected 3 incorrect authorization attempts via ssh logs
2. Fail2ban created a rule to block an IP address at the firewall level iptables
which allows you to block access to port 22 (SSH) for an IP address
To demonstrate the importance of protecting the host against brute force, I will demonstrate a log from my server in the cloud, without fail2ban configured:
sudo journalctl -r | grep 'invalid user' | head -n 20
Setting up an Ansible server on Orange PI
To install Orenge Pi, go to the official website and download the latest current distribution with drivers for address
We write the image to the SD card using Rufus (or a similar program). After that, we insert the card into the Orange Pi, boot the device and connect it to our network using a cable. If there is a DHCP server, we will find the IP address issued to the device and connect through our favorite SSH client (Putty, PowerShell), entering the default login and password (login: orangepi, password: orangepi). If you encounter any problems, you can connect a monitor and keyboard directly to the device and set up a network locally.
Network setup
By default, Orange Pi has NetworkManager installed, which is configured to obtain an IP address via DHCP. We will not configure a static IP on the wired network adapter, but will connect to our phone's Wi-Fi access point. To do this, let’s scan available Wi-Fi networks:
nmcli device wifi list
From the list of available access points, select the one you need and enter the password for access.
nmcli device wifi connect --ask
We will also configure auto-connection to the mobile phone’s access point so that the device automatically connects to the network when rebooted.
nmcli connection modify connection.autoconnect yes
The wireless connection config is available at /etc/NetworkManager/system-connections/
1.2 System setup
First of all, we will change the repositories from Chinese to standard ones
sudo nano /etc/apt/sources.list
deb http://deb.debian.org/debian/ bookworm main non-free-firmware
deb-src http://deb.debian.org/debian/ bookworm main non-free-firmware
deb http://security.debian.org/debian-security bookworm-security main non-free-firmware
deb-src http://security.debian.org/debian-security bookworm-security main non-free-firmware
deb http://deb.debian.org/debian/ bookworm-updates main non-free-firmware
deb-src http://deb.debian.org/debian/ bookworm-updates main non-free-firmware
Let's update the repository cache and the system itself
sudo apt update
sudo apt upgrade
Let's create a new user to replace the standard one (in my case, the user login will be “ch“)
sudo useradd -m -s /bin/bash ch
groups # просмотрим в каких группах дефолтный пользователь
sudo usermod -aG tty,disk,dialout,sudo,audio,video,plugdev,games,users,systemd-journal,input,netdev,ssh ch
sudo passwd ch
And delete the old user
sudo userdel -r orangepi
sudo rm -rf /var/mail/orangepi
Let's set the time
sudo timedatectl set-timezone Europe/Moscow
timedatectl
Hostname
sudo hostnamectl set-hostname opi
Don't forget to make edits in the files /etc/hosts And /etc/hostname then reboot the system
sudo reboot
1.3 Connecting to Github
Let's create rsa keys to access a private repository on GitHub
ssh-keygen -t rsa
Open the public key, copy its contents to your personal account on Github in the section Settings -> SSH and GPG keys -> SSH keys
sudo cat ~/.ssh/id_rsa.pub
Then we test the connection
ssh -T git@github.com
After authorization, we copy the repository with our Ansible worker roles
git@github.com:/.gite
Connecting your phone to Orange PI and launching the playbook
To connect via ssh from a mobile phone on Android OS I use 2 applications, ConnectBot – as an ssh client, and Network Utilities – to determine the client's address.
To work we need:
Determine the access point network
Scan it and find the client's address
Connect to the client via ssh
Allow Fingerprint
Editing the inventory file and group_vars
Let's launch the playbook and enjoy the report!
If you have questions about the article or want to learn more about tools that we did not have time to consider, write about it in the comments. I will address your questions in future articles! Thank you for your attention!