Skip to content

Security Headers

Dwaar automatically adds a baseline set of security response headers to every proxied response. No configuration is required. The headers defend against MIME-sniffing, clickjacking, referer leakage, and server fingerprinting. HSTS is applied only on TLS connections to avoid locking browsers out of intentionally HTTP-only routes.

The SecurityHeadersPlugin runs at priority 100 — after all request-phase plugins — and modifies response headers before they reach the client.


Security headers are built in and always active. Add a site block and they apply automatically:

api.example.com {
reverse_proxy localhost:8080
}

Every response from this site includes:

Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Referrer-Policy: strict-origin-when-cross-origin
Server: Dwaar

If the connection is not TLS (e.g. http://api.example.com), Strict-Transport-Security is omitted. All other headers are always present.


HeaderValueWhat it prevents
Strict-Transport-Securitymax-age=31536000; includeSubDomainsDowngrade attacks and cookie hijacking over plain HTTP. Tells browsers to use HTTPS for one year, including all subdomains. TLS connections only.
X-Content-Type-OptionsnosniffMIME-sniffing attacks where a browser executes an uploaded .txt file as JavaScript. Forces the browser to honour the declared Content-Type.
X-Frame-OptionsSAMEORIGINClickjacking via invisible iframe overlays. Prevents your pages from being embedded in frames on external origins.
Referrer-Policystrict-origin-when-cross-originReferer header leakage to third-party sites. Full URL is sent for same-origin navigations; only the origin is sent cross-origin; nothing is sent on HTTP→HTTPS downgrade.
ServerDwaarServer fingerprinting. Replaces whatever banner the upstream sends (e.g. Apache/2.4.57, Express) with a neutral value that reveals nothing about the backend stack.

The HSTS header uses max-age=31536000 (one year) and includeSubDomains. Once a browser has seen this header for a site, it refuses plain-HTTP connections to that site and all its subdomains for one year, even before the first HTTPS request completes. This eliminates the TOFU (trust-on-first-use) window that would otherwise allow a downgrade attack on the very first visit.

If your deployment includes subdomains that intentionally serve only HTTP (e.g. an internal monitoring endpoint), place those on a separate top-level domain rather than a subdomain, so HSTS on your main domain does not affect them.


The plugin now strips a fixed set of upstream response headers that commonly fingerprint the backend stack, before it applies its own baseline headers. The stripped set is:

HeaderTypical source
X-Powered-ByPHP, Express, ASP.NET
X-AspNet-VersionASP.NET
X-AspNetMvc-VersionASP.NET MVC
X-RuntimeRails, Rack
X-GeneratorDrupal, static generators
ServerEvery HTTP server on the planet

Stripping runs on every response, including static files and error bodies, and happens before the plugin writes its own Server: Dwaar banner. The result is that an attacker scraping responses cannot tell whether the upstream is Rails, ASP.NET, or a static file server — all they see is Dwaar’s banner.

The behaviour is controlled by the strip_leaky_headers: bool field on the plugin config (default true). If you need the upstream banner to pass through unchanged — for example on an internal debug endpoint where you want to know which backend answered — disable it:

debug.example.com {
reverse_proxy localhost:9000
security_headers {
strip_leaky_headers false
}
}

When disabled, the plugin still applies its own baseline headers, but leaves any upstream Server, X-Powered-By, etc. untouched. The default is true on every site block, so the strip is on by default.

Default CSP remains opt-in by design — the plugin does not emit Content-Security-Policy or Content-Security-Policy-Report-Only unless you configure one explicitly (see next section). This preserves backwards compatibility with existing sites that would otherwise break on a forced restrictive default.

The SecurityHeadersPlugin has two optional CSP fields: content_security_policy and content_security_policy_report_only. Both default to None — no Content-Security-Policy or Content-Security-Policy-Report-Only header is sent unless explicitly configured. Configure them via the header directive inside a site block (the plugin picks up overrides at response-header time):

api.example.com {
reverse_proxy localhost:8080
header {
Content-Security-Policy "default-src 'self'; script-src 'self' cdn.example.com"
Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report"
}
}

Use Content-Security-Policy-Report-Only during rollout to log violations without blocking requests. Once the policy is stable, switch to Content-Security-Policy.


Use the header directive inside a site block to override any header the plugin sets, or to add additional security headers:

api.example.com {
reverse_proxy localhost:8080
header {
# Extend HSTS to two years and add preload eligibility
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
# Tighten frame policy to deny all embedding
X-Frame-Options "DENY"
# Add a Content Security Policy
Content-Security-Policy "default-src 'self'; script-src 'self' cdn.example.com"
# Add Permissions-Policy to disable unused browser features
Permissions-Policy "camera=(), microphone=(), geolocation=()"
}
}

The header directive runs after SecurityHeadersPlugin, so your values replace the defaults.

Prefix the header name with - to remove it entirely:

www.example.com {
reverse_proxy localhost:3000
header {
# Allow embedding on any origin (e.g. a widget meant to be iframed)
-X-Frame-Options
}
}

Suppressing the Server banner on a specific route

Section titled “Suppressing the Server banner on a specific route”
internal.example.com {
reverse_proxy localhost:9000
header {
# Pass the upstream's own Server header through unchanged
-Server
}
}

# Global config — TLS certificate provisioning
{
email admin@example.com
}
# Public marketing site — extend HSTS and add CSP
www.example.com {
reverse_proxy localhost:3000
header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
Content-Security-Policy "default-src 'self'; img-src 'self' data: https://cdn.example.com; font-src 'self' https://fonts.gstatic.com"
Permissions-Policy "camera=(), microphone=()"
}
}
# API — defaults are sufficient; add CORS header for browser clients
api.example.com {
reverse_proxy localhost:8080
header {
Access-Control-Allow-Origin "https://www.example.com"
Access-Control-Allow-Methods "GET, POST, OPTIONS"
}
}
# Admin panel — deny all framing, strict CSP
admin.example.com {
basic_auth {
admin $2y$12$...
}
reverse_proxy localhost:8090
header {
X-Frame-Options "DENY"
Content-Security-Policy "default-src 'self'"
Referrer-Policy "no-referrer"
}
}
# Widget endpoint — allow cross-origin framing for embed use case
widget.example.com {
reverse_proxy localhost:8070
header {
-X-Frame-Options
Content-Security-Policy "frame-ancestors https://www.example.com https://partner.com"
}
}

  • Basic Auth — HTTP Basic Authentication for protecting routes with a username and password
  • Forward Auth — delegate authentication decisions to an external service
  • Rate Limiting — per-IP sliding-window rate limiting to slow bots and brute-force attacks