После успешного перехода c MongoDB полнотекстового поиска на ElasticSearch, мы успели запустить несколько новых сервисов работающих на Elastic'е, расширение для браузера и в общем и целом, я был крайне доволен миграцией.
Но в бочке меда, оказалась одна ложка дегтя — примерно через месяц после конфигурации и успешной работы, LogEntries / NewRelic в один голос закричали о том, что сервер поиска не отвечает. После логина на дешбоард Digital Ocean'a, я увидел письмо от поддержки, что сервер был приостановлен в связи с большим исходящим UDP трафиком, что скорее всего свидетельствовало о том, что сервер скомрометирован.
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, поэтому имеет смысл сразу выбрать его. Как обычно, поднятие новой машины занимает пару десятков секунд и мы готовы к работе.
Первое что нам нужно, это Docker образ с ElasticSearch. Несмотря на то, что для Dokku существуют несколько плагинов, я решил пойти путем самостоятельной установки, так мне показалось будет проще с конфигурацией.
Образ для Elastic'а уже готов и тут есть хорошие инструкции по его применению.
Как только образ загрузится, мы должны приготовить том, который будет внешним для работающего контейнера (даже в том случае, если контейнер остановится и будут перезапущен, данные будут хранится на файловой системе хоста).
В этом фолдере мы создадим конфигурационный файл, elasticsearch.yml. В моем случае он очень простой, у меня кластер из одной машины, поэтому меня удовлетворяют все настройки по умолчанию. Но, как было сказано выше, небходимо отключить динамические скрипты.
Который будет состоять только из одной строчки,
После этого можно запускать сервер. Я создал простой скрипт, для на время конфигурации и отладки, может понадобится перезапускать несколько раз,
Обратите внимание на,
Теперь нужно запустить проксирующий Node.js сервер, который будет сервить трафик от/к localhost:9200 во внеший мир, безопасно. Я сделал маленький проект, основанный на http-proxy, названный elastic-proxy, он очень простой и вполне может быть переиспользанным в других проектах.
Сам код сервера,
Он проксирирует все реквесты и «пропускает» лишь те, которые указывают access_token как параметр запроса. access_token конфигурируется на сервере, через переменную окружения
Так сервер уже сконфигурирован для Dokku, то все что остается сделать, это «пушуть» исходники и Dokku развернет новый сервис.
После деплоймента, идем на сервер и конфигурируем токен доступа,
Я также хотел, чтобы все шло через SSL, с Dokku этого очень легко добиться, копируем
Перезапускаем прокси, чтобы применить последние изменения, убедимся что все ок, перейдя по ссылке https://search.likeastore.com — если все хорошо, он выдаст:
Нам нужно связать два контейнера между собой, первый с Node.js прокси, второй собственно с ElasticSearch. Мне очень понравился dokku-link плагин, который делает как раз, то что нужно. Установим его,
И после установки связываем прокси с эластиком,
После этого прокси нужно будет еще раз перезапустить. Если все хорошо, то перейдя по ссылке
Осталось сконфигурировать клиент таким образом, чтобы на все реквесты к серверу он передавал access_token. Для Node.js приложения это выглядит вот так,
Теперь можно перезапустить приложение, убедится что все работает как нужно… и выдохнуть.
Данный сетап, сработал (и работает сейчас) для Likeastore на отлично. Однако с течением времени, я увидел некий overhead, данного подхода. Скорее всего, можно избавится от проксируещего сервера, и сконфигурировать nginx c
Также, хорошей идей, наверняка будет держать Elastic в private network, и все реквесты к нему делать через API приложения. Это может быть не очень удобно с точки зрения разработки, но более надежно с точки зрения безопасности.
ЗЫ. Это пересказ на русском моего поста из личного блога.
Но в бочке меда, оказалась одна ложка дегтя — примерно через месяц после конфигурации и успешной работы, LogEntries / NewRelic в один голос закричали о том, что сервер поиска не отвечает. После логина на дешбоард Digital Ocean'a, я увидел письмо от поддержки, что сервер был приостановлен в связи с большим исходящим UDP трафиком, что скорее всего свидетельствовало о том, что сервер скомрометирован.
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
После этого прокси нужно будет еще раз перезапустить. Если все хорошо, то перейдя по ссылке
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 приложения. Это может быть не очень удобно с точки зрения разработки, но более надежно с точки зрения безопасности.
ЗЫ. Это пересказ на русском моего поста из личного блога.