CfBouncer
Automatically syncs a Cloudflare WAF block rule from your Phoenix router. Any request to a path not defined in your routes, sockets, or static files gets blocked.
Keeps your WAF in sync with your code - run it in your deploy script and forget about it.
How it works
- Reads all route prefixes from your Phoenix router
- Reads WebSocket paths from your endpoint
- Reads static file paths from your web module
- Builds a Cloudflare WAF expression that blocks everything else
- Pushes the rule to Cloudflare (only if it changed)
Installation
Add cf_bouncer to your dependencies:
def deps do
[
{:cf_bouncer, "~> 0.2"}
]
endConfiguration
# config/config.exs
config :cf_bouncer,
router: MyAppWeb.Router,
endpoint: MyAppWeb.Endpoint,
static_module: MyAppWeb,
rule_description: "[CfBouncer] Block non-allowlisted paths"
# config/runtime.exs
config :cf_bouncer,
zone_id: System.get_env("CLOUDFLARE_ZONE_ID"),
api_token: System.get_env("CLOUDFLARE_API_TOKEN")Options
| Key | Required | Description |
|---|---|---|
router | yes |
Your Phoenix router module (e.g. MyAppWeb.Router) |
endpoint | yes |
Your Phoenix endpoint module (e.g. MyAppWeb.Endpoint) |
static_module | yes |
Module with static_paths/0 (e.g. MyAppWeb) |
rule_description | yes | Description used to identify the rule in Cloudflare |
zone_id | yes | Cloudflare Zone ID (see below) |
api_token | yes | Cloudflare API token with Zone WAF and Firewall Services edit permissions |
extra_paths | no |
Additional path prefixes to allow (e.g. ["/cf-fonts/", "/webhooks/"]) |
Cloudflare Zone ID
Found in the Cloudflare dashboard: select your domain, then look in the right sidebar on the Overview page under API > Zone ID.
Cloudflare API token
Create at https://dash.cloudflare.com/profile/api-tokens. Use the "Edit zone WAF" template, or create a custom token with these permissions scoped to your zone:
- Zone > Zone WAF > Edit
- Zone > Firewall Services > Edit
Usage
Mix task
Preview the generated WAF expression:
mix cf_bouncer.sync --dry-runPush to Cloudflare:
mix cf_bouncer.syncForce push even if unchanged:
mix cf_bouncer.sync --force
The rule is only updated if the expression has changed. Use --force to push regardless.
In a deploy script
echo "Updating Cloudflare WAF..."
mix cf_bouncer.sync
echo "Syncing files..."
# ... rest of deployProgrammatic usage
You can also call the library functions directly by passing config as a keyword list:
opts = [
router: MyAppWeb.Router,
endpoint: MyAppWeb.Endpoint,
static_module: MyAppWeb,
extra_paths: ["/cf-fonts/"],
zone_id: "your-zone-id",
api_token: "your-api-token",
rule_description: "[CfBouncer] Block non-allowlisted paths"
]
# Just build the expression
CfBouncer.build_expression(opts)
# Build and sync to Cloudflare
CfBouncer.sync(opts)Generated expression
The generated expression looks like:
not (
http.request.uri.path eq "/"
or starts_with(http.request.uri.path, "/auth")
or starts_with(http.request.uri.path, "/users")
or starts_with(http.request.uri.path, "/live")
or starts_with(http.request.uri.path, "/assets/")
or starts_with(http.request.uri.path, "/images/")
or starts_with(http.request.uri.path, "/robots.txt")
or starts_with(http.request.uri.path, "/cf-fonts/")
)
Static paths containing a . (like robots.txt) are treated as files. Paths without a . (like assets) are treated as directories and get a trailing /.