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:

  1. Configuring and creating Ansible roles for Debian hosts

  2. 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:

  1. Test virtual host

  2. Orange Pi 3 LTS

  3. 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.

Create a test virtual machine

Create a test virtual machine

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

Successful Host Communication Test Report

Successful Host Communication Test Report

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 command output we can conclude that everything was successful

From the command output we can conclude that everything was successful

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:

  1. Installing and configuring rsyslog and iptables which reads fail2ban

  2. Configures fail2ban, namely transfers the jinja template of the configuration file

  3. 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

Everything went well

Everything went well

Let's check the operation of fail2ban by deliberately making a mistake in ssh authorization when connecting to our test server

After three attempts to enter incorrect credentials, our host was blocked for 10 minutes

After three attempts to enter incorrect credentials, our host was blocked for 10 minutes

1. Fail2ban detected 3 incorrect authorization attempts via ssh logs

2. Fail2ban created a rule to block an IP address at the firewall level iptableswhich 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

Every couple of minutes a host on the network is attacked by bots

Every couple of minutes a host on the network is attacked by bots

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:

  1. Determine the access point network

    Our clients connect to the network 192.168.222.38

    Our clients connect to the network 192.168.222.38

    Scan it and find the client's address

    Client address 192.168.222.127

    Client address 192.168.222.127

    Connect to the client via ssh

    Open an ssh session

    Open an ssh session

    Allow Fingerprint

    1. Editing the inventory file and group_vars

    You can work from your phone, all the necessary buttons are in the application

    You can work from your phone, all the necessary buttons are in the application

    Let's launch the playbook and enjoy the report!

    What does it look like

    What does it look like


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!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *