Безопасное развертывание ElasticSearch сервера

После успешного перехода c MongoDB полнотекстового поиска на ElasticSearch, мы успели запустить несколько новых сервисов работающих на Elastic’е, расширение для браузера и в общем и целом, я был крайне доволен миграцией.Но в бочке меда, оказалась одна ложка дегтя — примерно через месяц после конфигурации и успешной работы, LogEntries / NewRelic в один голос закричали о том, что сервер поиска не отвечает. После логина на дешбоард Digital Ocean’a, я увидел письмо от поддержки, что сервер был приостановлен в связи с большим исходящим UPD трафиком, что скорее всего свидетельствовало о том, что сервер скомрометирован.DigitalOcean предоставил линк на инструкции, что надо делать в таком случае. Но самое интересно было в комментариях, почти все кто пострадал от атак в последние время, имели развернутый ElasticSeach кластер с открытым 9200 портом. Злоумышленники пользовались уязвимостями Java и ES, получали доступ к серверу и первращали его в составную часть какой нибудь bot-сети.

Мне предстояло восстановить сервер с нуля, но в этот раз я не буду таким наивным, сервер будет надежно защищен. Я опишу свой сетап использующий Node.js, Dokku / Docker, SSL.

Почему так? Не смотря на всю мощь ElasticSearch, в нем не предусмотрено никаких внутренних средств защиты и авторизации, все нужно делать самому. Тут есть хорошая статья на эту тему.Злоумышленники (скорее всего) пользуются уязвимостью динамических скриптов эластика, поэтому — если они не используются (как в моем случае) их рекомендуют отключать.

И наконец, открытый 9200 порт это как приманка, его нужно закрыть.

Какой будет план? Мой план был такой — поднять «чистый» Digital Ocean дроплет, развернуть Elastic Search внутри Docker контейнера (даже если инстанс будет скомпрометирован, все что нужно будет сделать, перезапустить контейнер), закрыть 9200/9300 для доступа из вне и сервить весь трафик к эластику через Node.js прокси сервер, с простой моделью авторизации, через «shared secret».Поднимаем новый дроплет DigitalOcean предоставляет заранее подготовленный образ с Dokku/Docker на борту на Ubuntu 14, поэтому имеет смысл сразу выбрать его. Как обычно, поднятие новой машины занимает пару десятков секунд и мы готовы к работе.image

Разворачиваем ElasticSearch в контейнере Первое что нам нужно, это Docker образ с ElasticSearch. Несмотря на то, что для Dokku существуют несколько плагинов, я решил пойти путем самостоятельной установки, так мне показалось будет проще с конфигурацией.Образ для Elastic’а уже готов и тут есть хорошие инструкции по его применению.

$ docker pull docker pull dockerfile/elasticsearch Как только образ загрузится, мы должны приготовить том, который будет внешним для работающего контейнера (даже в том случае, если контейнер остановится и будут перезапущен, данные будут хранится на файловой системе хоста). $ cd / $ mkdir elastic В этом фолдере мы создадим конфигурационный файл, elasticsearch.yml. В моем случае он очень простой, у меня кластер из одной машины, поэтому меня удовлетворяют все настройки по умолчанию. Но, как было сказано выше, небходимо отключить динамические скрипты. $ nano elasticsearch.yml Который будет состоять только из одной строчки, script.disable_dynamic: true После этого можно запускать сервер. Я создал простой скрипт, для на время конфигурации и отладки, может понадобится перезапускать несколько раз, docker run --name elastic -d -p 127.0.0.1:9200:9200 -p 127.0.0.1:9300:9300 -v /elastic:/data dockerfile/elasticsearch /elasticsearch/bin/elasticsearch -Des.config=/data/elasticsearch.yml Обратите внимание на, -p 127.0.0.1:9200:9200, тут мы «привязываем» использование 9200 только с localhost. Я потратил несколько часов в попытках конфигурации iptables и закрытия 9200/9300 портов, безрезультатно. Благодаря помощи darkproger and @kkdoo все заработало как надо.-v /elastic:/data will маппит том контейрера /data в локальный /elastic.

Проксирующий Node.js сервер Теперь нужно запустить проксирующий Node.js сервер, который будет сервить трафик от/к localhost:9200 во внеший мир, безопасно. Я сделал маленький проект, основанный на http-proxy, названный elastic-proxy, он очень простой и вполне может быть переиспользанным в других проектах. $ git clone https://github.com/likeastore/elastic-proxy $ cd elastic-proxy Сам код сервера, var http = require ('http'); var httpProxy = require ('http-proxy'); var url = require ('url');

var config = require ('./config'); var logger = require ('./source/utils/logger');

var port = process.env.PORT || 3010; var proxy = httpProxy.createProxyServer ();

var parseAccessToken = function (req) { var request = url.parse (req.url, true).query; var referer = url.parse (req.headers.referer || '', true).query;

return request.access_token || referer.access_token; };

var server = http.createServer (function (req, res) { var accessToken = parseAccessToken (req);

logger.info ('request: ' + req.url + ' accessToken: ' + accessToken + ' referer: ' + req.headers.referer);

if (! accessToken || accessToken!== config.accessToken) { res.statusCode = 401; return res.end ('Missing access_token query parameter'); }

proxy.web (req, res, {target: config.target}); });

server.listen (port, function () { logger.info ('Likeastore Elastic-Proxy started at: ' + port); }); Он проксирирует все реквесты и «пропускает» лишь те, которые указывают access_token как параметр запроса. access_token конфигурируется на сервере, через переменную окружения PROXY_ACCESS_TOKEN.Так сервер уже сконфигурирован для Dokku, то все что остается сделать, это «пушуть» исходники и Dokku развернет новый сервис.

$ git push master production После деплоймента, идем на сервер и конфигурируем токен доступа, $ dokku config proxy set PROXY_ACCESS_TOKEN=«your_secret_value» Я также хотел, чтобы все шло через SSL, с Dokku этого очень легко добиться, копируем server.crt и server.key в /home/dokku/proxy/tls.Перезапускаем прокси, чтобы применить последние изменения, убедимся что все ок, перейдя по ссылке https://search.likeastore.com — если все хорошо, он выдаст:

Missing access_token query parameter Связываем контейнеры Proxy и ElasticSeach Нам нужно связать два контейнера между собой, первый с Node.js прокси, второй собственно с ElasticSearch. Мне очень понравился dokku-link плагин, который делает как раз, то что нужно. Установим его, $ cd /var/lib/dokku/plugins $ git clone https://github.com/rlaneve/dokku-link И после установки связываем прокси с эластиком, $ dokku link proxy elastic После этого прокси нужно будет еще раз перезапустить. Если все хорошо, то перейдя по ссылке https://proxy.yourserver.com? access_token=your_secret_value, мы увидем ответ от ElasticSearch, { status: 200, name: «Tundra», version: { number:»1.2.1», build_hash:»6c95b759f9e7ef0f8e17f77d850da43ce8a4b364», build_timestamp:»2014–06–03T15:02:52Z», build_snapshot: false, lucene_version:»4.8» }, tagline: «You Know, for Search» } Подстраиваем клиент Осталось сконфигурировать клиент таким образом, чтобы на все реквесты к серверу он передавал access_token. Для Node.js приложения это выглядит вот так, var client = elasticsearch.Client ({ host: { protocol: 'https', host: 'search.likeastore.com', port: 443, query: { access_token: process.env.ELASTIC_ACCESS_TOKEN } }, requestTimeout: 5000 }); Теперь можно перезапустить приложение, убедится что все работает как нужно… и выдохнуть.Послесловие Данный сетап, сработал (и работает сейчас) для Likeastore на отлично. Однако с течением времени, я увидел некий overhead, данного подхода. Скорее всего, можно избавится от проксируещего сервера, и сконфигурировать nginx c basic-authorization, с upstream в доккер контейнер, также с поддержкой SSL.Также, хорошей идей, наверняка будет держать Elastic в private network, и все реквесты к нему делать через API приложения. Это может быть не очень удобно с точки зрения разработки, но более надежно с точки зрения безопасности.

ЗЫ. Это пересказ на русском моего поста из личного блога.

© Habrahabr.ru