# Security

cronpad isolates workspaces from each other, encrypts stored secrets, signs every outgoing webhook, and refuses to send requests to internal networks on the hosted service. Self-hosted servers trust their operators and enable powerful features (shell commands, private-network webhooks) by default; setting `mode = "hosted"` switches every one of them to the safe default for a public, multi-tenant server.

## Accounts and access

- Sign-in only through Google or GitHub (verified email required); no passwords are stored.
- API keys are random, shown once and stored as SHA-256 hashes. Each belongs to one workspace and can be scoped read-only.
- Session cookies are `HttpOnly`, `SameSite=Lax` and `Secure` over HTTPS; requests that change data with a cookie also need a CSRF token.
- Every database query is scoped to the caller's workspace. Requests for another workspace's objects return `404`, so IDs can't be probed.
- Every change (create, update, pause, delete, key creation, secret change, auto-disable) is written to the audit log with the actor.

Development authentication is opt-in with `auth.dev_login = true`. It trusts `X-Dev-User` (default `developer`) and creates a personal workspace for that identity; use it only on a trusted development server, never on a public deployment.

## Secrets

Stored [secrets](../secrets/index.md) are encrypted with AES-256-GCM, are write-only through the API, and are redacted from run history, logs and traces. Webhook signing secrets and cached idempotency responses are also encrypted with the same key. Protect the database, encryption key and backups as credentials.

## Outgoing requests

- **Signed.** Every webhook carries a Standard Webhooks HMAC signature and timestamp so receivers can reject forgeries and replays. See [Verify signatures](../sinks/webhooks.md#verify-signatures).
- **SSRF protection.** In hosted mode, cronpad resolves each hostname itself and refuses loopback, private, link-local (including cloud metadata at `169.254.169.254`), carrier-grade NAT, IPv6 unique-local and deprecated site-local, multicast, unspecified, documentation and benchmarking addresses, the IETF `192.0.0.0/24` range, and NAT64, 6to4, Teredo and deprecated IPv4-compatible translation/tunnel prefixes, on every connection and every redirect hop. Only ports 80 and 443 are allowed.
- **Bounded.** Timeouts, response size caps, and [auto-disable](../retries-and-failures/auto-disable.md) stop a job from hammering an endpoint indefinitely.

## Hosted mode

`mode = "hosted"` is how cronpad Cloud runs, and what you should use if strangers can sign up to your server.

| Setting | Self-hosted default | Hosted mode |
|---|---|---|
| Command sink | On | Off |
| Webhooks to private networks | Allowed | Blocked |
| Email alert channels | Usable immediately | Address must be verified |
| Sign-up | Off after the first user (`auth.allow_signup`) | Rate-limited per IP; turn on with `auth.allow_signup = true` |
| Retry attempts, timeouts, auto-disable threshold | Unlimited / per job | Capped by server limits |
| Usage limits | Unlimited | From `[limits]` |

Each can still be overridden explicitly in configuration.

## Reporting a vulnerability

Please report security issues privately through the repository's security advisory page (**Security → Report a vulnerability**) rather than in a public issue.