Введение
Здравствуйте! В своей профессиональной деятельности я часто работаю с системами, находящимися в различных сетях, изолированных как друг от друга, так и от Интернета.
Часто эти сети содержат 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_cacheinventory
Директория в которой мы определяем с чем мы будем работать.
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 при подключении к нашему тестовому серверу

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 
  С телефона работать можно, все необходимые кнопки в приложении есть Запускаем плейбук и наслаждаемся отчетом!  Как это выглядит 
- 
Если у вас есть вопросы по статье или вы хотите подробнее узнать об инструментах, которые мы не успели рассмотреть, напишите об этом в комментариях. Я рассмотрю ваши вопросы в следующих статьях! Спасибо за внимание!
Комментарии (11)
 - YouAreEmpty19.10.2024 08:15- Спасибо за статью! А почему просто не использовать termux, если это android смартфон?  - Dron_Ch Автор19.10.2024 08:15- Спасибо за идею! termux отличен для работы с телефона, нужно будет испытать как он поведет себя с Ansible. К сожалению, если в сети нет точек wifi, приходится искать пограничные устройства типа микрокомпьютеров с соответствующими модулями. 
 
 - ashkraba19.10.2024 08:15- Супер гуд конечно,но не совсем понимаю зачем столько действий. Можно ведь просто поднять тот же дженкинс или любой ci/cd, сделать свой контейнер с ансиблом и нужными настройками и с телефона просто запускать джобу, передавая через параметры уже более тонкие настройки. Я такой подход уже много лет использую для почти сотни серверов - полет нормальный. Гибкость и универсальность зависят полностью от ваших скилов.  - Dron_Ch Автор19.10.2024 08:15- Спасибо за идею! Проблема в том что иногда приходится работать с изолированными сетями. с настроенным локальным зеркалом репозиториев. Все равно приходится лезть в сеть аппаратно. Предлагаете развернуть контейнер Дженкинса на микрокомпьютере?  - jamaze19.10.2024 08:15- По идее, в вашем случае лучше подойдёт Semaphore https://github.com/semaphoreui/semaphore 
 
 
 - VenbergV19.10.2024 08:15- Возможно я что-то пропустил, но вы вроде бы на Debian 12 (bookworm) все запускаете? Тогда для fail2ban наверное надо прописывать - nftables-multiport, вместо- iptables-multiport.
 И уж простите за занудство, но править sshd_config очень сильно не рекомендуется. Все необходимые исправления надо сложить в свой конфиг, расположив его в отведенной директории sshd_config.d - Dron_Ch Автор19.10.2024 08:15- Можно и так, не стал просто заморачиваться, на iptables все работает отлично. В идеале да, во вложенную директорию вносим все изменения, чтобы ничего не сломать. 
 
 
           
 
Dron_Ch Автор
С