Caddy CEL Matcher: How to Match Requests With Missing Query Parameter?
1. The problem I’m having:
I’m trying to configure Caddy to reverse proxy for an app at edit.hacking.xxxxxx.net
.
I want to allow all requests that do not have a folder
query parameter, and only allow requests with folder=/doc
or folder=/doc/*
.
All other requests with a folder
query param should be denied.
Additionally, the app uses WebSockets, so WebSocket upgrade requests must always be proxied.
The issue:
I cannot find a reliable way to match requests that do not have a folder
query parameter using the CEL expression matcher.
I’ve tried several expressions (see below), but either get errors like no such overload
or they do not match as expected.
The Caddy docs/examples only show how to match a query parameter with a value, not how to match when it is missing.
Example curl:
curl -v 'https://edit.hacking.xxxxxx.net/'
curl -v 'https://edit.hacking.xxxxxx.net/?folder=/doc'
curl -v 'https://edit.hacking.xxxxxx.net/?folder=/other'
/
and/?folder=/doc
should be allowed./?folder=/other
should be denied.
2. Error messages and/or full log output:
{"level":"error","logger":"http.matchers.expression","msg":"evaluating expression","error":"no such overload"}
ERROR: <input>:1:25: undeclared reference to 'nil' (in container '')
ERROR: <input>:1:19: undeclared reference to 'has' (in container '')
3. Caddy version:
v2.9.0 h1:rteY8N18LsQn+2KVk6R10Vg/AlNsID1N/Ek9JLjm2yE=
4. How I installed and ran Caddy:
a. System environment:
- Arch Linux
- systemd
- Caddy installed from official release
b. Command:
/servers/caddy/caddy run --config /servers/caddy/.config/caddy/Caddyfile
c. Service/unit/compose file:
systemd service file and override
# caddy.service
#
# For using Caddy with a config file.
#
# Make sure the ExecStart and ExecReload commands are correct
# for your installation.
#
# See https://caddyserver.com/docs/install for instructions.
#
# WARNING: This service does not use the --resume flag, so if you
# use the API to make changes, they will be overwritten by the
# Caddyfile next time the service is restarted. If you intend to
# use Caddy's API to configure it, add the --resume flag to the
# `caddy run` command or use the caddy-api.service file instead.
[Unit]
Description=Caddy web server
Documentation=https://caddyserver.com/docs/
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=14400
StartLimitBurst=10
[Service]
Type=notify
User=caddy
Group=caddy
Environment=XDG_DATA_HOME=/var/lib
Environment=XDG_CONFIG_HOME=/etc
ExecStartPre=/usr/bin/caddy validate --config /etc/caddy/Caddyfile
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
ExecStopPost=/usr/bin/rm -f /run/caddy/admin.socket
# Do not allow the process to be restarted in a tight loop. If the
# process fails to start, something critical needs to be fixed.
Restart=on-abnormal
# Use graceful shutdown with a reasonable timeout
TimeoutStopSec=5s
LimitNOFILE=1048576
LimitNPROC=512
# Hardening options
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
DevicePolicy=closed
LockPersonality=true
MemoryAccounting=true
MemoryDenyWriteExecute=true
NoNewPrivileges=true
PrivateDevices=true
PrivateTmp=true
ProcSubset=pid
ProtectClock=true
ProtectControlGroups=true
ProtectHome=true
ProtectHostname=true
ProtectKernelLogs=true
ProtectKernelModules=true
ProtectKernelTunables=true
ProtectProc=invisible
ProtectSystem=strict
RemoveIPC=true
ReadWritePaths=/var/lib/caddy /var/log/caddy /run/caddy
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
[Install]
WantedBy=multi-user.target
# /etc/systemd/system/caddy.service.d/01--override.conf
[Service]
Environment=
Type=simple
User=xxxxxx
Group=xxxxxx
WorkingDirectory=/servers/caddy
ExecStartPre=
ExecStart=
ExecStart=/servers/caddy/caddy run
ExecReload=
ExecStopPost=
ReadWritePaths=
ReadWritePaths=/servers/caddy/.data/caddy
d. My complete Caddy config:
@notes host notes.hacking.xxxxxx.net notes.hacking.xxxxxx.net
handle @notes {
reverse_proxy trantor.238.xxxxxx.net:8900
}
@editnotes host edit.hacking.xxxxxx.net edit.hacking.xxxxxx.net
# Match WebSocket upgrades
@ws header Connection *Upgrade*
# Match requests with NO folder query
# Tried all of these, none work:
# @no_folder expression `!request.query.folder`
# @no_folder expression `request.query.folder == nil`
# @no_folder expression `!request.query.has('folder')`
@no_folder expression `request.query.folder == ''`
# Match only requests for /doc or /doc/*
@allowedfolder expression `{query.folder} == '/doc' || {query.folder}.startsWith('/doc/')`
handle @editnotes {
handle @ws {
reverse_proxy trantor.238.xxxxxx.net:8901
}
handle @no_folder {
reverse_proxy trantor.238.xxxxxx.net:8901
}
handle @allowedfolder {
reverse_proxy trantor.238.xxxxxx.net:8901
}
handle {
respond "Access denied" 403
}
}
5. Links to relevant resources:
- Caddy docs: Expression matcher
- Caddy forum: How to match if query parameter is missing?
- Caddy GitHub Issue #5742
Summary:
Is there a supported way to match requests where a query parameter is missing in the CEL matcher?
If not, is there a recommended workaround for this use case?