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

в 10:17, , рубрики: deployment, docker, dokku, elasticsearch, likeastore, security, Блог компании Likeastore, информационная безопасность, Поисковые машины и технологии

После успешного перехода 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 приложения. Это может быть не очень удобно с точки зрения разработки, но более надежно с точки зрения безопасности.

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

Автор: alexbeletsky

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js