KhTech - Заметки

Рабочие заметки и инструкции

View the Project on GitHub

Тут описываю механики безболезненной репликации серверов с использованием Ansible и плейбуков.

О чём вообще

Вообще суть такого подхода в том, чтобы создавать сценарии взаимодействия с сервером под управлением Linux. И Ansible обладает рядом преимуществ по сравнению с обычными шелл-скриптами.

  1. Удобство и читаемость. Ибо плэйбук (сценарий) Ansible пишется в yml.
  2. Возможность применять изменения сразу на несколько серверов. Особенно важно при использовании связки тестового и основного сервера.
  3. Идемпотентность. Ну правда, в шелл-скриптах задолбёшься обыгрывать все уже установленные пакеты и активные политики if-ами.
  4. Декларативный подход. При использовании Ansible достаточно описать конечное состояние. А что для этого потребуется, он определяет сам.
  5. Лёгкость в запуске. В отличие от скрипта, который надо сначала отправить на сервер, Ansible все действия выполняет непосредственно по SSH.
  6. Множество уже готовых сценариев на Ansible Galaxy.
  7. Лёгкая интеграция с CI/CD, как Jenkins.

Предварительная настройка

Подготовка управляемых серверов

Что делаем

На управляемых серверах необходимо создать пользователя в группе wheel, пароль для sudo команд у которого не будет запрашиваться.

ToDo

На тестовом сервере создаём файл подкачки

dd if=/dev/zero of=/swapfile bs=512M count=2
chmod 600 /swapfile
mkswap /swapfile
swapon /swapfile

И обновляем vim /etc/fstab

vim /etc/fstab
/swapfile none swap sw 0 0

Обновляем систему

dnf upgrade -y

Создаём пользователя ansible на каждом из серверов

useradd ansible
passwd ansible
usermod -aG wheel ansible

Разрешаем исполнение sudo без пароля

echo "ansible ALL=(ALL) NOPASSWD: ALL" | sudo tee /etc/sudoers.d/ansible

Установка пакетов

sudo dnf install -y epel-release python3-pip

Установка зависимостей (локально для юзера ansible; использовать sudo su - ansible)

pip3 install passlib

Подготовка управляющего сервера

Установка пакетов

sudo dnf install -y epel-release python3-pip

Установка Ansible (локально для юзера ansible; использовать sudo su - ansible)

sudo dnf install -y pipx
pipx ensurepath
pipx install --include-deps ansible
ansible --version
pip3 install passlib

Генерируем пару ключей для управляемых серверов, используем имя ansible-key для примера; отправляем ключ на сервера, которыми будем управлять.

cd ~/.ssh/
ssh-keygen -t rsa -b 4096 -f ~/.ssh/ansible-key
ssh-copy-id -i ~/.ssh/ansible-key.pub user@server

Создаём конфигурацию SSH на управляющем сервере. Задача этой конфигурации - указать, с каким ключом и под каким юзером будет происходить подключение. Сохраняется в ~/.ssh/config

Host prod.khudoley.pro
    User ansible
    Port 22
    IdentityFile ~/.ssh/ansible-key
Host test.khudoley.pro
    User ansible
    Port 22
    IdentityFile ~/.ssh/ansible-key

Подготовка Ansible

Создаём в корне домашнего раздела пользователя файл конфигурации ~/ansible.cfg

[defaults]
inventory = /home/ansible/hosts.ini
remote_user = ansible
host_key_checking = False

Там же создаём файл инвентаря ~/hosts.ini

[prod]
live_server ansible_host=prod.khudoley.pro ansible_user=ansible
[test]
test_server ansible_host=test.khudoley.pro ansible_user=ansible

Создаём тестовый плейбук и проверяем подключение к серверам ~/playbooks/test.yml

---
- name: Проверка соединения с серверами
  hosts: all
  gather_facts: false
  tasks:
    - name: Тест соединения с хостами
      ansible.builtin.ping:

Запускаем проверку

ansible-playbook ~/playbooks/test.yml

В ответ должно вернуться нечто вроде этого

PLAY [Проверка соединения с серверами] *********************
TASK [Тест соединения с хостами] ***************************
ok: [live_server]
ok: [test_server]

PLAY RECAP *************************************************
live_server:    ok=1    ...   
test_server:    ok=1    ...

Если всё окей - можно переходить к созданию плейбуков.

Плейбук для активации SELinux

---
- name: Ensure SELinux is enabled and enforcing
  hosts: all
  become: true

  tasks:
    - name: Check SELinux status
      command: sestatus
      register: selinux_status

    - name: Enable SELinux in config if disabled
      lineinfile:
        path: /etc/selinux/config
        regexp: '^SELINUX='
        line: 'SELINUX=enforcing'
      when: "'disabled' in selinux_status.stdout"

    - name: Reboot system to enable SELinux
      reboot:
      when: "'disabled' in selinux_status.stdout"

    - name: Set SELinux to enforcing if permissive
      command: setenforce 1
      when: "'permissive' in selinux_status.stdout"

    - name: Confirm SELinux is enforcing
      command: sestatus
      register: selinux_final_status

    - name: Debug SELinux final status
      debug:
        msg: "SELinux final status: "

Плейбук для базовой настройки сервера

Устанавливаем необходимые коллекции

ansible-galaxy collection install community.general ansible.posix

Копируем плейбук

---
- name: Universal CentOS 9 Stream Setup
  hosts: all
  become: true

  vars:
    username: anley  # Имя пользователя, создаваемого на сервере.
    user_password: "<ENCRYPTED_PASSWORD>"  # Пароль для пользователя в зашифрованном виде.
    root_password: "<ENCRYPTED_PASSWORD>"  # Пароль для root в зашифрованном виде.
    hostname: test  # Имя хоста, устанавливаемое на сервере.
    my_ip: "<YOUR_IP_ADDRESS>"  # Твой IP-адрес.
    master_ip: "<SERVER_IP>"  # IP-адрес управляющего сервера.
    ssh_port: 22323  # Пользовательский порт для SSH.
    packages:
      - screen
      - vim
      - nftables
      - policycoreutils-python-utils
    network_interface: ens3  # Сетевой интерфейс управляемого сервера для настройки NAT.

  tasks:
    - name: Set root user password
      user:
        name: root
        password: ""
      # Устанавливает пароль для root-пользователя.

    - name: Create user '' and add to wheel group
      user:
        name: ""
        state: present
        password: ""
        groups: wheel
        append: true
      # Создаёт пользователя с заданным паролем и добавляет его в группу wheel для административного доступа.

    - name: Set system hostname to ''
      hostname:
        name: ""
      # Устанавливает имя хоста для системы.

    - name: Update /etc/hosts file
      copy:
        dest: /etc/hosts
        content: |
          127.0.0.1   localhost localhost.localdomain
          ::1         localhost localhost.localdomain
          127.0.1.1   
      # Обновляет файл hosts для поддержки локального разрешения имени хоста.

    - name: Upgrade all packages
      dnf:
        name: "*"
        state: latest
      # Обновляет все установленные пакеты до последних версий.

    - name: Install EPEL repository
      dnf:
        name: epel-release
        state: present
      # Устанавливает репозиторий EPEL для расширенного набора пакетов.

    - name: Install necessary packages
      dnf:
        name: ""
        state: present
      # Устанавливает необходимые пакеты из переменной packages.

    - name: Stop firewalld service
      systemd:
        name: firewalld
        state: stopped
        enabled: false
      # Отключает и останавливает firewalld, так как используется nftables.

    - name: Enable and start nftables service
      systemd:
        name: nftables
        state: started
        enabled: true
      # Включает и запускает сервис nftables для настройки файрвола.

    - name: Configure nftables rules
      copy:
        dest: /etc/sysconfig/nftables.conf
        content: |
          table inet main {
                  chain prerouting {
                          type filter hook prerouting priority raw; policy accept;
                  }

                  chain input {
                          type filter hook input priority filter; policy drop;
                          iif "lo" accept
                          ct state invalid drop
                          ct state { established, related } accept
                          tcp flags syn limit rate 50/second accept
                          icmp type { destination-unreachable, echo-request, router-advertisement, router-solicitation, time-exceeded, parameter-problem } limit rate 10/second accept
                          tcp dport { 22, 80, 443 } ct state new accept
                          ip saddr { ,  } tcp dport  ct state new accept
                          counter log prefix "input_drop: " reject
                  }

                  chain output {
                          type filter hook output priority filter; policy accept;
                  }

                  chain forward {
                          type filter hook forward priority filter; policy accept;
                  }

                  chain postrouting {
                          type filter hook postrouting priority mangle; policy accept;
                  }
          }
          table ip nat {
                  chain postrouting {
                          type nat hook postrouting priority srcnat; policy accept;
                          oifname "" ip saddr 10.8.0.0/24 counter masquerade
                  }
          }
      # Настраивает правила файрвола в конфигурационном файле.

    - name: Load nftables rules from configuration file
      command: nft -f /etc/sysconfig/nftables.conf
      # Загружает правила nftables из конфигурационного файла.

    - name: Configure SELinux for custom SSH port
      command: semanage port -a -t ssh_port_t -p tcp 
      args:
        creates: "/var/lib/selinux/targeted/active/ports"
      # Добавляет пользовательский порт SSH в SELinux.

    - name: Configure SSH to disable password login for ansible user
      lineinfile:
        path: /etc/ssh/sshd_config.d/04-passwordauth.conf
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
        create: true
      # Отключает вход по паролю для ansible пользователя.

    - name: Configure SSH to disable root login
      lineinfile:
        path: /etc/ssh/sshd_config.d/01-permitrootlogin.conf
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin no'
        create: true
      # Запрещает вход по SSH для root-пользователя.

    - name: Configure SSH port
      lineinfile:
        path: /etc/ssh/sshd_config.d/02-port.conf
        regexp: '^#?Port'
        line: 'Port '
        create: true
      # Устанавливает пользовательский порт для SSH.

    - name: Configure SSH client alive settings
      copy:
        dest: /etc/ssh/sshd_config.d/03-clientalive.conf
        content: |
          ClientAliveInterval 1800
          ClientAliveCountMax 16
      # Настраивает параметры для поддержания активности SSH-клиентов.

    - name: Restart SSH service
      systemd:
        name: sshd
        state: restarted
      # Перезапускает SSH для применения изменений конфигурации.

Для использования этого плейбука нам понадобится ip адрес свой, ip адрес управляющего сервера, имена сетевых интерфейсов, смотрящих наружу, на управляемых серверах, а также пароли, которые мы предварительно зашифруем для безопасного использования.

Внешний IP и интерфейс, который его получил, узнаём командой

ip addr

IP адреса, соответственно, для управляющего сервера используем “как есть”, а для себя устанавливаем по маске /24

Шифруем пароли командами

ansible-vault encrypt_string 'пароль юзера' --name 'user_password'
ansible-vault encrypt_string 'пароль рута' --name 'root_password'

Меняем остальные переменные при необходимости.

Создаём файл для хранения пароля от хранилища (не секурно, но вводить его каждый раз при запуске плейбука - твоё право, а я не хочу; если уж ломанут этот сервер - то ключ с подключением по ssh лежит перед носом)

echo 'Ключ шифрования паролей ansible-vault' > ~/.vault_pass
chmod 600 ~/.vault_pass

Запуск плейбука

Теперь, имея на руках плейбук с заполненными переменными для каждого сервера, мы можем выполнить его сначала для тестового, а потом, убедившись в результате, для живого сервера.

ansible-playbook ~/path/to/your_playbook.yml --vault-password-file ~/.vault_pass

Чуть больше политик безопасности

После того, как предыдущий плэйбук отработал, необходимо закрыть вход под рутом, запретить вход по паролю, увеличить окно жизни сессии SSH.

Перед этим генерим и отправляем на сервер клиентские ключи, чтобы не потерять доступ.

--
- name: Update SSH configuration
  hosts: all
  become: true
  tasks:

    - name: Configure SSH to disable password login for ansible user
      lineinfile:
        path: /etc/ssh/sshd_config.d/04-passwordauth.conf
        regexp: '^#?PasswordAuthentication'
        line: 'PasswordAuthentication no'
        create: true
      tags:
        - ssh
        - security

    - name: Configure SSH to disable root login
      lineinfile:
        path: /etc/ssh/sshd_config.d/01-permitrootlogin.conf
        regexp: '^#?PermitRootLogin'
        line: 'PermitRootLogin no'
        create: true
      tags:
        - ssh
        - security

    - name: Configure SSH client alive settings
      copy:
        dest: /etc/ssh/sshd_config.d/03-clientalive.conf
        content: |
          ClientAliveInterval 1800
          ClientAliveCountMax 16
      tags:
        - ssh

    - name: Restart SSH service to apply changes
      service:
        name: sshd
        state: restarted
      tags:
        - ssh
        - restart