Кибербезопасность собственного VPS
Hack ProofНачало. NGINX
После покупки VPS и развертывания небольшого приложения, нужно не полениться развернуть ELK стэк для мониторинга своей системы и подробно пронаблюдать за её поведением. Буквально в первые пять минут можно заметить нагрузку на NGINX приложения, когда клиентские приложения еще не включены. Если посмотреть в логи внимательно можно увидеть следующую картину:

О чем говорят логи? Они говорят о том, что кто-то пингует и "дергает" наш IP адрес, подыскивает всяческие уязвимые места. Почему уязвимые? Ниже список URL, которые в основном приходят в запросе:
- POST /etc/nginx/html/index.html
- GET //.well-known/security.txt
- GET /shopdb/index.php
- GET /login
- GET /.env
- GET /var/log/nginx/data-access.log
- GET /jenkins/login
- GET /${jndi:ldap://127.0.0.1
- GET /MySQLAdmin/index.php
- POST /axis2/axis2-admin/login
- GET \x00\x0E8\xA6fd\xDA\xA9\x9Az\x8B\x00\x00\x00\x00\x00"
Как можно заметить: это не безобидные запросы для healthchek. В этих запросах кроется злой умысел, ведь есть и инъекции log4j, и запросы для login страниц, и попытки пробраться к определенным файлам с чувствительной информацией.
В процессе заметно, что подобные наплывы запросов одинаковые, равномерные и происходят с определенными периодами, но с разных клиентов. Мы можем сделать здесь смелый вывод, что это чьи-то боты-сканнеры, которые ищут Ваши уязвимые места, а не хаккер-одиночка с браузера тыкает все эти запросы.
Защищаем NGINX
Для этого нам необходимо иметь доступ к редактированию файла конфигурации nginx. Обычно он находится по пути /etc/nginx/nginx.conf. Подробнее про конфигурирование этого замечательного приложения можно найти здесь.
1) Защищаемся на уровне валидации доменного имени, если таковое имеется. Это необходимо, чтобы явно сказать серверу, что мы не ждем гостей, которые пришли к нам, обращаясь без уважения по IP-адресу
if ($host !~ ^(example.com|www.example.com|docs.example.com)$ ) {
return 444;
}
2) Если на нашем сервисе не допустимы некоторые HTTP-методы, их лучше исключить. Обычно исключают редкие HEAD, OPTIONS, но для примера укажем любые:
if ($request_method !~ ^(GET|HEAD|POST)$ ) {
return 444;
}
3) Раз боты к нам ходят по тривиальным путям, то и мы изучаем и видим некоторые закономерности в этих ботах. Заметен заголовок User-Agents, у них есть такие значения, по которым можно понять, что это за потребитель. Большинство ботов научилось себя маскировать под Safari, Mozila, Chrome браузеры, против такой маскировки наш метод не подойдет.
# Некоторые известные боты носят именно такой user-agent в заголовке.
if ($http_user_agent ~* msnbot|scrapbot) {
return 403;
}
# Не позволим тривиальным скриптовым вещам нас трогать
if ($http_user_agent ~* LWP::Simple|BBBike|wget) {
return 403;
}
4) Про HTTPS и SSL можно посмотреть тут.
5) Если есть смысл определенные Path сервиса запаролить дополнительно - лучше это сделать (ссылка).
6) Маленький совет: мы заметили, что ходят по тривиальным URL для логина и подкидывают тривиальные basic-auth-credentials к ним. Можно защититься банальным использованием нетипичных username, ставить сложные криптостойкие пароли и попробовать сделать так, чтобы аутентификация могла быть только при ручном переходе на страницу, используя GET параметры state, consumer, api_key, etc или токены при редиректе в headers запроса. Тогда даже при брутфорсе одного верного логина и пароля злоумышленнику будет недостаточно. Нужно будет попотеть.
7) Если у вас есть только определенные URL, которые доступны, например приложение только для API, стоит открыть в Nginx только этот путь. Например, хотим выделить только для /api/v1/my_service/:
location /api/v1/my_service/ {
...
}
Полный конфиг, который получился:
server {
listen 443 ssl; # открыт только 443
listen [::]:443 default_server ssl;
ssl_certificate /ssl/nginx.crt; # Часть с сертификатами
ssl_certificate_key /ssl/nginx.key;
ssl_session_tickets off;
access_log /var/log/nginx/data-access.log combined; # Обязательно логируем процессы
return 444;
location / {
proxy_pass http://example.com;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_redirect http:// https://;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 20d;
proxy_buffering off;
}
}
Поехали дальше, не один nginx и http порты подвергаются подобным обстрелам зловредными запросами, которые "что-то ищут" с подстановкой типовых паролей и логинов. У нас есть сервисы, которые открыты по другим протоколам, например TCP. На данном порту могут быть любые соединения: socket proxy, database, redis, ssh, etc.
Чем богаты, тем и рады, продолжим и поговорим о БД.
PostgreSQL. За БД нужен глаз да глаз.
Наш подопытный сервис имеет базу данных, до которой тоже дошли злые руки ботов, которые пытаются подобрать пароли. Это видно по следующим логам:

Изучая логи, можно заметить следующее:
- IP-адреса злоумышленников
- Имена баз данных, к которым злоумышленники подключаются.
- Username базы данных, под которыми злоумышленники хотели проникнуть
А также, читая логи, вы можете заметить, что у них ничегошеньки не получается. Сейчас расскажу, почему:
Чтобы не было страшно, что наши данные из Базы данных похитят, хочу поделиться советами, как максимально-надежно защитить свою PostgreSQL от таких нападок. В логах видно, что злоумышленники получают отказ на вход в нашу БД. Это достигнуто некоторыми моментами.
Защищаем PostgreSQL
1) Мы не используем стандартные имена для БД. То есть для БД следует исключить очевидные названия: database, postgres, shop, db, sa, psql, mydb, my_database.
2) Мы не используем стандартные имена учетных записей для БД. То есть исключаем очевидные username: root, sa, psql, pgsql, admin, postgres, owner, analyzer, exporter, telegraf, user, some_user, test, etc...
3) Конечно же, защищаем свою БД самым криптостойким паролем на свете. Этого мы добиваемся командой:
# при создании пользователя CREATE USER my_service_name_user WITH ENCRYPTED PASSWORD 'СамыйСложныйПарольНаЗемле'; # при обновлении пользователя ALTER USER my_service_name_user WITH ENCRYPTED PASSWORD 'СамыйСложныйПарольНаЗемле';
4) Этим не ограничиваемся. БД - очень тонкая и дорогая вещь, особенно, когда там есть данные и конфиденциальная информация. Поэтому не стоит позволять, чтобы кто-то снаружи вообще смог достучаться до вашей базы данных.
Для этого нам необходимо конфигурить файл доступов к БД pg_hba.conf. Инструкция по настройке здесь.
Покажу на примере, где до сервера с postgres разрешил доступ только для приложения, которое находится на другом сервере.
host my_service_name_database my_service_name_user my_app_server.com trust
Тут мы явно говорим нашей СУБД, что мы принимаем подключения только от пользователя my_service_name_user в нашу БД my_service_name_database и только тогда, когда получили соединение с хоста приложения my_app_server.com.
ELK. Логи многое скажут о тебе!
Да, мы используем данный стек технологий для удобства, но его никто не должен видеть, кроме доверенных лиц. Потому что логи приложения дают столько информации, что и представить сложно. Тяжело, прочитав логи, не найти ни единого способа к НСД.
Правила защиты на самом деле просты и банальны:
1) Закрываем elastic и logstash на аутентификацию и тогда следом kibana тоже будет на нее и закрыта.
2) Можно постараться сменить дефолтные порты на какие-нибудь другие. Так будет меньше риска, что у ботов будет работать их сценарий по поводу ELK.
# Для примера:
Для elasticsearch по-умолчанию стоит порт 9200 -> меняем на 9222.
Для kibana по-умолчанию стоит порт 5601 -> меняем на 5611.
Приложение. Беречь надо как зеницу ока!
Каждого программиста всегда наставляют на написание безопасного приложения. Чтобы ключи где попало не валялись, чтобы доступы были только по защищенному соединению, по логину-паролю, не иначе. Долго здесь останавливаться не будем, у каждого свой уникальный рецепт готовить безопасность приложения. Некоторым сервисам это даже не нужно, когда они находятся глубоко за всеми возможными файрволами и замками от внешнего мира и к ним доходят уже валидные запросы. Пробежимся по основному:
1) Контейнеризация. Docker — отличная программа, позволяющая запускать изолированные контейнеры. Поэтому мы должны использовать их для размещения и запуска наших серверных приложений.
Таким образом, все приложения запускаются в своем собственном контейнере, поэтому они могут работать в своей собственной среде. Это означает, что любые двоичные файлы, которым мы не доверяем, не могут работать вне контейнера Docker. Кроме того, любые изменения, которые мы вносим в контейнер Docker, удаляются, если мы перестраиваем контейнер Docker, поэтому, если мы или какая-то атака испортят контейнер, мы можем просто перестроить его и вернуть в рабочее состояние.
Библиотеки и зависимости разных приложений не будут мешать друг другу.
Кроме того, несколько контейнеров, работающих на одном и том же сервере, будут иметь некоторый уровень доступа к другим контейнерам и самому хосту. Поэтому мы должны защищать все хосты и запускать контейнеры с минимальным набором возможностей. Мы можем делать такие вещи, как отключить сетевой доступ к контейнеру, если нам нужно.
Важно: не используйте пользователя root внутри вашего образа. Создавайте пользователей в Dockerfile:
RUN groupadd --gid 2000 node \ && useradd --uid 2000 --gid node --shell /bin/bash --create-home node
Важно: не пишите в docker-compose.yml пароли, ключи, токены. Доверьте это файлу с переменными окружения через:
env_file:
- /root/environments.env
Либо через secrets:
secrets:
my_external_secret: # создание секрета через команду docker secret create
external: true
my_file_secret: # секрет лежит в файлике
file: my_file_secret.txt
2) Набор джентельмена, от которого должен себя обезопасить веб-разработчик сразу: SQL инъекции, межсайтовый скриптинг (XSS), межсайтовая подделка запроса (CSRF). Также использовать механизм кроссдоменных запросов (CORS).
Можно отдельно прорабатывать каждый пункт, но как показывает текущая практика: большинство веб-фраймворков из коробки реализуют данный функционал и не дают разработчику ошибиться и сделать своё детище небезопасным.
3) Всегда помним про шифрование уязвимых данных. Пароли никогда не должны храниться в виде обычного текста. Если нам нужно записывать пароли, то они должны храниться в чем-то, что не показывает пароль. Мы можем использовать асимметричное шифрование для хранения наших паролей, чтобы мы могли хранить их в зашифрованном виде с помощью открытого ключа, а затем расшифровывать их обратно в обычный текст с помощью закрытого ключа. В большинстве случаев пароли следует хранить в виде одностороннего хэша, чтобы их нельзя было расшифровать, даже если злоумышленники войдут в нашу базу данных. Мы шифруем каждый пароль с помощью безопасного hash и salt и просто проверяем хешированный пароль, если нам нужно проверить его для аутентификации.
4) Не храним уязвимые данные в репозитории. Использовать лучше секретные переменные, которые может себе позволить любой CI\CD инструмент, либо просто храним в очень недоступном месте на нашем сервере в файликах.
5) Необходимо вести аудит входов в систему. Это важно, потому что позволит мониторить, кто и когда пытался использовать учетные данные, с какого адреса и зачем. Это сокращает массу работы на выявление уязвимых мест.
Сервер. Он должен быть неприступен.
Да, мы подошли к самому основному. С этого стоило и начинать данную статью, ведь безопасность начинается с безопасного сервера. LINUX - это очень стабильная и безопасная система из коробки, благодаря гибкой работе с сетью и очень надежной работе с правами, группами и пользователями. Но попробуем вернуться к нашим ботам и посмотреть, где самое узкое место.
В каждый сервер можно осуществить удаленный доступ, обычно он осуществляется с помощью ssh. Доступ по ssh напрямую связан с пользователем операционной системы или группой. Для каждого пользователя должны иметься пароли или ключи для доступа.
Боты очень хорошо пытаются попасть по логину-паролю на мой сервер. Как это можно заметить? Запоминаем!
Необходимо прочитать те логи системы, которые отвечают за вход по ssh (для сервера это сервер sshd):
tail -n 500 /var/log/auth.log | grep 'sshd'
Вот, как выглядит результат с признаками того, что на наш сервер пытались проникнуть:

Мы прекрасно по логам можем заметить, что злоумышленники перебирают с разных IP-адресов ssh подключения, используя логины: root, user, debianuser, kuhniitalia, uggi, ubuntu, etc. Также мы видим, что проникнуть у них не получается, для этого стоит придерживаться следующих практик:
1) Придумать очень крипто-стойкий пароль. Основное!
2) Создать отдельного пользователя с нетривиальным username, чтобы избежать того, чтобы боты угадали с логином пользователя.
sudo useradd -m my_hard_username && sudo passwd мойСамыйСложныйПарольОдин1
3) Дать созданному пользователю необходимый sudo
usermod -aG sudo my_hard_username
4) Запрещаем пользователю root логиниться в систему удаленно через ssh. Правим файл /etc/ssh/sshd_config и ставим следующие параметр:
PermitRootLogin no
5) Гораздо эффективнее, когда к серверу невозможно подобрать строковый пароль, а когда у него аутентификация только по private-public key. Это легко реализовать. Сначала пропишем ключи для нашего пользователя my_hard_username:
echo <Мой публичный ключик> >> cd ~/.ssh/authorized_keys
Далее возвращаемся в файл /etc/ssh/sshd_config и запрещаем доступ по паролю и доступ с пустым паролем:
PasswordAuthentication no PermitEmptyPasswords no
После всех манипуляций с sshd_config перезапускаем сервер удаленного доступа.
sudo systemctl restart ssh.service # Для Ubuntu\Debian # или sudo systemctl restart sshd.service # Для RHEL/CentOS
6) Открытые порты. Надо позаботиться о том, чтобы "наружу" из сервера торчало как можно меньше портов, в целом, необходимо сузить порты до самого необходимого, это самое надежное, что можно порекомендовать для счастливой и веселой жизни в безопасности. Для начала посмотрим, какие порты открыты командой:
sudo netstat -tulpn # или sudo ss -tulwn | grep LISTEN
Результат будет следующий:

Прекрасно видим, какие порты и от какого приложения были открыты на сервере. Можем провести анализ и закрыть ненужные порты, мне в данном примере очень не нужно, чтобы порт 9090 торчал всему Миру на радость наружу, потому что он содержит в себе приложение, которое крутится в docker. А доступ к нему обеспечит мое Nginx приложение и так. Обращаемся к следующим командам для работы с портами:
iptables -A INPUT -p tcp --destination-port 9090 -j DROP
INPUT - входящий порт. tcp - протокол. destination-port - порт, который убираем. DROP - команда о том, что мы закрываем порт.
Важно: Если у вас сервер на диструбутиве Debian\Ubuntu, а ковыряться в iptables тяжело, то есть прекрасная альтернатива в виде утилиты UFW (Uncomplicated Firewall), который настраивается за пару команд на нужные порты и адреса.
Важно: Для настоящей безопасности обычно закрывают все порты, а потом открывают нужные. Учитывайте это