cicadel

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

HeaderValueControlled by
X-Content-Type-Optionsnosniffalways
Referrer-Policysame-originalways
X-Frame-OptionsDENYalways
Content-Security-Policyyour policyContentSecurityPolicy
Strict-Transport-Securitymax-age=N on HTTPS responsesHSTSSeconds, 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:

Go
s := settings.Defaults()
s.HSTSSeconds = settings.HSTSYear
s.ContentSecurityPolicy = settings.DefaultContentSecurityPolicy
return settings.Load(s)
FieldEnvironment variableDefaultPurpose
HSTSSecondsCICADEL_HSTS_SECONDS0 (generated: one year)Tell browsers to use HTTPS only. Sent only on HTTPS responses with debug off.
SecureRedirectCICADEL_SECURE_REDIRECTfalseRedirect plain-HTTP requests to HTTPS with 308, after the host check, debug off only.
TrustedProxiesCICADEL_TRUSTED_PROXIESnoneComma-separated IPs or CIDR prefixes of your reverse proxies.
ContentSecurityPolicyCICADEL_CONTENT_SECURITY_POLICYempty (generated: default policy)Policy for responses that set none. Empty sends no header.
FrameSourcesCICADEL_FRAME_SOURCESnoneComma-separated origins that ui.Embed may frame. Added to the policy as frame-src.
StyleAttributesCICADEL_STYLE_ATTRIBUTESfalseAdds 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:

Text
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 and style="…" attributes are blocked. Use external scripts with ui.Script and JSON data islands with ui.JSONData; turn on StyleAttributes for computed ui.Style values.

#Embeds and style attributes

Two settings extend the policy instead of replacing it. Settings.Policy() returns the header that is sent:

Go
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'
  • FrameSources entries are lowercase https://host[:port] origins, with no path, wildcard or keyword. http:// is accepted only with Debug. The same list is the allow-list ui.Embed checks at render time, so a page cannot emit a frame the policy would block.
  • StyleAttributes relaxes only style attributes. <style> elements and stylesheets stay same-origin. Components emit style attributes only from validated ui.Style values or templ-sanitized strings.
  • Validation fails if ContentSecurityPolicy already sets frame-src or style-src-attr while 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:

Terminal
CICADEL_TRUSTED_PROXIES=10.0.0.0/8

For a request whose direct peer is trusted:

  • X-Forwarded-Proto: https (last value) marks it as HTTPS. HSTS is sent and SecureRedirect does not redirect it.
  • r.RemoteAddr becomes the rightmost X-Forwarded-For entry 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:

  1. Add the two settings lines above to project/settings/settings.go.
  2. Set CICADEL_TRUSTED_PROXIES.
  3. In production, set CICADEL_SECURE_REDIRECT=true, unless your proxy already redirects HTTP.
  4. Run go run manage.go check --deploy.

Type to search this version.