OpenSearch часто выбирают как open-source альтернативу Elasticsearch для логирования, поиска и аналитики. Но между «скачал Docker-образ» и «рабочий кластер» лежит заметный путь: роли нод, TLS между нодами и для API, security plugin с ролевым доступом, индексы и их жизненный цикл. Ручная установка такого набора быстро перестаёт воспроизводиться — через месяц никто не вспомнит, какой флаг был выставлен вручную на третьей ноде.
Поэтому ниже — не «как накатить OpenSearch руками», а как интегрировать официальный opensearch-project/ansible-playbook со своим inventory и своей PKI: кластер поднимается одной командой, а TLS-сертификаты приходят из HashiCorp Vault, а не лежат самопальными файлами в репозитории.
В статье
- Когда 3-нодный кластер оправдан
- Архитектура и роли нод
- Развёртывание официальным плейбуком
- Первый green: проверка кластера
- TLS между нодами и для API
- Security plugin и ролевой доступ
- Здоровье кластера и повседневные операции
- Типичные ошибки
- Что дальше в серии
- Источники
О версии. Примеры в статье проверены на OpenSearch 3.5.0 — включая живой прогон security-цепочки (TLS,
securityadmin.sh, RBAC) в контейнере (актуальный минор ветки 3.x может отличаться — уточните перед воспроизведением). Между 2.x и 3.x поменялось поведение части security-настроек и внутренних API, поэтому команды из статей про OpenSearch 2.x переносить один в один не стоит. Если у вас уже поднят кластер — точный минор надёжнее всего смотреть прямо на нём:GET /возвращаетversion.numberв ответе.
Когда 3-нодный кластер оправдан
Один инстанс OpenSearch поднимается за пять минут и отлично годится для dev-стенда: посмотреть маппинги, погонять запросы, проверить интеграцию с приложением. Но у одной ноды нет второй копии данных — падение диска или неудачный docker compose down -v означает потерю индекса целиком. Для чего-то важнее черновика это не вариант.
На другом конце спектра — managed-сервис (Amazon OpenSearch Service, Aiven, Bonsai и подобные). Кто-то другой держит кворум, ротирует сертификаты и чинит split-brain по ночам, а вы платите за это по счёту. Разумный выбор, если нет причин управлять кластером самому.
Между ними — собственный кластер из нескольких нод. Он оправдан, когда решение принимается для приложения, которому OpenSearch реально нужен постоянно (логи, полнотекстовый поиск, аналитика), а не как разовый эксперимент, и когда есть где его размещать и кому обслуживать.
Когда стоит разворачивать 3-нодный кластер:
- нужна отказоустойчивость — потеря одной ноды не должна останавливать запись и чтение;
- объём данных или нагрузка на поиск требуют распределения шардов по нескольким машинам;
- OpenSearch — постоянный компонент инфраструктуры, а не временный стенд;
- есть возможность автоматизировать деплой и обслуживание (Ansible, PKI, мониторинг) — без этого 3 ноды сложнее в поддержке, чем одна.
Когда 3 ноды избыточны:
- задача — посмотреть, как индекс выглядит, или прогнать интеграционные тесты в CI: хватит одной ноды в Docker;
- нет ресурсов или желания заниматься TLS, ротацией сертификатов и мониторингом кворума самостоятельно — managed-сервис снимет эту заботу;
- нагрузка настолько мала, что риск простоя от одной ноды приемлем, а деньги/время важнее отказоустойчивости.
Ниже — про третий случай: кластер, который развёрнут осознанно и должен пережить падение одной ноды без потери данных.
Архитектура и роли нод
У OpenSearch каждая нода играет одну или несколько ролей:
data— хранит шарды индексов, исполняет запросы на чтение/запись, самая ресурсоёмкая роль (диск, heap, CPU);master(master-eligible) — участвует в выборах master-ноды и поддержании cluster state (какие индексы существуют, где лежат шарды, кто сейчас master);ingest— прогоняет документы через ingest pipelines (парсинг, обогащение) перед индексацией;coordinating— принимает запрос, распределяет его по data-нодам и собирает результат; в OpenSearch эта роль есть у любой ноды по умолчанию, отдельный «чистый coordinating»-узел нужен только на большем масштабе.
В стенде, о котором эта статья, три ноды покрывают data и master, а часть ещё и ingest:
os-node-1— data, master, ingestos-node-2— data, masteros-node-3— data, master, ingest
Почему именно три master-eligible ноды, а не одна и не две. Master выбирается голосованием, и для этого нужен кворум — больше половины master-eligible нод должны быть живы и согласны, кто сейчас master. При трёх нодах кворум — 2 из 3: кластер переживёт потерю любой одной ноды и продолжит работать с оставшимися двумя. При двух нодах кворума в 2 из 2 добиться нельзя без риска split-brain: если сеть между ними рвётся, каждая нода может решить, что она главная, и данные разъедутся. Поэтому классическое минимальное число master-eligible нод для отказоустойчивого кластера — три, а не два.
Отдельно от кластера — os-dashboard, выделенный хост под OpenSearch Dashboards. Он не участвует в хранении данных и не голосует за master, а обращается к кластеру как обычный клиент — по REST API поверх TLS. Вынесение UI на отдельную машину — сознательное решение: падение или перегрузка Dashboards не задевает сам кластер, а обновлять и перезапускать UI можно независимо.
master · data · ingest"] N2["os-node-2
master · data"] N3["os-node-3
master · data · ingest"] N1 <-->|"transport TLS · 9300"| N2 N2 <-->|"transport TLS · 9300"| N3 N3 <-->|"transport TLS · 9300"| N1 end DASH["os-dashboard
OpenSearch Dashboards"] DASH -->|"REST TLS · 9200"| cluster
flowchart TB
subgraph cluster["Кластер OpenSearch"]
direction LR
N1["os-node-1
master · data · ingest"]
N2["os-node-2
master · data"]
N3["os-node-3
master · data · ingest"]
N1 <-->|"transport TLS · 9300"| N2
N2 <-->|"transport TLS · 9300"| N3
N3 <-->|"transport TLS · 9300"| N1
end
DASH["os-dashboard
OpenSearch Dashboards"]
DASH -->|"REST TLS · 9200"| cluster
Две сети на схеме — не случайность, а разделение по протоколам. Между нодами кластера ходит transport-трафик (порт 9300 по умолчанию) — это внутренний binary-протокол для репликации, cluster state и координации запросов. Dashboards и любой внешний клиент используют REST/HTTP (порт 9200) — обычные HTTP-запросы к API. Оба слоя закрыты TLS, но это разные сертификаты и разные соединения; детали настройки — в разделе про TLS дальше в статье.
Развёртывание официальным плейбуком
Первый соблазн — написать свою Ansible-роль под OpenSearch: пару задач на установку пакета, шаблон opensearch.yml, systemd-unit. На практике это плохая идея. У проекта уже есть официальный плейбук — opensearch-project/ansible-playbook — который знает про поддерживаемые дистрибутивы, версии Java, нюансы systemd-юнита и обновления между минорами лучше, чем самописная роль, написанная за один вечер. Изобретать это заново — тратить время на то, что уже поддерживается апстримом и будет обновляться вместе с новыми версиями OpenSearch.
Поэтому здесь плейбук не переписывается, а интегрируется: клонируется в рабочую директорию и запускается со своим inventory и своими group_vars, где описана конкретная топология кластера.
git clone https://github.com/opensearch-project/ansible-playbook
cd ansible-playbook
ansible-playbook -i <inventory>/hosts.ini opensearch.ymlInventory — список хостов кластера, и именно здесь официальный плейбук читает роли нод: per-host, через переменную roles= (data / master / ingest). Плейбук ожидает у каждого хоста ещё и ip= — адрес, который попадёт в сетевые настройки и в discovery:
[os-cluster]
os-node-1 ansible_host=10.0.0.11 ip=10.0.0.11 roles=data,master,ingest
os-node-2 ansible_host=10.0.0.12 ip=10.0.0.12 roles=data,master
os-node-3 ansible_host=10.0.0.13 ip=10.0.0.13 roles=data,master,ingestВерсию и размер heap плейбук берёт из group_vars. Роли уже заданы в inventory выше, discovery он выводит из них сам:
os_version: "3.5.x" # проверено на 3.5; актуальный минор ветки уточните перед запуском
xms_value: 8 # -Xms, в ГБ: не больше 50% RAM ноды
xmx_value: 8 # -Xmx, в ГБ: ниже порога compressed oops (~32)
# роли нод (data/master/ingest) — per-host в inventory через roles=...
# discovery.seed_hosts и cluster.initial_cluster_manager_nodes плейбук выводит из inventoryВажная оговорка: os_version, xms_value/xmx_value и роли в inventory — это переменные самого официального плейбука, а не самописный слой автора. Heap задаётся парой xms_value/xmx_value (значения в гигабайтах, -Xms/-Xmx), и правило стандартное для JVM-приложений — не больше половины RAM ноды и обязательно ниже порога compressed oops (обычно около 32 ГБ: выше него JVM переключается на 64-битные указатели объектов, и часть выигрыша от увеличения heap съедается ростом самих указателей). Discovery-параметры — discovery.seed_hosts и cluster.initial_cluster_manager_nodes — плейбук выводит из ролей в inventory, вручную их прописывать не нужно. Точные цифры heap для конкретного стенда стоит проверять прямо на нодах, а не полагаться на значение из документации.
Что делает плейбук под капотом, если коротко: ставит пакет OpenSearch из официального репозитория (или устанавливает из архива, если репозиторий недоступен), создаёт systemd-unit и добавляет его в автозапуск, рендерит opensearch.yml из собранных переменных (роли ноды, heap через jvm.options, discovery, сетевые настройки) и на первом прогоне запускает bootstrap security plugin — генерацию начальной security-конфигурации, которая до применения через securityadmin.sh работает в демо-режиме с дефолтными сертификатами. TLS уже с реальными сертификатами из Vault PKI и осмысленный securityconfig — отдельный слой поверх этого базового плейбука, о нём дальше в статье.
Отдельно стоит сказать про плейбук, который не про установку. В репозитории автора рядом лежит небольшой вспомогательный playbook deploy-ism.yml — но он не разворачивает кластер, а работает поверх уже поднятого: подключается локально (hosts: localhost) и накатывает ISM-политики через REST API (ansible.builtin.uri). Это операционная задача, а не установка — к теме этого раздела он не относится, разве что как пример того, что не все Ansible-задачи вокруг OpenSearch — про bootstrap кластера.
PKCS#1 → PKCS#8 (handler)"] INST --> CONF --> CERT end SEC["securityadmin.sh
применяет securityconfig"] GREEN["_cluster/health = green"] PB -->|"SSH"| nodes PKI -->|"выпуск / ротация"| CERT nodes --> SEC --> GREEN
flowchart LR
subgraph control["Управляющий узел"]
PB["ansible-playbook opensearch.yml"]
PKI["ansible-playbook renew-certs.yml"]
end
subgraph nodes["Узлы кластера"]
direction TB
INST["Установка пакета + systemd"]
CONF["opensearch.yml: роли, heap, discovery"]
CERT["Сертификаты из Vault PKI
PKCS#1 → PKCS#8 (handler)"]
INST --> CONF --> CERT
end
SEC["securityadmin.sh
применяет securityconfig"]
GREEN["_cluster/health = green"]
PB -->|"SSH"| nodes
PKI -->|"выпуск / ротация"| CERT
nodes --> SEC --> GREEN
Два управляющих плейбука на схеме запускаются раздельно: opensearch.yml — установка и базовая конфигурация, renew-certs.yml — выпуск и ротация TLS-сертификатов из Vault PKI (описан в разделе про TLS дальше). На каждой ноде они сходятся в одной точке — файлах сертификатов, — а дальше кластер приходит в состояние green только после того, как securityadmin.sh применит security-конфигурацию поверх уже настроенного TLS. Порядок здесь важен: без валидных сертификатов на транспортном слое ноды не смогут даже договориться о том, кто master, а без применённого securityconfig REST API будет отвечать в демо-режиме, а не с реальными ролями и правами доступа.
Первый green
Прежде чем что-то из этого запустится, у OpenSearch есть входные требования к самой операционной системе — из тех, что молча ломают старт процесса, если про них забыть. Официальный плейбук проверяет их на этапе bootstrap:
vm.max_map_count=262144— Lucene (движок хранения индексов внутри OpenSearch) активно использует memory-mapped файлы, и дефолтное значение этого sysctl-параметра в большинстве дистрибутивов (65530) слишком мало — процесс не стартует и сразу падает с ошибкой о нехватке virtual memory areas.- ulimits
nofile/nproc— OpenSearch держит много одновременных файловых дескрипторов (сегменты индексов, соединения) и тредов. Дефолтные лимиты ОС на них обычно занижены для JVM-процесса такого профиля; плейбук выставляет более высокие значения через systemd-unit или/etc/security/limits.d.
Если эти проверки пропустить и попытаться завести ноду вручную в обход плейбука, процесс либо не стартует вовсе, либо упадёт при первой ощутимой нагрузке — оба сценария неприятнее, чем сбой на этапе bootstrap.
Когда все три ноды подняты, отрендерен opensearch.yml и применён securityconfig, финальная проверка — обычный запрос к _cluster/health:
# sysctl-требование до старта: vm.max_map_count=262144, ulimit nofile/nproc
curl -sk -u "admin:<OPENSEARCH_ADMIN_PASSWORD>" \
"https://os-node-1.example.internal:9200/_cluster/health?pretty"Ответ на здоровом трёхнодовом кластере — "status": "green": все primary- и replica-шарды назначены, кворум master-eligible нод собран, кластер готов принимать запросы. yellow на этом этапе означал бы, что primary-шарды есть, а реплики ещё не разъехались по нодам (например, только что созданный индекс без реплик или кластер ещё не успел ребалансироваться); red — что часть primary-шардов недоступна вовсе, и это уже повод разбираться, а не ждать. Флаг -k в примере — потому что на bootstrap-этапе TLS-сертификат ещё может быть демо-сертификатом плейбука, а не выпущенным из Vault PKI; после того как TLS настроен по-настоящему (следующий раздел), от -k стоит отказаться и проверять цепочку по CA.
Тот же _cluster/health из OpenSearch Dashboards (Dev Tools): "status": "green", все шарды назначены. Стенд на скриншотах поднят на OpenSearch 3.5.0 — на нём же проверялись примеры этой статьи.
GET / показывает точную версию узла ("number": "3.5.0", Lucene 10.3.2) — именно так надёжнее всего уточнять минор прямо на своём стенде.
TLS через Vault PKI
Демо-сертификаты, с которыми плейбук поднимает кластер на bootstrap-этапе, годятся ровно для того, чтобы проверить, что процесс стартует. Для чего-то более постоянного нужны настоящие сертификаты, у которых понятно, кто их выпустил, когда они истекут и кто может их перевыпустить. На этом стенде — Vault PKI: единый механизм выпуска и ротации сертификатов, который уже обслуживает добрый десяток других сервисов инфраструктуры (etcd, Kafka, NATS, ClickHouse, MinIO, HAProxy и так далее), и OpenSearch просто ещё один потребитель этого же механизма.
У OpenSearch два независимых TLS-слоя, и оба обязательны:
- transport (порт 9300) — внутренний binary-протокол между нодами кластера: репликация, cluster state, координация запросов. Здесь нужен mutual TLS — каждая нода одновременно клиент и сервер для соседей, и обе стороны проверяют сертификат друг друга. Без этого любой, кто достучится до порта 9300, мог бы притвориться нодой кластера.
- REST/HTTP (порт 9200) — обычный клиентский TLS для API: Dashboards, curl, приложения. Здесь сервер (нода) предъявляет сертификат клиенту, как на любом HTTPS-сайте.
На этом стенде для обоих слоёв используется один и тот же сертификат, а не отдельная пара для transport и отдельная для REST. Это возможно, потому что при выпуске сертификат помечен сразу и как серверный, и как клиентский (server_flag=true client_flag=true в роли Vault) — то есть годится и предъявлять себя (сервер), и подтверждать себя перед соседом (клиент), что как раз нужно для mTLS между нодами.
Роль выпуска в Vault
Сертификаты выпускаются через отдельную PKI-роль в Vault, специфичную для OpenSearch (в статье — os-pki):
vault write pki/roles/os-pki \
allowed_domains=<internal-domain> \
allow_subdomains=true \
allow_bare_domains=true \
key_type=rsa key_bits=2048 \
max_ttl=2160h \
server_flag=true client_flag=true \
enforce_hostnames=falseКлючевые параметры роли:
key_type=rsa key_bits=2048— RSA, а не ECDSA. Выбор осознанный (Vault PKI умеет выпускать и ECDSA-ключи), но именно RSA создаёт нюанс, из-за которого ниже появляется отдельный шаг конвертации ключа.max_ttl=2160h— 90 дней. Достаточно редкая ротация, чтобы не дёргать сертификаты каждую неделю, но достаточно короткий срок, чтобы скомпрометированный или забытый сертификат не жил в кластере годами.server_flag=true client_flag=true— один сертификат покрывает обе роли: сервер для REST, сервер и клиент одновременно для transport mTLS.
Аутентификация Ansible к Vault — AppRole (role_id + secret_id), а не root-токен и не статический пароль. Эти два значения передаются плейбуку через env-файл:
ansible-playbook -i inventory/hosts.yml pki/renew-certs.yml \
-l os_nodes -e @pki/vault-approle.env
# handler после ротации: конвертация PKCS#1 → PKCS#8 + restart сервисаvault-approle.env — файл с role_id/secret_id, лежит в .gitignore и никогда не коммитится: это учётные данные, по которым плейбук логинится в Vault (POST /v1/auth/approle/login), получает временный client token и уже им обращается к POST /v1/pki/issue/os-pki с common_name, alt_names, ip_sans и нужным TTL. В ответ Vault отдаёт certificate, issuing_ca, private_key и ca_chain. Из этого на диск ложатся три файла: полная цепочка сертификата (leaf + issuing CA), приватный ключ и отдельно CA chain. Приватный ключ записывается с правами 0600, а задача, которая это делает, помечена no_log: true — содержимое ключа никогда не попадает в вывод Ansible и в лог CI.
PKCS#1 → PKCS#8: главная ловушка
Здесь и кроется тот самый нюанс, из-за которого «выпустить сертификат и положить файл» не работает сразу. Vault PKI при key_type=rsa отдаёт приватный ключ в формате PKCS#1 — файл начинается со строки -----BEGIN RSA PRIVATE KEY-----. А security-плагин OpenSearch ожидает ключ в формате PKCS#8 — -----BEGIN PRIVATE KEY-----, без RSA в заголовке. Форматы кодируют один и тот же ключ по-разному, и OpenSearch просто откажется читать файл, полученный от Vault, без промежуточного шага.
Поэтому после выпуска или ротации сертификата запускается handler, который конвертирует ключ через openssl и идемпотентно проверяет, нужна ли конвертация вообще — по первой строке файла:
for key in /etc/opensearch/tls/*-key.pem; do
if head -1 "$key" | grep -q "RSA PRIVATE KEY"; then
openssl pkcs8 -topk8 -nocrypt -in "$key" -out "$key.tmp" && mv "$key.tmp" "$key"
fi
done
chown -R opensearch:opensearch /etc/opensearch/tls # если пользователь существует
chmod 0600 /etc/opensearch/tls/*-key.pemЛогика проверки простая: если первая строка файла — RSA PRIVATE KEY, это PKCS#1, конвертируем; если просто PRIVATE KEY без RSA, ключ уже в PKCS#8, и повторная конвертация не нужна. Это делает handler безопасным для повторных прогонов — гонять его на каждой ротации не страшно, лишний раз он ничего не тронет. Флаг -nocrypt означает, что на диске ключ хранится без пароля — доступ к нему защищён не шифрованием файла, а правами 0600 и владельцем opensearch.
Ротация без serial и джиттер порога
Ротация сертификатов на всех нодах кластера идёт без serial — то есть не поочерёдно, нода за нодой, а параллельно. На первый взгляд это выглядит рискованнее последовательного rolling-обновления, но здесь обратная логика: transport mTLS между нодами кластера ломается именно при rolling-ротации, если у всех нод сертификаты истекают в одну и ту же секунду. Нода, которая уже получила свежий сертификат, начинает отвергать mTLS-соединения от соседей, ещё сидящих на старом, — и наоборот. Если истечение синхронное для всего кластера, единственный безопасный вариант — рестартовать все ноды одновременно, а не по очереди.
Чтобы вообще не доводить до одновременного истечения, порог, при котором плейбук считает сертификат подлежащим перевыпуску (pki_renew_threshold_days), рандомизируется per-host по хешу имени хоста. В результате ноды кластера ротируют сертификаты не в одну и ту же дату, а размазанно по времени — и синхронный рестарт всего кластера просто не требуется чаще, чем это оправдано.
После конвертации ключа handler перезапускает сервис командой systemctl restart opensearch --no-block — не дожидаясь, пока нода полностью поднимется, playbook идёт дальше. Это согласуется с решением «без serial»: раз ноды и так перевыпускают сертификаты параллельно, ждать последовательного подъёма каждой было бы избыточно. Сам шаг рестарта помечен failed_when: false — если перезапуск на одной ноде не удался с первой попытки, это не должно ронять весь прогон плейбука по остальным нодам; такую ноду проще перепроверить отдельно, чем блокировать ротацию у всех.
Admin-сертификат и DN allowlist
Отдельно, run_once: true — то есть один раз за весь прогон, а не на каждой ноде, — выпускается admin-сертификат, тоже через роль os-pki. Он нужен для securityadmin.sh: административной утилиты security-плагина, которая применяет securityconfig (роли, маппинги пользователей, права доступа) к уже поднятому кластеру. Без действительного admin-сертификата securityadmin.sh просто не пройдёт авторизацию.
Важный принцип, который здесь стоит проговорить отдельно: security-плагин OpenSearch авторизует и admin-доступ, и inter-node доступ по Distinguished Name (DN) сертификата, а не только по факту, что сертификат подписан правильным CA. Валидной цепочки до доверенного CA недостаточно — в конфигурации security-плагина (plugins.security.authcz.admin_dn для admin-сертификата и plugins.security.nodes_dn для сертификатов нод) должен быть явный allowlist ожидаемых DN. Это защита от сценария «кто угодно с сертификатом от нашего CA может администрировать кластер»: даже если у CA украдут возможность подписать посторонний сертификат для домена, без совпадения DN с allowlist он не получит admin-прав или статуса ноды кластера. Точный формат DN — производная от common_name/alt_names конкретной роли Vault и специфичен для каждого стенда, здесь общий принцип важнее конкретного значения.
Если Vault нет: self-signed вариант
Vault PKI — не единственный способ поднять TLS для OpenSearch, а решение, оправданное там, где сертификаты и так выпускаются централизованно для десятка сервисов. Если Vault в инфраструктуре нет, у самого OpenSearch есть демо-скрипты, которые генерируют самоподписанные сертификаты — свой mini-CA, сертификаты нод и клиента, без внешней зависимости от PKI. Именно этот путь — тот, что проще воспроизвести без Vault, — лежит в runnable-примере digital-cookbook: там кластер поднимается с самоподписанными сертификатами, чтобы пример можно было запустить локально без развёрнутой PKI-инфраструктуры.
Security plugin
TLS отвечает за то, что трафик зашифрован и стороны предъявили сертификаты друг другу. А кто из аутентифицированных сторон что может делать внутри кластера — решает отдельный компонент, security plugin. Он встроен в OpenSearch по умолчанию (это форк x-pack security из Elasticsearch до открытия лицензии) и отвечает за пользователей, роли и то, какие индексы и действия каждой роли доступны.
Результат включённого security plugin: доступ к кластеру и к Dashboards — только после аутентификации. Логин проверяется по той же ролевой модели, что применяется ниже через securityadmin.sh.
Вся эта конфигурация — не файл на диске, который OpenSearch читает при старте, а набор документов в служебном системном индексе (.opendistro_security). Именно поэтому её нельзя просто положить в opensearch.yml и перезапустить процесс: конфигурацию нужно загрузить в кластер через отдельную административную утилиту — securityadmin.sh. Она идёт в комплекте с дистрибутивом OpenSearch (plugins/opensearch-security/tools/securityadmin.sh) и обращается к кластеру не как обычный клиент с логином и паролем, а по mTLS admin-сертификату — тому самому, что был выпущен run_once в предыдущем разделе и чей DN прописан в plugins.security.authcz.admin_dn. Это осознанный дизайн: применение прав доступа — операция, для которой обычной авторизации недостаточно, нужен отдельный, более узкий канал доверия.
Исходники конфигурации — набор YAML-файлов в директории securityconfig/, из которых основные:
internal_users.yml— локальные пользователи security-плагина (логин + хеш пароля + бэкенд-роли);roles.yml— роли: какие кластерные и индексные действия разрешены;roles_mapping.yml— какие пользователи или бэкенд-роли получают какую роль изroles.yml.
internal_users.yml: пароль — это хеш, не строка
Первое, на что стоит обратить внимание: internal_users.yml никогда не содержит пароль в открытом виде — только bcrypt-хеш.
app_writer:
hash: "<BCRYPT_HASH>" # сгенерировать hash.sh, не хранить пароль в открытом виде
backend_roles: ["writer"]Хеш получают отдельной утилитой из того же комплекта — hash.sh (hash.bat на Windows), обёрткой над Java-классом org.opensearch.security.tools.Hasher, который использует OpenBSDBCrypt из Bouncy Castle и печатает готовую bcrypt-строку (по умолчанию — BCrypt, 12 раундов). Утилита берёт пароль в интерактивном режиме или как аргумент. Сам пароль после этого нигде не сохраняется и не коммитится — в репозитории и в internal_users.yml живёт только хеш. Поле backend_roles здесь — это не имя роли из roles.yml напрямую, а промежуточная метка, которую дальше использует roles_mapping.yml, чтобы связать пользователя с конкретной ролью; такое разделение даёт возможность переиспользовать одну и ту же метку backend_roles и для внутренних пользователей, и для пользователей, пришедших через внешний auth-бэкенд (LDAP, SAML, JWT), без дублирования маппинга.
roles.yml: RBAC на уровне индекс-паттерна
Роль в OpenSearch — это пара из кластерных прав (cluster_permissions) и прав на конкретные индекс-паттерны (index_permissions). Разберём это на примере трёх ролей поверх одного и того же семейства индексов app-logs-*: reader только читает, writer пишет и создаёт индексы, admin может ещё и управлять настройками/удалением.
writer:
cluster_permissions:
- "cluster_monitor"
- "indices:data/write/bulk" # bulk-запись проверяется на cluster-уровне (см. ниже)
index_permissions:
- index_patterns: ["app-logs-*"]
allowed_actions: ["write", "create_index", "indices:data/write/bulk*"]По аналогии со writer в этой же схеме определяются reader (только read-действия на app-logs-*, без права писать или создавать индексы) и admin (полный набор действий на индекс-паттерн плюс более широкие cluster_permissions, например управление шаблонами индексов). Важный момент, который легко упустить: index_patterns — это именно паттерн, а не список конкретных индексов, поэтому роль writer автоматически действует на все будущие app-logs-2026.08.*, app-logs-2026.09.* и так далее, без необходимости перевыпускать роль при ротации индексов по дате.
Отдельно стоит разобрать cluster_permissions у writer — здесь их два, и второе неочевидно. cluster_monitor — это право смотреть на здоровье кластера, не меняя его настроек. А вот indices:data/write/bulk в кластерных правах выглядит странно (речь ведь про запись в индекс), но без него роль writer не запишет ни одного документа: в OpenSearch одиночный PUT /app-logs-…/_doc/… и POST /_bulk внутри резолвятся в действие indices:data/write/bulk, которое проверяется именно на cluster-уровне, а не на уровне индекса. То есть полные index_permissions на app-logs-* эту проверку не покрывают — запрос упадёт с 403 no permissions for [indices:data/write/bulk], даже если на индекс выданы indices_all. Это частая и трудноуловимая ошибка настройки RBAC. При этом ограничение по индекс-паттерну продолжает работать: cluster-право лишь открывает саму возможность bulk-записи, а куда писать — по-прежнему решает index_permissions, так что вне app-logs-* writer получит 403. cluster_permissions — отдельная ось от index_permissions, и её стоит держать как можно уже для ролей, не связанных с администрированием.
roles_mapping.yml: кто получает какую роль
Роль сама по себе никому не назначена — назначение происходит в roles_mapping.yml, который связывает роль либо с конкретным именем пользователя (users), либо с backend_roles из internal_users.yml (что удобнее, если ролей меньше, чем людей, и группировка происходит по бэкенд-роли, а не поштучно):
writer:
backend_roles: ["writer"]
users: ["app_writer"]Обе формы (backend_roles и users) можно комбинировать в одной роли — например, дать роль writer всем, у кого backend_roles: ["writer"], и дополнительно одному конкретному сервисному пользователю по имени, даже если у него другая бэкенд-роль.
Применение конфигурации: securityadmin.sh
Когда internal_users.yml, roles.yml и roles_mapping.yml отредактированы, их нужно загрузить в кластер — именно этот шаг превращает YAML-файлы на диске в реально действующую конфигурацию в системном индексе:
securityadmin.sh -cd ../securityconfig -icl -nhnv \
-cacert root-ca.pem -cert admin.pem -key admin-key.pem \
-h os-node-1.example.internalРазбор флагов: -cd ../securityconfig — директория с YAML-файлами конфигурации; -icl (ignore-cluster-name) отключает проверку соответствия имени кластера, что удобно при накатке через инструмент, не знающий имени кластера заранее; -nhnv (no-hostname-verification) отключает сверку имени хоста в сертификате с адресом подключения — уместно во внутренней сети с DNS-именами, не всегда совпадающими с CN/SAN сертификата, но не то, что стоит переносить бездумно во внешний периметр; -cacert, -cert, -key — тот самый admin-сертификат и CA-цепочка из раздела про TLS; -h — адрес любой ноды кластера, через которую накатывается конфигурация (она сама разнесёт изменения по остальным нодам, так как securityconfig — общий для всего кластера индекс, а не локальный файл на каждой ноде). Прогонять securityadmin.sh нужно каждый раз при изменении любого из YAML-файлов — сами по себе файлы на диске ни на что не влияют, пока не применены этой командой.
Ещё одна деталь, на которой легко споткнуться при первом реальном запуске на чистом (не demo) стенде: securityadmin.sh -cd ожидает в директории полный набор конфигурационных типов, а не только internal_users.yml/roles.yml/roles_mapping.yml. Как минимум нужен config.yml — в нём задаётся хотя бы один authentication domain (без него аутентификация вообще не настроена, это не косметика), — а также action_groups.yml, tenants.yml и nodes_dn.yml. Последние три вполне могут быть валидными «пустышками» с одним блоком _meta, но отсутствие любого из файлов приводит к аварийному завершению всей накатки (exit 255), причём не применяется вообще ничего. Практичнее всего взять полный комплект из demo-конфигурации дистрибутива как основу и заменить в нём то, что нужно, — иначе Ansible-шаг с securityadmin.sh упадёт на первом же прогоне.
Production-hardening: то, что нельзя оставить как из коробки
Дистрибутив OpenSearch из коробки разворачивается с demo-конфигурацией security plugin: демонстрационный internal_users.yml с несколькими предопределёнными пользователями (admin, kibanaserver и другими служебными), у которых пароли — общеизвестные строки из документации, а не что-то, сгенерированное под конкретный стенд. Это специально сделано, чтобы кластер можно было поднять и потрогать сразу после установки, без предварительной настройки security plugin. Но именно поэтому demo-конфигурация — то, что обязательно нужно заменить перед тем, как кластер увидит что-то важнее локального теста:
- сменить пароли всех demo-пользователей, в первую очередь
admin— на bcrypt-хеш собственного пароля (<OPENSEARCH_ADMIN_PASSWORD>в примерах health-проверок из этой статьи — это уже не demo-пароль, а секрет, который нужно сгенерировать и хранить вне репозитория); - не запускать
install_demo_configuration.sh— это одноразовый bootstrap-скрипт из комплекта дистрибутива, который накатывает demo-securityconfigпри первом старте; в проде его просто не вызывают, никакого «переключателя» здесь нет — это установочное удобство для дев-стенда, а не режим, который включается или выключается; - осознанно выставить
plugins.security.allow_default_init_securityindexвopensearch.yml— отдельный параметр, который решает, может ли сам security plugin при старте автоматически проинициализировать системный индекс.opendistro_securityиз демо-конфигурации, если индекса ещё нет. Документация OpenSearch прямо предупреждает: включать это вне приватной, доверенной сети небезопасно, поэтому в проде значение должно быть осознанным выбором (как правилоfalse), а не значением по умолчанию; - заменить demo
internal_users.yml/roles.yml/roles_mapping.ymlна собственные — под конкретных пользователей и роли, как в примерах выше, а не оставлять служебных demo-пользователей рядом с боевыми ролями «на всякий случай».
Разница между demo-конфигурацией и bootstrap-сертификатами из раздела про TLS — по сути одна и та же ловушка в двух разных слоях: и то и другое существует, чтобы кластер стартовал без предварительной настройки, и оба варианта не рассчитаны на то, чтобы пережить переход стенда в постоянную эксплуатацию.
Health и эксплуатация
Когда кластер поднят, TLS настроен и securityconfig применён, повседневная работа с ним сводится к небольшому набору REST-запросов к _cat и _cluster API. Ниже — тот же хост и те же переменные окружения, что и в проверке первого green:
OS_URL="https://os-node-1.example.internal:9200"
OS_AUTH="admin:<OPENSEARCH_ADMIN_PASSWORD>"Статус кластера и список нод. Первое, что стоит посмотреть при любом подозрении на проблему:
# Статус кластера: green / yellow / red
curl -sk -u $OS_AUTH "$OS_URL/_cluster/health?pretty"
# Список нод: роли, heap, load
curl -sk -u $OS_AUTH "$OS_URL/_cat/nodes?v"Шарды и индексы. _cat/shards показывает, где физически лежит каждый шард и в каком он состоянии — это первое место, куда стоит смотреть, если _cluster/health не green:
# Распределение шардов по нодам
curl -sk -u $OS_AUTH "$OS_URL/_cat/shards?v&s=index"
# Только unassigned-шарды — диагностика проблем
curl -sk -u $OS_AUTH "$OS_URL/_cat/shards?v&h=index,shard,prirep,state,node&s=state"
# Размер индексов, отсортировано по убыванию
curl -sk -u $OS_AUTH "$OS_URL/_cat/indices?v&h=index,store.size&s=store.size:desc"Unassigned-шард почти всегда означает одно из трёх: нода, на которой должна была лежать реплика, сейчас недоступна; на оставшихся нодах не хватает места под ещё одну копию; либо кластер только что пережил ребалансировку и ещё не закончил её. _cat/shards с сортировкой по state выводит все такие шарды разом — не нужно прокликивать полный список индексов, чтобы найти проблему.
Реплики. Число реплик — настройка на уровне индекса, и её можно менять на лету, без пересоздания индекса:
# Для одного индекса
curl -sk -u $OS_AUTH -X PUT "$OS_URL/<index>/_settings" \
-H 'Content-Type: application/json' \
-d '{"index": {"number_of_replicas": 1}}'
# Для ВСЕХ индексов разом
curl -sk -u $OS_AUTH -X PUT "$OS_URL/_all/_settings" \
-H 'Content-Type: application/json' \
-d '{"index": {"number_of_replicas": 1}}'
# Шаблон реплик по умолчанию для будущих индексов
curl -sk -u $OS_AUTH -X PUT "$OS_URL/_template/default_replicas" \
-H 'Content-Type: application/json' \
-d '{"index_patterns": ["*"], "settings": {"index": {"number_of_replicas": 1}}}'Шаблон в последнем примере важен отдельно: без него у только что созданного индекса реплики будут такими, какими их создало приложение или дефолт OpenSearch, а не тем значением, которое задумано для кластера — и придётся руками донастраивать каждый новый индекс задним числом.
Sizing шардов — ориентир, а не формула. Готового числа «сколько гигабайт на шард» не существует, но два крайних случая одинаково вредны. Слишком мелкие шарды (десятки мегабайт) — это лишние накладные расходы: у каждого шарда есть своя доля памяти под кластерное состояние на master-ноде, и тысяча крошечных шардов на индексах, ротируемых по дням, съедает эти ресурсы без пользы. Слишком крупные шарды (десятки-сотни гигабайт на шард) — наоборот, дольше восстанавливаются при переезде на другую ноду и хуже параллелятся при поиске, потому что один шард всегда обрабатывается одним потоком на одной ноде. Практический ориентир — держать размер шарда в диапазоне, комфортном для восстановления за разумное время (обычно единицы-десятки гигабайт, а не сотни мегабайт и не сотни гигабайт), и настраивать это через периодичность ротации индексов и число primary-шардов на индекс, а не подгонять постфактум.
Удаление индекса — необратимо. Синтаксис простой, но команда должна вызывать инстинктивную настороженность:
# Внимание: необратимо, без подтверждения, без корзины
curl -sk -u $OS_AUTH -X DELETE "$OS_URL/<index>"
curl -sk -u $OS_AUTH -X DELETE "$OS_URL/app-logs-2026.01.*"DELETE на индекс или на wildcard-паттерн индексов выполняется мгновенно и без подтверждения — нет ни корзины, ни soft-delete. Особенно опасен второй вариант с маской: опечатка в дате или расширение диапазона на день больше, чем нужно, удалит данные, которые не планировалось трогать. Прежде чем выполнять такую команду на боевом кластере, стоит явно свериться, что паттерн соответствует именно тому, что нужно, — например через _cat/indices с тем же паттерном перед DELETE, а не после.
Та же осторожность действует и на уровне Ansible: операционная практика, закреплённая в README автоматизации, — не гонять плейбуки по production вслепую. Сначала прогон с --check (dry-run, без реальных изменений) и с явным ограничением на целевой кластер через --limit, и только после этого — реальный запуск:
ansible-playbook -i inventory/hosts.ini opensearch.yml --limit os-cluster --check
# и только после ревью diff — без --check
ansible-playbook -i inventory/hosts.ini opensearch.yml --limit os-clusterТипичные ошибки
- Недостаточный heap. JVM-процесс OpenSearch с heap, зажатым слишком туго относительно рабочей нагрузки, начинает часто и подолгу уходить в full GC вместо обработки запросов — кластер внешне выглядит «подвисшим», а не упавшим, что сложнее диагностировать, чем явный краш. Ориентир — не больше половины RAM ноды и обязательно ниже порога compressed oops (см. раздел про развёртывание), но конкретное значение всё равно нужно сверять с реальным профилем нагрузки, а не брать по умолчанию навсегда.
- Discovery/split-brain из-за неверной конфигурации. Если
discovery.seed_hostsили список master-eligible нод настроены неконсистентно между нодами (например, одна нода не знает о существовании другой), кластер может разделиться на две группы, каждая из которых считает себя основной. При нечётном числе master-eligible нод и корректном кворуме (2 из 3, как в этой статье) риск ниже, но конфигурацию discovery всё равно стоит держать одинаковой и выводить из одного источника (inventory), а не редактировать вручную на отдельных нодах. - Demo security в production. Самая частая причина инцидентов вокруг OpenSearch — не забытый TLS, а забытая demo-конфигурация security plugin: пароли
adminи других служебных пользователей, которые известны любому, кто читал документацию OpenSearch. Кластер с demo-паролями, доступный из внешней сети, — это не «пока не настроено», а открытая дверь. - «Залипший»
cluster.initial_cluster_manager_nodes. Этот параметр (в старых версиях —cluster.initial_master_nodes) нужен только в момент самого первого формирования кластера — чтобы ноды, у которых ещё нет сохранённого cluster state, договорились, кто проводит первые выборы master. После того как кластер один раз успешно сформировался, параметр нужно убрать из конфигурации. Если оставить его вopensearch.ymlпостоянно, при последующих перезапусках или при добавлении новых нод в уже существующий кластер поведение может стать неожиданным — ноды попытаются повторно бутстрапить кластер вместо того, чтобы присоединиться к уже существующему, что и создаёт риск повторного split-brain на ровном месте. - Шарды не той величины. Слишком мелкие или слишком крупные шарды — не разовая ошибка, а накапливающаяся проблема, которая обычно всплывает через месяцы, когда индексы уже наросли по существующей схеме ротации, а менять primary-шарды у существующего индекса без reindex нельзя. Дешевле продумать sizing до того, как первый индекс ушёл в продакшен, чем переразмечать постфактум.
- Забытая ротация сертификатов Vault PKI. TLS-слой из этой статьи держится на per-host джиттере порога ротации и на handler’е, который конвертирует ключ и рестартует сервис (раздел про TLS). Если cron-job или systemd-timer, который запускает
renew-certs.yml, останавливается или ломается незаметно, сертификаты продолжают приближаться кmax_ttlбез предупреждения — и когда они всё-таки истекут, эффект будет не «постепенный», а сразу на всех нодах близко по времени (несмотря на джиттер, если ротация не запускалась достаточно долго, окна джиттера тоже пройдут). Мониторинг самого факта, чтоrenew-certs.ymlрегулярно выполняется и завершается успешно, — отдельная задача, которую легко забыть настроить одновременно с самой ротацией.
Что дальше
Эта статья закрывает базу: кластер поднят официальным плейбуком, TLS выпускается из Vault PKI, а security plugin разграничивает доступ по ролям. Но за кадром осталось то, что происходит с данными уже внутри поднятого и защищённого кластера — а это отдельная большая тема.
Следующая статья серии — про то, как данные структурируются внутри кластера: индексы, маппинги и шаблоны — типы полей и анализаторы, index templates и алиасы для ротации.
Дальше по плану серии:
- Сбор данных: клиенты и bulk-загрузка — как отправлять данные в OpenSearch из приложений на Go, Java, Rust и Python: REST API, официальные клиенты, пакетная загрузка и обработка ошибок.
- Retention через ISM — Index State Management: автоматический переход старых индексов в warm и удаление по возрасту, без ручной уборки. На стенде, о котором эта статья, такие политики уже настроены и работают — подробности намеренно оставлены за скобками и достанутся отдельному материалу.
- OpenSearch Dashboards — детальный разбор
os-dashboardиз раздела про архитектуру: визуализации, saved objects, доступ поверх той же ролевой модели security plugin. - Контур Vector → NATS → OpenSearch — как логи и события реально доезжают до кластера: сбор Vector’ом, буферизация через NATS, доставка в индексы с нужным маппингом — сквозной пайплайн наблюдаемости от источника до поиска по логам.
Забегая вперёд: так выглядит Discover в OpenSearch Dashboards (здесь — на встроенном демо-датасете). Просмотр и визуализация данных — тема отдельной статьи серии про Dashboards.
Официальные источники
- OpenSearch Documentation — Installation — установка OpenSearch, поддерживаемые способы (Docker, Debian/RPM-пакеты, tarball, Ansible).
- OpenSearch Documentation — Security plugin — конфигурация ролей, пользователей,
securityadmin.sh, RBAC на уровне индексов. - OpenSearch Documentation — Configuring TLS certificates — генерация и настройка сертификатов для transport- и REST-слоёв.
- opensearch-project/ansible-playbook — официальный Ansible-плейбук для развёртывания кластера, использованный в этой статье.
- HashiCorp Vault — PKI Secrets Engine — документация по PKI secrets engine: роли, выпуск сертификатов, TTL, ротация.
Комментарии