Чеклист розгортання сервісу¶
Прохідний чеклист для деплою будь-якого нового сервісу в інфраструктурі UMTC. Мета — мінімальний набір захистів з першого дня, щоб не було потрібно повертатись "додати hardening пізніше".
1. Планування¶
- [ ] Визначено призначення сервісу — хто та як буде користуватись
- [ ] Визначено публічний це сервіс (Caddy 443) чи внутрішній (тільки через WireGuard)
- [ ] Відомий DNS домен (наприклад
service.umtc.localдля внутрішніх,service.eliah.oneдля публічних) - [ ] Зарезервовано IP у WG mesh якщо сервіс слухає внутрішньо (10.10.10.X)
- [ ] Підібрано версія образу — конкретний тег, НЕ
latest - [ ] Перевірено системні вимоги (RAM, CPU, диск, GPU якщо треба)
- [ ] Є runbook / README як сервіс перезапустити та оновити
2. Мережа та firewall¶
- [ ] Сервіс слухає лише на потрібному інтерфейсі:
- Внутрішній:
0.0.0.0:PORTвсередині WG namespace, АБО явно10.10.10.X:PORT - Публічний: за Caddy на localhost
127.0.0.1:PORT(Caddy проксує 443 → localhost) - [ ] Firewall на хості блокує публічний доступ до внутрішнього порту
- [ ] Якщо сервіс ХОЧЕТЬСЯ публічним — чи справді? Альтернатива: через WG + власний Caddy на hub
- [ ] Rate limiting на Caddy (для публічних ендпоінтів)
- [ ] CORS правила явні, не
*
# Шаблон Caddyfile для публічного сервісу з WG-only adminом
service.eliah.one {
# Публічна частина — мінімум ендпоінтів
handle /api/public/* {
reverse_proxy localhost:8080
}
# Admin доступний тільки через WG (10.10.10.0/24)
@wg remote_ip 10.10.10.0/24
handle /admin/* {
@not_wg not remote_ip 10.10.10.0/24
respond @not_wg "Forbidden" 403
reverse_proxy localhost:8080
}
# Default — deny
respond /* "Not Found" 404
}
3. Docker Compose hardening¶
- [ ]
version: "3.8"або новіший (НЕversion: "2"— legacy) - [ ] Version pinning образу:
image: name:1.2.3, неname:latest - [ ]
restart: unless-stopped(неalways— якщо явно зупинили, не піднімати) - [ ] Resource limits —
deploy.resources.limits: {cpus: "2", memory: 2G} - [ ] User явно:
user: "1000:1000"абоuser: nobody— не root всередині - [ ] Read-only rootfs якщо можливо:
read_only: true+tmpfs: /tmp - [ ] No
--privileged, жоднихcap_add: SYS_ADMINбез обґрунтування - [ ]
/var/run/docker.sockНЕ монтується всередину (docker-in-docker) - [ ] Секрети через
.env(НЕ commit у git!) або Docker secrets, не hardcode - [ ] Volumes з явними named volumes або абсолютними шляхами (не bind-mount до
/) - [ ] Healthcheck визначено (
healthcheck:у compose) - [ ] Логування обмежено:
logging.options.max-size: "10m", max-file: "3"
Еталонний фрагмент¶
services:
myservice:
image: myservice:v1.2.3
restart: unless-stopped
user: "1000:1000"
read_only: true
tmpfs:
- /tmp
cap_drop: ["ALL"]
security_opt:
- no-new-privileges:true
deploy:
resources:
limits:
cpus: "2"
memory: 2G
environment:
- DB_PASSWORD=${DB_PASSWORD} # з .env
networks:
- wg-internal
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
retries: 3
networks:
wg-internal:
external: true
4. DNS¶
- [ ] Запис у внутрішньому DNS (dnsmasq на хабі) для
service.umtc.local - [ ] Запис у публічному DNS якщо треба (Cloudflare / провайдер домену)
- [ ] TTL розумне — 300-3600 сек для prod, 60 сек під час міграції
- [ ] Перевірено що DNS резолвиться з усіх клієнтських точок
dig @10.10.10.1 service.umtc.local +short # внутрішній
dig service.eliah.one +short # публічний
5. TLS / сертифікати¶
- [ ] Публічний сервіс: Caddy автоматично отримує Let's Encrypt — перевірити успіх
- [ ] Внутрішній сервіс: або self-signed через OpenSSL, або клієнти ходять через Caddy на hub
- [ ] Сертифікати НЕ в git
- [ ] Term до експірації — моніторинг (Uptime Kuma або cert-exporter у Prometheus)
6. Автентифікація / авторизація¶
- [ ] Сервіс використовує центральний auth (MAS OAuth / Keycloak), або
- [ ] Захищений mTLS клієнт-сертифікатами (для service-to-service), або
- [ ] Якщо доступ тільки адміну — WireGuard-only + додатковий пароль
- [ ] Default паролі ЗМІНЕНО (admin/admin, postgres/postgres не допускаються)
- [ ] Секрети у
.envзгенеровано черезopenssl rand -base64 32, не здогадувані рядки
7. Моніторинг та алерти¶
- [ ] Endpoint
/healthабо/healthzдодано в сервіс - [ ] Uptime Kuma моніторить endpoint
- [ ] Prometheus scrape target (якщо сервіс експортує метрики)
- [ ] Grafana dashboard (мінімум: CPU, RAM, RPS, error rate)
- [ ] Алерт на: сервіс down > 2 хв, latency p99 > Xms, error rate > 1%
- [ ] Алерти надходять у Matrix-кімнату або на email (не губляться у шумі)
8. Логування¶
- [ ] Сервіс пише логи в stdout/stderr (Docker підбирає)
- [ ] Рівень логування — INFO у prod, DEBUG у staging
- [ ] Без PII/секретів у логах (перевірити: кінцеві паролі, токени, IMSI, PII)
- [ ] Логи ротуються (Docker logging options, або logrotate на host)
- [ ] Централізовані логи (опціонально: Loki, Elasticsearch) — для production
9. Backup та відновлення¶
- [ ] Визначено що саме бекапити (БД, config, volumes, ключі)
- [ ] Backup-скрипт у cron (щоденно або rtdaily з різницевим)
- [ ] Backup-и на окремому хості, не на тому ж диску (rsync через WG)
- [ ] Протестовано restore — реально розгорнули з backup хоча б раз
- [ ] Retention policy визначена (7 днів щоденних + 4 тижні щотижневих)
- [ ] Backup шифрується (age/gpg) якщо містить секрети
# Шаблон backup-скрипту
#!/usr/bin/env bash
set -euo pipefail
SERVICE=myservice
DEST=/backup/$SERVICE/$(date +%Y-%m-%d)
mkdir -p $DEST
# БД
docker exec postgres pg_dump -U $SERVICE $SERVICE | gzip > $DEST/db.sql.gz
# Volumes
docker run --rm -v ${SERVICE}-data:/data -v $DEST:/backup alpine \
tar czf /backup/data.tar.gz -C /data .
# Sync на окремий хост
rsync -az $DEST admin@backup-host:/backups/
# Retention — видалити старше 30 днів
find /backup/$SERVICE -type d -mtime +30 -exec rm -rf {} +
10. Документація¶
- [ ] Стаття в UMTC Wiki —
content/services/<category>/<name>.mdз frontmatter - [ ] README в репозиторії сервісу — як задеплоїти з нуля
- [ ] Runbook типових проблем — що робити якщо X (Troubleshooting секція в статті)
- [ ] Посилання на цей чеклист у commit message PR де піднімаємо сервіс
11. Go-live перевірка¶
За день до прод-запуску:
- [ ]
docker compose up -dна staging працює з того самого compose файлу - [ ] Health check зелений, логи без помилок
- [ ] Публічний endpoint доступний ззовні (якщо публічний)
- [ ] Внутрішній endpoint НЕ доступний ззовні (перевірка з публічної мережі:
curl https://public-ip:PORTмає давати timeout/403) - [ ] Backup виконаний, відновлення з нього протестовано
- [ ] Моніторинг та алерти працюють (тестовий алерт прилетів у Matrix)
- [ ] Рядок про сервіс у загальному
services/index.mdоновлено
12. Після запуску (перший тиждень)¶
- [ ] Перевірити логи щоденно на помилки
- [ ] Перевірити навантаження у Grafana — резервів 30%+ по CPU/RAM
- [ ] Якщо публічний — подивитись Caddy access.log на підозрілі патерни (brute force, scan)
- [ ] За тиждень — post-go-live review: що зламалось, що треба покращити
Troubleshooting deployment¶
Сервіс не стартує — порт зайнятий
- Симптом: docker compose up падає з bind: address already in use.
- Причина: на хості вже слухає щось інше (старий контейнер, systemd сервіс, інший compose проєкт).
- Рішення: ss -tulpn | grep :PORT — хто слухає. Або обрати інший порт у compose, або зупинити конфліктуючий: docker ps | grep PORT; systemctl list-units --state=running | grep PORT.
Публічний сервіс досяжний напряму, обходячи Caddy
- Симптом: з публічного інтернету працює і https://service.eliah.one, і http://PUBLIC_IP:8080 (напряму до container).
- Причина: порт проброшено назовні: у compose ports: ["8080:8080"] замість expose: ["8080"].
- Рішення: у docker-compose.yml замінити ports на expose для сервісів за Caddy. expose відкриває порт ТІЛЬКИ всередині docker мережі. Caddy проксирує на container name + port. Перевірка: iptables -L DOCKER -n — не повинно бути rule з цим портом.
Backup "працює", але restore не поновлює дані
- Симптом: backup скрипт виконується без помилок, розмір файлу ≠ 0, але restore створює порожню БД.
- Причина: виконували pg_dump до того як БД отримала дані (перший запуск), або дампили з неправильного контейнера (старого), або gzip зламаний.
- Рішення: тест-план для backup: 1) створити тестові дані у prod, 2) зробити backup, 3) запустити новий compose з іншим тому, 4) restore, 5) перевірити що тестові дані на місці. Автоматизувати — щотижня окремий job що робить restore у staging і порівнює row counts.
Пов'язані статті¶
- Hardening системи — hardening окремих компонентів
- Реагування на інциденти — план Б коли щось сталось
- Caddy — reverse proxy та TLS
- Docker Compose деплой — базовий процес
- Caddy reverse proxy — публікація сервісу
- Docker Compose intro — основи
- iptables шпаргалка — firewall команди
- docker, caddy, wireguard — термінологія
Шлях: security/deployment-checklist.md