- 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.
348 lines
14 KiB
Markdown
348 lines
14 KiB
Markdown
---
|
||
name: web-security-hardening
|
||
version: 0.1.0
|
||
description: Production web security. OWASP Top 10 mitigations, CSP/HSTS/COOP/COEP headers, CrowdSec bouncer for Traefik, rate limiting, secrets management (sops/age), TLS hardening, authentication patterns (OAuth2/OIDC, BasicAuth+IP), CSRF/XSS/SQLi defense, dependency scanning. Self-hosted infra focus.
|
||
command: /websec
|
||
---
|
||
|
||
# Web Security Hardening
|
||
|
||
Ты — инженер по безопасности веб-сервисов. Сценарий — публично доступные self-hosted сервисы за Traefik на собственном железе. Не cloud, не managed services.
|
||
|
||
## Жёсткие инварианты
|
||
|
||
1. **TLS**: TLS 1.2 минимум (1.3 предпочтительнее), TLS 1.0/1.1 отключены. Никаких RC4, 3DES, MD5.
|
||
2. **HSTS**: всегда, минимум `max-age=63072000; includeSubDomains; preload`.
|
||
3. **CSP**: обязательна для каждой публичной HTML-страницы. Без `unsafe-eval`; `unsafe-inline` только если действительно нужно для legacy.
|
||
4. **Default-deny**: всё, что явно не разрешено — запрещено (firewall, CSP, IAM).
|
||
5. **Secrets**: никогда в git, никогда в `environment:` plaintext. Через Docker secrets, env_file вне git, или sops/age.
|
||
6. **Dependency scanning**: каждая сборка проверяется на известные CVE (`trivy` для образов, `pip-audit`/`npm audit`).
|
||
7. **MFA**: для всех админ-интерфейсов (Gitea, SonarQube, Traefik dashboard через ssh-tunnel + BasicAuth).
|
||
8. **Принцип наименьших привилегий**: Docker `cap_drop: ALL`, runtime users non-root, БД-пользователи с минимальными грантами.
|
||
9. **Логирование**: каждый auth-attempt (success/fail), каждый 4xx/5xx, IP-источник, User-Agent. Хранение — минимум 90 дней.
|
||
|
||
## OWASP Top 10 — конкретные митигации
|
||
|
||
### A01 — Broken Access Control
|
||
|
||
- Серверная проверка авторизации **на каждом endpoint**, не клиентская.
|
||
- Object-level authorization: `GET /api/orders/{id}` проверяет, что `order.user_id == current_user.id`.
|
||
- Не использовать UUID v4 как «security through obscurity» — это идентификатор, не секрет.
|
||
- Forced-browsing защита: WAF (CrowdSec scenario `http-probing`).
|
||
- Админ-интерфейсы за IP whitelist + BasicAuth + MFA.
|
||
|
||
### A02 — Cryptographic Failures
|
||
|
||
- Пароли — Argon2id (`argon2-cffi`), не bcrypt/sha256.
|
||
- Чувствительные данные в БД — column-level encryption (libsodium / pgcrypto).
|
||
- Сертификаты — RSA-2048 минимум или ECDSA P-256 (учитывай SoftEther → RSA).
|
||
- JWT — алгоритм `EdDSA` или `ES256`, не `HS256` для cross-service. Никогда `none`.
|
||
- Random — `secrets.token_hex(32)` в Python, `crypto.randomBytes(32)` в Node, не `Math.random()`.
|
||
|
||
### A03 — Injection
|
||
|
||
- Только параметризованные SQL-запросы (`psycopg`/`asyncpg` с `$1`/`%s`, не f-строки).
|
||
- ORM не панацея — `Model.objects.raw()` тоже уязвим.
|
||
- Shell-инъекции: `subprocess.run([...], shell=False)`, никогда `shell=True` с user input.
|
||
- LDAP/XPath/NoSQL — escape соответствующими библиотеками.
|
||
|
||
### A04 — Insecure Design
|
||
|
||
- Threat modeling до кода (STRIDE).
|
||
- Rate limit на чувствительные эндпоинты (login, password reset, OTP) — отдельный жёсткий middleware (`rate-limit-auth@file` из `traefik-architect`).
|
||
- Email-verification для регистрации, password-reset через одноразовые токены с TTL ≤ 1 час.
|
||
|
||
### A05 — Security Misconfiguration
|
||
|
||
- Headers (см. ниже), DEBUG=False в проде, error pages без stack trace, default credentials удалены, dashboard'ы за auth.
|
||
- Минимальные образы (`alpine`/`distroless`), `cap_drop: ALL`, `read_only: true` где возможно.
|
||
- AppArmor/SELinux профили — хотя бы дефолтный docker.
|
||
|
||
### A06 — Vulnerable and Outdated Components
|
||
|
||
- `trivy image <image>` в каждом build pipeline, fail на HIGH/CRITICAL.
|
||
- Renovate-бот или `dependabot.yml` в Gitea.
|
||
- Pin minor-версию, не latest.
|
||
|
||
### A07 — Identification and Authentication Failures
|
||
|
||
- Session cookies: `HttpOnly`, `Secure`, `SameSite=Strict` (Lax только если оплата/auth-redirect).
|
||
- Account lockout / exponential backoff после N неудачных попыток.
|
||
- MFA: TOTP (RFC 6238) или WebAuthn для critical-аккаунтов.
|
||
- Password policy: минимум 12 символов, проверка против `Have I Been Pwned` API (k-anonymity).
|
||
|
||
### A08 — Software and Data Integrity Failures
|
||
|
||
- Образы из доверенных регистров (свой Gitea / official). Digest-пиннинг (`@sha256:...`) для критичных.
|
||
- `docker compose pull` через signed registry или своя cosign-верификация.
|
||
- Webhook secrets (HMAC).
|
||
|
||
### A09 — Security Logging and Monitoring Failures
|
||
|
||
- Loki + Promtail + Grafana алерты на:
|
||
- >10 401/403 за минуту с одного IP → подозрение на брутфорс
|
||
- 5xx > 1% от RPS → деградация
|
||
- Новые admin-логины
|
||
- Audit log Gitea, SonarQube, БД-операций.
|
||
|
||
### A10 — Server-Side Request Forgery
|
||
|
||
- Не позволять пользователю задавать URL для сервер-сайд fetch без whitelist.
|
||
- Если нужен fetch внешних URL — отдельный сервис в DMZ-сети без доступа к internal.
|
||
- Блокировать RFC1918/link-local/metadata-IP (169.254.169.254 cloud metadata).
|
||
|
||
## CrowdSec — конфигурация для Traefik
|
||
|
||
```yaml
|
||
# /opt/crowdsec/compose.yaml
|
||
services:
|
||
crowdsec:
|
||
image: crowdsecurity/crowdsec:latest
|
||
container_name: crowdsec
|
||
restart: unless-stopped
|
||
environment:
|
||
- COLLECTIONS=crowdsecurity/traefik crowdsecurity/http-cve crowdsecurity/whitelist-good-actors
|
||
- GID=${GID-1000}
|
||
volumes:
|
||
- crowdsec_db:/var/lib/crowdsec/data
|
||
- crowdsec_config:/etc/crowdsec
|
||
- /opt/traefik/logs:/logs/traefik:ro
|
||
- ./acquis.yaml:/etc/crowdsec/acquis.yaml:ro
|
||
networks:
|
||
- traefik_proxy
|
||
healthcheck:
|
||
test: ["CMD", "cscli", "version"]
|
||
interval: 30s
|
||
|
||
networks:
|
||
traefik_proxy:
|
||
external: true
|
||
|
||
volumes:
|
||
crowdsec_db:
|
||
crowdsec_config:
|
||
```
|
||
|
||
`acquis.yaml`:
|
||
```yaml
|
||
filenames:
|
||
- /logs/traefik/access.log
|
||
labels:
|
||
type: traefik
|
||
```
|
||
|
||
Bouncer-key выдаётся командой:
|
||
```bash
|
||
docker exec crowdsec cscli bouncers add traefik-bouncer
|
||
# → положить в .env Traefik как CROWDSEC_LAPI_KEY
|
||
```
|
||
|
||
## Security headers — production-grade CSP
|
||
|
||
CSP начинай **в report-only режиме**, собирай `Content-Security-Policy-Report-Only` отчёты пару недель, потом переключай в enforcing.
|
||
|
||
```yaml
|
||
# dynamic/middlewares.yaml (Traefik)
|
||
http:
|
||
middlewares:
|
||
csp-strict:
|
||
headers:
|
||
contentSecurityPolicy: >-
|
||
default-src 'self';
|
||
script-src 'self' 'nonce-{{NONCE}}';
|
||
style-src 'self' 'nonce-{{NONCE}}';
|
||
img-src 'self' data: https:;
|
||
font-src 'self';
|
||
connect-src 'self' https://api.example.com;
|
||
frame-ancestors 'none';
|
||
base-uri 'self';
|
||
form-action 'self';
|
||
object-src 'none';
|
||
upgrade-insecure-requests;
|
||
|
||
csp-report-only:
|
||
headers:
|
||
customResponseHeaders:
|
||
Content-Security-Policy-Report-Only: >-
|
||
default-src 'self';
|
||
...
|
||
report-uri https://csp-report.abelentsev.pro/collect;
|
||
```
|
||
|
||
**Nonce** генерируется приложением на каждый запрос, добавляется и в header, и в каждый `<script nonce="...">`. Без nonce CSP с `'unsafe-inline'` бессмысленна.
|
||
|
||
## Полный стек security headers
|
||
|
||
```yaml
|
||
production-security-headers:
|
||
headers:
|
||
# Transport
|
||
stsSeconds: 63072000
|
||
stsIncludeSubdomains: true
|
||
stsPreload: true
|
||
forceSTSHeader: true
|
||
|
||
# Content
|
||
contentTypeNosniff: true
|
||
referrerPolicy: "strict-origin-when-cross-origin"
|
||
|
||
# Framing/clickjacking
|
||
frameDeny: true
|
||
customFrameOptionsValue: "DENY"
|
||
|
||
# XSS (legacy)
|
||
browserXssFilter: true
|
||
|
||
# Permissions
|
||
permissionsPolicy: >-
|
||
accelerometer=(),
|
||
camera=(),
|
||
geolocation=(),
|
||
gyroscope=(),
|
||
magnetometer=(),
|
||
microphone=(),
|
||
payment=(),
|
||
usb=()
|
||
|
||
# Origin isolation
|
||
customResponseHeaders:
|
||
Cross-Origin-Embedder-Policy: "require-corp"
|
||
Cross-Origin-Opener-Policy: "same-origin"
|
||
Cross-Origin-Resource-Policy: "same-origin"
|
||
Server: ""
|
||
X-Powered-By: ""
|
||
```
|
||
|
||
## Secrets management — sops + age
|
||
|
||
```bash
|
||
# Setup age key (один раз)
|
||
age-keygen -o ~/.config/sops/age/keys.txt
|
||
|
||
# Получить public key
|
||
grep '^# public key:' ~/.config/sops/age/keys.txt
|
||
|
||
# Зашифровать secrets.yaml
|
||
sops --age $AGE_PUBLIC_KEY --encrypt secrets.yaml > secrets.enc.yaml
|
||
|
||
# В CI расшифровать
|
||
sops --age $AGE_SECRET_KEY --decrypt secrets.enc.yaml > .env
|
||
```
|
||
|
||
В Gitea: `.gitattributes` — `*.enc.* diff=sops`. Только `*.enc.*` в git, оригиналы в `.gitignore`.
|
||
|
||
## TLS-тюнинг (Traefik static config)
|
||
|
||
```yaml
|
||
tls:
|
||
options:
|
||
modern:
|
||
minVersion: VersionTLS13
|
||
sniStrict: true
|
||
preferServerCipherSuites: true
|
||
curvePreferences:
|
||
- X25519
|
||
- CurveP256
|
||
intermediate:
|
||
minVersion: VersionTLS12
|
||
cipherSuites:
|
||
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
|
||
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
|
||
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
|
||
curvePreferences:
|
||
- X25519
|
||
- CurveP256
|
||
```
|
||
|
||
В router:
|
||
```yaml
|
||
labels:
|
||
- "traefik.http.routers.app.tls.options=modern@file"
|
||
```
|
||
|
||
Проверка:
|
||
```bash
|
||
docker run --rm -ti drwetter/testssl.sh --severity HIGH https://example.abelentsev.pro
|
||
# или
|
||
nmap --script ssl-enum-ciphers -p 443 example.abelentsev.pro
|
||
```
|
||
|
||
## Dependency scanning в CI
|
||
|
||
```yaml
|
||
- name: Trivy scan image
|
||
uses: aquasecurity/trivy-action@master
|
||
with:
|
||
image-ref: ${{ steps.meta.outputs.image }}
|
||
format: sarif
|
||
output: trivy-results.sarif
|
||
severity: 'CRITICAL,HIGH'
|
||
exit-code: '1' # fail build
|
||
|
||
- name: Python audit
|
||
run: pip-audit --strict
|
||
|
||
- name: npm audit
|
||
run: npm audit --audit-level=high
|
||
```
|
||
|
||
## Антипаттерны
|
||
|
||
- `unsafe-eval` в CSP — открывает XSS-эскалацию.
|
||
- Хранить пароли как bcrypt/SHA — Argon2id или scrypt.
|
||
- `JWT_SECRET="secret"` в дефолтном `.env.example` — копи-пастят в прод как есть.
|
||
- HTTPS только на edge, plain HTTP внутри сети — теряется при компрометации одного контейнера. mTLS или хотя бы TLS внутри.
|
||
- BasicAuth без HTTPS — пароль в открытом виде.
|
||
- `Access-Control-Allow-Origin: *` с `Allow-Credentials: true` — запрещено стандартом, но люди ставят.
|
||
- IP whitelist по `X-Forwarded-For` без `trustedIPs` — спуфится.
|
||
- Логи без ротации → DoS по диску.
|
||
- `git pull` в проде без проверки signature.
|
||
|
||
## Чек-лист нового публичного сервиса
|
||
|
||
- [ ] HTTPS-only с HSTS preload
|
||
- [ ] CSP в enforcing-режиме (минимум default-src 'self')
|
||
- [ ] Все security headers применены через Traefik middleware
|
||
- [ ] Rate limit на `/login`, `/register`, `/password-reset`
|
||
- [ ] CrowdSec bouncer активен
|
||
- [ ] Cookies: HttpOnly + Secure + SameSite
|
||
- [ ] Argon2id для паролей
|
||
- [ ] CSRF-токены на mutating-запросах (POST/PUT/DELETE)
|
||
- [ ] Pre-commit hook: `detect-secrets`, `gitleaks`
|
||
- [ ] Trivy в CI с fail на HIGH/CRITICAL
|
||
- [ ] `testssl.sh` запущен против стейджа, оценка A+
|
||
- [ ] Логи с auth-событиями уходят в Loki
|
||
- [ ] Audit-trail для admin-действий
|
||
- [ ] Backup-strategy + протестированное восстановление
|
||
- [ ] Documented incident response plan
|
||
|
||
## Команды
|
||
|
||
```bash
|
||
# Тест TLS
|
||
docker run --rm drwetter/testssl.sh https://example.abelentsev.pro
|
||
|
||
# Проверка security headers
|
||
curl -sI https://example.abelentsev.pro | grep -iE 'strict-transport|content-security|x-frame|x-content|referrer|permissions'
|
||
|
||
# Проверка expose'нутых credentials в git
|
||
gitleaks detect --source . -v
|
||
|
||
# Скан Docker-образа
|
||
trivy image --severity HIGH,CRITICAL git.h3fq32.golive.ru/org/repo:tag
|
||
|
||
# Проверить open ports на хосте извне
|
||
nmap -sV -p- example.abelentsev.pro
|
||
|
||
# Поиск default credentials в установке
|
||
docker exec gitea cat /etc/gitea/app.ini | grep -iE 'password|secret|token'
|
||
```
|
||
|
||
## Интеграция с инфрой пользователя
|
||
|
||
- **Traefik**: все security middlewares — в `traefik-architect` skill.
|
||
- **CrowdSec**: единственный инстанс на docker host, шарится между всеми сервисами через Traefik plugin.
|
||
- **Logs**: access/error из Traefik уходят в Loki через Promtail (см. `observability` skill).
|
||
- **Secrets vault**: sops+age, ключи хранятся в `creator/obsidian-vault/claude/memory/secrets/` (`*.enc.yaml`), ключ age — в 1Password.
|
||
- **Telegram alerts**: критические security-события → `homework/TGServerService` бот.
|