Secure headers and HTTPS
Cicadel adds security headers to every response and enforces an HTTPS policy you
control from settings. If you know Django, this is the job of its
SecurityMiddleware.
Generated projects start with secure values. go run manage.go check --deploy
warns about anything left off.
#Ask your agent
Configure HSTS, the HTTPS redirect, trusted proxies and the content security
policy for my Cicadel project, then make check --deploy pass without warnings.
#What every response gets
| Header | Value | Controlled by |
|---|---|---|
X-Content-Type-Options | nosniff | always |
Referrer-Policy | same-origin | always |
X-Frame-Options | DENY | always |
Content-Security-Policy | your policy | ContentSecurityPolicy |
Strict-Transport-Security | max-age=N on HTTPS responses | HSTSSeconds, never in debug |
A handler can set its own Content-Security-Policy. It replaces the default for
that response. The account pages do this with a stricter policy.
#Settings
Generated projects set these in project/settings/settings.go:
s := settings.Defaults()
s.HSTSSeconds = settings.HSTSYear
s.ContentSecurityPolicy = settings.DefaultContentSecurityPolicy
return settings.Load(s)| Field | Environment variable | Default | Purpose |
|---|---|---|---|
HSTSSeconds | CICADEL_HSTS_SECONDS | 0 (generated: one year) | Tell browsers to use HTTPS only. Sent only on HTTPS responses with debug off. |
SecureRedirect | CICADEL_SECURE_REDIRECT | false | Redirect plain-HTTP requests to HTTPS with 308, after the host check, debug off only. |
TrustedProxies | CICADEL_TRUSTED_PROXIES | none | Comma-separated IPs or CIDR prefixes of your reverse proxies. |
ContentSecurityPolicy | CICADEL_CONTENT_SECURITY_POLICY | empty (generated: default policy) | Policy for responses that set none. Empty sends no header. |
FrameSources | CICADEL_FRAME_SOURCES | none | Comma-separated origins that ui.Embed may frame. Added to the policy as frame-src. |
StyleAttributes | CICADEL_STYLE_ATTRIBUTES | false | Adds style-src-attr 'unsafe-inline' so browsers apply style={ ui.Style(...) } values. |
Warning
HSTS is hard to undo. Browsers remember it for max-age seconds. If you are not
sure your whole domain serves HTTPS, start with a short value such as 300.
Cicadel never adds includeSubDomains or preload.
#Content security policy
settings.DefaultContentSecurityPolicy is:
default-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'- It allows scripts, styles, images, fonts and requests from your own origin only.
- That fits the starter pages, Tailwind output and self-hosted htmx.
- Inline
<script>code andstyle="…"attributes are blocked. Use external scripts withui.Scriptand JSON data islands withui.JSONData; turn onStyleAttributesfor computedui.Stylevalues.
#Embeds and style attributes
Two settings extend the policy instead of replacing it. Settings.Policy() returns
the header that is sent:
s.ContentSecurityPolicy = settings.DefaultContentSecurityPolicy
s.FrameSources = []string{"https://www.youtube-nocookie.com"} // ui.Embed origins
s.StyleAttributes = true // ui.Style values
// default-src 'self'; …; frame-src https://www.youtube-nocookie.com; style-src-attr 'unsafe-inline'FrameSourcesentries are lowercasehttps://host[:port]origins, with no path, wildcard or keyword.http://is accepted only withDebug. The same list is the allow-listui.Embedchecks at render time, so a page cannot emit a frame the policy would block.StyleAttributesrelaxes onlystyleattributes.<style>elements and stylesheets stay same-origin. Components emit style attributes only from validatedui.Stylevalues or templ-sanitized strings.- Validation fails if
ContentSecurityPolicyalready setsframe-srcorstyle-src-attrwhile the matching setting is on. Pick one place. - With an empty
ContentSecurityPolicy, no header is sent and the settings add nothing.
See SVG, styles, scripts and embeds.
#Static files on a CDN
If your static files come from a CDN (CICADEL_STATIC_URL=https://cdn.example.net/…),
add its origin, for example default-src 'self' https://cdn.example.net; ….
check --deploy warns when it is missing.
#Behind a reverse proxy
Most deployments terminate TLS at a proxy or load balancer. Cicadel learns that a
request used HTTPS, and the real client address, only from proxies listed in
TrustedProxies:
CICADEL_TRUSTED_PROXIES=10.0.0.0/8For a request whose direct peer is trusted:
X-Forwarded-Proto: https(last value) marks it as HTTPS. HSTS is sent andSecureRedirectdoes not redirect it.r.RemoteAddrbecomes the rightmostX-Forwarded-Forentry that is not itself a trusted proxy. Clients cannot spoof it by prepending values.
Requests from any other peer keep their real address, and their forwarded headers are ignored.
Good to know
Forwarded and X-Forwarded-Host are never used. The host check always applies
to the request’s own Host.
Check the scheme in handlers with security.IsHTTPS(r).
#Existing projects
Projects generated before v0.2.1 keep their behavior. To upgrade:
- Add the two settings lines above to
project/settings/settings.go. - Set
CICADEL_TRUSTED_PROXIES. - In production, set
CICADEL_SECURE_REDIRECT=true, unless your proxy already redirects HTTP. - Run
go run manage.go check --deploy.