Secrets & Key Management
EmDash uses a small set of secrets across previews, comments, authentication, storage, and plugins. This page is the complete inventory: where each secret comes from, where it is stored, how to rotate it, and what breaks if it is lost.
Overview
Section titled “Overview”| Secret | Source | Stored in | Lost key impact |
|---|---|---|---|
EMDASH_ENCRYPTION_KEY | Operator (emdash secrets generate) | Environment / Worker secret only | Encrypted plugin secrets become unrecoverable (once encryption at rest ships) |
| Preview secret | Auto-generated (env override) | options table (emdash:preview_secret) | Outstanding preview links stop working; new ones are fine |
| IP salt | Auto-generated (env override) | options table (emdash:ip_salt) | Past comment rate-limit continuity resets |
| Session & API tokens | Generated per session/token | Session store / database (hashes only) | Nothing — plaintext is never stored |
| OAuth provider credentials | You (Google/GitHub console) | Environment | Sign-in via that provider stops until replaced |
| Turnstile secret | You (Cloudflare dashboard) | Environment | Comment CAPTCHA verification fails |
| S3 credentials | You (storage provider) | Environment or config | Media upload/download fails until replaced |
| Plugin secrets | You (admin settings UI) | Database (plugin settings / storage) | Re-enter in the admin |
| CLI credentials | emdash login / emdash plugin publish device flows | ~/.config/emdash/auth.json (mode 0600) | Run the device flow again |
| Registry CLI credentials | emdash-plugin atproto OAuth | ~/.emdash/oauth/, ~/.emdash/credentials.json (mode 0600) | Log in again; identity lives at your PDS |
The encryption key
Section titled “The encryption key”EMDASH_ENCRYPTION_KEY is the site’s key for encrypting plugin secrets at rest. It is operator-provided and never stored in the database — the database only ever holds ciphertext, so a leaked database backup does not expose the key.
Generate one and set it as an environment variable (or Worker secret):
npx emdash secrets generate# emdash_enc_v1_<43 base64url chars>
# Cloudflare:wrangler secret put EMDASH_ENCRYPTION_KEYThe format is emdash_enc_v1_ followed by 32 random bytes as unpadded base64url. The key is validated at runtime startup; a malformed value logs an operator-facing error without taking down request paths.
Rotation
Section titled “Rotation”The variable accepts a comma-separated list of keys. The first entry is the primary and is used for new writes; all entries are tried for decryption. Every encrypted value is tagged with an 8-character key fingerprint (the kid, printable via emdash secrets fingerprint <key>), so the runtime picks the right key automatically.
To rotate: generate a new key, prepend it to the list (EMDASH_ENCRYPTION_KEY="new,old"), redeploy, and drop the old key once existing values have been re-encrypted.
Generated site secrets
Section titled “Generated site secrets”Two secrets are generated automatically on first use and persisted in the options table, so they are stable across requests, deployments, and isolates. Generation is atomic — concurrent cold starts converge on one value.
Preview secret
Section titled “Preview secret”Signs preview URLs (HMAC). Stored as emdash:preview_secret; 32 random bytes, base64url.
- Override: set
EMDASH_PREVIEW_SECRET(legacy alias:PREVIEW_SECRET) if you need the same secret across multiple processes or want to pin it for audit reasons. The environment always wins over the stored value. - Rotation: delete the
emdash:preview_secretrow (or change the env var) and redeploy. Impact: previously issued preview links stop validating. Nothing else breaks — a fresh secret is generated (or read from the env) on the next preview request. - If lost: nothing is unrecoverable. Preview links are short-lived by design.
See the preview guide for how preview URLs are built and verified.
IP salt
Section titled “IP salt”Salts the SHA-256 hash of commenter IP addresses (ip_hash on comments) used for comment rate limiting. Stored as emdash:ip_salt. Site-specific, so hashes are not correlatable across EmDash installs.
- Override: set
EMDASH_IP_SALT. For backward compatibility,EMDASH_AUTH_SECRET/AUTH_SECRETare also consulted — installs that historically derived the salt from those keep stable hashes. - Rotation: change the env var or delete the
emdash:ip_saltrow. Impact: new comment submissions hash to different values, so rate-limit counting restarts for everyone. Existing comments and their stored hashes are untouched. - If lost: no data loss. Only rate-limit continuity resets.
Session and API tokens
Section titled “Session and API tokens”- Sessions use Astro’s session store (Workers KV on Cloudflare, filesystem on Node). The cookie carries an opaque session ID; there is no signing secret to manage. Sign out to end a session, or clear the session store (e.g. the KV namespace) to force everyone to sign in again.
- API tokens (
ec_pat_,ec_oat_,ec_ort_prefixes) are opaque 256-bit random values; only their SHA-256 hash is stored. The plaintext is shown once at creation. Rotate by revoking and re-creating in the admin. - Invite, magic-link, and recovery tokens are single-purpose, stored as SHA-256 hashes in
auth_tokens, and time-limited (invites 7 days, magic links 15 minutes).
There is nothing to back up or rotate proactively: a database leak exposes only hashes, and every token can be revoked or reissued from the admin.
User-provided service credentials
Section titled “User-provided service credentials”Credentials for external services are read from the environment and never written to the database. Rotate them at the provider, update the variable, redeploy.
| Service | Variables |
|---|---|
| Google sign-in | EMDASH_OAUTH_GOOGLE_CLIENT_ID, EMDASH_OAUTH_GOOGLE_CLIENT_SECRET (or unprefixed aliases) |
| GitHub sign-in | EMDASH_OAUTH_GITHUB_CLIENT_ID, EMDASH_OAUTH_GITHUB_CLIENT_SECRET (or unprefixed aliases) |
| Marketplace publishing (CI) | EMDASH_MARKETPLACE_TOKEN |
| Turnstile (comments) | EMDASH_TURNSTILE_SECRET_KEY (or TURNSTILE_SECRET_KEY) |
| S3-compatible storage | S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, S3_BUCKET, S3_REGION |
On Cloudflare, set these with wrangler secret put; locally, put them in .env. R2 via binding needs no credentials at all — access is granted by the binding in wrangler.jsonc, which is the recommended setup on Workers. See storage options.
Plugin secrets
Section titled “Plugin secrets”Settings a plugin declares with type: "secret" (API keys for email providers, form CAPTCHAs, etc.) are entered in the admin UI and stored in the database — in the options table under plugin:<id>:settings:<key>, or in the plugin’s key-value storage. Whether a stored secret is echoed back to the admin UI is up to the plugin; well-behaved plugins return only a “value is set” flag instead of the secret itself (the bundled forms plugin does this).
- Rotation: rotate the key at the provider and paste the new value into the plugin’s settings page. Takes effect immediately.
- If lost: re-enter the value in the admin. Nothing else depends on it.
CLI credentials
Section titled “CLI credentials”The emdash CLI holds two kinds of credentials, both in ~/.config/emdash/auth.json (respecting XDG_CONFIG_HOME), created with owner-only permissions (0600):
- Site tokens —
emdash loginauthenticates against your EmDash instance via an OAuth device flow and stores the resulting token keyed by instance URL.emdash logoutremoves it; per invocation,--tokenorEMDASH_TOKENoverrides the stored token. - Marketplace tokens —
emdash plugin publishauthenticates to the EmDash Marketplace via a GitHub device flow and stores the resulting JWT keyed bymarketplace:<origin>. For CI publishing, setEMDASH_MARKETPLACE_TOKENinstead — it takes priority over the stored credential.
Losing the file is harmless: run emdash login (or emdash plugin publish, which re-runs the device flow) again.
Plugin registry CLI credentials
Section titled “Plugin registry CLI credentials”The separate emdash-plugin CLI (package @emdash-cms/plugin-cli) targets the experimental AT Protocol registry. Publishing there is tied to your AT Protocol identity (your publisher DID) — the site itself holds no publishing credentials, and installs verify artifacts against checksums from release records attributed to that DID.
- It authenticates via atproto OAuth. The OAuth session/state blobs live in
~/.emdash/oauth/, and the publisher identity (DID, handle, PDS) is cached in~/.emdash/credentials.json; both are written with owner-only permissions. - In CI, provide identity via
EMDASH_PUBLISHER_DID,EMDASH_PUBLISHER_HANDLE, andEMDASH_PUBLISHER_PDS;EMDASH_REGISTRY_URLoverrides the registry host. Automatedpublishfrom CI still needs the OAuth session files in~/.emdash/oauth/on the runner — the env vars alone do not carry the OAuth session. - Rotating or revoking publishing access happens at your AT Protocol account (e.g. app passwords), not in EmDash. See Atmosphere auth.
Rotation quick reference
Section titled “Rotation quick reference”| I want to… | Do this |
|---|---|
| Rotate the encryption key | Prepend a new key: EMDASH_ENCRYPTION_KEY="new,old", redeploy, drop old later |
| Invalidate all preview links | Delete the emdash:preview_secret option row (or change the env override) |
| Reset comment rate-limit hashing | Change EMDASH_IP_SALT (or delete the emdash:ip_salt option row) |
| Revoke a leaked API token | Admin → Users → API tokens → revoke, then create a replacement |
| Kill all sessions | Clear the session store (Workers KV namespace / session directory) |
| Replace a provider credential | Rotate at the provider, update the env var, redeploy |
| Replace a plugin API key | Rotate at the provider, re-enter in the plugin’s admin settings |