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.

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> and style="…" attributes are blocked.

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.