Skip to content
Omicron

Self-hosting

Security

The security properties of a default install, and the handful of things an operator can get wrong.

What is secure by default

  • Debug ports are loopback-only. The backend (:8000) and frontend (:5173) are published on 127.0.0.1 for local troubleshooting. All public traffic goes through Caddy on 80/443 over TLS.
  • Secrets are generated, never typed. The database password and session secret are created on first boot into the secrets volume.
  • HTTPS is automatic. Certificates are fetched on demand and renewed by Caddy, and only for your saved domain and its www. alias.
  • Sessions are server-side. Session records live in Postgres behind an httpOnly cookie; there is no session state in the token itself.
  • Rate limiting is on for logins, registrations, API writes, remote discovery, the federation inbox, and the content webhook.
  • Webhook credentials are stored hashed. Publishing tokens are SHA-256’d before they touch the database, so a dump cannot be replayed against the endpoint. Unknown or revoked credentials get a flat 401, and a token can only ever create posts for the account that minted it.
  • The federation inbox is capped. Oversized payloads are rejected by declared length and again while buffering, so a missing or spoofed Content-Length cannot stream an unbounded body into the parser.

What you can get wrong

  • Do not disable rate limiting (RATE_LIMIT_ENABLED=false) unless a trusted upstream already throttles for you.
  • Do not leave the wizard unfinished on a publicly reachable server — the first person to complete it becomes the admin.
  • Do not store backups on the same machine, and remember that Method B backups contain your secrets.
  • Do not paste a publishing credential into a CMS template or a shared document. Whoever holds one can publish, edit, and unpublish posts as its owner. A per-user token is revoked from Settings → Integrations; WEBHOOK_SECRET is rotated by changing the value and restarting.

Rate limits

Defaults, tunable in .env:

Variable Default Limit
RL_LOGIN_MAX 15 login attempts per IP / 15 min
RL_REGISTER_MAX 5 registrations per IP / hour
RL_API_WRITE_MAX 120 writes / min per signed-in user (or per IP)
RL_UPLOAD_MAX 10 media uploads / min per signed-in user (or per IP)
RL_INBOX_MAX 300 federation inbox POSTs / min per source IP
RL_WEBHOOK_MAX 30 content-webhook POSTs / min per source IP
INBOX_MAX_BODY_BYTES 1000000 maximum inbox payload size
WEBHOOK_MAX_BODY_BYTES 512000 maximum content-webhook payload size
RL_REMOTE_MAX 30 anonymous remote-discovery GETs / min per IP
RL_REMOTE_MISS_MAX 20 cache-miss (outbound) remote lookups / min per caller
RL_REMOTE_MAX_OUTBOUND 10 global concurrent outbound federation lookups
RL_REMOTE_MAX_PER_ORIGIN 3 concurrent outbound lookups per remote host
REMOTE_LOOKUP_TIMEOUT_MS 10000 deadline for one WebFinger + actor + outbox lookup
REMOTE_NEGATIVE_CACHE_TTL_MS 60000 negative-cache time for failed/not-found lookups
REMOTE_CACHE_RETENTION_DAYS 30 days a cached remote actor survives unreferenced (0 disables pruning)

Cached remote actors and their posts are also pruned daily once they are stale and unreferenced — see REMOTE_CACHE_RETENTION_DAYS above — so a hostile instance serving many distinct actors cannot grow the database without bound.

Without Redis these counters are per-process and reset on restart. With REDIS_URL set they are shared and durable. See Rate limits.

Upload storage

Uploaded media is bounded by the upload rate limit above plus two storage quotas, enforced when the upload happens: UPLOAD_QUOTA_USER_MB (default 200 MB per account) and UPLOAD_QUOTA_TOTAL_MB (default 2048 MB per instance). 0 disables a cap; a breach answers the upload with HTTP 413. Unreferenced files are garbage-collected by a daily sweep after UPLOAD_GC_GRACE_DAYS (default 30) days, which is also why replacing an avatar does not free disk space immediately. Details in Environment → Upload storage.

Session secret rotation

Admin → Instance → Session secret rotates the secret that signs sessions. It signs everyone out — including you — on the next restart. Use it only if you suspect exposure.

If you pinned SESSION_SECRET in the environment, rotate it there instead; an env-supplied value takes precedence over the stored one.

Federation exposure

Enabling federation opens a public, unauthenticated POST endpoint (the inbox) to the entire internet. Fedify verifies HTTP signatures on inbound activities, and the inbox is rate-limited and size-capped, but you are now a network participant:

  • Expect background noise from crawlers and probing servers.
  • Use Admin → Federation to defederate abusive domains; blocking also purges cached content from them.
  • Use Admin → Reports for user-reported content.

AI-scraper protection

Off by default. When on, it puts a proof-of-work interstitial in front of the interactive routes — composing, search, the dashboard, settings, admin, sign-in — and leaves alone everything a reader or a crawler fetches: articles, profiles, tag pages, feeds, sitemaps, the API, federation and OpenGraph crawlers. It is a bandwidth and abuse mitigation, not a security control, and it does not stop a scraper that executes JavaScript from reading your posts — see Admin panel.

A challenged request is answered with 503 and Retry-After, not 200, so an automated client is told the truth about what happened. Expect it in your monitoring; see what a challenged request gets back.

Set ANUBIS_REDIRECT_DOMAINS if you turn it on. Without it, the endpoint that redeems a solved challenge will forward a reader to any domain, which lets a link to your instance land the visitor elsewhere. See Environment variables.

Reader privacy

Omicron does not profile readers. On-instance view counts are aggregate integers keyed by a one-way hash, with no IP address, fingerprint, or per-visit row stored anywhere. The full account, including what is deliberately never collected, is in Writer dashboard.

Keeping up to date

git pull && docker compose up -d --build && docker image prune -f && docker builder prune -f

Pull and rebuild regularly for security fixes, back up before upgrades, and test that your backups actually restore.

Reporting a vulnerability

Report security issues privately through GitHub security advisories rather than in a public issue, so a fix can ship before the details are public.

Found a mistake?Edit this page on GitHub.