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 on127.0.0.1for 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
secretsvolume. - 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-Lengthcannot 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_SECRETis 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.