Карманный Ansible и защита от брутфорс-атак
Введение
Здравствуйте! В своей профессиональной деятельности я часто работаю с системами, находящимися в различных сетях, изолированных как друг от друга, так и от Интернета.
Часто эти сети содержат Linux-хосты с разнообразным функционалом, но, как правило, имеют ряд общих конфигураций. Например, настройка точек подключения к общим сетевым папкам, безопасность, зеркала репозиториев и другие аспекты — все это требует значительных временных затрат, особенно с учетом большого количества таких устройств.
Для управления многими Linux-хостами (да и Windows, кстати, тоже) существует отличный инструмент, который я очень люблю — Ansible. Однако для его использования требуется сервер, с которого будут запускаться плейбуки. Это подразумевает необходимость настройки рабочей машины на Linux или виртуальной машины.
Не всегда удобно и целесообразно носить с собой ноутбук, и я предпочитаю использовать Windows на своем рабочем устройстве, хотя хорошо ориентируюсь в терминале Linux. Я считаю, что от каждой системы стоит заимствовать лучшее: Windows для десктопа, а Linux — для серверов и open-source решений (это мое личное мнение, и я уверен, что многие с ним не согласятся, но о вкусах не спорят).
Как упростить себе жизнь? Один из вариантов — использовать OrangePi, а если нет рабочего ноутбука, то можно управлять всем этим с мобильного телефона. К счастью, запуск настроенных плейбуков не представляет сложности даже на небольшом экране мобильного устройства.
Содержание:
Настройка конфигурации и создание ролей Ansible для для Debian-хостов
Настройка сервера Ansible на Orange PI и спряжение его с мобильным телефоном
Зачем это нужно?
Подытожим вышесказанное и определимся, чем может быть полезен такой проект:
Проведение аудита информационной безопасности
Работа с различными, изолированными друг от друга сетями
Работа в сети без интернета (при наличии локального зеркала репозитория)
Мобильность и простота применения
Что мы имеем:
Тестовый виртуальный хост
Orange Pi 3 LTS
Мобильный телефон с ОС Android
Настройка Ansible
В данной статье мы коснемся темы информационной безопасности и подготовим роли, централизованно закрывающие на указанных хостах все порты, кроме тех, которые нам нужны. Также мы защитим наши машины от брутфорса (перебора паролей) с помощью fail2ban. Обычно я настраиваю sudo и делаю авторизацию по RSA-ключам, но в этой статье мы рассматривать это не будем, чтобы не делать статью слишком объемной. Если этот вопрос вам интересен, напишите в комментариях, и мы его рассмотрим в дальнейшем.
Установим Ansible
sudo apt install ansible
Роли подготовим заранее в тестовой среде, а потом через git перенесем на наш мобильный сервер для дальнейшей работы. Начнем с окружения, которое крайне важно для Ansible. Для его корректной работы должна быть создана правильная структура каталогов. Конфигураций может быть несколько, но мой рабочий вариант без лишних деталей выглядит следующим образом:
.
├── 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 и requirements.yml
предназначены для загрузки зависимостей: requirements.yml используется для зависимостей с ansible-galaxy, а requirements.txt — для библиотек в виртуальное окружение Python. Также рекомендую использовать Ansible-lint — отличную библиотеку для проверки ролей. Настоятельно советую изучить этот инструмент, если вы хотите писать чистые роли и избегать возможных ошибок в дальнейшем применении.
ansible.cfg
В этом файле мы указываем пути к нашим ролям, ключам, методам авторизации, а также способу хранения кэша, что сокращает время тестирования новых ролей.
[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
Директория в которой мы определяем с чем мы будем работать.
group_vars/all.yml удобен для указания переменных, общих для всех ролей. Это могут быть адреса серверов, имена служб, прокси и, в общем, все, что периодически нужно в наших ролях и что не хотелось бы дублировать.
Файлы, описывающие конфигурацию хостов, хранятся в директории host_vars, в файле secrets.yml удобно хранить пароли и прочие конфиденциальные данные. Эти факты я рекомендую хранить в шифрованном виде с использованием Ansible Vault. Также важно добавить этот файлы в .gitignore, чтобы хранить его локально на устройстве и не отправлять в удаленный репозиторий.
.gitignore
# Игнорировать все файлы в директории inventory/host_vars/
inventory/host_vars/*
При шифровании мы вводим пароль, который в дальнейшем будем использовать при запуске плейбуков:
ansible-vault encrypt inventory/secrets.yml # шифровать
ansible-vault edit inventory/secrets.yml # редактировать
ansible-vault decrypt inventory/secrets.yml # расшифровать
hosts
В этом файле мы укажем хосты, а также группы, в которые входят эти хосты.
[test]
ansible-test
[localhost]
127.0.0.1 ansible_connection=local
Также в этом файле можно указать адреса и пароли наших хостов, но с точки зрения безопасности это не самое лучшее решение, поэтому их мы укажем в отдельных файлах, соответствующих именам хостов в директории host_vars
host_vars/ansible-test
ansible_become: true
ansible_host: 192.168.2.16
ansible_user: vagrant
ansible_ssh_pass: vagrant
ansible_become_pass: vagrant
Не забываем зашифровать файл через ansible-vault
ansible-vault encrypt inventory/host_vars/ansible-test
Vagrantfile
Этот файл мы настраиваем с учетом нашего гипервизора, к теме нашей статьи настройка vagrant не относится, но если кратко, то этот конфигурационный файл позволят описать все аспекты желаемой виртуальной машины и запустить ее одной командой.
Запускаем наш Vagrantfile на гипервизоре или создаем тестовую виртуальную машину самостоятельно. Я предпочитаю автоматизацию, поэтому доверю все заранее прописанному коду.
Создаем тестовую виртуальную машину
Для того чтобы использовать парольную аутентификацию Ansible вместо аутентификации по ключам, используемой по умолчанию, нам необходимо установить соответствующую утилиту:
sudo apt install sshpass
Теперь мы готовы проверить, насколько правильно настроен наш инвентарь. Не забывайте указывать запрос пароля для Ansible Vault:
ansible all -m ping --ask-vault-pass
Успешный отчет о тестировании связи с хостами
Настройка ролей
Роль UFW
Есть два подхода в работе с Ansible, которые зависят от масштабности ваших задач. Если вы хотите выполнить простое действие, например, установить пакеты, скопировать файлы или перенести данные, вам будет достаточно использовать одиночный Ansible файл с указанием хостов, переменных и плейбуков. Он легко переносится, прост в оформлении и достаточно удобен для выполнения простых действий. Однако с ростом масштабов работать с такими файлами становится сложнее, и тут на помощь приходят роли.
Роли позволяют структурировать ваши плейбуки и упростить процесс управления конфигурацией, разбивая его на более мелкие и понятные части. Каждая роль представляет собой набор задач, обработчиков, переменных и других файлов, сгруппированных по вашему выбору. Это упрощает повторное использование кода и делает его более читаемым. Главное правило — каждая роль должна выполнять конкретное завершённое действие и быть максимально универсальной. В сложных ролях я, например, добавляю переменные опций для большей вариативности и универсальности, чтобы не создавать множество ролей, а объединять несколько смежных в одну.
В нашем примере мы создадим две роли: первая — ufw, которая настроит файрвол, и вторая — fail2ban, которая обеспечит защиту от брутфорс-атак.
Для создания всей структуры каталогов роли используем команду
ansible-galaxy role init roles/secure/ufw
перейдем в каталог роли
cd roles/secure/ufw
Изучим структуру
tree
.
├── defaults
│ └── main.yml
├── files
├── handlers
│ └── main.yml
├── meta
│ └── main.yml
├── README.md
├── tasks
│ └── main.yml
├── templates
├── tests
│ ├── inventory
│ └── test.yml
└── vars
└── main.yml
Роль простая, тут нам нужно будет исправить только 2 файла, файл с задачами tasks/main.yml и файл с переменными по умолчанию 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
В первой задаче мы убеждаемся, что ufw у нас установлен. Если он не установлен, Ansible выполнит его установку. Прекрасное свойство Ansible, называемое идемпотентностью, не позволит выполнить одно и то же действие несколько раз, поэтому мы можем не беспокоиться о возможных ошибках при повторном запуске кода.
Однако важно помнить, что это правило касается задач, запускаемых модулями Ansible, и не относится к командам, выполняемым через модули терминала!
Например такой модуль не будет иметь свойства идемпотентности
- name: Установить пакет ufw через команду
command: apt-get install -y ufw
А такой будет
- name: Ensure UFW is installed
ansible.builtin.apt:
name: ufw
state: present
become: true
Список всех модулей и опций можно просмотреть на официальном сайте Ansible
- 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
В этом примере мы разрешаем 22 порт для работы по ssh и включаем ufw в автозапуск, одновременно запуская его с настроенными правилами.
- name: Add custom rules
community.general.ufw:
rule: "{{ item.rule }}"
port: "{{ item.port }}"
proto: "{{ item.proto }}"
loop: "{{ ufw__rules }}"
become: true
Здесь уже интереснее, мы используем цикл для разрешения указанных портов, из переменной ufw__rules, которую укажем по умолчанию и сможем переназначать при запуске плейбука.
Зададим переменные по умолчанию:
defaults/main.yml
---
# defaults file for roles/common/ufw
ufw__rules: # настройка UFW
- { rule: 'allow', port: '80', proto: 'tcp' }
- { rule: 'allow', port: '443', proto: 'tcp' }
Роль готова, осталось ее протестировать. Для этого создадим плейбук и заполним его:
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' }
В этом примере мы открываем порт для службы DNS, чтобы продемонстрировать, как игнорировать переменные по умолчанию из роли UFW.
Запуск плейбука — Testing ufw
После написания плейбука, запустим его с помощью следующей команды:
ansible-playbook playbooks/test/test.yml --ask-vault-pass
Из вывода команды можно сделать вывод, что все прошло успешно
Из вывода команды можно сделать вывод, что все прошло успешно и все порты, кроме необходимых у нас закрыты.
Роль fail2ban
Для создания всей структуры каталогов роли используем команду
ansible-galaxy role init roles/secure/fail2ban
перейдем в каталог роли
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] - Служба запущена"
В этом файле задачи разбиты на 3 части:
Установка и настройка rsyslog и iptables который читает fail2ban
Настраивает fail2ban, а именно передает шаблон jinja конфигурационного фала
Тестируем успешность проведенной операции путем проверки статуса службы
Изучим шаблон конфигурационного файла
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
В этот шаблон мы передаем 3 переменные, которые определим по умолчаниюю.
defaults/main.yml
---
# defaults file for roles/secure/fail2ban
fail2ban__bantime: 600 # время бана
fail2ban__findtime: 600 # частота бана
fail2ban__maxretry: 5 # число неудачных попыток
Не забываем про обработчик которой вызываем в задаче под именем Restart_service
handlers/main.yml
---
# handlers file for roles/secure/fail2ban
- name: Restart_service
ansible.builtin.systemd:
name: fail2ban
state: restarted
Добавляем в плейбук нашу роль
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 # число неудачных попыток
Я люблю переопределять ключевые переменные в плейбуках для более гибкого управления ролями.
Запуск плейбука — Testing ufw + fail2ban
ansible-playbook playbooks/test/test.yml --ask-vault-pass
Все прошло успешно
Проверим работу fail2ban умышленно ошибаясь в авторизации по ssh при подключении к нашему тестовому серверу
После трех попыток неверного ввода учетных данных наш хост заблокирован на 10 минут
1. Fail2ban через логи ssh обнаружил 3 неверные попытки авторизации
2. Fail2ban создал правило для блокировки IP-адреса на уровне брандмауэра iptables
, что позволяет блокировать доступ к порту 22 (SSH) для IP-адреса
Для демонстрации важности защиты хоста о брутфорса я продемонстрирую лог со своего сервера в облаке, без настроенного fail2ban:
sudo journalctl -r | grep 'invalid user' | head -n 20
Каждые пару минут хост в сети атакуют боты
Настройка сервера Ansible на Orange PI
Для установки Orenge Pi зайдем на официальный сайт и загрузим последний актуальный дистрибутив с драйверами по адресу
Записываем образ на SD карту с помощью Rufus (или аналогичной программы). После этого вставляем карту в Orange Pi, загружаем устройство и подключаем его к нашей сети с помощью кабеля. При наличии DHCP сервера найдем IP-адрес, выданного устройству, и подключимся через любимый SSH клиент (Putty, PowerShell), введя логин и пароль по умолчанию (логин: orangepi, пароль: orangepi). Если возникнут какие-либо проблемы, можно подключить монитор и клавиатуру непосредственно к устройству и настроить сеть локально.
Настройка сети
По умолчанию на Orange Pi установлен NetworkManager, который настроен на получение IP-адреса по DHCP. Мы не будем настраивать статический IP на проводном сетевом адаптере, а подключимся к Wi-Fi точке доступа нашего телефона. Для этого просканируем доступные Wi-Fi сети:
nmcli device wifi list
Из списка доступных точек доступа выберем нужную и введем пароль для доступа.
nmcli device wifi connect --ask
Также настроим автоподключение к точке доступа мобильного телефона, чтобы устройство автоматически подключалось к сети при перезагрузке.
nmcli connection modify connection.autoconnect yes
Конфиг беспроводного подключения доступен по адресу /etc/NetworkManager/system-connections/
1.2 Настройка системы
В первую очередь сменим репозитории с Китайских на стандартные
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
Обновим кэш репозиториев и саму систему
sudo apt update
sudo apt upgrade
Создадим нового пользователя взамен стандартного (в моем случаем логин пользователя будет »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
И удалим старого пользователя
sudo userdel -r orangepi
sudo rm -rf /var/mail/orangepi
Настроим время
sudo timedatectl set-timezone Europe/Moscow
timedatectl
Имя хоста
sudo hostnamectl set-hostname opi
Не забываем сделать правки в файлах /etc/hosts и /etc/hostname после чего перезагружаем систему
sudo reboot
1.3 Подключение к Github
Создадим rsa ключи для доступа к закрытому репозиторию на GitHub
ssh-keygen -t rsa
Открываем публичный ключ, копируем его содержимое в личный кабинет на Github в разделе Settings → SSH and GPG keys → SSH keys
sudo cat ~/.ssh/id_rsa.pub
После чего тестируем соединение
ssh -T git@github.com
После авторизации, копируем репозиторий с нашими рабочими ролями Ansible
git@github.com:/.gite
Подключение телефона к Orange PI и запуск плейбука
Для подключения по ssh с мобильного телефона на ОС Android я использую 2 приложения, ConnectBot — как ssh клиент, и Network Utilites — для определения адреса клиента.
Для работы нам нужно:
Определить сеть точки доступа
Наши клиенты подключаются к сети 192.168.222.38
Сканировать ее и найти адрес клиента
Адрес клиента 192.168.222.127
Подключится к клиенту по ssh
Открываем ssh сессию
Разрешаем Fingerprint
Редактируем файл инвентаря и group_vars
С телефона работать можно, все необходимые кнопки в приложении есть
Запускаем плейбук и наслаждаемся отчетом!
Как это выглядит
Если у вас есть вопросы по статье или вы хотите подробнее узнать об инструментах, которые мы не успели рассмотреть, напишите об этом в комментариях. Я рассмотрю ваши вопросы в следующих статьях! Спасибо за внимание!