# 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`:

```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:

```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](https://cicadel.fauzaanu.com/en/0.2.8/topics/component-ui/index.md#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`:

```sh
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.

> [!NOTE]
> `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`.
