DanLevy.net

Ключевые рекомендации по безопасности Docker для самостоятельного хостинга

Защитите свои самохостинг‑сервисы: от защиты до мониторинга!

Содержание

🧗‍♀️ Для отважных

Если вы развёртываете Docker‑сервисы самостоятельно, безопасность полностью лежит на вас — нет облачного провайдера, который спасёт от сканирования портов или небрежных конфигураций. Будь то запуск приложений в домашней сети или аренда VPS у провайдеров вроде Vultr, DigitalOcean, Linode, AWS, Azure или Google Cloud, вам придётся закрыть все дыры и убедиться, что всё сделано правильно.

В этом руководстве мы пройдёмся по вопросам безопасности Docker — от менее известных до сложных в реализации техник; рассмотрим канарейковые токены, тома только для чтения, правила firewall, сегментацию и жёсткую настройку сети, добавление аутентифицированных прокси и многое другое.

Мы такжесравним домашние сети с публичными облачными развертываниями и покажем, как настроить базовый прокси с аутентификацией на Nginx. К концу вы получите несколько вариантов, позволяющих держать наружу нежелательных (друзей, семью и иногда даже себя…)

Это огромный объём информации! Но многие части взаимосвязаны, и вы можете выбрать то, что действительно подходит к вашей конфигурации. 🍀

🔄 Танец :latest

Обновление образов критически важно для безопасности. Однако полагаться на :latest значит рисковать внезапными изменениями или уязвимыми сборками без этапа проверки.

Безопасный способ обновления

Сочетайте команды обновления с pull или build, чтобы сознательно обновлять образы, а затем перезапускайте их в окне, когда вы сможете заметить возможные поломки.

update-and-run.sh
#!/bin/bash
docker compose pull && \
docker compose up -d

Привязка версии vs :latest

Выбор правильной версии для привязки — это компромисс между стабильностью и безопасностью. Ниже перечислены типичные стратегии:

docker-compose.yml
# ...
# Точная привязка версии, лучше всего для критически важных сервисов
image: postgres:17.2
# Привязка к патч‑версии, подходит для некритичных сервисов
image: postgres:17.2
# Привязка к мажорной версии, идеальна для хобби‑проектов
image: postgres:17
# YOLO, избегайте если возможно
image: postgres:latest

Используйте Dependabot или Renovate для создания проверяемых PR‑ов с обновлениями. Если вы не хотите собирать образ в 2 утра, привязывайте его к конкретной версии или дайджесту и позволяйте автоматике подсказывать, когда стоит перейти на новую.

Поделитесь, какие инструменты вы предпочитаете для поддержания Docker‑образов в актуальном состоянии!

🔐 Управление секретами

Существует множество способов управления секретами, но одно из самых важных правил — никогда не вшивать секреты в образы Docker и не коммитить их в git. Это одна из самых распространённых ошибок в безопасности, она создаёт долгосрочный риск и её исправление доставляет массу хлопот.

Безопасное хранение секретов — обширная тема с множеством вариантов: от файлов .env, Docker secrets, 1Password/Bitwarden, до менеджеров секретов вроде HashiCorp Vault или AWS Secrets Manager.

Вам придётся выбрать «правильный» уровень усилий и защиты для вашего сценария.

Генерировать надёжные секреты

Вот небольшойскрипт для генерации новых секретов в файле .env:

generate-secrets.sh
#!/bin/bash
generate_secret() {
local length=${1:-30}
local generate_length=$((length + 4))
openssl rand -base64 "$generate_length" | tr -d '+=/\n' | cut -c1-"$length"
}
[ -f .env ] && { echo ".env file already exists!"; exit 1; }
cat > .env << EOL
POSTGRES_PASSWORD=$(generate_secret)
JWT_SECRET=$(generate_secret 64)
SESSION_KEY=$(generate_secret 24)
REDIS_PASSWORD=$(generate_secret 20)
UNSAFE_PLACEHOLDER=__WARNING_REPLACE_RANDOM_TEXT__
EOL
echo "New .env file generated with secure random values!"

Canary Tokens

Canary Tokens — отличный способ обнаружить компрометацию ваших секретов (и их использование). Это своего рода ловушка, которую можно добавить в любые чувствительные файлы, URL‑ы и токены.

Размещайте их рядом с теми секретами, о которых действительно беспокоитесь: файлы .env, переменные CI, менеджеры паролей, папки резервных копий и облачные учётные данные. Не превращайте это в театральное представление; ставьте ловушки там, где реальный злоумышленник или будущий вы могут их задеть.

Существует множество типов канарей‑«токенов», от токенов AWS, фальшивых номеров кредитных карт, Excel и Word‑файлов, kubeconfig‑файлов, VPN‑учётных данных, до sql‑дампов, в которые тоже можно встроить ловушку!

Лучшие практики Canary Token

Переход от .env к Keychain macOS

Для пользователей macOS один из самых простых вариантов — использовать Keychain.

Ниже простой способ автоматизировать загрузку секретов из Keychain macOS, поддерживает TouchID и немного безопаснее, чем файлы .env.

Original credit: Brian Hetfield and Jan Schaumann.

Helper commandsPersist secrets in environmentUse secrets per command
keychain-secrets.sh
### Functions for setting and getting environment variables from the OSX keychain ###
### Adapted from: https://www.netmeister.org/blog/keychain-passwords.html and
Original credit: [Brian Hetfield](https://gist.github.com/bmhatfield/f613c10e360b4f27033761bbee4404fd) and [Jan Schaumann](https://www.netmeister.org/).
# Use: get-keychain-secret SECRET_ENV_VAR
function get-keychain-secret () {
security find-generic-password -w -a ${USER} -D "environment variable" -s "${1}"
}
# Use: set-keychain-secret SECRET_ENV_VAR
# You will be prompted to enter the secret value!
function set-keychain-secret () {
[ -n "$1" ] || print "Missing environment variable name"
# prompt user for secret
echo -n "Enter secret for ${1}"
read secret
[ -n "$secret" ] || return 1
( [ -n "$1" ] || [ -n "$secret" ] ) || return 1
security add-generic-password -U -a ${USER} -D "environment variable" -s "${1}" -w "${secret}"
}
~/code/app/.env-secrets.sh
source ~/keychain-secrets.sh
# Load Env vars into the current shell
export AWS_ACCESS_KEY_ID=$(get-keychain-secret AWS_ACCESS_KEY_ID);
export AWS_SECRET_ACCESS_KEY=$(get-keychain-secret AWS_SECRET_ACCESS_KEY);
# Note: If an attack can run `env` in your shell, then these secrets could be exposed!
~/code/app/scripts/env-run.sh
#!/usr/bin/env bash
source ~/keychain-secrets.sh
# Specify all secrets for this project
AWS_ACCESS_KEY_ID=$(get-keychain-secret AWS_ACCESS_KEY_ID) \
AWS_SECRET_ACCESS_KEY=$(get-keychain-secret AWS_SECRET_ACCESS_KEY) \
"$@"
# Note: Using a shell wrapper helps prevent secrets from staying
# around in the environment. And it's safe to commit.
# Usage:
# ./scripts/env-run.sh docker compose up -d
# ./scripts/env-run.sh docker run -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS ...

🌐 Сетевая опасность

Пользовательские сети и внутренние порты

Правильная изоляция сервисов с помощью Docker‑сетей — важный способ уменьшить площадь атаки.

Будьте осторожны, пробивая дыры в своей сети! Одна неправильно настроенная переадресация порта может закончиться очень плохо.

По умолчанию сервисы в частной локальной сети не будут доступны из интернета — вы должны явно пробрасывать порты на роутере.

Docker в локальной сети

Будь то разработчик, запускающий локальные dev‑серверы, или администратор, размещающий сервисы в своей локальной сети, неправильные представления о сетевой модели Docker могут привести к проблемам.

Разработчики часто удивляются, когда традиционные методы защиты Linux‑серверов (iptables, ограничение параметров tcp/ip sysctl) могут тихо проваливаться на хостах с Docker! Это особенно актуально при самостоятельном хостинге или работе в типичной домашней сети. (Для тех, кто в задней части: это может открыть доступ к контейнерам разработки на вашем MacBook!!!)

⚠️ Предупреждение #1: Публикуемые Docker‑порты могут обходить правила файрвола, которые, как вы думали, защищают хост, особенно при использовании UFW в Ubuntu/Debian. Это не делает каждое правило файрвола бесполезным, но означает, что «UFW говорит deny» не является доказательством. См. issue #690: Docker bypasses ufw firewall rules.

⚠️ Предупреждение #2: Привязка портов к локальным IP‑адресам (например, -p 127.0.0.1:8080:80) является правильным значением по умолчанию, но версии Docker Engine старее 28.0.0 имели случаи, когда хосты в той же L2‑сети всё‑равно могли достать до опубликованных локально портов. Docker документирует эту оговорку в руководстве по публикации портов, и привычка проверять с помощью nmap, описанная ниже, всё ещё актуальна.

Если вы удивлены, узнав об этом, вы не одиноки!

Привязка к локальным IP‑адресам всё ещё хорошая практика и имеет существенное значение в управляемых облачных средах и специально сконфигурированных сетях.

Пример Docker Compose

Вот пример файла docker-compose.yml, который привязывает сервис app к 127.0.0.1:8080 и подключает оба контейнера к пользовательской сети backend.

docker-compose.yml
networks:
backend:
services:
app:
networks:
- backend
ports:
# Привязываем к localhost, если возможно
- "127.0.0.1:8080:8080"
# ... другие настройки
database:
image: postgres:17.1
# Порты не нужны; доступно внутри сети backend.
networks:
- backend

Лучшие практики сети

🛡️ Управление доступом

Контроль доступа — ключевая часть защиты ваших Docker‑служб. Это включает ограничение возможностей и прав контейнеров, ограничение доступа к сокету Docker и многое другое.

Ограничение возможностей контейнеров

Еще одна надёжная практика контроля доступа — ограничить возможности ваших контейнеров. Это уменьшает радиус поражения различных угроз, от эскалации привилегий до перехвата трафика. Это не «защитный щит», но убирает права, которые большинству контейнеров никогда не нужны.

Что такое возможности? Это именованные разрешения или способности, определённые ядром Linux. (Полный список см. в руководстве capabilities.) Среди них — CAP_CHOWN (изменение владельца файлов), CAP_NET_ADMIN (настройка сетевых интерфейсов), CAP_KILL (завершение любого процесса) и многие другие.

Два способа определить необходимые возможности:

  1. Метод проб и ошибок: Этот более медленный, но эффективный подход заставляет начать без каких‑либо возможностей, а затем добавлять их по одной, пока приложение не заработает.
  2. Поиск готовых решений: Ищите “project-name cap_drop Dockerfile” или “project-name cap_drop docker-compose.yml”, чтобы увидеть, не сделали ли другие уже эту работу за вас. LLM может подсказать стартовую точку, но рассматривайте её как предположение, пока не протестируете контейнер и не изучите документацию образа.

Лучшие практики возможностей

Example: Drop/Limit Capabilities
services:
database:
image: postgres:17.1
networks: [ db-network ]
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- CHOWN
- DAC_READ_SEARCH
- FOWNER
- SETGID
- SETUID
db-admin:
image: dpage/pgadmin4:4.1
networks: [ db-network ]
ports:
- "8081:80"
# ... другие настройки
networks:
db-network:

Теперь ваши сервисы могут общаться друг с другом через сеть db-network. Docker Compose создаст эту сеть автоматически.

Используйте параметр --external/external: для подключения к существующей сети. Оставьте его пустым, чтобы создать новую сеть.

Доступ к Docker Socket

⚠️ Внимание: docker.sock — по сути доступ администратора хоста

⚠️ Параметр :ro не влияет на ввод‑вывод, передаваемый через сокет!

Он лишь гарантирует, что путь к сокету смонтирован только для чтения. API‑запросы, проходящие через этот сокет, всё равно могут создавать контейнеры, монтировать пути хоста и выполнять прочие весьма «интересные» действия, которые вы, вероятно, не собирались делегировать.

Лучшие практики для сокета

Блокировка по странам!

Иногда полезно, но это не реальное средство защиты.

Речь идёт о геополитическом объекте, а не о музыке…

Если вы обслуживаете приложения в основном для локальной семьи и друзей, можете блокировать трафик из стран, откуда не ожидаете запросов. Или наоборот — разрешать только трафик из ожидаемых стран. Это уменьшит шум; не остановит VPN, прокси, ботнеты или терпеливых злоумышленников.

block-china.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone | \
while read line; do ufw deny from $line to any; done
block-china.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/cn.zone | \
while read line; do ufw deny from $line to any; done

Аналогично, можно разрешить только трафик из США:

allow-usa.sh
curl -fsSL https://www.ipdeny.com/ipblocks/data/countries/us.zone | \
while read line; do ufw allow from $line to any; done

Ужесточение хоста‑прокси CloudFlare

Если ваш домашний сервер находится за IP‑прокси CloudFlare, вы можете ограничить доступ лишь IP‑адресами CloudFlare и вашей локальной сетью.

Это отчасти похоже на блокировку стран выше, но с гораздо более строгим контролем.

whitelist-ingress-from-cloudflare.sh
ufw default deny incoming # Блокировать весь входящий трафик!!!
ufw default allow outgoing # Разрешить весь исходящий
ufw allow ssh # Разрешить SSH
# Разрешить доступ для локальной подсети (желательно выделить отдельный DMZ/VLAN для размещаемых сервисов)
ufw allow from 10.0.0.0/8 to any port 443

Разрешить IP‑адреса CloudFlare

curl -fsSL https://www.cloudflare.com/ips-v4 |
while read line; do ufw allow from $line to any port 443; done

Добавить поддержку IPv6

curl -fsSL https://www.cloudflare.com/ips-v6 | \

while read line; do ufw allow from $line to any port 443; done

Для тестирования изменений, зависящих от геолокации, удобно использовать VPN с выходом в нужную страну. Подробнее — раздел [Monitoring & Verification](#-monitoring--verification).
### Безопасность уровня приложений
После того как вы [закрепите безопасность сети и хоста,](#-network-hazard) может оказаться, что работы ещё не достаточно.
Теперь нужно задуматься о слое «приложения» самих сервисов.
<p class="inset">Есть ли у этой базы данных надёжный пароль? Автоматизирует ли контейнер HTTPS/сертификаты? Встроена ли в приложение аутентификация? Есть ли ограничения на регистрацию email‑ов? Существуют ли учётные данные по умолчанию или переменные окружения, которые нужно изменить?</p>
Единственный способ _узнать_ — проверить. В данном случае начните с `README` и других ключевых файлов, таких как `docker-compose.yml`, `Dockerfile` и `.env.*`. Делайте это как для самого проекта, так и, по возможности, для его вспомогательных сервисов (например, Postgres, Redis и т.п.).
#### Обратный прокси
Ещё один уровень защиты — basic auth. Не используйте его без HTTPS. Для устаревших сервисов размещение basic auth перед административным маршрутом часто достаточно, чтобы остановить случайные запросы и неавторизованных сканеров, пытающихся напрямую обратиться к приложению.
```nginx
# /etc/nginx/conf.d/secure-admin.conf
location /admin {
auth_basic "Restricted Access";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://internal_admin:80;
proxy_set_header X-Real-IP $remote_addr;
}

Создайте учётные данные:

Terminal window
htpasswd -c /etc/nginx/.htpasswd admin

С прокси, использующим basic auth, злоумышленнику придётся сначала пройти дополнительный барьер — имя пользователя и пароль — прежде чем попасть к вашему внутреннему сервису.

Другой вариант — использовать сервисы вроде Traefik или Caddy, которые могут автоматически управлять HTTPS и basic auth за вас.

Если вам нужно управлять множеством доменов и сервисов через графический интерфейс, я советую воспользоваться Nginx Proxy Manager.

🔍 Мониторинг и проверка

Это самый важный и часто упускаемый шаг. Можно иметь лучший брандмауэр, лучшую сеть и безупречные практики, но без проверки вы не узнаете, работает ли всё действительно.

К тому же, знание нескольких ключевых команд — или умение быстро их найти — может стать разницей между предотвращённым и реальным нарушением. Ощущение «хакера» — лишь приятный бонус. (Подробности и примеры см. в разделе Monitoring & Verification.)

Не доверяй, проверяй дважды

Проверьте свои порты

⚠️ ВАЖНО: Не сканируйте хосты, которые вам не принадлежат.

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

Есть 2 способа сделать это:

Тестирование из‑за пределов вашей сети

Вам понадобится ваш текущий (публичный) IP, который легко получить через сервисы вроде ifconfig.me: curl https://ifconfig.me. Либо посмотрите его в панели управления вашего хостинг‑провайдера.

Get Public IP
curl -fsSL https://ifconfig.me
# --> CURRENT PUBLIC IP

Получив публичный IP, теперь нужно подключиться к внешней сети. Можно воспользоваться компьютером друга, телефоном/точкой доступа 5G или отдельным сервером.

nmap External Scan
target_host="$(curl -fsSL https://ifconfig.me)"
# Note: Ensure `target_host` is the desired IP
# Scan specific ports:
nmap -A -p 80,443,8080 --open --reason $target_host
# Top 100 ports:
nmap -A --top-ports 100 --open --reason $target_host
# All ports
nmap -A -p1-65535 --open --reason $target_host
#### Тестирование внутри вашей сети
Практикуйтесь с `nmap`: сканируйте локальную сеть или один из серверов, проверьте роутер, принтер, «умный» холодильник.
{/* Хотя сканирование портов — обычное дело, в США это может нарушать CFAA (Computer Fraud and Abuse Act). Поэтому сканируйте только то, что вам принадлежит. */}
#### Примеры команд сканирования
```bash
# Сканировать localhost на предмет всех открытых портов
nmap -sT localhost
# Сканировать приватный IP вашей машины на сервисы
nmap -sV 192.168.1.10
# Найти сервисы в вашей сети
nmap -sn 192.168.0.0/24
nmap -sn 10.0.0.0/24
# Или в Docker‑сети 172.18.0.1/16
nmap -sn 172.18.0.1/16
Сканирование nmap
% nmap -A --open --reason 192.168.0.87
Starting Nmap 7.95 ( https://nmap.org ) at 2025-01-06 13:51 MST
Nmap scan report for dev02.local (192.168.0.87)
Host is up, received syn-ack (0.0067s latency).
Not shown: 995 closed tcp ports (conn-refused)
PORT STATE SERVICE REASON VERSION
22/tcp open ssh syn-ack OpenSSH 9.6p1 Ubuntu 3ubuntu13.5 (Ubuntu Linux; protocol 2.0)
| ssh-hostkey:
|_ 256 {FINGERPRINT} (ED25519)
80/tcp open http syn-ack Caddy httpd
|_http-server-header: Caddy
|_http-title: Dev02.DanLevy.net
443/tcp open ssl/https syn-ack
|_http-title: Dev02.DanLevy.net
1234/tcp open http syn-ack Node.js Express framework
|_http-cors: GET POST PUT DELETE PATCH
|_http-title: Dev02.DanLevy.net (application/json; charset=utf-8).
Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 13.36 seconds

Просмотр открытых портов

Ознакомьтесь с lsof — он доступен в macOS и Linux. Показывает детальное состояние сети и активность диска.

Команды lsof
# Мониторинг конкретного порта
sudo lsof -i:80 -Pn

Мониторинг ESTABLISHED‑соединений

sudo lsof -i -Pn | grep ESTABLISHED

Просмотр LISTEN

sudo lsof -i -Pn | grep LISTEN

Чтобы видеть имена сетей вместо IP‑адресов (обратный DNS может быть очень медленным)

sudo lsof -i -P | grep LISTEN

Мониторинг всех сетевых соединений

sudo watch -n1 “lsof -i -Pn”

#### Пример вывода
![nmap scan for listeners](../lsof-scan-listen.webp)
### Мониторинг файлов
Чтобы определить, какие **процессы** используют наибольшую **полосу пропускания диска**, можно воспользоваться `iotop`:
```bash
sudo iotop

Для отслеживания изменений отдельных файлов используйте inotifywait в Linux или fswatch в macOS:

Это помогает выявлять неавторизованное или странное поведение как в отдельных каталогах, так и глобально.

Terminal window
# Мониторинг всех изменений файлов в каталоге
sudo inotifywait -m /path/to/directory

В macOS можно использовать fswatch:

Установите через brew install fswatch

Terminal window
fswatch -r /path/to/directory
## ⏰ Часто упускаемые советы
1. **Ограничение скорости** для попыток аутентификации и любых других ключевых эндпоинтов. Будь то модуль `limit_req` в Nginx или `fail2ban` для доступа по SSH, throttling brute‑force _скорее всего_ хорошая идея. Я говорю «скорее всего», потому что в эпоху IPv6 и дешевых ботнетов это уже не то, что было раньше.
2. **Используйте тома только для чтения** где это возможно:
```yaml
services:
webapp:
volumes:
- ./config:/config:ro

В сочетании с другими лучшими практиками (не‑root пользователи, минимальные права на папки) опция монтирования тома :ro добавляет защиту от случайных изменений и некоторых попыток записи изнутри контейнера. Она не защищает хост от процесса, уже обладающего более широкими привилегиями.

  1. Регулярно проверяйте доступ к контейнерам.
    Если контейнеру не нужен секрет, порт или монтирование — удаляйте их!

  2. Осторожно с Wi‑Fi шумом
    Вы, конечно, не раздаёте пароль от Wi‑Fi посторонним, особенно странным людям, верно? Ну, кроме некоторых друзей… Ладно, может, и семье тоже. Никогда не знаешь, какие приложения у них установлены и могут ли они раскрыть ваш SSID и пароль всему миру.

Домашняя сеть vs. публичный провайдер vs. туннелирование

  1. Виртуальная изоляция/DMZ: Для домашних серверов размещайте их в отдельном VLAN или DMZ, если это возможно. Это держит ваши внутренние устройства вне досягаемости потенциального компромета от серверной стороны.

    • Используйте отдельный роутер или VLAN для домашнего сервера.
    • Используйте отдельную Wi‑Fi сеть для домашнего сервера.
    • Используйте отдельную подсеть для домашнего сервера.
  2. Облачные провайдеры: Hetzner, Vultr, DigitalOcean, Linode, AWS, Azure и Google Cloud предоставляют разные возможности брандмауэра.

    • Некоторые провайдеры и сервисы блокируют порты по умолчанию. Некоторые предлагают опцию включения или дополнительные модули. Ознакомьтесь с документацией вашего провайдера.
    • Многие провайдеры предлагают расширенный мониторинг и сервисы обнаружения угроз.
  3. VPN и туннелирование: Рассмотрите возможность использования VPN‑подобного решения или туннельного сервиса для безопасного соединения сервисов через интернет без их публичного раскрытия.

    • TailScale, ngrok, ZeroTier.
    • WireGuard, OpenVPN.

🚀 Production Checklist

📚 Further Reading

Спасибо

Отдельная благодарность любознательным реддиторам:

Спасибо за внимание! Надеюсь, руководство оказалось полезным. Если есть вопросы или предложения, пишите в моих соцсетях ниже, либо нажмите Edit on GitHub, чтобы открыть PR! ❤️