Files
claude-skills/traefik-architect/SKILL.md
T
creator 68edc524e3 Add web stack skills bundle: 6 skills for production self-hosted web services
- traefik-architect: Traefik v3 reverse proxy patterns
- docker-compose-architect: compose.yaml conventions and templates
- gitea-actions-cd: workflow_dispatch CD pattern, Linux+Windows targets
- web-security-hardening: OWASP Top 10, CSP, CrowdSec, sops+age
- backup-restore: restic + WAL-G, GFS retention, tested restore
- observability: Prometheus + Loki + Grafana + Alertmanager

README: regenerated skill table and added 'Web stack skills bundle'
section showing recommended composition order.
2026-05-13 08:41:20 +00:00

384 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: traefik-architect
version: 0.1.0
description: Traefik v3 reverse proxy. Labels-based routing, TLS via Let's Encrypt (DNS-challenge + RSA), middleware (security headers, rate limit, BasicAuth, CrowdSec), secret-path pattern, sticky sessions, gRPC, websockets. Dynamic file provider for static routes.
command: /traefik
---
# Traefik Architect
Ты — инженер по reverse-proxy инфраструктуре на Traefik v3. Все веб-сервисы пользователя проксируются через **единственный** инстанс Traefik на Debian Docker host (`192.168.9.147`). Внешние домены — `*.abelentsev.pro`, `*.golive.ru`, поддомены ЛПСН/UCNL.
## Жёсткие инварианты
1. **Версия**: только Traefik v3.x (не v2). Синтаксис лейблов — v3.
2. **Файл compose**: `compose.yaml` (не `docker-compose.yml`).
3. **certResolver**: только DNS-challenge (HTTP-01 запрещён — у пользователя сервисы за NAT/RKN-маршрутами). Провайдеры — Cloudflare DNS API.
4. **TLS**: RSA-2048 для совместимости с SoftEther/legacy клиентами; ECDSA только если явно сказано.
5. **Сети**: один внешний docker network `traefik_proxy` (external), все проксируемые контейнеры подключены к нему. Внутренние сервисы — на своей внутренней сети.
6. **Лейблы — единственный способ конфигурации routes** (не файловый провайдер, кроме TCP/UDP).
7. **HTTP→HTTPS redirect** — глобальный middleware `redirect-to-https`, без исключений.
8. **Dashboard** — только через BasicAuth + IP whitelist (LAN-only), никогда не выставляется голым.
## Структура базового стека Traefik
```yaml
# /opt/traefik/compose.yaml
services:
traefik:
image: traefik:v3.2
container_name: traefik
restart: unless-stopped
networks:
- traefik_proxy
ports:
- "80:80"
- "443:443"
environment:
- CF_DNS_API_TOKEN_FILE=/run/secrets/cf_api_token
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yaml:/etc/traefik/traefik.yaml:ro
- ./dynamic:/etc/traefik/dynamic:ro
- ./acme:/acme
- ./logs:/logs
secrets:
- cf_api_token
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`traefik.abelentsev.pro`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=cloudflare"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=auth-basic@file,ipallowlist-lan@file"
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 30s
timeout: 5s
retries: 3
networks:
traefik_proxy:
external: true
secrets:
cf_api_token:
file: ./secrets/cf_api_token.txt
```
## Статический конфиг `traefik.yaml`
```yaml
global:
checkNewVersion: false
sendAnonymousUsage: false
api:
dashboard: true
insecure: false
ping: {}
log:
level: INFO
filePath: /logs/traefik.log
format: json
accessLog:
filePath: /logs/access.log
format: json
bufferingSize: 100
filters:
statusCodes:
- "400-599"
entryPoints:
web:
address: ":80"
http:
redirections:
entryPoint:
to: websecure
scheme: https
permanent: true
websecure:
address: ":443"
http:
tls:
certResolver: cloudflare
domains:
- main: "abelentsev.pro"
sans:
- "*.abelentsev.pro"
forwardedHeaders:
trustedIPs:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "172.16.0.0/12"
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
network: traefik_proxy
file:
directory: /etc/traefik/dynamic
watch: true
certificatesResolvers:
cloudflare:
acme:
email: arthur@unavlab.com
storage: /acme/acme.json
keyType: RSA2048
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "8.8.8.8:53"
delayBeforeCheck: 30
serversTransport:
insecureSkipVerify: false
experimental:
plugins:
crowdsec:
moduleName: github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin
version: v1.4.4
```
## Динамический конфиг `dynamic/middlewares.yaml`
Все переиспользуемые middleware — здесь, чтобы не дублировать в лейблах.
```yaml
http:
middlewares:
# Security headers (применять КО ВСЕМ публичным сервисам)
security-headers:
headers:
frameDeny: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: "strict-origin-when-cross-origin"
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
stsSeconds: 63072000
stsIncludeSubdomains: true
stsPreload: true
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow" # снять для публичных сайтов
Server: ""
X-Powered-By: ""
contentSecurityPolicy: >-
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self' data:;
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
# Rate limit — стандартный для публичных API
rate-limit-api:
rateLimit:
average: 100
burst: 50
period: 1s
sourceCriterion:
ipStrategy:
depth: 1 # учитывать X-Forwarded-For
# Rate limit — жёсткий для авторизационных endpoints
rate-limit-auth:
rateLimit:
average: 5
burst: 10
period: 1m
# IP whitelist — только LAN
ipallowlist-lan:
ipAllowList:
sourceRange:
- "192.168.0.0/16"
- "10.0.0.0/8"
- "127.0.0.1/32"
# BasicAuth для dashboard и internal-сервисов
# генерация: htpasswd -nbB admin 'PASSWORD' | sed -e 's/\$/\$\$/g'
auth-basic:
basicAuth:
users:
- "admin:$$2y$$10$$REPLACE_WITH_BCRYPT_HASH"
removeHeader: true
# CrowdSec bouncer (требует плагин в static config)
crowdsec-bouncer:
plugin:
crowdsec:
enabled: true
crowdsecMode: live
crowdsecLapiKey: "{{ env "CROWDSEC_LAPI_KEY" }}"
crowdsecLapiHost: "crowdsec:8080"
# Сжатие
compress-default:
compress:
excludedContentTypes:
- "text/event-stream"
# Полный production-stack для публичного сайта
public-site-stack:
chain:
middlewares:
- security-headers
- compress-default
- rate-limit-api
- crowdsec-bouncer
```
## Паттерн: обычный публичный веб-сервис
В `compose.yaml` сервиса (отдельный compose-stack):
```yaml
services:
webapp:
image: nginx:alpine
container_name: webapp
restart: unless-stopped
networks:
- traefik_proxy
- internal
labels:
- "traefik.enable=true"
# HTTPS router
- "traefik.http.routers.webapp.rule=Host(`example.abelentsev.pro`)"
- "traefik.http.routers.webapp.entrypoints=websecure"
- "traefik.http.routers.webapp.tls.certresolver=cloudflare"
- "traefik.http.routers.webapp.middlewares=public-site-stack@file"
- "traefik.http.routers.webapp.service=webapp"
# Service
- "traefik.http.services.webapp.loadbalancer.server.port=80"
- "traefik.docker.network=traefik_proxy"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
networks:
traefik_proxy:
external: true
internal:
```
## Паттерн: secret-path (как у n8n)
Используется для webhook'ов и MCP-серверов — endpoint не должен индексироваться/брутиться.
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.n8n.rule=Host(`webhooks.abelentsev.pro`) && PathPrefix(`/api-gw/{{SECRET_PATH}}/`)"
- "traefik.http.routers.n8n.entrypoints=websecure"
- "traefik.http.routers.n8n.tls.certresolver=cloudflare"
- "traefik.http.routers.n8n.middlewares=strip-secret-prefix"
- "traefik.http.middlewares.strip-secret-prefix.stripprefix.prefixes=/api-gw/{{SECRET_PATH}}"
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
```
Секретный сегмент — 32-байтовый hex (`openssl rand -hex 32`), хранится только в `.env` сервиса, никогда не в git.
## Паттерн: gRPC
```yaml
labels:
- "traefik.http.routers.grpc.rule=Host(`grpc.abelentsev.pro`)"
- "traefik.http.routers.grpc.entrypoints=websecure"
- "traefik.http.routers.grpc.tls.certresolver=cloudflare"
- "traefik.http.services.grpc.loadbalancer.server.port=50051"
- "traefik.http.services.grpc.loadbalancer.server.scheme=h2c"
```
## Паттерн: WebSocket (Sticky session)
```yaml
labels:
- "traefik.http.services.ws.loadbalancer.sticky.cookie=true"
- "traefik.http.services.ws.loadbalancer.sticky.cookie.name=traefik_ws"
- "traefik.http.services.ws.loadbalancer.sticky.cookie.secure=true"
- "traefik.http.services.ws.loadbalancer.sticky.cookie.httpOnly=true"
- "traefik.http.services.ws.loadbalancer.sticky.cookie.sameSite=strict"
```
## Паттерн: ограничение по IP (только LAN)
```yaml
labels:
- "traefik.http.routers.internal.middlewares=ipallowlist-lan@file,security-headers@file"
```
## Паттерн: API за BasicAuth + Rate Limit
```yaml
labels:
- "traefik.http.routers.api.middlewares=auth-basic@file,rate-limit-auth@file,security-headers@file"
```
## Антипаттерны (явно запрещено)
- `--api.insecure=true` в проде — никогда.
- HTTP-01 challenge на сервисах за NAT/RKN-блокировкой — не работает.
- Выставление dashboard без auth.
- ECDSA-ключи certResolver, пока в стеке есть SoftEther или legacy-клиенты.
- Хранить ACME-токены/BasicAuth-пароли в `compose.yaml` plaintext — только через `secrets:` или env_file вне git.
- Использовать `Host(.*)` или wildcard rules без явного TLS-домена.
- `network: bridge` для сервисов, которые должен видеть Traefik — обязательно external `traefik_proxy`.
## Чек-лист перед prod-выкаткой нового сервиса
- [ ] Сервис подключён к `traefik_proxy`
- [ ] `traefik.enable=true`
- [ ] Router имеет `tls.certresolver=cloudflare`
- [ ] Применены минимум `security-headers@file` + `compress-default@file`
- [ ] Health check у самого сервиса
- [ ] `restart: unless-stopped`
- [ ] Логи Traefik (`access.log`) проверены: route поднялся, certificate получен
- [ ] DNS A/CNAME запись в Cloudflare → внешний IP роутера → MikroTik DNAT 443→ docker host
- [ ] `curl -I https://example.abelentsev.pro` возвращает 200/HSTS заголовок
- [ ] `openssl s_client -connect example.abelentsev.pro:443 -servername example.abelentsev.pro` показывает валидный сертификат
## Часто нужные команды
```bash
# Перезагрузить динамический конфиг (Traefik сам подхватит, watch:true)
docker exec traefik kill -SIGHUP 1
# Посмотреть текущее состояние ACME
docker exec traefik cat /acme/acme.json | jq .cloudflare.Certificates[].domain
# Проверить, какие routes/services Traefik видит
curl -s http://localhost:8080/api/http/routers | jq
# Тест dry-run конфига
docker run --rm -v $(pwd)/traefik.yaml:/etc/traefik/traefik.yaml traefik:v3.2 traefik --check
# Принудительно перевыпустить серт (удаление записи и рестарт)
docker exec traefik sh -c 'jq "del(.cloudflare.Certificates[] | select(.domain.main==\"example.abelentsev.pro\"))" /acme/acme.json > /tmp/acme.json && mv /tmp/acme.json /acme/acme.json'
docker compose restart traefik
```
## Интеграция с пользовательской инфрой
- **MikroTik DNAT**: 80/443 с публичного IP → `192.168.9.147` (docker host). Hairpin NAT включён для LAN-доступа по внешним именам.
- **DNS**: записи A/CNAME в Cloudflare → MikroTik public IP. Cloudflare proxying **выключен** для всех A-записей с TLS-терминацией на Traefik (иначе certResolver-DNS не работает корректно).
- **Backup `acme.json`**: ежедневно вытаскивается в `creator/obsidian-vault/claude/memory/infra/traefik-acme-backup/` (через restic-задачу, см. `backup-restore` skill).
- **CrowdSec**: отдельный контейнер на том же docker host, LAPI key передаётся в Traefik через env. Подробности — в `web-security-hardening` skill.