✨ Highlights
- Custom Deny Responses at path and domain level
Choose status code, headers (e.g.,Location), content type, and body (HTML/JSON/plain). - Safe templating for headers and body using
[[ ... ]]delimiters
Avoids conflicts with Traefik’s{{ ... }}templating. - Smart defaults
Auto-detectstext/htmlwhen the body looks like HTML; falls back to403 text/plainif no custom response is set. - Better developer experience
New docs and tests (unit + integration) to lock in behavior and prevent regressions.
🔧 What’s New
Custom responses (denyResponse)
You can now configure a response to be sent when a request is denied—either for a specific path rule or as a domain-wide fallback.
- Precedence: path-level
denyResponse→ domain-leveldenyResponse→ default403 Forbidden - Templating context:
ClientIP,Host,Path,RawQuery,RequestURI,MatchedRule - Template helpers:
urlquery,json,upper,lower
http:
middlewares:
domain-sentinel:
plugin:
domainSentinel:
domainPathRules:
"demo.localhost":
sourceIPs:
- "0.0.0.0/0"
pathRules:
- path: "/admin/*"
sourceIPs:
- "10.10.4.0/24"
denyResponse:
statusCode: 302
headers:
Location: "/login?next=[[ .RequestURI | urlquery ]]"
contentType: "text/plain; charset=utf-8"
body: "Redirecting to login…"
denyResponse:
statusCode: 503
headers:
ClientIP: "[[ .ClientIP ]]"
contentType: "text/html; charset=utf-8"
body: |
<!doctype html>
<html><h1>Service unavailable for [[ .Host ]]</h1></html>Note: Use
[[ ... ]]in your dynamic file configs. Using{{ ... }}will be consumed by Traefik’s own templater and fail to load.
🛡️ Security & Behavior
- Client IP source: By default, DomainSentinel derives the client IP from
RemoteAddrand ignoresX-Forwarded-Forto prevent spoofing. - Header hygiene: Rendered header values are sanitized (CR/LF removed).
- Redirects: Prefer 303 to force GET after POST, or 307/308 to preserve the method.
🧪 Tests & Tooling
- Unit tests (
deny_response_test.go): status/headers/body rendering, templating helpers, HTML detection, fallback behavior. - Integration tests (
middleware_integration_test.go): full in-process flowConfig -> New -> ServeHTTP, path/domain precedence, pass-through tonext.
Run everything:
go test -v ./...📚 Documentation
A new guide is available:
- Custom Deny Responses:
docs/custom-deny-responses.md
Deep dive into configuration, templating, examples, and troubleshooting.
🔄 Migration / Compatibility
- No breaking changes. If you don’t set
denyResponse, behavior is unchanged (default403 text/plain). - To adopt custom responses, add a
denyResponseblock at either the path or domain level (see example above).
🧩 Changelog
- feat: configurable
denyResponseat path & domain level (status, headers, content type, body) - feat: templating with
[[ ... ]]in headers and body; helpersurlquery,json,upper,lower - feat: auto content type (HTML/plain) based on body
- docs: new
docs/custom-deny-responses.md - test: unit & integration coverage
- refactor: cleaner deny rendering with header templating and sanitization