После успешного перехода 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, поэтому имеет смысл сразу выбрать его. Как обычно, поднятие новой машины занимает пару десятков секунд и мы готовы к работе.
Разворачиваем 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 приложения. Это может быть не очень удобно с точки зрения разработки, но более надежно с точки зрения безопасности.
ЗЫ. Это пересказ на русском моего поста из личного блога.
Автор: alexbeletsky