# Cloudflare Free-Max Security — Checklista Implementacyjna dla Agenta

> **Dla agentów:** Implementuj zadania po kolei. Każde zadanie jest niezależnym commitem. Nie uruchamiaj `terraform apply` bez jawnej zgody operatora. Nie commituj sekretów, tokenów API, pliku `terraform.tfstate` ani `terraform.tfvars`.

**Cel:** Zbudowanie maksymalnego bezpieczeństwa SaaS na darmowym planie Cloudflare, zarządzanego przez Terraform jako kod (IaC).

**Architektura:** Cloudflare odrzuca wrogich ruch na brzegu sieci (edge), zanim dotrze do aplikacji. Terraform zarządza konfiguracją Cloudflare jako kodem. Sekrety runtime pozostają w zmiennych środowiskowych i narzędziach wdrożeniowych (np. Wrangler, Secrets Manager) — nigdy w plikach Terraform.

**Stack:** Cloudflare Free, Terraform ≥ 1.6, `cf-terraforming` (do importu), opcjonalnie: Cloudflare Workers / Wrangler.

---

## Zakres i granice

- Zadania 1–3: Terraform skeleton i konfiguracja edge Cloudflare (bez `apply` na produkcji)
- Zadania 4–5: WAF i Rate Limiting
- Zadanie 6: Nagłówki bezpieczeństwa (Transform Rules)
- Zadanie 7: Weryfikacja i skrypty CI/CD
- Zadanie 8: Runbook operatorski i bramki upgrade

Nie uruchamiaj produkcyjnego `terraform apply`, nie rotuj sekretów, nie zmieniaj planów Cloudflare bez jawnej zgody operatora.

---

## Struktura plików do stworzenia

```
infra/cloudflare/terraform/
├── providers.tf              # Provider Cloudflare i wersje
├── variables.tf              # Zmienne: zone_id, account_id, hostname, przełączniki
├── locals.tf                 # Stałe lokalne: hosty, ścieżki wysokiego ryzyka
├── outputs.tf                # Bezpieczne outputy (bez sekretów)
├── dns.tf                    # Zasoby DNS (import-first)
├── ssl_tls.tf                # Ustawienia SSL/TLS strefy
├── waf_free.tf               # 5 reguł WAF (Free-compatible)
├── rate_limit_free.tf        # 1 reguła Rate Limiting (Free)
├── access.tf                 # Placeholder: Cloudflare Access (opcjonalny)
├── terraform.tfvars.example  # Przykładowe zmienne (bez sekretów)
├── README.md                 # Runbook: init/plan/apply/rollback
├── imports.md                # Workflow: cf-terraforming + terraform import
└── inventory.md              # Inwentarz Cloudflare + dowód importu (gate)

scripts/security/
├── assert-cloudflare-security.mjs      # Statyczne sprawdzenie repo
└── cloudflare-waf-expressions.test.mjs # Testy wyrażeń WAF (Free constraints)
```

---

## Zadanie 1: Terraform Skeleton i Polityka Bezpieczeństwa State

**Pliki do stworzenia:**
- `infra/cloudflare/terraform/providers.tf`
- `infra/cloudflare/terraform/variables.tf`
- `infra/cloudflare/terraform/locals.tf`
- `infra/cloudflare/terraform/outputs.tf`
- `infra/cloudflare/terraform/terraform.tfvars.example`
- `infra/cloudflare/terraform/README.md`
- `infra/cloudflare/terraform/imports.md`
- `infra/cloudflare/terraform/inventory.md`
- Modyfikacja: `.gitignore`

- [ ] **Krok 1: Zaktualizuj `.gitignore` — PRZED stworzeniem jakichkolwiek plików Terraform**

```gitignore
# Terraform — state i sekrety
.terraform/
*.tfstate
*.tfstate.*
terraform.tfvars
*.auto.tfvars
crash.log
crash.*.log
```

Plik `.terraform.lock.hcl` POZOSTAW śledzony przez Git — przypina wersje providerów.

- [ ] **Krok 2: Stwórz `providers.tf`**

```hcl
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 5.0"
    }
  }
}

provider "cloudflare" {
  api_token = var.cloudflare_api_token
}
```

- [ ] **Krok 3: Stwórz `variables.tf`**

```hcl
variable "cloudflare_api_token" {
  description = "Cloudflare API token. Przekazuj przez TF_VAR_cloudflare_api_token lub bezpieczny backend. Nigdy nie commituj."
  type        = string
  sensitive   = true
}

variable "cloudflare_account_id" {
  description = "Cloudflare Account ID."
  type        = string
}

variable "cloudflare_zone_id" {
  description = "Cloudflare Zone ID dla Twojej domeny."
  type        = string
}

variable "zone_name" {
  description = "Główna domena Cloudflare."
  type        = string
  default     = "twoja-domena.pl"
}

variable "enable_free_rate_limit" {
  description = "Czy zarządzać jedyną regułą Rate Limiting (Free)."
  type        = bool
  default     = false
}

variable "enable_access_admin" {
  description = "Czy zarządzać Cloudflare Access dla panelu admina."
  type        = bool
  default     = false
}
```

- [ ] **Krok 4: Stwórz `locals.tf`**

Dostosuj hosty i ścieżki do swojej aplikacji:

```hcl
locals {
  public_hosts = {
    landing = "twoja-domena.pl"
    app     = "app.twoja-domena.pl"
    api     = "api.twoja-domena.pl"
    auth    = "auth.twoja-domena.pl"
  }

  # Ścieżki wysokiego ryzyka — auth i operacje wrażliwe
  high_risk_auth_paths = [
    "/api/login",
    "/api/register",
    "/api/auth/sign-in/email",
    "/api/auth/sign-up/email",
    "/api/auth/forget-password",
    "/api/auth/reset-password",
    "/api/auth/magic-link",
  ]

  # Prefixy skanerów i automatycznych ataków
  scanner_path_prefixes = [
    "/wp-",
    "/wordpress",
    "/phpmyadmin",
    "/.git",
    "/.env",
    "/backup",
  ]
}
```

- [ ] **Krok 5: Stwórz `outputs.tf`**

```hcl
output "zone_name" {
  value = var.zone_name
}

output "public_hosts" {
  value = local.public_hosts
}
```

- [ ] **Krok 6: Stwórz `terraform.tfvars.example`** (bez sekretów)

```hcl
cloudflare_account_id = "zastap-swoim-account-id"
cloudflare_zone_id    = "zastap-swoim-zone-id"
zone_name             = "twoja-domena.pl"

enable_free_rate_limit = false
enable_access_admin    = false
```

- [ ] **Krok 7: Stwórz `README.md`**

```markdown
# Cloudflare Terraform — Bezpieczeństwo Free-Max

Katalog zarządza posturą bezpieczeństwa Cloudflare dla strefy domeny.

## Polityka State

Terraform state jest wrażliwy. Wartości computed przez provider mogą zawierać sekrety.
Nie zarządzaj Turnstile widgets ani innych zasobów zawierających sekrety tutaj,
dopóki storage state nie jest szyfrowany i dostęp nie jest ograniczony.

## Komendy

```powershell
terraform init
terraform fmt -recursive
terraform validate
```

`terraform plan` uruchamiaj TYLKO gdy:
- `TF_VAR_cloudflare_api_token` ustawiony z bezpiecznego źródła
- prawdziwe `cloudflare_account_id` i `cloudflare_zone_id` dostępne
- operator wyraził jawną zgodę na dostęp do Cloudflare API w tej sesji

Nie uruchamiaj `terraform apply` na produkcji bez jawnej zgody operatora i notatki rollbacku.
```

- [ ] **Krok 8: Stwórz `imports.md`**

```markdown
# Workflow Importowania Cloudflare

Używaj import-first. Terraform nie może zastępować istniejących reguł Cloudflare bez wiedzy operatora.

## Kroki

1. Zainstaluj `cf-terraforming`:
   ```bash
   go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest
   ```

2. Wygeneruj listę istniejących zasobów:
   ```bash
   cf-terraforming generate --resource-type cloudflare_ruleset --zone $ZONE_ID
   cf-terraforming generate --resource-type cloudflare_record --zone $ZONE_ID
   ```

3. Wygeneruj kod HCL dla istniejących zasobów:
   ```bash
   cf-terraforming generate --resource-type cloudflare_ruleset --zone $ZONE_ID > generated_rulesets.tf
   ```

4. Zaimportuj zasoby do state:
   ```bash
   terraform import cloudflare_ruleset.zone_custom_firewall <zone_id>/<ruleset_id>
   ```

5. Uruchom `terraform plan` — musi pokazać "No changes" (no-op) ZANIM dodasz nowe reguły.

6. Dopiero po potwierdzeniu no-op dodawaj/modyfikuj zasoby.
```

- [ ] **Krok 9: Stwórz `inventory.md`**

```markdown
# Inwentarz Cloudflare i Dowód Importu

Status: nie zinwentaryzowany.

Terraform NIE może stosować zmian DNS, WAF, Rate Limiting, Access, Turnstile ani
ustawień strefy dopóki ten plik nie zawiera aktualnego stanu Cloudflare i:
- zaimportowanych ID zasobów z potwierdzonym no-op `terraform plan`, LUB
- jawnej zgody operatora na stworzenie nowego zasobu (nie zastępującego istniejącego)

## Wymagany inwentarz

- Rekordy DNS dla głównych hostów
- Ustawienia strefy: SSL/TLS, HTTPS, HSTS
- Ruleset `http_request_firewall_custom` (jeśli istnieje)
- Reguły Rate Limiting (jeśli istnieją)
- Widżety Turnstile (jeśli istnieją)
- Aplikacje/polityki Cloudflare Access (jeśli istnieją)
- Worker routes / custom domains
```

- [ ] **Krok 10: Walidacja Terraform (jeśli zainstalowany)**

```powershell
cd infra/cloudflare/terraform
terraform fmt -recursive
terraform init -backend=false
terraform validate
```

Oczekiwane: `fmt` nie zmienia nic; `validate` przechodzi.

- [ ] **Krok 11: Commit**

```bash
git add .gitignore infra/cloudflare/terraform
git commit -m "chore: scaffold Cloudflare Terraform security baseline"
```

---

## Zadanie 2: DNS, SSL i Publiczne Hosty

**Pliki do stworzenia:**
- `infra/cloudflare/terraform/dns.tf`
- `infra/cloudflare/terraform/ssl_tls.tf`

- [ ] **Krok 1: Stwórz `dns.tf` — import-first placeholders**

```hcl
# WAŻNE: Zaimportuj istniejące rekordy DNS przed odkomentowaniem zasobów.
# Użyj cf-terraforming do wygenerowania kodu dla istniejących rekordów.
#
# Przykład (odkomentuj po imporcie):
# resource "cloudflare_dns_record" "api" {
#   zone_id = var.cloudflare_zone_id
#   name    = "api"
#   content = "twoja-aplikacja.workers.dev"
#   type    = "CNAME"
#   proxied = true
#   ttl     = 1
# }
#
# Wszystkie hosty produkcyjne MUSZĄ być proxied = true przez Cloudflare.
```

- [ ] **Krok 2: Stwórz `ssl_tls.tf`**

```hcl
# Docelowe ustawienia SSL/TLS:
# - ssl = "strict"                    (weryfikacja certyfikatu origin)
# - always_use_https = "on"           (redirect HTTP → HTTPS)
# - min_tls_version = "1.2"           (odrzuć TLS 1.0 i 1.1)
# - tls_1_3 = "on"                    (włącz TLS 1.3)
# - automatic_https_rewrites = "on"   (przepisuj http:// linki w HTML)
#
# HSTS: implementuj stopniowo
# 1. max-age=86400 (1 dzień) — obserwuj
# 2. max-age=2592000 (30 dni)
# 3. max-age=31536000 (1 rok) — dopiero po potwierdzeniu, że WSZYSTKIE
#    subdomeny mają działające certyfikaty TLS
# 4. Preload — tylko po pełnej weryfikacji; trudno cofnąć
#
# UWAGA: includeSubDomains wymusi TLS na WSZYSTKICH subdomenach,
# w tym środowiskach testowych. Dodawaj dopiero po audycie.
```

- [ ] **Krok 3: Sprawdź aktualną wersję providera Cloudflare**

Sprawdź oficjalną dokumentację dla aktualnych nazw zasobów:
```bash
# Sprawdź dostępne zasoby dla bieżącej wersji providera
terraform providers schema -json | jq '.provider_schemas."registry.terraform.io/cloudflare/cloudflare".resource_schemas | keys[]'
```

- [ ] **Krok 4: Commit**

```bash
git add infra/cloudflare/terraform/dns.tf infra/cloudflare/terraform/ssl_tls.tf
git commit -m "docs: define Cloudflare SSL/TLS and DNS controls"
```

---

## Zadanie 3: DNSSEC

- [ ] **Krok 1: Aktywuj DNSSEC**

**Opcja A — Cloudflare Registrar (zalecana, one-click):**
1. W panelu Cloudflare → Twoja strefa → DNS → Settings
2. Kliknij "Enable DNSSEC"
3. Gotowe — Cloudflare zarządza kluczami automatycznie

**Opcja B — Zewnętrzny rejestrator:**
1. W panelu Cloudflare → DNS → Settings → DNSSEC → Enable
2. Skopiuj rekord DS (Delegation Signer):
   - Key Tag
   - Algorithm
   - Digest Type
   - Digest
3. W panelu rejestratora domeny dodaj rekord DS z powyższymi wartościami
4. Poczekaj na propagację (do 48h)

- [ ] **Krok 2: Zweryfikuj DNSSEC**

```bash
# Sprawdź czy DNSSEC jest aktywny
dig +dnssec twoja-domena.pl

# Alternatywnie, użyj narzędzia online
# https://dnssec-debugger.verisignlabs.com/twoja-domena.pl
```

Oczekiwane: odpowiedź zawiera flagę `ad` (Authenticated Data) lub podpis RRSIG.

---

## Zadanie 4: WAF — 5 Reguł Custom (Free)

**Plik do stworzenia:** `infra/cloudflare/terraform/waf_free.tf`

**Zasady wyrażeń WAF na Free planie:**
- ❌ BEZ `matches` (regex) — nie jest obsługiwany na Free
- ✅ Używaj `starts_with`, `ends_with`, `contains`, `eq`
- ✅ Używaj `and`, `or`, `not` dla logiki
- ✅ Wykluczaj `OPTIONS` z reguł challenge/block (nie blokuj CORS preflight)
- ❌ NIE blokuj ścieżek OAuth callbacks, `/sw.js`, `/manifest.json`, ikon PWA

- [ ] **Krok 1: Stwórz `waf_free.tf`**

```hcl
# WAŻNE: Import-first.
# Przed `terraform apply` sprawdź inventory.md i potwierdź no-op plan.

resource "cloudflare_ruleset" "zone_custom_firewall" {
  zone_id = var.cloudflare_zone_id
  name    = "Custom Free WAF Rules"
  kind    = "zone"
  phase   = "http_request_firewall_custom"

  rules = [
    {
      ref         = "protocol_method_guard"
      description = "Blokuj niestandardowe porty i nieoczekiwane metody HTTP"
      expression  = "(not cf.edge.server_port in {80 443}) or (not http.request.method in {\"GET\" \"HEAD\" \"POST\" \"OPTIONS\"})"
      action      = "block"
      enabled     = true
    },
    {
      ref         = "auth_abuse_guard"
      description = "Managed Challenge dla endpointów auth wysokiego ryzyka"
      expression  = "(http.request.method ne \"OPTIONS\") and (starts_with(http.request.uri.path, \"/api/auth/\") or starts_with(http.request.uri.path, \"/api/login\") or starts_with(http.request.uri.path, \"/api/register\"))"
      action      = "managed_challenge"
      enabled     = true
    },
    {
      ref         = "scanner_garbage_guard"
      description = "Blokuj skanery i popularne ścieżki ataków"
      expression  = "(starts_with(http.request.uri.path, \"/wp-\") or starts_with(http.request.uri.path, \"/wordpress\") or starts_with(http.request.uri.path, \"/phpmyadmin\") or starts_with(http.request.uri.path, \"/.git\") or starts_with(http.request.uri.path, \"/.env\") or starts_with(http.request.uri.path, \"/backup\"))"
      action      = "block"
      enabled     = true
    },
    {
      ref         = "admin_preview_guard"
      description = "Blokuj publiczny dostęp do ścieżek admin i subdomen preview/stage"
      expression  = "(http.request.method ne \"OPTIONS\") and (starts_with(http.request.uri.path, \"/api/admin/\") or starts_with(http.request.uri.path, \"/admin\") or starts_with(http.host, \"preview.\") or starts_with(http.host, \"stage.\"))"
      action      = "block"
      enabled     = true
    },
    {
      ref         = "emergency_cost_shield"
      description = "Awaryjna tarcza API — wyłączona domyślnie, aktywuj ręcznie przy incydencie"
      expression  = "(http.host eq \"api.twoja-domena.pl\") and starts_with(http.request.uri.path, \"/api/\") and (http.request.method ne \"OPTIONS\")"
      action      = "managed_challenge"
      enabled     = false
    },
  ]
}
```

- [ ] **Krok 2: Zweryfikuj wyrażenia WAF**

Sprawdź ręcznie lub użyj skryptu (Zadanie 7):
- Brak `matches "` w pliku (regex niedozwolony)
- Reguły z `managed_challenge` wykluczają `OPTIONS`
- Brak ścieżek: `/sw.js`, `/manifest.json`, `/api/auth/callback`

- [ ] **Krok 3: Commit**

```bash
git add infra/cloudflare/terraform/waf_free.tf
git commit -m "chore: add Cloudflare Free WAF custom rules"
```

---

## Zadanie 5: Rate Limiting (1 reguła)

**Plik do stworzenia:** `infra/cloudflare/terraform/rate_limit_free.tf`

- [ ] **Krok 1: Stwórz `rate_limit_free.tf`**

```hcl
# Cloudflare Free Rate Limiting: 1 reguła, tylko warunki bazujące na ścieżce.
# NIE używaj: host, method, nagłówki, body, JWT, custom fields.
#
# Najlepsza strategia: skieruj regułę na endpointy auth:
# - /api/login
# - /api/register
# - /api/auth/forget-password
# - /api/auth/reset-password
#
# Sugerowany limit: 5 żądań / 60 sekund per IP
# Sugerowana akcja: managed_challenge lub block
#
# Reguła pozostaje wyłączona (enable_free_rate_limit = false) dopóki
# nie zostanie zaimportowana i ręcznie zweryfikowana.
#
# Jeśli provider Cloudflare v5 obsługuje zarządzany Rate Limiting,
# dodaj zasób za przełącznikiem var.enable_free_rate_limit:

# resource "cloudflare_rate_limit" "auth_paths" {
#   count   = var.enable_free_rate_limit ? 1 : 0
#   zone_id = var.cloudflare_zone_id
#   ...
# }
```

- [ ] **Krok 2: Sprawdź aktualną składnię rate limiting w provider docs**

Provider Cloudflare v5 może używać `cloudflare_ruleset` z `phase = "http_ratelimit"`.
Sprawdź oficjalną dokumentację przed implementacją zasobu.

---

## Zadanie 6: Nagłówki Bezpieczeństwa (Transform Rules)

- [ ] **Krok 1: Skonfiguruj nagłówki przez panel Cloudflare lub Terraform**

**Wymagane nagłówki:**

| Nagłówek | Wartość | Priorytet |
|---|---|---|
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` | Krytyczny |
| `X-Frame-Options` | `DENY` | Krytyczny |
| `Content-Security-Policy` | patrz niżej | Wysoki |
| `Referrer-Policy` | `strict-origin-when-cross-origin` | Wysoki |
| `Permissions-Policy` | `geolocation=(), microphone=(), camera=()` | Wysoki |
| `X-Content-Type-Options` | `nosniff` | Standardowy |

**CSP (Content-Security-Policy) — startowa konfiguracja:**
```
default-src 'self';
script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
frame-src https://challenges.cloudflare.com;
frame-ancestors 'none';
upgrade-insecure-requests;
```

> Dostosuj CSP do faktycznych zależności aplikacji. Zacznij od `Content-Security-Policy-Report-Only` i monitoruj naruszenia przed włączeniem trybu blokowania.

- [ ] **Krok 2: Implementacja przez Terraform (Transform Rules)**

```hcl
# W waf_free.tf lub oddzielnym transform_rules.tf:
# Użyj cloudflare_ruleset z phase = "http_response_headers_transform"
# do dodawania nagłówków bezpieczeństwa do wszystkich odpowiedzi.
#
# Przykład dla X-Frame-Options:
# action_parameters = {
#   headers = {
#     "X-Frame-Options" = {
#       operation = "set"
#       value     = "DENY"
#     }
#   }
# }
```

- [ ] **Krok 3: Zweryfikuj nagłówki**

```bash
# Sprawdź nagłówki odpowiedzi
curl -I https://twoja-domena.pl

# Lub użyj narzędzia online
# https://securityheaders.com/?q=twoja-domena.pl
```

Oczekiwane: ocena A lub A+ na securityheaders.com.

---

## Zadanie 7: Skrypty Weryfikacyjne CI/CD

**Pliki do stworzenia:**
- `scripts/security/assert-cloudflare-security.mjs`
- `scripts/security/cloudflare-waf-expressions.test.mjs`

- [ ] **Krok 1: Stwórz `assert-cloudflare-security.mjs`**

```js
import fs from "node:fs";
import path from "node:path";
import assert from "node:assert/strict";

const terraformDir = path.resolve("infra/cloudflare/terraform");

// Sprawdź wymagane pliki
const requiredFiles = [
  "providers.tf",
  "variables.tf",
  "locals.tf",
  "waf_free.tf",
  "rate_limit_free.tf",
  "README.md",
  "imports.md",
  "inventory.md",
];

for (const file of requiredFiles) {
  assert.ok(
    fs.existsSync(path.join(terraformDir, file)),
    `Brakuje wymaganego pliku: ${file}`
  );
}

// Sprawdź czy Terraform nie zawiera sekretów aplikacji
const allTf = fs
  .readdirSync(terraformDir)
  .filter((f) => f.endsWith(".tf"))
  .map((f) => fs.readFileSync(path.join(terraformDir, f), "utf8"))
  .join("\n");

assert.equal(
  /DATABASE_URL|SECRET_KEY|API_SECRET|PRIVATE_KEY|PASSWORD/i.test(allTf),
  false,
  "Pliki Terraform NIE mogą zawierać sekretów aplikacji"
);

// Sprawdź .gitignore
const gitignore = fs.readFileSync(path.resolve(".gitignore"), "utf8");
for (const pattern of [
  ".terraform/",
  "*.tfstate",
  "*.tfstate.*",
  "terraform.tfvars",
  "*.auto.tfvars",
]) {
  assert.ok(
    gitignore.includes(pattern),
    `.gitignore musi zawierać: ${pattern}`
  );
}

console.log("✅ Cloudflare security config checks passed.");
```

- [ ] **Krok 2: Stwórz `cloudflare-waf-expressions.test.mjs`**

```js
import fs from "node:fs";
import path from "node:path";
import assert from "node:assert/strict";

const wafPath = path.resolve("infra/cloudflare/terraform/waf_free.tf");
const source = fs.readFileSync(wafPath, "utf8");

// Free plan nie obsługuje regex
assert.equal(
  /matches\s+"/.test(source),
  false,
  "Wyrażenia WAF Free NIE mogą używać operatora regex 'matches'"
);

// Wymagane referencje reguł
const requiredRefs = [
  "protocol_method_guard",
  "auth_abuse_guard",
  "scanner_garbage_guard",
  "admin_preview_guard",
  "emergency_cost_shield",
];

for (const ref of requiredRefs) {
  assert.ok(source.includes(ref), `Brakuje reguły WAF: ${ref}`);
}

// Reguły managed_challenge muszą wykluczać OPTIONS
const challengeRules = ["auth_abuse_guard", "emergency_cost_shield"];
for (const ref of challengeRules) {
  const idx = source.indexOf(`ref         = "${ref}"`);
  assert.notEqual(idx, -1, `Brakuje reguły: ${ref}`);
  const nextIdx = source.indexOf('ref         = "', idx + 1);
  const block = source.slice(idx, nextIdx === -1 ? source.length : nextIdx);
  assert.ok(
    block.includes('http.request.method ne "OPTIONS"'),
    `Reguła ${ref} musi wykluczać CORS preflight (OPTIONS)`
  );
}

// Nie blokuj krytycznych ścieżek systemu
for (const safePath of [
  "/sw.js",
  "/manifest.json",
  "/favicon",
  "/api/auth/callback",
]) {
  assert.equal(
    source.includes(safePath),
    false,
    `Reguły WAF NIE powinny celować w bezpieczną ścieżkę: ${safePath}`
  );
}

console.log("✅ Cloudflare Free WAF expression checks passed.");
```

- [ ] **Krok 3: Dodaj skrypty do `package.json`**

```json
{
  "scripts": {
    "security:cloudflare": "node scripts/security/assert-cloudflare-security.mjs && node scripts/security/cloudflare-waf-expressions.test.mjs"
  }
}
```

- [ ] **Krok 4: Uruchom i potwierdź przejście**

```bash
npm run security:cloudflare
```

Oczekiwane: oba komunikaty `✅ ... passed`.

- [ ] **Krok 5: Commit**

```bash
git add scripts/security package.json
git commit -m "chore: add Cloudflare security CI gates"
```

---

## Zadanie 8: Hardening Serwera Origin (Cloudflare Tunnel)

- [ ] **Krok 1: Zainstaluj cloudflared na serwerze origin**

```bash
# Linux (Debian/Ubuntu)
curl -L --output cloudflared.deb \
  https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
sudo dpkg -i cloudflared.deb

# macOS
brew install cloudflare/cloudflare/cloudflared
```

- [ ] **Krok 2: Uwierzytelnij cloudflared**

```bash
cloudflared tunnel login
```

Przeglądarka otworzy panel Cloudflare — wybierz domenę.

- [ ] **Krok 3: Stwórz tunel**

```bash
cloudflared tunnel create nazwa-tunelu
```

Zapisz wyświetlony UUID tunelu.

- [ ] **Krok 4: Skonfiguruj tunel**

Stwórz `~/.cloudflared/config.yml`:

```yaml
tunnel: <UUID-tunelu>
credentials-file: /root/.cloudflared/<UUID-tunelu>.json

ingress:
  - hostname: twoja-domena.pl
    service: http://localhost:3000
  - hostname: api.twoja-domena.pl
    service: http://localhost:3001
  - service: http_status:404
```

- [ ] **Krok 5: Stwórz rekord DNS CNAME dla tunelu**

```bash
cloudflared tunnel route dns nazwa-tunelu twoja-domena.pl
cloudflared tunnel route dns nazwa-tunelu api.twoja-domena.pl
```

- [ ] **Krok 6: Uruchom tunel jako usługa systemowa**

```bash
cloudflared service install
systemctl start cloudflared
systemctl enable cloudflared
```

- [ ] **Krok 7: Zamknij wszystkie porty przychodzące na firewallu**

```bash
# UFW (Ubuntu)
ufw default deny incoming
ufw allow out 7844/tcp  # cloudflared outbound
ufw allow out 443/tcp
ufw allow out 80/tcp
ufw enable
```

- [ ] **Krok 8: Zweryfikuj**

```bash
# Test — serwer origin nie powinien odpowiadać bezpośrednio
curl -v http://<IP-serwera-origin>

# Test — domena powinna odpowiadać przez Cloudflare
curl -I https://twoja-domena.pl
```

Oczekiwane: bezpośredni dostęp do IP odmówiony; domena działa przez CF.

---

## Zadanie 9: Runbook Operatorski i Bramki Upgrade

**Plik do stworzenia:** `docs/operations/cloudflare-free-security-runbook.md`

- [ ] **Krok 1: Stwórz runbook**

```markdown
# Cloudflare Free Security — Runbook Operatorski

## Standardowa weryfikacja

1. Uruchom `npm run security:cloudflare`
2. Uruchom `terraform fmt -recursive`
3. Uruchom `terraform validate`
4. Sprawdź nagłówki: `curl -I https://twoja-domena.pl`
5. Sprawdź DNSSEC: `dig +dnssec twoja-domena.pl`

## Procedura Lockdown (przy ataku)

1. Włącz regułę `emergency_cost_shield` w panelu Cloudflare
   (Ruleset → emergency_cost_shield → Enable)
2. Ustaw zmienną środowiskową `COST_GUARD_MODE=lockdown` w Workers
3. Potwierdź, że endpointy health/status nadal odpowiadają
4. Potwierdź, że wysokie ryzyka żądania są blokowane
5. Zaloguj: czas startu, czas zakończenia, evidence

## Rollback

1. Wyłącz regułę `emergency_cost_shield`
2. Przywróć `COST_GUARD_MODE=normal`
3. Uruchom ponownie smoke testy

## Bramki upgrade (kiedy Free przestaje wystarczać)

- **Workers Paid**: gdy przekraczasz 100k żądań/dzień regularnie
- **Pro/Business WAF**: gdy 5 reguł Free jest niewystarczające
- **Bot Management**: gdy Turnstile + WAF evidence pokazuje credential stuffing
- **API Shield**: gdy OpenAPI contracts są stabilne i chcesz schema validation
- **Cloudflare Access**: gdy Admin panel wymaga Zero Trust (zamiast IP allowlisting)
```

- [ ] **Krok 2: Commit**

```bash
git add docs/operations/cloudflare-free-security-runbook.md
git commit -m "docs: add Cloudflare Free security runbook and upgrade gates"
```

---

## Zadanie 10: Weryfikacja Końcowa

- [ ] **Krok 1: Uruchom wszystkie security gates**

```bash
npm run security:cloudflare
```

- [ ] **Krok 2: Sprawdź Terraform**

```bash
cd infra/cloudflare/terraform
terraform fmt -recursive
terraform init -backend=false
terraform validate
```

- [ ] **Krok 3: `terraform plan` (tylko przy jawnej zgodzie operatora)**

Wymagania przed uruchomieniem plan:
- [ ] `TF_VAR_cloudflare_api_token` ustawiony z bezpiecznego źródła
- [ ] Prawdziwe `cloudflare_account_id` i `cloudflare_zone_id` dostępne
- [ ] Operator wyraził jawną zgodę na Cloudflare API read access
- [ ] Żaden token/sekret NIE jest w `terraform.tfvars`

```bash
terraform plan \
  -var "cloudflare_account_id=$TF_VAR_cloudflare_account_id" \
  -var "cloudflare_zone_id=$TF_VAR_cloudflare_zone_id"
```

- [ ] **Krok 4: Weryfikacja live (po deploy)**

```bash
# HTTPS i nagłówki
curl -I https://twoja-domena.pl

# HSTS
curl -I https://twoja-domena.pl | grep -i strict-transport

# Security headers
curl -I https://twoja-domena.pl | grep -iE "x-frame|content-security|referrer-policy"

# DNSSEC
dig +dnssec twoja-domena.pl | grep -i "ad\|rrsig"
```

- [ ] **Krok 5: Raport zakończenia**

Podsumuj:
- Które zadania zostały zaimplementowane
- Które komendy weryfikacyjne przeszły
- Czy Terraform był dostępny i czy plan był no-op
- Które działania produkcyjne wymagają jeszcze zgody operatora

---

## Notatki Wykonawcze

- Używaj TDD dla logiki aplikacyjnej (cost guard, Turnstile)
- Trzymaj `terraform apply` na produkcji POZA automatyzacją dopóki import/no-op plan nie jest potwierdzony
- Nigdy nie commituj: API tokenów Cloudflare, sekretów Turnstile, URLs baz danych, sekretów auth, tokenów OAuth, wygenerowanego Terraform state
- Traktuj `terraform.tfstate`, `.terraform/` i lokalne tfvars jako wrażliwe
- Jeśli funkcja Cloudflare Free zachowuje się inaczej niż opisano — zaktualizuj plan zamiast zakładać wymaganie płatnego planu

---

*Plik wygenerowany: 2026-06-09 | Wersja: 1.0 | Przeznaczenie: implementacja przez agenta AI*
