@gfargo/doorman 3.14.2 → 3.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
package/skills/doorman/SKILL.md
CHANGED
|
@@ -76,7 +76,7 @@ FASTLY_WORKSPACE_ID=workspace_xxx
|
|
|
76
76
|
}
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
For Cloudflare or Fastly, add `provider` and `providers` fields instead of `projectId`/`teamId` — and note this switches the
|
|
79
|
+
For Cloudflare or Fastly, add `provider` and `providers` fields instead of `projectId`/`teamId` — and note this switches the _rule_ shape too, not just the top-level fields. See [Rule Shape](#rule-shape-minimal) below.
|
|
80
80
|
|
|
81
81
|
## Core Workflow
|
|
82
82
|
|
|
@@ -137,14 +137,14 @@ Two different rule shapes, picked by whether the config has `provider`/`provider
|
|
|
137
137
|
|
|
138
138
|
Load the relevant reference file for detailed documentation:
|
|
139
139
|
|
|
140
|
-
| Task
|
|
141
|
-
|
|
|
142
|
-
| Writing rules — full field docs, operators, actions, IP blocking, patterns
|
|
140
|
+
| Task | Reference |
|
|
141
|
+
| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
142
|
+
| Writing rules — full field docs, operators, actions, IP blocking, patterns | [references/rules.md](references/rules.md) |
|
|
143
143
|
| Cloudflare-specific setup, Lists API, managed rule groups, expression translation, limitations | [references/cloudflare.md](references/cloudflare.md) |
|
|
144
|
-
| Fastly-specific setup, condition/action mapping, rate-limit signal requirement, limitations
|
|
145
|
-
| GCP Cloud Armor setup, CEL translation, priority model, manual e2e verification runbook
|
|
146
|
-
| Available templates and what they protect against
|
|
147
|
-
| CI/CD integration, automation, export formats, validation in pipelines
|
|
144
|
+
| Fastly-specific setup, condition/action mapping, rate-limit signal requirement, limitations | [references/fastly.md](references/fastly.md) |
|
|
145
|
+
| GCP Cloud Armor setup, CEL translation, priority model, manual e2e verification runbook | [references/gcp.md](references/gcp.md) |
|
|
146
|
+
| Available templates and what they protect against | [references/templates.md](references/templates.md) |
|
|
147
|
+
| CI/CD integration, automation, export formats, validation in pipelines | [references/cicd.md](references/cicd.md) |
|
|
148
148
|
|
|
149
149
|
## Principles
|
|
150
150
|
|
|
@@ -60,23 +60,23 @@ Doorman translates its unified rule format (`conditions`/`enabled`/`action: {typ
|
|
|
60
60
|
|
|
61
61
|
Cloudflare supports all 15 unified condition fields:
|
|
62
62
|
|
|
63
|
-
| Doorman Field | Cloudflare Field | Notes
|
|
64
|
-
|
|
|
65
|
-
| `ip` | `ip.src` |
|
|
66
|
-
| `country` | `ip.geoip.country` |
|
|
67
|
-
| `region` | `ip.geoip.subdivision_1` |
|
|
68
|
-
| `city` | `ip.geoip.city`
|
|
69
|
-
| `asn` | `ip.geoip.asnum`
|
|
70
|
-
| `path` | `http.request.uri.path` |
|
|
71
|
-
| `host` | `http.host` |
|
|
72
|
-
| `method` | `http.request.method` |
|
|
73
|
-
| `header` | `http.request.headers["key"]` | Requires `key` (the header name)
|
|
63
|
+
| Doorman Field | Cloudflare Field | Notes |
|
|
64
|
+
| ------------- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
65
|
+
| `ip` | `ip.src` | |
|
|
66
|
+
| `country` | `ip.geoip.country` | |
|
|
67
|
+
| `region` | `ip.geoip.subdivision_1` | |
|
|
68
|
+
| `city` | `ip.geoip.city` | |
|
|
69
|
+
| `asn` | `ip.geoip.asnum` | |
|
|
70
|
+
| `path` | `http.request.uri.path` | |
|
|
71
|
+
| `host` | `http.host` | |
|
|
72
|
+
| `method` | `http.request.method` | |
|
|
73
|
+
| `header` | `http.request.headers["key"]` | Requires `key` (the header name) |
|
|
74
74
|
| `query` | `http.request.uri.query` | ⚠️ `key` is currently ignored — matches the whole query string, not one parameter ([doorman#263](https://github.com/gfargo/doorman/issues/263)) |
|
|
75
|
-
| `cookie` | `http.cookie["key"]` | Requires `key` (the cookie name)
|
|
76
|
-
| `user_agent` | `http.user_agent` |
|
|
77
|
-
| `referer` | `http.referer` |
|
|
78
|
-
| `scheme` | `ssl` (boolean)
|
|
79
|
-
| `port` | `cf.edge.server_port` |
|
|
75
|
+
| `cookie` | `http.cookie["key"]` | Requires `key` (the cookie name) |
|
|
76
|
+
| `user_agent` | `http.user_agent` | |
|
|
77
|
+
| `referer` | `http.referer` | |
|
|
78
|
+
| `scheme` | `ssl` (boolean) | |
|
|
79
|
+
| `port` | `cf.edge.server_port` | |
|
|
80
80
|
|
|
81
81
|
### Operator Mapping
|
|
82
82
|
|
|
@@ -88,16 +88,16 @@ Cloudflare supports all 15 unified operators exactly, no approximation — `not_
|
|
|
88
88
|
|
|
89
89
|
### Action Mapping
|
|
90
90
|
|
|
91
|
-
| Doorman Action | Cloudflare Action | Notes
|
|
92
|
-
|
|
|
93
|
-
| `log` | `log`
|
|
94
|
-
| `deny` | `block`
|
|
95
|
-
| `challenge` | `managed_challenge`
|
|
96
|
-
| `bypass` | `skip`
|
|
97
|
-
| `allow` | `allow`
|
|
98
|
-
| `block` | `block`
|
|
99
|
-
| `rate_limit` | `block` + `ratelimit` config
|
|
100
|
-
| `redirect` | `redirect` + `from_value` params
|
|
91
|
+
| Doorman Action | Cloudflare Action | Notes |
|
|
92
|
+
| -------------- | -------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
93
|
+
| `log` | `log` | |
|
|
94
|
+
| `deny` | `block` | |
|
|
95
|
+
| `challenge` | `managed_challenge` | Cloudflare's `challenge`/`managed_challenge`/`js_challenge` all fold back to unified `challenge` on `download` — a one-way narrowing, not a failure |
|
|
96
|
+
| `bypass` | `skip` | |
|
|
97
|
+
| `allow` | `allow` | |
|
|
98
|
+
| `block` | `block` | Same target as `deny` |
|
|
99
|
+
| `rate_limit` | `block` + `ratelimit` config | ⚠️ If `action.rateLimit` is omitted, this silently becomes a plain unconditional `block` rule with no rate-limit effect — no error raised locally |
|
|
100
|
+
| `redirect` | `redirect` + `from_value` params | ⚠️ If `action.redirect` is omitted, this silently becomes a rule with no redirect target — no error raised locally |
|
|
101
101
|
|
|
102
102
|
## Lists API (Bulk IP Management)
|
|
103
103
|
|
|
@@ -112,16 +112,16 @@ Without `CLOUDFLARE_ACCOUNT_ID`, IP blocking falls back to individual WAF rules
|
|
|
112
112
|
|
|
113
113
|
## Limitations & Differences
|
|
114
114
|
|
|
115
|
-
`environment`, `ja3_digest`, `ja4_digest` are **legacy-format-only concepts** — they exist on Vercel's native rule type but have no unified-format equivalent at all, so they're not reachable through a `provider`/`providers`-tagged config for
|
|
115
|
+
`environment`, `ja3_digest`, `ja4_digest` are **legacy-format-only concepts** — they exist on Vercel's native rule type but have no unified-format equivalent at all, so they're not reachable through a `provider`/`providers`-tagged config for _any_ provider, not just Cloudflare. Every other unified field maps to something on Cloudflare — see Field Mapping above.
|
|
116
116
|
|
|
117
|
-
| Feature
|
|
118
|
-
|
|
|
119
|
-
| `matches` (regex)
|
|
120
|
-
| IP Lists (bulk)
|
|
121
|
-
| Max custom rules
|
|
122
|
-
| Rate limit
|
|
123
|
-
| Rule order
|
|
124
|
-
| Managed rule groups
|
|
117
|
+
| Feature | Cloudflare | Notes |
|
|
118
|
+
| ------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
119
|
+
| `matches` (regex) | Enterprise-plan-restricted on some plans | Doorman emits it regardless; Cloudflare's API may reject it — see Operator Mapping above |
|
|
120
|
+
| IP Lists (bulk) | Lists API | Needs `accountId`; without it, falls back to individual `ip.src` rules |
|
|
121
|
+
| Max custom rules | 5-125 (plan dependent) | Free: 5, Pro: 20, Business: 100, Enterprise: 125+ |
|
|
122
|
+
| Rate limit | Separate phase | `ratelimit` config attached to a `block` action, not a distinct rule type |
|
|
123
|
+
| Rule order | Sequential (first match wins) | `priority` is fully honoured — unlike Vercel, which can't reposition rules that already exist remotely |
|
|
124
|
+
| Managed rule groups | Supported via `managedRules` | See [Managed Rule Groups](#managed-rule-groups) below — Cloudflare-only among doorman's providers today |
|
|
125
125
|
|
|
126
126
|
## Translation Warnings
|
|
127
127
|
|
|
@@ -174,14 +174,14 @@ Cloudflare is the only provider doorman can deploy vendor-managed rulesets (Clou
|
|
|
174
174
|
}
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
| Property
|
|
178
|
-
|
|
|
179
|
-
| `id`
|
|
180
|
-
| `ruleset`
|
|
181
|
-
| `name`
|
|
182
|
-
| `enabled`
|
|
183
|
-
| `action`
|
|
184
|
-
| `overrides` | array
|
|
177
|
+
| Property | Type | Required | Description |
|
|
178
|
+
| ----------- | ------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
179
|
+
| `id` | string | No | Doorman's diff/sync identifier for this deployment. Omit for a new declaration — doorman assigns one on first sync. |
|
|
180
|
+
| `ruleset` | string | Yes | The vendor ruleset id to deploy (e.g. Cloudflare Managed Ruleset's well-known id shown above) |
|
|
181
|
+
| `name` | string | No | Human label |
|
|
182
|
+
| `enabled` | boolean | Yes | Whether this deployment is active |
|
|
183
|
+
| `action` | string | No | Ruleset-wide override — downgrade every rule in the group to this action. One of `log`, `deny`, `challenge`, `allow` |
|
|
184
|
+
| `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 |
|
|
185
185
|
|
|
186
186
|
Managed rule groups deploy in Cloudflare's separate managed-rules phase, evaluated independently of custom `rules` — no ordering interaction to think about between the two. `getChanges`/`syncRules` diff `managedRules` the same way as `rules`/`ips` — `doorman diff`/`doorman sync` cover it with no extra flags.
|
|
187
187
|
|
|
@@ -115,12 +115,12 @@ Unlike classic Fastly VCL services, Next-Gen WAF rule and list writes take effec
|
|
|
115
115
|
|
|
116
116
|
## Limitations & Differences
|
|
117
117
|
|
|
118
|
-
| Feature | Vercel | Fastly | Notes
|
|
119
|
-
| -------------------------- | ----------------------------- | ---------------------- |
|
|
120
|
-
| Geo targeting | Country/city/continent/region | Country only | No sub-country condition field
|
|
121
|
-
| Rule ordering | Best-effort (insertion order) | None | Next-Gen WAF rules are evaluated independently — `priority` has no effect
|
|
118
|
+
| Feature | Vercel | Fastly | Notes |
|
|
119
|
+
| -------------------------- | ----------------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
120
|
+
| Geo targeting | Country/city/continent/region | Country only | No sub-country condition field |
|
|
121
|
+
| Rule ordering | Best-effort (insertion order) | None | Next-Gen WAF rules are evaluated independently — `priority` has no effect |
|
|
122
122
|
| Managed/vendor rule groups | CRS (enterprise) | Templated signal rules | Doorman-managed on Cloudflare only ([cloudflare.md](cloudflare.md)); not yet configurable through Doorman for Vercel or Fastly |
|
|
123
|
-
| Signal/exclusion rules | — | `type: signal` | Not managed by Doorman — see below
|
|
123
|
+
| Signal/exclusion rules | — | `type: signal` | Not managed by Doorman — see below |
|
|
124
124
|
|
|
125
125
|
Fastly rules of type `signal` or `templated_signal` (which tag or exclude WAF signals rather than allow/block/redirect a request) are skipped entirely by `doorman download`/`fetchConfig` rather than forced into a lossy `UnifiedRule` — they simply don't appear in the local config.
|
|
126
126
|
|
|
@@ -23,7 +23,7 @@ GCP_POLICY_NAME=your-policy-name # Required — Cloud Armor sec
|
|
|
23
23
|
2. **Authenticate** — two options:
|
|
24
24
|
- **Application Default Credentials** (recommended for local/dev use): `gcloud auth application-default login`. Uses your own Google account, no key file to create, store, or later revoke. Leave `serviceAccountKeyPath`/`GOOGLE_APPLICATION_CREDENTIALS` unset — `GoogleAuth` falls through to ADC automatically.
|
|
25
25
|
- **Service account key** (for CI/production): create a service account with `roles/compute.securityAdmin` (the narrowest predefined role covering `securityPolicies.get/insert/patch/addRule/patchRule/removeRule/list`), generate a JSON key, and point `GOOGLE_APPLICATION_CREDENTIALS`/`serviceAccountKeyPath` at it.
|
|
26
|
-
3. **Create the security policy itself** — Doorman manages rules
|
|
26
|
+
3. **Create the security policy itself** — Doorman manages rules _within_ an existing policy, it does not create the policy resource:
|
|
27
27
|
```bash
|
|
28
28
|
gcloud compute security-policies create YOUR_POLICY_NAME --description="managed by doorman"
|
|
29
29
|
```
|
|
@@ -67,37 +67,37 @@ Cloud Armor rules match on CEL (Common Expression Language) — an expression-st
|
|
|
67
67
|
|
|
68
68
|
### Field Mapping
|
|
69
69
|
|
|
70
|
-
| Doorman Field | Cloud Armor CEL
|
|
71
|
-
|
|
|
72
|
-
| `ip`
|
|
73
|
-
| `path`
|
|
74
|
-
| `method`
|
|
75
|
-
| `country`
|
|
76
|
-
| `asn`
|
|
77
|
-
| `host`
|
|
78
|
-
| `user_agent`
|
|
79
|
-
| `referer`
|
|
80
|
-
| `header`
|
|
81
|
-
| `query`
|
|
82
|
-
| `cookie`
|
|
70
|
+
| Doorman Field | Cloud Armor CEL | Notes |
|
|
71
|
+
| ------------- | ----------------------------------------------------- | ------------------------------------------------------------------------------ |
|
|
72
|
+
| `ip` | `origin.ip == '...'` or `inIpRange(origin.ip, '...')` | Bare address uses `==`; a CIDR value (contains `/`) uses `inIpRange()` |
|
|
73
|
+
| `path` | `request.path` | |
|
|
74
|
+
| `method` | `request.method` | |
|
|
75
|
+
| `country` | `origin.region_code` | Country-level only |
|
|
76
|
+
| `asn` | `origin.asn` | Numeric comparison |
|
|
77
|
+
| `host` | `request.headers['host']` | Guarded with `has(...)` — indexing an absent header key is a CEL runtime error |
|
|
78
|
+
| `user_agent` | `request.headers['user-agent']` | Guarded with `has(...)` |
|
|
79
|
+
| `referer` | `request.headers['referer']` | Guarded with `has(...)` |
|
|
80
|
+
| `header` | `request.headers['<key>']` | Requires `key` (the header name); guarded with `has(...)` |
|
|
81
|
+
| `query` | `request.query` | Raw, undecoded query string — no parsed per-parameter map |
|
|
82
|
+
| `cookie` | `request.headers['cookie']` | No parsed cookie map — see Cookie Matching below |
|
|
83
83
|
|
|
84
84
|
Fields with **no CEL equivalent at all** (`region`, `city`, `port`, `scheme`) fail loudly with a clear error rather than being silently dropped or approximated — Cloud Armor has no continent/region/city-level geo attribute (only country-level `origin.region_code`), and no per-request port attribute (Cloud Armor operates at L7 behind a load balancer). A rule using one of these doesn't sync at all; there's no partial/lossy version of it to fall back to.
|
|
85
85
|
|
|
86
86
|
### Cookie Matching
|
|
87
87
|
|
|
88
|
-
Cloud Armor has no parsed per-cookie map, only the raw `Cookie` header string. Without a `key`, a cookie condition checks the whole header.
|
|
88
|
+
Cloud Armor has no parsed per-cookie map, only the raw `Cookie` header string. Without a `key`, a cookie condition checks the whole header. _With_ a `key` (the common case — "is this specific cookie set to this value") Doorman composes a `key=value` substring and searches for it in the raw header — the only thing CEL can actually check. Only `eq`/`ne`/`contains`/`not_contains` have an unambiguous meaning against that synthesized substring; any other operator on a keyed cookie condition throws rather than emitting a check that looks precise but isn't.
|
|
89
89
|
|
|
90
90
|
### Action Mapping
|
|
91
91
|
|
|
92
|
-
| Doorman Action
|
|
93
|
-
| --------------- |
|
|
94
|
-
| `deny`, `block` | `deny(403)`
|
|
95
|
-
| `allow` | `allow`
|
|
96
|
-
| `bypass` | `allow`
|
|
97
|
-
| `rate_limit` | `throttle`
|
|
98
|
-
| `redirect` | `redirect`
|
|
99
|
-
| `log` | `allow` + `preview: true`
|
|
100
|
-
| `challenge` | `deny(403)` (with a warning) | No standalone challenge action for ordinary custom rules
|
|
92
|
+
| Doorman Action | Cloud Armor Action | Notes |
|
|
93
|
+
| --------------- | ---------------------------- | ------------------------------------------------------------------- |
|
|
94
|
+
| `deny`, `block` | `deny(403)` | |
|
|
95
|
+
| `allow` | `allow` | |
|
|
96
|
+
| `bypass` | `allow` | |
|
|
97
|
+
| `rate_limit` | `throttle` | Requires `rateLimitOptions` — see below |
|
|
98
|
+
| `redirect` | `redirect` | Requires `redirectOptions` (target URL) |
|
|
99
|
+
| `log` | `allow` + `preview: true` | No dedicated log-only action — evaluates and logs without enforcing |
|
|
100
|
+
| `challenge` | `deny(403)` (with a warning) | No standalone challenge action for ordinary custom rules |
|
|
101
101
|
|
|
102
102
|
## The Priority Model
|
|
103
103
|
|
|
@@ -109,13 +109,13 @@ Cloud Armor's real structural difference from every other provider: there is no
|
|
|
109
109
|
|
|
110
110
|
## No Dedicated IP-Blocking Resource
|
|
111
111
|
|
|
112
|
-
Unlike Vercel/Cloudflare/Fastly, Cloud Armor has no separate IP-list resource — an `ips[]` entry is just an ordinary rule under the hood, with a single `ip == X` (or `inIpRange(...)` for CIDR) CEL condition and nothing else layered on. Doorman classifies a fetched rule as an IP entry only when it has
|
|
112
|
+
Unlike Vercel/Cloudflare/Fastly, Cloud Armor has no separate IP-list resource — an `ips[]` entry is just an ordinary rule under the hood, with a single `ip == X` (or `inIpRange(...)` for CIDR) CEL condition and nothing else layered on. Doorman classifies a fetched rule as an IP entry only when it has _exactly_ that shape (one condition, `allow`/`deny(403)` action, nothing else) — a hand-authored `rules[]` entry that happens to match on `ip` alone is left as a rule, not silently reclassified into `ips[]`.
|
|
113
113
|
|
|
114
114
|
## The Mandatory Default Rule
|
|
115
115
|
|
|
116
116
|
Every real Cloud Armor security policy carries one rule Doorman never manages: a server-injected default catch-all at the maximum priority (`2147483647`), using a different match shape (`versionedExpr`/`config.srcIpRanges`, not CEL) — it cannot be removed, only its action changed. Doorman skips it entirely on fetch; it never appears in your local config and is never a candidate for deletion. If you need to change its action (rarely necessary — it only fires when nothing else matches), use `gcloud compute security-policies describe`/`update` directly.
|
|
117
117
|
|
|
118
|
-
Any
|
|
118
|
+
Any _other_ rule using that same basic (non-CEL) match shape — e.g. hand-created via `gcloud ... --src-ip-ranges` or the Console — gets an explicit warning on fetch rather than a crash or a silent drop: Doorman can't represent it, and leaves it unmanaged.
|
|
119
119
|
|
|
120
120
|
## Rate Limiting
|
|
121
121
|
|
|
@@ -127,15 +127,15 @@ Every mutating call (`addRule`/`patchRule`/`removeRule`) returns a long-running
|
|
|
127
127
|
|
|
128
128
|
## Limitations & Differences
|
|
129
129
|
|
|
130
|
-
| Feature | Vercel | GCP Cloud Armor
|
|
131
|
-
|
|
|
132
|
-
| Rule ordering
|
|
133
|
-
| IP blocking
|
|
134
|
-
| Geo targeting
|
|
135
|
-
| Port / scheme conditions
|
|
136
|
-
| Managed/vendor rule groups
|
|
137
|
-
| Standalone challenge action
|
|
138
|
-
| Change a rule's priority
|
|
130
|
+
| Feature | Vercel | GCP Cloud Armor | Notes |
|
|
131
|
+
| --------------------------- | ----------------------------- | --------------------------------------------- | -------------------------------------------------------------------------- |
|
|
132
|
+
| Rule ordering | Best-effort (insertion order) | Explicit required `priority` | Lower priority number evaluates first; collisions rejected outright |
|
|
133
|
+
| IP blocking | Dedicated resource | Just a specially-shaped `rules[]` entry | No separate list/resource — see above |
|
|
134
|
+
| Geo targeting | Country/city/continent/region | Country only (`origin.region_code`) | `region`/`city` fields throw rather than silently drop |
|
|
135
|
+
| Port / scheme conditions | Yes | No CEL equivalent | Throw rather than silently drop |
|
|
136
|
+
| Managed/vendor rule groups | — | Preconfigured WAF rules exist on the real API | Not yet configurable through Doorman for any provider (tracked separately) |
|
|
137
|
+
| Standalone challenge action | Yes | No | Falls back to `deny(403)` with a warning |
|
|
138
|
+
| Change a rule's priority | N/A (no priority concept) | Remove + re-add under the new priority | No in-place relocation on the real API |
|
|
139
139
|
|
|
140
140
|
## Manual End-to-End Verification
|
|
141
141
|
|