Привет всем обитателям Хабра, до кого ещё не добрались вечные перебои сети Интернет!

Так уж сложилось, что я, как и многие мои коллеги по цеху автоматизации и ИБ (SecOps, DevOps и SRE) достаточно ленивы не любят рутинный, ручной и особенно монотонный труд, особенно когда время можно потратить на гораздо более полезные вещи.

И так как я имею под собственным управлением достаточно немалую инфраструктуру для специалиста по ИБ (около 30 продуктивных ВМ и более 10 тестовых), парк которых обновляется и пополняется весьма динамично и хаотично - у меня возник вопрос о том, каким образом мне забыть про ручную первоначальную настройку ВМ на пилотах и внедрениях.

Начать его я решил с базы для себя - заведения ВМ под управлением Linux в службу каталогов для централизованной аутентификации и управления правами (по моим расчётам, каждое такое мероприятие занимает в среднем 10-15 минут на одну машину, с постоянным лазанием в заметки, и если "всё идёт по плану").

Торопыжкам, желающим вкусить файлы конфигураций и плейбуки - рекомендую перейти к заголовку "Реализация".

Дисклеймер: плейбуки составлены с учётом того, что ВМ в инвентаре имеют ось на базе Ubuntu 18.04 и старше, сам же узел управления - ansible core 2.20.3, в качестве службы каталогов используется Active Directory.

Выбор решений

Естественно для автоматизации управления инфраструктурой на базе Linux - золотым стандартом является давно зарекомендовавший себя в сообществе Ansible.

Но используя любой подобный оркестратор мы повышаем риски по ИБ, поскольку появляется новая точка для потенциальной централизованной компрометации инфраструктуры и закрепления в ней.

Основными рисками, при компрометации узла управления Ansible - были выделены:

  • Утечка учётных данных для подключения к серверам и сервисам;

  • Несанкционированная модификация задач оркестратора (плейбуков);

Они могут привести не только к ошибкам из разряда человеческого фактора (если инженер случайно, или не совсем, добавит в task'у shutdown серверов вместо reboot'а), но и к горизонтальному перемещению в инфраструктуре, установке вредоносных компонентов (сторонних библиотек, C2-агентов и иной малвари), настройке неконтролируемого удалённого доступа и так далее по списку.

А вот и "плейбук" одного знакомого инженера, которого лишили 13-ой З/П
А вот и "плейбук" одного знакомого инженера, которого лишили 13-ой З/П

Нивелировать второй, в моей инфраструктуре, было достаточно целесообразно используя уже развёрнутые инструменты (хранилище репозиториев Gitlab с проверкой исходного кода через SAST, настройка процессов CI&CD вместе с Gitlab Runner для доставки плейбуков на управляющий узел Ansible, применение модуля контроля целостности (FIM) в OSSEC-агенте). Про это возможно, чуть позже, появится отдельная статья.

Но вот первый - достаточно неплохой вызов, т.к. необходимо применять не только харденинг ОС и наложенные средства защиты и мониторинга, но и защитить учётные данные от утечки.

Именно по этому, наперекор всем кустарным системным администратором, было принято волевое решение - убрать все учётные данные в HashiCorp Vault.

Лирическое отступление x1

У многих мог появится весьма резонный вопрос, а почему, собственно не Ansible Vault?

При использовании встроенного механизма Ansible Vault мы сталкиваемся с классической проблемой управления секретами в файловой форме. Он позволяет зашифровывать отдельные секреты или файлы инвентаря, но ключ шифрования в любом случае нужно где-либо хранить и передавать для работы оркестратора.

От этого появляется ряд проблем:

  • Статичность секретов – после расшифровки секрет попадает в память, и остаётся действительным неограниченное время;

  • Сложность ротации – чтобы сменить даже один секрет - нужно перешифровать все файлы, содержащие этот пароль, и перевыпустить ключи доступа;

  • Отсутствие аудита – невозможно точно определить, кто и когда запрашивал конкретный секрет;

  • Отсутствие управления доступом - любой, у кого есть пароль от Vault получает доступ ко всем секретам, хранящимся в зашифрованных файлах.

HCV решает эти задачи на архитектурном уровне. Он предоставляет централизованное API для хранения и выдачи секретов с контролем времени жизни (TTL). Вместо статичных зашифрованных файлов мы получаем:

  • Динамические секреты – например, Vault может сгенерировать уникальные учетные данные с ограниченным сроком действия, которые автоматически отзываются по истечении TTL;

  • Аренда и ротация секретов (leases) – каждый выданный секрет имеет время жизни, по истечении которого Vault автоматически его отзывает;

  • Детальный аудит – Vault журналирует каждый запрос к секретам, что позволяет отследить, когда и какое приложение запрашивало доступ;

  • Политики доступа – можно определить, какие действия возможно выполнить над секретами на основе ролей, используя пути к хранилищам.

А вот собственно и все возможности в HCV, которые можно применить в policies
А вот собственно и все возможности в HCV, которые можно применить в policies

Лирическое отступление x2

Однако те, кто так же как я выбрал путь ниндзя HCV - могут из-за соображений удобства, либо же по незнанию, использовать не совсем корректный способ аутентификации в самом Vault для оркестраторов задач.

Самый простой способ подключить Ansible к HCV – использовать Token (долгоживущий токен, полученный при инициализации или созданный через CLI).

Однако токен – действует до момента отзыва или истечения срока действия.

Если токен скомпрометирован (например, попал в логи или был случайно, или заведомо, захардкожен в исходный код) - злоумышленник получает доступ ко всем ресурсам, разрешённым политикой этого токена, до тех пор, пока администратор вручную его не отзовёт при обнаружении утечки.

Для таких сценариев более уместен сценарий с AppRole. Он отличается от Token тем, что заведомо предназначен для машинных задач. Работает он благодаря двум частям - role_id (идентификатор роли, часто публичный) и secret_id (секретный ключ, который можно сделать одноразовым или ограниченным по времени).

Процесс получения токена выглядит так:

  1. Приложение отправляет в Vault запрос с role_id и secret_id;

  2. Vault проверяет, что пара валидна и что для данной роли разрешена аутентификация;

  3. Vault возвращает временный токен с заданными TTL и политиками, привязанными к роли.

Из этого вытекают следующие преимущества AppRole над Token-аутентификацией:

  • Ограничение времени жизни (TTL) – полученный токен действует ограниченное время (например, 1 час), после чего Vault автоматически его отзывает;

  • Возможность отзыва secret_id – даже если secret_id был скомпрометирован, его можно отозвать в Vault, не затрагивая другие экземпляры приложения (если они используют другие secret_id);

  • Интеграция с Ansible – модуль community.hashi_vault.vault_login позволяет выполнять аутентификацию по approle прямо в плейбуке.

Небольшой How-To по approle c использованием HCV CLI (т.к. создать его из веб-интерфейса методом "натыкивания" невозможно):

  • Включаем AppRole-аутентификацию:
    vault auth enable approle

  • Создаём политику доступа к секретам в файле ansible-policy.hcl (можно сделать в веб-интерфейсе):
    path "test/data/domain_join" {
    capabilities = ["read"]
    }

  • Применяем политику (можно не делать, если создавали через веб-интерфейс):
    vault policy write ansible-policy ansible-policy.hcl

  • Создаём approle с привязкой к политике:
    vault write auth/approle/role/ansible-role \ token_policies="ansible-policy" \ token_ttl=1h \ token_max_ttl=4h

  • Получаем role_id и генерируем secret_id:
    vault read auth/approle/role/ansible-role/role-id
    vault write -f auth/approle/role/ansible-role/secret-id

Ну а теперь, разобрав весьма спорные моменты, можем приступать к реализации.

Реализация

Структуру для данного проекта, на текущем этапе, решил упростить максимально, насколько это в принципе возможно в моём случае:

domain_join
- group_vars
-- all.yml (содержит все переменные, необходимые для работы плейбука)
- inventory.ini (содержит ВМ, которые необходимо ввести в домен)
- playbook.yml (непосредственно плейбук)

Содержание файла all.yml:

vault_addr: "{{ lookup('env', 'VAULT_ADDR') }}"
vault_role_id: "{{ lookup('env', 'VAULT_APPROLE_ROLE_ID') }}"
vault_secret_id: "{{ lookup('env', 'VAULT_APPROLE_SECRET_ID') }}"
vault_domain_data: >-
  {{ lookup(
      'community.hashi_vault.hashi_vault',
      '<тут путь до вашего KV хранилища>',
      url=vault_addr,
      auth_method='approle',
      role_id=vault_role_id,
      secret_id=vault_secret_id
  ) }}
domain_fqdn: "{{ vault_domain_data.domain_fqdn }}"
domain_fqdn_uppercase: "{{ vault_domain_data.domain_fqdn_uppercase }}"
dc_ip: "{{ vault_domain_data.dc_ip }}"
user_samaccountname: "{{ vault_domain_data.user_samaccountname }}"
user_pwd: "{{ vault_domain_data.user_pwd }}"
computer_ou: "{{ vault_domain_data.computer_ou }}"
ansible_user: "{{ vault_domain_data.local_user }}" 
ansible_password: "{{ vault_domain_data.local_password }}"
ansible_become_password: "{{ vault_domain_data.local_password }}"
# здесь имя доменной группы, члены которой могут зайти на сервер
allowed_group: "linux admins"
# здесь имя доменной группы, имеющей права sudo, и их привилегии
sudoers_group_line: "%linux\\ admins ALL=(ALL:ALL) ALL"
# при стандартной инсталляции Ubuntu - создаётся отдельный пользователь
# с правами sudo, поэтому ставим become в true, а method в sudo
ansible_become: true
ansible_become_method: sudo

Для любителей точечной настройки прав sudo: ссылка на официальный мануал.

Пример файла inventory.ini:

[linux_domain_members]
srv-01 ansible_host=192.168.88.11
srv-02 ansible_host=192.168.88.14
srv-03 ansible_host=192.168.88.22
srv-04 ansible_host=192.168.88.38
srv-05 ansible_host=192.168.88.88

Содержание playbook.yml:

- name: Join Linux hosts to AD domain and configure access
  hosts: linux_domain_members
  become: true

  pre_tasks:
    - name: Ensure required tools for debconf are installed
      apt:
        name: debconf-utils
        state: present
        update_cache: true

    - name: Preseed krb5-config default realm
      debconf:
        name: krb5-config
        question: krb5-config/default_realm
        value: "{{ domain_fqdn_uppercase }}"
        vtype: string

  tasks:
    - name: Remove systemd-timesyncd to avoid conflict with ntp
      apt:
        name: systemd-timesyncd
        state: absent
      become: yes

    - name: Install required packages
      apt:
        name:
          - krb5-user
          - samba
          - sssd
          - sssd-tools
          - libnss-sss
          - libpam-sss
          - ntp
          - ntpdate
          - realmd
          - adcli
          - packagekit
        state: present
        update_cache: true

    - name: Configure ntp to use domain controller
      copy:
        dest: /etc/ntp.conf
        content: |
          server {{ dc_ip }}
        owner: root
        group: root
        mode: '0644'

    - name: Stop NTP service before manual time sync
      service:
        name: ntp
        state: stopped

    - name: Sync time with domain using ntpdate
      command: "ntpdate {{ domain_fqdn }}"
      register: ntpdate_result
      changed_when: >
        'adjust time server' in ntpdate_result.stdout or
        'step time server' in ntpdate_result.stdout
      failed_when: ntpdate_result.rc != 0

    - name: Start and enable NTP service
      service:
        name: ntp
        state: started
        enabled: true

    - name: Check if host is already joined to the domain
      command: realm list
      register: realm_list
      changed_when: false
      failed_when: false

    - name: Join host to domain
      command: >
        realm join
        --user={{ user_samaccountname }}
        --computer-ou='{{ computer_ou }}'
        {{ domain_fqdn }}
      args:
        stdin: "{{ user_pwd }}"
      when: domain_fqdn_uppercase not in realm_list.stdout
      register: realm_join
      changed_when: realm_join.rc == 0
      failed_when: realm_join.rc != 0

    - name: Permit specific AD group to access the server
      command: >
        realm permit --groups "{{ allowed_group }}"
      register: realm_permit
      changed_when: "'already allowed' not in realm_permit.stderr | default('')"
      failed_when: realm_permit.rc != 0

    - name: Ensure use_fully_qualified_names is set to False in sssd.conf
      lineinfile:
        path: /etc/sssd/sssd.conf
        regexp: '^\s*use_fully_qualified_names\s*='
        line: 'use_fully_qualified_names = False'
        backrefs: false
      notify:
        - restart sssd

    - name: Ensure mkhomedir line exists after pam_sss.so
      lineinfile:
        path: /etc/pam.d/common-session
        regexp: '^session required pam_mkhomedir\.so skel=/etc/skel/ umask=0077$'
        line: 'session required pam_mkhomedir.so skel=/etc/skel/ umask=0077'
        insertafter: 'session optional\s+pam_sss\.so'
        state: present
        backrefs: false

    - name: Configure sudoers for linux administrators group
      copy:
        dest: /etc/sudoers.d/admins
        content: "{{ sudoers_group_line }}\n"
        owner: root
        group: root
        mode: '0440'
        validate: '/usr/sbin/visudo -cf %s'

  handlers:
    - name: restart ntp
      service:
        name: ntp
        state: restarted

    - name: restart sssd
      service:
        name: sssd
        state: restarted

Команда для запуска плейбука:

  • ansible-playbook -i inventory.ini playbook.yml

Итоги

С использованием данного плейбука - возможно буквально за 3 минуты ввести в домен неограниченное количество ВМ, что при иных условиях сулило бы часы мучений и страданий от ручного труда.

Надеюсь что данный плейбук сможет сэкономить Вам львиную долю времени на действительно интересные задачи, не создавая рисков по ИБ.

Всем добра, и поменьше рутины!

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Написать ли статью про контроль и версионирование плейбуков Ansible через Gitlab?
100%Да5
0%Нет0
0%Люблю работать руками0
Проголосовали 5 пользователей. Воздержавшихся нет.