@gfargo/doorman 2.1.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/LICENSE +21 -0
- package/README.md +506 -0
- package/bin/run +3 -0
- package/bin/run-deprecated +11 -0
- package/bin/run.ts +23 -0
- package/dist/bin/run.d.mts +2 -0
- package/dist/bin/run.d.ts +2 -0
- package/dist/bin/run.js +167 -0
- package/dist/bin/run.mjs +167 -0
- package/dist/src/index.d.mts +23 -0
- package/dist/src/index.d.ts +23 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.mjs +1 -0
- package/package.json +157 -0
- package/skills/doorman/SKILL.md +132 -0
- package/skills/doorman/rules-reference.md +478 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
# Rule Authoring Reference
|
|
2
|
+
|
|
3
|
+
Complete reference for creating and configuring Doorman firewall rules.
|
|
4
|
+
|
|
5
|
+
## Rule Structure
|
|
6
|
+
|
|
7
|
+
Every rule in the `rules` array has this shape:
|
|
8
|
+
|
|
9
|
+
```json
|
|
10
|
+
{
|
|
11
|
+
"id": "rule_descriptive_name",
|
|
12
|
+
"name": "Human-Readable Name",
|
|
13
|
+
"description": "What this rule does and why",
|
|
14
|
+
"active": true,
|
|
15
|
+
"conditionGroup": [
|
|
16
|
+
/* condition groups */
|
|
17
|
+
],
|
|
18
|
+
"action": {
|
|
19
|
+
/* action config */
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Fields
|
|
25
|
+
|
|
26
|
+
| Field | Type | Required | Description |
|
|
27
|
+
| ---------------- | ------- | -------- | ------------------------------------------------------------------------------------- |
|
|
28
|
+
| `id` | string | No | Unique identifier. Convention: `rule_` prefix, snake_case. Auto-generated if omitted. |
|
|
29
|
+
| `name` | string | Yes | Display name shown in tables and logs. |
|
|
30
|
+
| `description` | string | No | Explains the rule's purpose. Improves health score. |
|
|
31
|
+
| `active` | boolean | Yes | `true` to enforce, `false` to disable without deleting. |
|
|
32
|
+
| `conditionGroup` | array | Yes | Array of condition groups (see below). |
|
|
33
|
+
| `action` | object | Yes | What to do when conditions match (see below). |
|
|
34
|
+
|
|
35
|
+
## Condition Groups
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
"conditionGroup": [
|
|
39
|
+
{
|
|
40
|
+
"conditions": [ /* AND — all must match */ ]
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"conditions": [ /* OR — this group is an alternative */ ]
|
|
44
|
+
}
|
|
45
|
+
]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Logic**:
|
|
49
|
+
|
|
50
|
+
- Conditions **within** a group: AND (all must match)
|
|
51
|
+
- **Between** groups: OR (any group matching triggers the rule)
|
|
52
|
+
|
|
53
|
+
### Example: Block POST to /admin OR any request to /wp-admin
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
"conditionGroup": [
|
|
57
|
+
{
|
|
58
|
+
"conditions": [
|
|
59
|
+
{ "type": "path", "op": "pre", "value": "/admin" },
|
|
60
|
+
{ "type": "method", "op": "eq", "value": "POST" }
|
|
61
|
+
]
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"conditions": [
|
|
65
|
+
{ "type": "path", "op": "pre", "value": "/wp-admin" }
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Conditions
|
|
72
|
+
|
|
73
|
+
Each condition has:
|
|
74
|
+
|
|
75
|
+
```json
|
|
76
|
+
{
|
|
77
|
+
"type": "field_type",
|
|
78
|
+
"op": "operator",
|
|
79
|
+
"value": "match_value",
|
|
80
|
+
"key": "header_name",
|
|
81
|
+
"neg": false
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Condition Fields
|
|
86
|
+
|
|
87
|
+
| Field | Type | Required | Description |
|
|
88
|
+
| ------- | ------------------- | -------- | ------------------------------------------------------------------------------------ |
|
|
89
|
+
| `type` | string | Yes | What to match against (see types below). |
|
|
90
|
+
| `op` | string | Yes | How to compare (see operators below). |
|
|
91
|
+
| `value` | string/number/array | Yes | Value to match. Arrays for `inc` operator. |
|
|
92
|
+
| `key` | string | No | Required for `header`, `query`, `cookie` types. Specifies which header/param/cookie. |
|
|
93
|
+
| `neg` | boolean | No | `true` to negate the condition (NOT logic). Default: `false`. |
|
|
94
|
+
|
|
95
|
+
### Condition Types
|
|
96
|
+
|
|
97
|
+
| Type | Description | Example Value |
|
|
98
|
+
| -------------------- | -------------------------------- | ----------------------- |
|
|
99
|
+
| `path` | URL path | `"/api/users"` |
|
|
100
|
+
| `method` | HTTP method | `"POST"` |
|
|
101
|
+
| `host` | Hostname | `"example.com"` |
|
|
102
|
+
| `user_agent` | User-Agent header | `"Googlebot"` |
|
|
103
|
+
| `ip_address` | Client IP | `"192.168.1.1"` |
|
|
104
|
+
| `header` | HTTP header (requires `key`) | `"application/json"` |
|
|
105
|
+
| `query` | Query parameter (requires `key`) | `"true"` |
|
|
106
|
+
| `cookie` | Cookie value (requires `key`) | `"session_abc"` |
|
|
107
|
+
| `geo_country` | Country code (ISO 3166-1) | `"US"` or `["US","CA"]` |
|
|
108
|
+
| `geo_city` | City name | `"New York"` |
|
|
109
|
+
| `geo_continent` | Continent code | `"NA"` |
|
|
110
|
+
| `geo_country_region` | Region/state code | `"CA"` |
|
|
111
|
+
| `geo_as_number` | ASN number | `13335` |
|
|
112
|
+
| `scheme` | URL scheme | `"https"` |
|
|
113
|
+
| `protocol` | HTTP protocol version | `"HTTP/2"` |
|
|
114
|
+
|
|
115
|
+
**Vercel-only types** (not available on Cloudflare):
|
|
116
|
+
|
|
117
|
+
- `environment` — deployment environment (`"production"`, `"preview"`)
|
|
118
|
+
- `ja3_digest` — TLS fingerprint
|
|
119
|
+
- `ja4_digest` — TLS fingerprint v4
|
|
120
|
+
- `region` — Vercel edge region
|
|
121
|
+
- `rate_limit_api_id` — rate limit API identifier
|
|
122
|
+
|
|
123
|
+
### Operators
|
|
124
|
+
|
|
125
|
+
| Operator | Name | Description | Value Type |
|
|
126
|
+
| -------- | ---------- | --------------------------- | ---------------------- |
|
|
127
|
+
| `eq` | Equals | Exact match | string/number |
|
|
128
|
+
| `pre` | Prefix | Starts with | string |
|
|
129
|
+
| `suf` | Suffix | Ends with | string |
|
|
130
|
+
| `sub` | Substring | Contains | string |
|
|
131
|
+
| `inc` | Includes | Is any of (array match) | string[] |
|
|
132
|
+
| `re` | Regex | Regular expression match | string (regex pattern) |
|
|
133
|
+
| `ex` | Exists | Field/header exists | `true` |
|
|
134
|
+
| `nex` | Not Exists | Field/header does not exist | `true` |
|
|
135
|
+
|
|
136
|
+
### Using `key` for Header/Query/Cookie
|
|
137
|
+
|
|
138
|
+
```json
|
|
139
|
+
{ "type": "header", "op": "eq", "value": "application/json", "key": "Content-Type" }
|
|
140
|
+
{ "type": "query", "op": "eq", "value": "true", "key": "debug" }
|
|
141
|
+
{ "type": "cookie", "op": "ex", "value": true, "key": "session_id" }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Using `neg` for Negation
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{ "type": "geo_country", "op": "inc", "value": ["US", "CA", "GB"], "neg": true }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
This matches requests NOT from US, CA, or GB.
|
|
151
|
+
|
|
152
|
+
### Using `inc` with Arrays
|
|
153
|
+
|
|
154
|
+
```json
|
|
155
|
+
{ "type": "geo_country", "op": "inc", "value": ["CN", "RU", "KP", "IR"] }
|
|
156
|
+
{ "type": "method", "op": "inc", "value": ["PUT", "DELETE", "PATCH"] }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Actions
|
|
160
|
+
|
|
161
|
+
### Deny (Block)
|
|
162
|
+
|
|
163
|
+
```json
|
|
164
|
+
{
|
|
165
|
+
"action": {
|
|
166
|
+
"mitigate": {
|
|
167
|
+
"action": "deny"
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Deny with Duration
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"action": {
|
|
178
|
+
"mitigate": {
|
|
179
|
+
"action": "deny",
|
|
180
|
+
"actionDuration": "1h"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Duration formats: `"30s"`, `"5m"`, `"1h"`, `"1d"`, `"permanent"`
|
|
187
|
+
|
|
188
|
+
### Challenge (CAPTCHA)
|
|
189
|
+
|
|
190
|
+
```json
|
|
191
|
+
{
|
|
192
|
+
"action": {
|
|
193
|
+
"mitigate": {
|
|
194
|
+
"action": "challenge"
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Rate Limit
|
|
201
|
+
|
|
202
|
+
```json
|
|
203
|
+
{
|
|
204
|
+
"action": {
|
|
205
|
+
"mitigate": {
|
|
206
|
+
"action": "rate_limit",
|
|
207
|
+
"rateLimit": {
|
|
208
|
+
"requests": 100,
|
|
209
|
+
"window": "60s"
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Rate Limit Fields
|
|
217
|
+
|
|
218
|
+
| Field | Type | Required | Description |
|
|
219
|
+
| -------------------- | -------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
|
220
|
+
| `requests` | number | Yes | Max requests allowed in window. |
|
|
221
|
+
| `window` | string | Yes | Time window: `"10s"`, `"1m"`, `"5m"`, `"1h"` |
|
|
222
|
+
| `characteristics` | string[] | No | What to rate limit by. Default: `["ip.src"]`. Options: `"ip.src"`, `"http.request.uri.path"`, `"http.request.headers[\"user-agent\"]"` |
|
|
223
|
+
| `mitigationTimeout` | number | No | How long (seconds) to block after limit exceeded. Default: 3600. |
|
|
224
|
+
| `countingExpression` | string | No | Cloudflare-specific: expression for what counts toward the limit. |
|
|
225
|
+
|
|
226
|
+
### Redirect
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"action": {
|
|
231
|
+
"mitigate": {
|
|
232
|
+
"action": "redirect",
|
|
233
|
+
"redirect": {
|
|
234
|
+
"location": "https://example.com/blocked",
|
|
235
|
+
"permanent": false
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
| Field | Type | Description |
|
|
243
|
+
| ----------- | ------- | ----------------------------------------- |
|
|
244
|
+
| `location` | string | Redirect URL (absolute or relative path). |
|
|
245
|
+
| `permanent` | boolean | `true` for 301, `false` for 302. |
|
|
246
|
+
|
|
247
|
+
### Log Only
|
|
248
|
+
|
|
249
|
+
```json
|
|
250
|
+
{
|
|
251
|
+
"action": {
|
|
252
|
+
"mitigate": {
|
|
253
|
+
"action": "log"
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Bypass
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"action": {
|
|
264
|
+
"mitigate": {
|
|
265
|
+
"action": "bypass"
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
## IP Blocking Rules
|
|
272
|
+
|
|
273
|
+
IP rules go in the `ips` array, separate from `rules`:
|
|
274
|
+
|
|
275
|
+
```json
|
|
276
|
+
{
|
|
277
|
+
"ips": [
|
|
278
|
+
{
|
|
279
|
+
"id": "ip_malicious_actor",
|
|
280
|
+
"ip": "192.168.1.100/32",
|
|
281
|
+
"hostname": "attacker.example.com",
|
|
282
|
+
"action": "deny",
|
|
283
|
+
"notes": "Blocked 2024-01-15: repeated brute force attempts"
|
|
284
|
+
}
|
|
285
|
+
]
|
|
286
|
+
}
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### IP Rule Fields
|
|
290
|
+
|
|
291
|
+
| Field | Type | Required | Description |
|
|
292
|
+
| ---------- | ------ | -------- | --------------------------------------------------- |
|
|
293
|
+
| `id` | string | No | Unique identifier. |
|
|
294
|
+
| `ip` | string | Yes | IP address or CIDR range. Use `/32` for single IPs. |
|
|
295
|
+
| `hostname` | string | No | Associated hostname (documentation only). |
|
|
296
|
+
| `action` | string | Yes | `"deny"` (only supported value currently). |
|
|
297
|
+
| `notes` | string | No | Why this IP was blocked. |
|
|
298
|
+
|
|
299
|
+
### CIDR Notation
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
192.168.1.1/32 → single IP
|
|
303
|
+
192.168.1.0/24 → 256 IPs (192.168.1.0 - 192.168.1.255)
|
|
304
|
+
10.0.0.0/8 → class A range
|
|
305
|
+
0.0.0.0/0 → all IPv4 (use with caution!)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
## Common Patterns
|
|
309
|
+
|
|
310
|
+
### Block multiple countries
|
|
311
|
+
|
|
312
|
+
```json
|
|
313
|
+
{
|
|
314
|
+
"name": "OFAC Sanctions Compliance",
|
|
315
|
+
"conditionGroup": [
|
|
316
|
+
{
|
|
317
|
+
"conditions": [{ "type": "geo_country", "op": "inc", "value": ["CU", "IR", "KP", "SY", "RU"] }]
|
|
318
|
+
}
|
|
319
|
+
],
|
|
320
|
+
"action": { "mitigate": { "action": "deny" } },
|
|
321
|
+
"active": true
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Allow only specific countries (block everyone else)
|
|
326
|
+
|
|
327
|
+
```json
|
|
328
|
+
{
|
|
329
|
+
"name": "Allow Only US/CA/GB",
|
|
330
|
+
"conditionGroup": [
|
|
331
|
+
{
|
|
332
|
+
"conditions": [{ "type": "geo_country", "op": "inc", "value": ["US", "CA", "GB"], "neg": true }]
|
|
333
|
+
}
|
|
334
|
+
],
|
|
335
|
+
"action": { "mitigate": { "action": "deny" } },
|
|
336
|
+
"active": true
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
### Rate limit API with path + method
|
|
341
|
+
|
|
342
|
+
```json
|
|
343
|
+
{
|
|
344
|
+
"name": "Rate Limit POST to API",
|
|
345
|
+
"conditionGroup": [
|
|
346
|
+
{
|
|
347
|
+
"conditions": [
|
|
348
|
+
{ "type": "path", "op": "pre", "value": "/api/" },
|
|
349
|
+
{ "type": "method", "op": "eq", "value": "POST" }
|
|
350
|
+
]
|
|
351
|
+
}
|
|
352
|
+
],
|
|
353
|
+
"action": {
|
|
354
|
+
"mitigate": {
|
|
355
|
+
"action": "rate_limit",
|
|
356
|
+
"rateLimit": { "requests": 50, "window": "1m" },
|
|
357
|
+
"actionDuration": "5m"
|
|
358
|
+
}
|
|
359
|
+
},
|
|
360
|
+
"active": true
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Block by header (missing auth)
|
|
365
|
+
|
|
366
|
+
```json
|
|
367
|
+
{
|
|
368
|
+
"name": "Block Missing API Key",
|
|
369
|
+
"conditionGroup": [
|
|
370
|
+
{
|
|
371
|
+
"conditions": [
|
|
372
|
+
{ "type": "path", "op": "pre", "value": "/api/" },
|
|
373
|
+
{ "type": "header", "op": "nex", "value": true, "key": "X-API-Key" }
|
|
374
|
+
]
|
|
375
|
+
}
|
|
376
|
+
],
|
|
377
|
+
"action": { "mitigate": { "action": "deny" } },
|
|
378
|
+
"active": true
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
### Challenge suspicious user agents
|
|
383
|
+
|
|
384
|
+
```json
|
|
385
|
+
{
|
|
386
|
+
"name": "Challenge Suspicious Bots",
|
|
387
|
+
"conditionGroup": [
|
|
388
|
+
{ "conditions": [{ "type": "user_agent", "op": "sub", "value": "curl" }] },
|
|
389
|
+
{ "conditions": [{ "type": "user_agent", "op": "sub", "value": "wget" }] },
|
|
390
|
+
{ "conditions": [{ "type": "user_agent", "op": "sub", "value": "python-requests" }] }
|
|
391
|
+
],
|
|
392
|
+
"action": { "mitigate": { "action": "challenge" } },
|
|
393
|
+
"active": true
|
|
394
|
+
}
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
### Block WordPress attack paths (regex)
|
|
398
|
+
|
|
399
|
+
```json
|
|
400
|
+
{
|
|
401
|
+
"name": "Block WordPress Paths",
|
|
402
|
+
"conditionGroup": [
|
|
403
|
+
{
|
|
404
|
+
"conditions": [{ "type": "path", "op": "re", "value": "/(wp-admin|wp-login\\.php|xmlrpc\\.php|wp-content)" }]
|
|
405
|
+
}
|
|
406
|
+
],
|
|
407
|
+
"action": { "mitigate": { "action": "deny" } },
|
|
408
|
+
"active": true
|
|
409
|
+
}
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Redirect old paths
|
|
413
|
+
|
|
414
|
+
```json
|
|
415
|
+
{
|
|
416
|
+
"name": "Redirect Legacy API",
|
|
417
|
+
"conditionGroup": [
|
|
418
|
+
{
|
|
419
|
+
"conditions": [{ "type": "path", "op": "pre", "value": "/v1/api/" }]
|
|
420
|
+
}
|
|
421
|
+
],
|
|
422
|
+
"action": {
|
|
423
|
+
"mitigate": {
|
|
424
|
+
"action": "redirect",
|
|
425
|
+
"redirect": { "location": "/v2/api/", "permanent": true }
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
"active": true
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
## Adding Rules to Config
|
|
433
|
+
|
|
434
|
+
To add a rule, append it to the `rules` array in `.doorman.json`:
|
|
435
|
+
|
|
436
|
+
```json
|
|
437
|
+
{
|
|
438
|
+
"rules": [
|
|
439
|
+
/* existing rules... */
|
|
440
|
+
{
|
|
441
|
+
"name": "New Rule",
|
|
442
|
+
"active": true,
|
|
443
|
+
"conditionGroup": [{ "conditions": [{ "type": "path", "op": "eq", "value": "/blocked" }] }],
|
|
444
|
+
"action": { "mitigate": { "action": "deny" } }
|
|
445
|
+
}
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Then deploy:
|
|
451
|
+
|
|
452
|
+
```bash
|
|
453
|
+
doorman validate # Check syntax
|
|
454
|
+
doorman diff # Preview changes
|
|
455
|
+
doorman sync # Deploy to provider
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
## Provider Differences
|
|
459
|
+
|
|
460
|
+
| Feature | Vercel | Cloudflare | Notes |
|
|
461
|
+
| ------------------------- | ---------------- | ---------------------- | ------------------------------------- |
|
|
462
|
+
| `re` operator | ✅ All plans | ⚠️ Enterprise only | Use `sub`/`pre`/`suf` as alternatives |
|
|
463
|
+
| `environment` type | ✅ | ❌ | Vercel-specific |
|
|
464
|
+
| `ja3_digest`/`ja4_digest` | ✅ | ❌ | Vercel-specific |
|
|
465
|
+
| IP Lists (bulk) | Individual rules | ✅ Lists API | Cloudflare needs `accountId` |
|
|
466
|
+
| Max rules | ~100 | 5-125 (plan dependent) | Free: 5, Pro: 20, Business: 100 |
|
|
467
|
+
|
|
468
|
+
## Health Score Tips
|
|
469
|
+
|
|
470
|
+
To maximize your configuration health score:
|
|
471
|
+
|
|
472
|
+
- Add `description` to every rule
|
|
473
|
+
- Use `id` fields with `rule_` prefix and snake_case
|
|
474
|
+
- Avoid regex when simpler operators work
|
|
475
|
+
- Remove disabled rules you no longer need
|
|
476
|
+
- Include rate limiting for API endpoints
|
|
477
|
+
- Include bot protection rules
|
|
478
|
+
- Use IP blocking for known threats
|