kubeWAF is under active development — feedback and stars on GitHub are very welcome!
kubeWAFkubeWAF
kubeWAFmodsecurity-proxy-wasmpow-proxy-wasm

Proof-of-Work challenge

Enable and configure the optional PoW challenge filter on a WAF resource

kubeWAF can install an optional browser proof-of-work (PoW) challenge filter in front of the WAF engine. Clients without a valid clearance cookie must solve a short CPU challenge before their traffic is evaluated by ModSecurity.

This page covers only what you configure on the WAF CR. For the filter’s internal design (cookies, crypto, standalone Envoy), see the separate pow-proxy-wasm project docs.


When to enable it

Use caseFit
Cheap bots / scrapers without a full JS runtimeStrong
Absorb bursty unauthenticated traffic before expensive WAF rulesStrong
“Pay with CPU” gate next to rate limitingStrong
Hard identity / account takeoverWeak — use auth
True one-time tokens with cluster-wide shared stateNot this design

Place challenge after coarse rate limiting and before WAF / auth filters (the operator installs it immediately before the WAF filter in the chain).


Enable on a WAF

apiVersion: waf.kubewaf.io/v1beta1
kind: WAF
metadata:
  name: shop-waf
  namespace: shop
spec:
  engine: ModSecurity
  provider:
    type: EnvoyGateway
  parentRefs:
    targetRef:
      group: gateway.networking.k8s.io
      kind: Gateway
      name: external
  ruleRefs:
  - kind: RuleSet
    name: shop-rules

  challenge:
    enabled: true
    # HMAC is auto-managed — no secret required for the default path
    baseDifficulty: 18
    minDifficulty: 12
    maxDifficulty: 26
    header: x-challenge-passed
    headerValue: "1"

When spec.challenge is present and enabled, the operator:

  1. Resolves (or creates) the HMAC Secret
  2. Publishes a second ECDS resource for the challenge filter
  3. Inserts the challenge filter before the WAF filter in the provider slot

spec.challenge properties

FieldTypeDefaultDescription
enabledbooltrue when the block is presentInstall the challenge filter when true
secretstringemptyOptional plaintext HMAC override (≥ 32 bytes). Dev / break-glass only
secretRef{ name, key }emptyUse an existing Secret in the same namespace as the WAF
baseDifficultyint (1–32)18Default PoW difficulty (leading zero bits in SHA-256)
minDifficultyint (1–32)(filter default 12)Lower bound for adaptive difficulty
maxDifficultyint (1–32)(filter default 26)Upper bound for adaptive difficulty
headerstringemptyOptional response header name injected after a successful pass
headerValuestringemptyValue for header
wasmHTTPstringoperator defaultOverride URL Envoy uses to fetch the challenge .wasm
wasmSHA256stringemptyOptional SHA-256 pin for the challenge binary

Difficulty intuition

Higher baseDifficulty means more client CPU before the first request succeeds. Rough order of magnitude (device-dependent):

baseDifficultyExpected SHA-256 tries
12~4k (light)
18~260k (default)
22~4M
26~67M (heavy)

Tune under real traffic; pair with rate limits for bots that never solve.


HMAC secret management

The challenge filter needs a shared HMAC secret on every Envoy replica.

ModeHow
Default (recommended)Operator creates Secret <waf-name>-challenge-hmac in the WAF namespace, key hmac (32 random bytes, base64url). Owned by the WAF; stable across reconciles so clearance cookies keep working
SecretRefsecretRef: { name: my-hmac, key: hmac } — bring your own Secret
Inlinesecret: "…" — plaintext in the CR (≥ 32 bytes; avoid in production)

Priority: inline secretsecretRef → managed Secret.

# Bring your own Secret
spec:
  challenge:
    enabled: true
    secretRef:
      name: shop-challenge-hmac
      key: hmac

Rotate a managed secret by deleting it and letting the controller recreate it (existing browser clearances will invalidate).


Status fields

kubectl get waf shop-waf -n shop -o jsonpath='{.status.challengeEnabled}{" "}{.status.challengeSecretName}{"\n"}'
# true shop-waf-challenge-hmac
Status fieldMeaning
challengeEnabledChallenge filter is installed for this WAF
challengeSecretNameSecret name used for HMAC (managed or secretRef name)

Also check overall WAF conditions (Ready, ResolvedRefs, …) as usual.


What the operator wires

PieceValue
ECDS name (challenge)kubewaf/<namespace>/<waf-name>/challenge
ECDS name (WAF)kubewaf/<namespace>/<waf-name>
Default wasm pathGET /wasm/challenge-proxy-wasm.wasm on the operator wasm server

You normally do not set raw plugin JSON — the controller maps CR fields + the resolved HMAC into the filter config. Override the binary only if you host wasm yourself (challenge.wasmHTTP / Helm dataplane.challengeWasm*).

Cluster-wide binary defaults (Helm / operator flags):

Helm / flagPurpose
dataplane.challengeWasmFile / --challenge-wasm-filePath inside the operator image
dataplane.challengeWasmSourceURL / --challenge-wasm-source-urlDownload at operator startup

Minimal examples

Challenge + CRS

spec:
  engine: ModSecurity
  crsEnable: true
  challenge:
    enabled: true
    baseDifficulty: 16
  ruleRefs:
  - kind: RuleSet
    name: app-rules

Disable without removing the block

spec:
  challenge:
    enabled: false

Pin a custom challenge wasm URL

spec:
  challenge:
    enabled: true
    wasmHTTP: https://cdn.example.com/challenge-proxy-wasm.wasm
    wasmSHA256: "abcdef..."

Troubleshooting (operator view)

SymptomWhat to check
challengeEnabled=false but you expected truespec.challenge missing or enabled: false
Filter never loadsOperator wasm serve (challengeWasmFile / SourceURL); ECDS status
Always challenge page in browserCookies blocked; clock skew; see pow-proxy-wasm troubleshooting
Clearance works then fails after ~30 minExpected clearance lifetime — re-solve
Wrong Secretstatus.challengeSecretName; Secret exists and is ≥ 32 bytes when BYO

General data-plane checks: Troubleshooting, Data plane (ECDS).


On this page