Configure security

Tune Resuma's HTTP hardening from Rust or environment variables.

LIVE

SecurityConfig snapshot

Live snapshot from this server process (no secrets) — compare with resuma doctor.

RESUMA_ENV
RESUMA_TRUST_PROXY
rate backend
CSRF
origin check

ServeOptions + SecurityConfig

use resuma::prelude::*;

pub fn serve_options() -> ServeOptions {
    ServeOptions {
        addr: "127.0.0.1:3000".parse().unwrap(),
        security: SecurityConfig {
            csrf: true,
            origin_check: true,
            trust_proxy: true,          // Fly.io / nginx
            body_limit_bytes: 256 * 1024,
            actions_per_minute: 90,
            submits_per_minute: 45,
            hide_benchmark: true,
            production: true,
        },
    }
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    ResumaApp::new()
        .page("/", || page())
        .serve(serve_options())
        .await
}

Environment variables

Local dev needs no env vars — cargo run and resuma dev use secure defaults. For production, SecurityConfig::from_env() reads RESUMA_ENV, RESUMA_TRUST_PROXY, rate limits, and CSP flags.

Full guide: Environment variables — what you need by app type (SSR vs workers), automatic vs manual setup, Fly scaffold, and resuma doctor.

RESUMA_CSRF=1              # default on
RESUMA_ORIGIN_CHECK=1      # default on
RESUMA_BODY_LIMIT=1048576
RESUMA_RATE_ACTIONS=120
RESUMA_RATE_SUBMITS=60
RESUMA_RATE_BACKEND=memory|disk   # disk auto when RESUMA_ENV=production
RESUMA_DATA_DIR=/data/resuma      # persistent volume for disk rate limits + exec

Built-in rate limiting (no Redis)

Resuma includes per-IP rate limiting for actions, submits, and exec routes. Dev uses memory; production uses {RESUMA_DATA_DIR}/rate-limit/ with file locks — shared across processes on the same volume. For multi-region deploys without a shared disk, add edge rate limiting (nginx, Fly proxy, Cloudflare) instead of an external datastore.

Resuma OS (exec layer)

Worker, queue, graph, scheduler, and webhook routes need RESUMA_EXEC_API_KEY in production (fail-closed without it). See Environment variables · Exec security · Ops & production.

CSP (Qwik-style, stronger defaults)

Like Qwik City's plugin@csp.ts, Resuma sets CSP on every SSR response — but uses a crypto nonce per request (not Date.now()), adds 'strict-dynamic' on scripts, and skips CSP in dev when RESUMA_DEV=1 unless RESUMA_CSP_DEV=1.

Inline &lt;style&gt; / &lt;script&gt; in with_head() get the nonce automatically. Host images on 'self' via public/images/… (see PWA & public/), static_asset, or extend CspConfig::img_src.

FlowApp::new()
    .with_head("<style>.hero { color: var(--accent); }</style>")
    .serve(FlowServeOptions {
        security: SecurityConfig {
            csp: CspConfig::production(["https://images.pexels.com"]),
            ..SecurityConfig::from_env()
        },
        ..FlowServeOptions::default()
    })

Environment: RESUMA_CSP=0, RESUMA_CSP_IMG_SRC=https://cdn.example.com, RESUMA_CSP_REPORT_ONLY=1. Validate at csp-evaluator.withgoogle.com.

Flow apps

use pages::PagesRegistry; // generated by resuma routes --generate

FlowApp::new()
    .auto_pages("src/pages", PagesRegistry)
    .serve(FlowServeOptions {
        addr: ([0, 0, 0, 0], 3000).into(),
        security: SecurityConfig::from_env(),
    })
    .await

Fly.io

Production deploy guide (Dockerfile, fly.toml, health checks): Docker deploy cookbook. This repo ships a working config at the workspace root — live at resuma-docs.fly.dev.

# fly.toml [env] — minimum for production
RESUMA_ENV = "production"
RESUMA_TRUST_PROXY = "1"
SITE_URL = "https://your-app.fly.dev"
HOST = "0.0.0.0"
PORT = "3000"

[http_service]
  force_https = true

Security checklist before deploy

  • RESUMA_ENV=production — sanitized errors
  • RESUMA_TRUST_PROXY=1 — real client IP + HTTPS detection
  • force_https = true on Fly / reverse proxy
  • Run container as non-root (see Dockerfile)
  • Auth + validation on sensitive #[server] actions — todo example