import: merge .wiki/concepts/ from temp prefix into existing dir (history preserved via merge+rename)

This commit is contained in:
2026-05-21 13:47:59 +03:00
parent 4837fb32c8
commit c40418239e
24 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,147 @@
---
title: DB TLS через traefik raw TCP с self-signed certs
type: concept
tags: [traefik, tls, postgres, mariadb, mongo, redis, self-signed, pattern]
sources: [../sources/vds-kzntsv-bootstrap-2026-05-20.md]
updated: 2026-05-20
---
# Self-signed DB TLS через Traefik raw TCP
Pattern для выставления нескольких DB-контейнеров наружу (public internet) через traefik с минимумом сложности cert management. Self-signed на старте, LE-extraction позже.
Сочетается с [`traefik-tcp-passthrough-vs-starttls`](traefik-tcp-passthrough-vs-starttls.md) — там объяснено почему `HostSNI(*)` + raw TCP, не SNI passthrough.
## Архитектура
```
Internet client
↓ TLS handshake (порт зависит от DB)
↓ <db>.vds.kzntsv.site:5432/3306/27017/6379
Traefik (raw TCP forward, без TLS inspection)
↓ docker network `proxy`
DB container (терминирует TLS своим self-signed cert)
↓ docker network `shared-dbs`
Other VDS containers (могут ходить без TLS по dns name `postgres`/`mariadb`/...)
```
Каждая DB:
- Подключена к двум networks: `proxy` (для traefik) и `shared-dbs` (для других контейнеров VDS).
- Имеет свой self-signed cert (CN = `<db>.vds.kzntsv.site`) в `./certs/`.
- Конфигурируется для TLS-required.
- Объявляет traefik label с `HostSNI(\`*\`)` на dedicated entrypoint port.
Traefik static config:
```yaml
entryPoints:
postgres: { address: ":5432" }
mariadb: { address: ":3306" }
mongo: { address: ":27017" }
redis: { address: ":6379" }
```
ufw: allow на эти 4 порта.
## Cert generation
```bash
for db in postgres mariadb mongo redis; do
openssl req -x509 -newkey rsa:2048 \
-keyout /opt/stacks/databases/$db/certs/server.key \
-out /opt/stacks/databases/$db/certs/server.crt \
-days 3650 -nodes \
-subj "/CN=$db.vds.kzntsv.site/O=kzntsv.site/C=RU" \
-addext "subjectAltName=DNS:$db.vds.kzntsv.site"
chmod 600 /opt/stacks/databases/$db/certs/server.key
done
```
## DB-specific quirks
### Postgres 16 (non-alpine, uid 999)
Postgres key file перм-checks: must be 600 + owned by postgres user OR root. Postgres alpine использует **uid 70**, не 999. Не-alpine Debian — **uid 999**. Если используем alpine — `chown -R 70:70 certs/server.key`; если debian — `chown 999:999`. Лучше debian (`postgres:16`) для совместимости с прочими стеками. Команда:
```yaml
command:
- postgres
- -c
- ssl=on
- -c
- ssl_cert_file=/etc/postgres-certs/server.crt
- -c
- ssl_key_file=/etc/postgres-certs/server.key
```
### MariaDB 11.4
```yaml
command:
- --ssl-cert=/etc/mariadb-certs/server.crt
- --ssl-key=/etc/mariadb-certs/server.key
- --require-secure-transport=ON # форсит TLS для всех клиентов
```
### MongoDB 7
Cert + key должны быть **в одном PEM-файле** (`cat server.crt server.key > server.pem`). Плюс Mongo 7 enforce'ит "chain of trust" — нужен `--tlsCAFile` (для self-signed — указываем server.crt сам как CA). `--tlsAllowConnectionsWithoutCertificates` нужен иначе сервер требует client cert.
```yaml
command:
- --tlsMode=requireTLS
- --tlsCertificateKeyFile=/etc/mongo-certs/server.pem
- --tlsCAFile=/etc/mongo-certs/server.crt
- --tlsAllowConnectionsWithoutCertificates
- --bind_ip_all
```
### Redis 7 alpine
```yaml
command:
- redis-server
- --port
- "0" # disable non-TLS port
- --tls-port
- "6379"
- --tls-cert-file
- /etc/redis-certs/server.crt
- --tls-key-file
- /etc/redis-certs/server.key
- --tls-auth-clients
- "no" # без mTLS
- --requirepass
- <password>
```
## Client connection examples
```bash
# Postgres (sslmode=require, не verify-full — self-signed)
psql "postgresql://postgres:$PG_PASS@postgres.vds.kzntsv.site:5432/postgres?sslmode=require"
# MariaDB (--ssl + --ssl-verify-server-cert=0)
mariadb -h mariadb.vds.kzntsv.site -P 3306 -u root -p$MARIA_PASS --ssl --ssl-verify-server-cert=0
# Mongo (tls=true + tlsAllowInvalidCertificates)
mongosh "mongodb://root:$MONGO_PASS@mongo.vds.kzntsv.site:27017/admin?tls=true&tlsAllowInvalidCertificates=true"
# Redis (--tls --insecure)
redis-cli --tls --insecure -h redis.vds.kzntsv.site -p 6379 -a $REDIS_PASS
```
## Trade-off
- ✔ Уровень входа: 5 минут на cert + 5 минут на compose. Никаких lego/cert-manager headache на старте.
- ✔ Каждый клиент явно отключает verify — predictable failure mode (если cert меняется случайно — клиент сразу пишет в логи).
- ✗ Клиенты должны помнить `verify=disable`. Production-tier клиенты обычно фейлят на self-signed по defaultу.
- ✗ Cert не ротируется автоматически. 10-year validity = временное обходное.
- ✗ Для каждой DB — отдельный cert (не wildcard). Расширяемо, но если будет mongo + mongo-readonly — у каждого свой.
## Roadmap к LE certs
Sidecar контейнер с lego (готовый container `goacme/lego` или собственный) который watch'ит traefik `acme.json` и каждый раз когда меняется (i.e. cert ротировался) — извлекает PEM-bundle на disk + триггерит `docker exec <db> kill -HUP 1` для reload. Каждый DB получает auto-renewed LE cert. Это deferred — см. follow-up task в [`vds-kzntsv`](../entities/vds-kzntsv.md).
## Где применено
4 shared DBs на [`vds-kzntsv`](../entities/vds-kzntsv.md): postgres / mariadb / mongo / redis. Self-signed 10-year certs в `/opt/stacks/databases/<engine>/certs/`. Strong random hex32 пароли в `~/projects/.common/secrets/vds-kzntsv.env`.