This runbook defines the DDoS-shield rules in front of the Sigil public RPC endpoints. Cloudflare is the edge layer; ingress-nginx and the sigil-rpc pods provide the application-layer defence.

Architecture

            ┌────────────────────────────────────────────┐
internet ─▶ │ Cloudflare DDoS shield (L3/L4)             │
            │ + WAF (L7)                                 │
            │ + Rate limiting (1000 req/min/IP)          │
            │ + Managed challenge for suspicious IPs     │
            └────────────────┬───────────────────────────┘


            ┌────────────────────────────────────────────┐
            │ ingress-nginx (k8s)                        │
            │ + body limit 4 MiB                         │
            │ + TLS termination (Cloudflare → LE)        │
            └────────────────┬───────────────────────────┘


            ┌────────────────────────────────────────────┐
            │ sigil-rpc pods (Service)                   │
            │ + axum tower middleware:                   │
            │     CORS allowlist                         │
            │     RequestBodyLimitLayer (4 MiB)          │
            │     governor token bucket (25/100 rps)     │
            │     RPC profile gate (public/validator)    │
            └────────────────────────────────────────────┘
The direct endpoint (rpc-direct.sigil.ml) skips the top layer and goes straight to ingress-nginx and the pods. The lower pod-layer rate limit (10/25 rps) compensates.

Cloudflare DNS configuration

HostnameTypeValueProxied
rpc.sigil.mlA/AAAALB IPyes (orange cloud)
rpc-direct.sigil.mlA/AAAALB IPno (grey cloud)
canary-rpc.sigil.mlA/AAAALB IPyes
canary-rpc-direct.sigil.mlA/AAAALB IPno
explorer.sigil.mlA/AAAAstatic-site IPyes
status.sigil.mlCNAME<R2 bucket>.r2.cloudflarestorage.comyes
The direct endpoints must remain grey-cloud (DNS-only). Proxying them removes the censorship-resistance escape hatch and the endpoint name no longer reflects reality. A Terraform lint rule enforces this.

WAF rules (Cloudflare)

Apply at the rpc.sigil.ml and canary-rpc.sigil.ml zones.

Managed rules

  • OWASP Core Rule Set, default sensitivity.
  • Cloudflare Managed Rules.
  • Exposed Credential Checks.

Custom rules

#MatchAction
1POST /rpc without content-type: application/jsonBlock
2Source ASN in the abuse listManaged Challenge
3Request body > 16 MiBBlock
4Per-IP rate > 5,000 / 5 minManaged Challenge
5User-Agent contains sqlmap, nikto, or nmapBlock
6Path not in {"/rpc","/health","/metrics","/ws","/"}Block

Edge rate-limit

The edge ceiling is 10x the application-layer cap so the application layer remains the primary defence.
PathMethodRateAction
/rpcPOST1,000 / 1 min / IPBlock (10 min)
/ws*30 connections / 1 hour / IPBlock (10 min)

Bot Fight Mode

  • Enabled on sigil.ml and canary-rpc.sigil.ml zones.
  • Super Bot Fight Mode: enabled on Pro+ plan.

TLS

  • TLS mode: Full (Strict). Origin must present a valid Let’s Encrypt cert.
  • Minimum TLS version: 1.2.
  • HSTS: max-age=31536000; includeSubDomains; preload.
  • cert-manager issues origin certs via HTTP-01 against a non-Cloudflare-proxied subdomain.

Direct endpoint hardening

Because rpc-direct.sigil.ml is grey-cloud, the application-layer compensates:
  • Stricter rate limit (10/25 rps vs 25/100).
  • Same 4 MiB body cap.
  • ingress-nginx sets proxy-body-size: 4m and proxy-read-timeout: 60s.
  • fail2ban on the LB host blocks IPs that emit 100+ HTTP 429 responses in 5 minutes for 1 hour.

Operator procedures

Add an abuse ASN to the challenge list

  1. Identify the ASN from the WAF event log.
  2. Update WAF rule #2 with the new ASN.
  3. Commit the rule change to Terraform.
  4. Verify by issuing a test request from the abusive ASN.

Respond to a live attack

  1. Inspect the Cloudflare Security tab for the attack profile.
  2. Enable “Under Attack” mode if the attack overwhelms default protection. Every request is forced through a managed challenge for 30 minutes.
  3. Confirm status.sigil.ml reflects the incident; post manually if not.
  4. After the attack subsides, run a post-incident review and add a permanent rule.

Verify rules

# Expect 403 (blocked).
curl -X POST https://rpc.sigil.ml/admin -d '{}'

# Expect 429 after the rate-limit threshold.
for i in $(seq 1 200); do
  curl -sX POST https://rpc.sigil.ml/rpc \
    -H 'content-type: application/json' \
    -d '{"jsonrpc":"2.0","method":"sigil_height","params":{},"id":1}' \
    -o /dev/null -w "%{http_code}\n"
done | sort | uniq -c

Status page integration

status.sigil.ml auto-updates from Alertmanager:
  • SigilRpcRateLimited{quantile=p99} > 0.5 for 5 minutes → degraded.
  • Cloudflare API “site under attack” boolean → degraded.
  • ingress-nginx availability < 99% → outage.
See status page runbook.