Skip to content

Web Application Firewall (WAF)

Ingress can run a first-version WAF on matched routes after routing and before redirects, handlers, or upstream proxies. It covers:

  • IP lists (deny then optional allow gate).
  • Host allowlist (allow_hosts): listed hosts skip all WAF phases (IP lists and signatures).
  • Per-rule host allowlist (rules[].allow_hosts): listed hosts skip that rule only; other rules still run.
  • Signature checks against path, query, assembled uri, all header lines (headers), or one header (header:User-Agent).
  • An optional embedded starter ruleset (SQLi/path traversal/reflected-scripting probes). Turn off with disable_builtin: true if it is too noisy.

There is no request-body scanning. Each signature rule (custom or built-in) can block (default), audit (log only), or pass (treat match as allowed and skip remaining signatures). Global or per-rule log_only: true is still supported and equals action: audit when action is omitted.

Enabling WAF

  • Top-level waf: is the typed global baseline.
  • rules[].waf is a partial YAML map merged onto that baseline for the matched host rule.

Config loader caveat

The generic config decoder cannot partially merge map keys into structs, so rules[].waf is applied in waf.ApplyRulePatchesFromYAML (core/waf/yaml.go) after config.Load. Embedded configs built in Go must set rules[i].WAFPatch manually when you need route overlays without a file.

Evaluation order

  1. Effective policy = merge(global waf, rules[i].WAFPatch).
  2. Exit if enabled is false.
  3. allow_hosts: skip all WAF when request Host matches (exact, * wildcard, or auto-detected Go regex — same inference as ingress host routing).
  4. Resolve client IP ( trust_proxy + xff_index ; default index 0 = leftmost segment in X-Forwarded-For ).
  5. Deny list, then Allow when non-empty (only listed nets may pass the IP phase).
  6. Signatures: starters unless disable_builtin, then custom rules in merged order. Before each signature, rules[].allow_hosts on that rule skips it when Host matches (same pattern syntax as global allow_hosts).

Blocked clients receive block_status_code (default 403), block_content_type, and block_body.

Built-in starter rules

When disable_builtin is not true, the following rules are appended before custom entries in waf.rules. IDs are stable so you can replace a builtin by adding your own rule with the same id (see examples/waf/rule-merge-by-id.yaml). Patterns use Go regexp syntax.

IDTargetsDescription
builtin:sqli-commonuriCommon SQL-injection style probes in path + raw query (union select, sleep(, benchmark(, ; drop/truncate/alter table, …).
builtin:path-traversalpathPath traversal probes (../, ..\, encoded .., etc/passwd).
builtin:xss-liteuriLight reflected-scripting probes (<script, javascript:, on*=-style event handlers).
builtin:rce-probesuriCommand injection probes (pipes, shell commands).
builtin:jndi-lookupuri, headersJNDI-style ${…jndi: injection.
builtin:sensitive-filespathSensitive paths (.env, .git/, admin consoles, …).
builtin:ssrf-probesuriCloud metadata / file:// / gopher:// SSRF probes (excludes localhost to reduce OAuth false positives).
builtin:scanner-uaheader:User-AgentKnown scanner User-Agents.
builtin:crlf-injectionuri, headersCRLF / response-splitting probes.
builtin:php-sstiuriPHP eval, php://, template-style probes.

Per-rule enablement:

  • disable_builtin: false (default): all built-in rules are on unless listed in waf.builtin_rules with false.
  • disable_builtin: true: all built-in rules are off unless waf.builtin_rules sets a rule to true.
  • Custom waf.rules[] entries support enabled: false (default is on when omitted).

Per built-in action (without disabling the rule):

yaml
waf:
  builtin_rule_actions:
    builtin:scanner-ua: audit
    builtin:xss-lite: pass

Example:

yaml
waf:
  enabled: true
  disable_builtin: false
  builtin_rules:
    builtin:scanner-ua: false
  rules:
    - id: block-secret-path
      enabled: false
      type: contains
      pattern: /internal
      targets: [path]
    - id: allow-health
      action: pass
      type: contains
      pattern: /healthz
      targets: [path]

Exact patterns (from core/waf/builtin.go; change there if you fork):

builtin:sqli-common
(?is)(union\s+select\b|sleep\s*\(|benchmark\s*\(|;\s*(drop|truncate|alter)\s+table\b)

builtin:path-traversal
(?:\.\./|\.\.\\|%2e%2e%2f|%2e%2e\\\\|etc/passwd\b)

builtin:xss-lite
(?is)(<\s*script\b|javascript:\s*[a-z]|\bon(?:click|load|error|focus|blur|change|submit|mouse\w*|key\w*|touch\w*|pointer\w*|scroll|dblclick|drag\w*|drop|input|reset|select|wheel|copy|cut|paste|abort|contextmenu|message|unload|beforeunload)\s*=)

Starters can false-positive on unusual but legitimate traffic — use log_only or disable_builtin: true and replace with stricter custom rules as needed. Regression cases for common benign traffic live in core/waf/builtin_false_positive_test.go.

Custom rules (waf.rules)

FieldNotes
idRequired. Route-level waf.rules with the same id replace the global rule of that id.
enabledOptional. Default true; set false to disable a custom rule without deleting it.
actionblock (default), audit (log only, keep checking), or pass (allow on match, stop further signatures).
log_onlyLegacy; when action is omitted, true means audit.
typeregex (default) or contains
patternCompiled at startup for regex.
targetsOne or more of path, query, uri, headers, header:…
allow_hostsOptional. Matching Host skips this rule only (same syntax as global allow_hosts). Use with same id as a builtin to scope exceptions without replacing the pattern.

Runnable files: examples/waf/.

See also

Released under the MIT License.