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

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

    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
    

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

    ЗЫ. Это пересказ на русском моего поста из личного блога.
    Likeastore
    27,00
    Компания
    Поделиться публикацией

    Похожие публикации

    Комментарии 12

      +5
      Простите, а файрвола на ES-сервере было недостаточно, что потребовалось добавлять лишнее звено в цепочке?
        0
        почему я сделал так:

        1. я не хотел опять напрягатся развертыванием эластика, установкой jvm и прочим, так как для этого придуман доккер. к сожалению, те порты которые «выставляет» доккер, нельзя закрыть через iptables (у меня не получилось)

        2. iptables, мне еще не подходит тем, что я работаю из разных мест и разными динимическими ip, поэтому «белый» список дня меня не работае, проще кастомная авторизация.

        3. файрвол тоже надо настраивавать, мне как javascript иженеру, проще и быстрее это было решить через прокси.
      –1
      Подскажите, Docker умеет работать с mmap?
        0
        Вы имеете ввиду memory mapped files api? приложение, которое работает внутри контейнера имеет такой же доступ к api как и в не его… Поэтому, не совсем корректно, но отвечу — да, умеет.
          0
          Я имею в виду мэпинг физических файлов в память, входящий в стандарт posix и активно используемый Еластиком при работе с индексами при включенном mmapfs. Я не в курсе подробностей, умеет ли контейнер работать с этой фичей. По-верхам погуглил и ничего внятного не нашёл.
          ЗЫ. Тому кто не понял вопрос и влепил минус отдельное спасибо.
        +7
        NodeJS proxy? Omg…
        man iptables не пробовали?
          +6
          Простите за чайниковый вопрос — а nginx с базовой авторизацией чем не подошёл?
            0
            Если вы прочтете до конца, то послесловии как раз об этом.
            +5
            > Также, хорошей идей, наверняка будет держать Elastic в private network
            Очень удивлён что так не сделано изначально.
              0
              Вот же блин. Что за мода пошла открывать по умполчанию все наружу без авторизации. У mongodb оказывается по умолчанию порт открыт для внешки, теперь выясняется что и у elasticsearch такая же фигня.
              Я после того как наткнулся на такое поведение в монге, даже и не подумал, что ES ведет себя так же. В обоих случаях это кстати решается прописыванием айпишника в конфиге.
                +3
                А зачем устраивать весь этот цирк с конями, если можно добавить в elasticsearch.yml строку вида
                network.host = 127.0.0.1
                ?
                Или если нужен доступ из приватной сети — просто повесить его на приватный ip, и на уровне iptables разрулить все?

                Странный подход.

                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                Самое читаемое