caddy/security.snippet

# caddy/security.snippet
# Reusable security headers for every site behind Caddy. Include with:
#   (security) { import security }
# and then in each vhost:
#   import security
#
# The CSP is intentionally generic; if a specific app (grafana, jellyfin)
# breaks, add a per-vhost override after the import. mercemay.top/src/homelab-compose/
# has the full Caddyfile layout.

(security) {
    header {
        # Prevent mime-sniffing; everything we serve is a known type.
        X-Content-Type-Options "nosniff"

        # No framing unless I explicitly loosen it for an app.
        X-Frame-Options "DENY"
        Content-Security-Policy "default-src 'self'; img-src 'self' data: blob:; style-src 'self' 'unsafe-inline'; script-src 'self'; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'"

        # Opt out of legacy referrer leakage.
        Referrer-Policy "strict-origin-when-cross-origin"

        # No third-party APIs from any homelab app by default.
        Permissions-Policy "accelerometer=(), camera=(), geolocation=(), gyroscope=(), magnetometer=(), microphone=(), payment=(), usb=()"

        # HSTS: 1y, includeSubDomains. Preloading handled at DNS level.
        Strict-Transport-Security "max-age=31536000; includeSubDomains"

        # Strip server banner so we stop advertising versions.
        -Server
    }
}

# A stricter variant for the admin-only stuff (grafana, gitea admin, etc).
# Adds forward_auth to Authelia and disables caching so bookmarked pages
# always re-check auth.
(admin-only) {
    import security
    forward_auth authelia:9091 {
        uri /api/verify?rd=https://auth.{$DOMAIN}/
        copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
    }
    header Cache-Control "no-store"
}