Files
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

14 KiB
Raw Permalink Blame History

name, version, description, command
name version description command
web-security-hardening 0.1.0 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. /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

# /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:

filenames:
  - /logs/traefik/access.log
labels:
  type: traefik

Bouncer-key выдаётся командой:

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.

# 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

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

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

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:

labels:
  - "traefik.http.routers.app.tls.options=modern@file"

Проверка:

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

- 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

Команды

# Тест 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 бот.