Skip to content

Configuration

Griffen Fargo edited this page Aug 24, 2026 · 5 revisions

Configuration

Doorman uses a JSON configuration file to define your firewall rules. The configuration is validated using JSON Schema and provides full TypeScript support.

Schema URL

Add the schema reference to your config file for editor autocompletion and validation:

{
  "$schema": "https://doorman.griffen.codes/schema.json"
}

Two Rule Formats

Doorman recognizes two on-disk config shapes, auto-detected by whether the file has a top-level provider or providers field:

  • Unified format — any config with provider and/or providers set. Required for Cloudflare and Fastly. Rules use conditions/enabled/flat action: { type }. This is the format the rest of this page documents.
  • Legacy format — a Vercel-only config with no provider/providers field at all. Rules use conditionGroup/active/action: { mitigate }. Kept for backward compatibility with configs written before multi-provider support existed. See Legacy (Vercel-only) Rule Format below.

These are genuinely different shapes, not a relabeling of the same fields — mixing them in one rule (e.g. conditionGroup inside a provider-tagged config) fails validation. If you're setting up Cloudflare or Fastly, use the unified format from the start.

Basic Structure

Vercel Configuration (legacy format)

{
  "$schema": "https://doorman.griffen.codes/schema.json",
  "projectId": "prj_abc123",
  "teamId": "team_xyz789",
  "rules": [],
  "ips": []
}

Cloudflare Configuration (unified format)

{
  "$schema": "https://doorman.griffen.codes/schema.json",
  "provider": "cloudflare",
  "providers": {
    "cloudflare": {
      "zoneId": "zone_abc123",
      "accountId": "acc_xyz789"
    }
  },
  "rules": [],
  "ips": []
}

Fastly Configuration (unified format)

{
  "$schema": "https://doorman.griffen.codes/schema.json",
  "provider": "fastly",
  "providers": {
    "fastly": {
      "workspaceId": "workspace_abc123"
    }
  },
  "rules": [],
  "ips": []
}

Multi-Provider Configuration (unified format)

{
  "$schema": "https://doorman.griffen.codes/schema.json",
  "provider": "cloudflare",
  "providers": {
    "vercel": {
      "projectId": "prj_abc123",
      "teamId": "team_xyz789"
    },
    "cloudflare": {
      "zoneId": "zone_abc123",
      "accountId": "acc_xyz789"
    },
    "fastly": {
      "workspaceId": "workspace_abc123"
    }
  },
  "rules": [],
  "ips": []
}

Root Properties

Property Type Required Description
$schema string No JSON Schema URL for validation
provider string No Default provider ("vercel", "cloudflare", or "fastly"). Presence of this field (or providers) switches the file into unified format.
projectId string Vercel Only Vercel project ID (legacy format)
teamId string Vercel Only Vercel team ID (legacy format, optional)
providers object Multi-Provider Provider-specific configurations
rules array Yes Array of firewall rules — shape depends on format, see above
ips array No Array of IP blocking rules
managedRules array No Cloudflare only. Vendor-managed rulesets (e.g. Cloudflare Managed Ruleset, OWASP CRS) to deploy — see Managed Rule Groups
version number No Configuration version
firewallEnabled boolean No Enable/disable firewall

Rules (Unified Format)

Use this format for Cloudflare, Fastly, or any config with provider/providers set — including a Vercel config you've explicitly tagged with "provider": "vercel".

Rule Structure

{
  "id": "rule_block_bots",
  "name": "Block Bad Bots",
  "description": "Block malicious bots and crawlers",
  "enabled": true,
  "conditions": [
    { "field": "user_agent", "operator": "contains", "value": "bot" }
  ],
  "action": { "type": "deny" }
}

Rule Properties

Property Type Required Description
id string No Unique rule identifier
name string Yes Human-readable rule name
description string No Rule description
enabled boolean Yes Whether rule is enabled
conditions array Yes Array of conditions (at least one required) — see below
conditionLogic string No "AND" (default) or "OR", applied across all conditions when none of them set group — see Grouping Conditions
action object Yes Action to take when conditions match
priority number No Evaluation order — lower runs first. Fully honoured on Cloudflare; best-effort on Vercel, which can't reposition rules that already exist remotely.

Conditions

Each condition has a field, operator, and (usually) a value:

{ "field": "path", "operator": "starts_with", "value": "/admin" }

exists/not_exists operators carry no value — everything else requires one. header/query/cookie conditions take an additional key to scope to a specific name:

{ "field": "header", "key": "X-Custom-Header", "operator": "eq", "value": "expected" }

Cloudflare caveat: key is only honoured for header and cookie today — a keyed query condition currently matches against the whole query string instead of the named parameter. Tracked in #263.

Grouping Conditions

For a simple rule, omit group entirely — all conditions implicitly share group 0 and are AND'd together (or OR'd, if you set conditionLogic: "OR").

For "OR of AND-groups" logic (e.g. (path starts with "/admin" AND method is POST) OR (IP is 192.168.1.1)), tag each condition with a group number — conditions sharing a group are AND'd, and distinct group values are OR'd against each other. group takes priority over conditionLogic the moment any condition sets it:

{
  "conditions": [
    { "field": "path", "operator": "starts_with", "value": "/admin", "group": 0 },
    { "field": "method", "operator": "eq", "value": "POST", "group": 0 },
    { "field": "ip", "operator": "eq", "value": "192.168.1.1", "group": 1 }
  ]
}

Field Support

Field Vercel Cloudflare Fastly
ip
country
region ❌ dropped, warned
city ❌ dropped, warned
asn ❌ dropped, warned
path
host
method
header ✅ requires key ⚠️ requires key, else dropped/warned
query ⚠️ key currently ignored — #263 ⚠️ requires key, else dropped/warned
cookie ✅ requires key ⚠️ requires key, else dropped/warned
user_agent
referer ❌ dropped, warned ❌ dropped, warned
scheme
port ❌ dropped, warned ❌ dropped, warned

exists/not_exists on Fastly only work on the keyed fields (header/query/cookie) — unsupported (dropped, warned) on every other Fastly field.

Operator Support

Operator Vercel Cloudflare Fastly
eq
ne 🚨 silently becomes eq#261
contains
not_contains 🚨 silently becomes eq#261
starts_with ⚠️ wildcard approximation, not a true prefix match
ends_with ⚠️ wildcard approximation, not a true suffix match (identical mechanism to starts_with)
matches (regex) ⚠️ supported, but regex syntax may need adjustment — always warned
in
not_in 🚨 silently becomes eq#261
gt 🚨 silently becomes eq#261 ⚠️ boundary-inclusive approximation (greater_equal)
ge 🚨 silently becomes eq#261
lt 🚨 silently becomes eq#261 ⚠️ boundary-inclusive approximation (lesser_equal)
le 🚨 silently becomes eq#261
exists ⚠️ only on keyed fields (header/query/cookie)
not_exists ⚠️ only on keyed fields (header/query/cookie)

⚠️ Known bug, not a documentation gap: the 🚨 rows above are not "unsupported" in the safe sense of being rejected or dropped with a warning — Vercel's translator currently mis-maps all seven of these operators to plain eq silently, which can invert a rule's intent (e.g. ne — "is not" — becomes "is"). Tracked in #261. Until that's fixed, avoid these seven operators in any config targeting Vercel; they all work correctly on Cloudflare.

Action Support

Action Vercel Cloudflare Fastly
log ⚠️ no dedicated action, maps to allow (request logging is separately always-on)
deny
challenge
bypass ⚠️ no equivalent, maps to allow
rate_limit ⚠️ silently becomes a plain rule with no rate-limit effect if rateLimit is omitted ⚠️ silently becomes a plain block rule with no rate-limit effect if rateLimit is omitted ✅ throws if rateLimit is missing; requires a pre-existing Fastly Signal named doorman-rate-limit-<ruleId> that doorman does not create for you
redirect ⚠️ silently becomes a rule with no redirect target if redirect is omitted; statusCode/preserveQueryString are silently dropped even when present ⚠️ silently becomes a rule with no redirect target if redirect is omitted ✅ falls back to allow (warned) if redirect is missing, otherwise fully supported
allow 🚨 not a valid native Vercel action — #262
block 🚨 not a valid native Vercel action — #262 ✅ (same as deny) ✅ (same as deny)

Use bypass (not allow) and deny (not block) in Vercel-targeted configs until #262 is fixed.

Rate Limiting

{
  "action": {
    "type": "rate_limit",
    "rateLimit": {
      "requests": 100,
      "window": "60s",
      "characteristics": ["ip.src"]
    }
  }
}

window accepts a number followed by s/m/h/d.

Redirect

{
  "action": {
    "type": "redirect",
    "redirect": {
      "location": "https://example.com/blocked",
      "statusCode": 302,
      "permanent": false,
      "preserveQueryString": false
    }
  }
}

location accepts an absolute URL or a path starting with /. On Vercel, only location/permanent are actually used — statusCode/preserveQueryString are silently dropped, so omit them if you need this rule to round-trip cleanly to Vercel.

Legacy (Vercel-only) Rule Format

Only applies to a config with no provider/providers field — i.e. the original Vercel-only shape shown in Basic Structure above.

Rule Structure

{
  "id": "rule_block_bots",
  "name": "Block Bad Bots",
  "description": "Block malicious bots and crawlers",
  "active": true,
  "conditionGroup": [
    {
      "conditions": [
        { "type": "user_agent", "op": "sub", "value": "bot", "neg": false }
      ]
    }
  ],
  "action": {
    "mitigate": { "action": "deny" }
  }
}

Rule Properties

Property Type Required Description
id string No Unique rule identifier
name string Yes Human-readable rule name
description string No Rule description
active boolean Yes Whether rule is enabled
conditionGroup array Yes Array of condition groups — OR logic between groups, AND logic within a group
action object Yes { "mitigate": { "action": ... } } — action to take when conditions match
{
  "conditionGroup": [
    {
      "conditions": [
        { "type": "path", "op": "pre", "value": "/admin" },
        { "type": "method", "op": "eq", "value": "POST" }
      ]
    },
    {
      "conditions": [
        { "type": "ip_address", "op": "eq", "value": "192.168.1.1" }
      ]
    }
  ]
}

This translates to: (path starts with "/admin" AND method equals "POST") OR (IP equals "192.168.1.1").

Legacy Condition Types

host, path, method, header, query, cookie, target_path, ip_address, region, protocol, scheme, environment, user_agent, geo_continent, geo_country, geo_country_region, geo_city, geo_as_number, ja4_digest, ja3_digest, rate_limit_api_id

header/cookie require a key. For ip_address/method/environment/protocol, op must be eq or inc — no other operator is valid for these four types.

Legacy Operators

Operator Meaning
eq Equals
pre Starts with
suf Ends with
sub Contains
inc Is any of (array) — requires value to be an array
re Regex match
ex Exists
nex Does not exist

neg: true negates a condition, but can't be combined with ex/nex — use nex directly instead of neg: true + ex.

Legacy Actions

log, deny, challenge, bypass, rate_limit, redirect — this is a closed set; allow/block are not valid here (use bypass/deny).

{ "action": { "mitigate": { "action": "rate_limit", "rateLimit": { "requests": 100, "window": "60s" } } } }
{ "action": { "mitigate": { "action": "redirect", "redirect": { "location": "/correct-path", "permanent": false } } } }

IP Blocking Rules

{
  "ips": [
    {
      "id": "ip_block_suspicious",
      "ip": "192.168.1.100/32",
      "hostname": "suspicious-host",
      "action": "deny",
      "notes": "Blocked due to suspicious activity"
    }
  ]
}
Property Type Required Description
ip string Yes IP address or CIDR range
hostname string No Hostname for documentation
action string Yes "deny" in both formats; "allow" is also valid in the unified format
notes string No Notes about the block

Same shape in both the legacy and unified formats.

Managed Rule Groups

Cloudflare only. Deploy a vendor-managed ruleset (Cloudflare Managed Ruleset, OWASP CRS, etc.) alongside your custom rules, with optional overrides — instead of hand-writing rules to replicate what a preconfigured WAF ruleset already covers.

{
  "managedRules": [
    {
      "id": "execute-owasp-crs",
      "ruleset": "efb7b8c949ac4650a09736fc376e9aee",
      "name": "OWASP Core Ruleset",
      "enabled": true,
      "action": "log",
      "overrides": [
        { "ruleId": "981176", "action": "deny" },
        { "ruleId": "981245", "enabled": false }
      ]
    }
  ]
}
Property Type Required Description
id string No Doorman's diff/sync identifier for this deployment. Omit for a new declaration; Doorman assigns one on first sync.
ruleset string Yes The vendor ruleset id to deploy (e.g. Cloudflare Managed Ruleset's well-known id shown above)
name string No Human label
enabled boolean Yes Whether this deployment is active
action string No Ruleset-wide override — downgrade every rule in the group to this action. One of log, deny, challenge, allow
overrides array No Per-rule overrides within the ruleset — { "ruleId": string, "action"?: string, "enabled"?: boolean }, referenced by the vendor's rule id within that ruleset, not a Doorman id

managedRules needs to live alongside a provider: "cloudflare"/providers.cloudflare block — it's part of the unified format, same as rules/ips.

Adding this to a provider/providers-tagged config that already has custom rules works alongside them — managed rulesets deploy in Cloudflare's separate managed-rules phase, evaluated independently of your custom rules, so there's no ordering interaction to think about between the two.

Environment Variables

Vercel

VERCEL_TOKEN="your_vercel_token"
VERCEL_PROJECT_ID="prj_abc123"
VERCEL_TEAM_ID="team_xyz789"

Cloudflare

CLOUDFLARE_API_TOKEN="your_api_token"
CLOUDFLARE_ZONE_ID="zone_abc123"
CLOUDFLARE_ACCOUNT_ID="acc_xyz789"

Fastly

FASTLY_API_TOKEN="your_api_token"
FASTLY_WORKSPACE_ID="workspace_abc123"

Provider Selection

DOORMAN_PROVIDER="cloudflare"  # or "vercel" or "fastly"

Best Practices

  1. Use descriptive names for rules and IPs
  2. Add descriptions to explain rule purposes
  3. Group related conditions logically
  4. Use CIDR notation for IP ranges
  5. Order rules by frequency (most common first)
  6. Test rules in staging before production
  7. Start with log actions before blocking
  8. Keep backups of working configurations
  9. Avoid the operators/actions flagged 🚨 above for whichever provider you're targeting, until their tracking issues are fixed

Related Pages

Clone this wiki locally