...

пятница, 1 августа 2014 г.

Безопасное развертывание 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 http://ift.tt/1oUTtEa
$ 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.

Перезапускаем прокси, чтобы применить последние изменения, убедимся что все ок, перейдя по ссылке http://ift.tt/1pLnSRA — если все хорошо, он выдаст:



Missing access_token query parameter




Связываем контейнеры Proxy и ElasticSeach




Нам нужно связать два контейнера между собой, первый с Node.js прокси, второй собственно с ElasticSearch. Мне очень понравился dokku-link плагин, который делает как раз, то что нужно. Установим его,

$ cd /var/lib/dokku/plugins
$ git clone http://ift.tt/1pLnSRC




И после установки связываем прокси с эластиком,

$ dokku link proxy elastic




После этого прокси нужно будет еще раз перезапустить. Если все хорошо, то перейдя по ссылке http://ift.tt/1oUTqYU, мы увидем ответ от 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 приложения. Это может быть не очень удобно с точки зрения разработки, но более надежно с точки зрения безопасности.


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


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий