Files
admin/.wiki/concepts/db-tls-self-signed-via-traefik-raw-tcp.md

6.1 KiB
Raw Permalink Blame History

title, type, tags, sources, updated
title type tags sources updated
DB TLS через traefik raw TCP с self-signed certs concept
traefik
tls
postgres
mariadb
mongo
redis
self-signed
pattern
../sources/vds-kzntsv-bootstrap-2026-05-20.md
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 — там объяснено почему 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:

entryPoints:
  postgres:  { address: ":5432" }
  mariadb:   { address: ":3306" }
  mongo:     { address: ":27017" }
  redis:     { address: ":6379" }

ufw: allow на эти 4 порта.

Cert generation

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) для совместимости с прочими стеками. Команда:

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

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.

command:
  - --tlsMode=requireTLS
  - --tlsCertificateKeyFile=/etc/mongo-certs/server.pem
  - --tlsCAFile=/etc/mongo-certs/server.crt
  - --tlsAllowConnectionsWithoutCertificates
  - --bind_ip_all

Redis 7 alpine

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

# 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.

Где применено

4 shared DBs на vds-kzntsv: postgres / mariadb / mongo / redis. Self-signed 10-year certs в /opt/stacks/databases/<engine>/certs/. Strong random hex32 пароли в ~/projects/.common/secrets/vds-kzntsv.env.