Security & Authentication
Homepage includes optional password protection. It is fully GUI-driven — no environment variables are required to enable it.
First-run setup
When you open Homepage for the first time, a setup screen asks whether you want to enable password protection:
- Yes, add a password — enter and confirm a password of at least 12 characters. All future visits will require login.
- No, continue without — Homepage runs without any authentication. This can be enabled later.
Logging in
If password protection is enabled, you will see a login screen on every new session. Check Stay logged in to persist the session in localStorage (survives browser restarts). Leave it unchecked to use sessionStorage instead (cleared when the tab or browser is closed).
Sessions expire after 7 days.
Managing authentication
Go to Settings → Security to:
| Action | Description |
|---|---|
| Enable password protection | Set up a password on an existing no-auth install |
| Change password | Requires your current password. Signs out every other device. |
| Sign out all devices | Ends every session, including this one |
| Disable password protection | Requires password confirmation; removes all auth requirements |
| Log out | Ends the current session |
How it works
- The password is stored as a bcrypt hash (cost 12) in the SQLite database — never in plain text.
- Auth material lives in a dedicated
authtable that no general-purpose API endpoint reads or writes. - A JWT secret is generated on first use. You do not need to configure it.
- Requests authenticate with a Bearer token. The same token is also set as an
httpOnly,SameSite=Strictcookie, which is what authenticates the requests a browser makes on its own —<img src="/api/icons/…">and backup downloads, neither of which can set a request header. The cookie is accepted forGET/HEADonly, so it cannot be used to change anything. - Login and setup endpoints are rate limited to 10 attempts per 15 minutes per IP.
- Changing your password re-keys the server, so every previously issued token stops working immediately.
Widget credentials
Widgets talk to third-party services — Jellyfin, Umami, FreshRSS, Last.fm and so on — and need credentials for them. Unlike your dashboard password, these have to be replayed to the upstream service, so they are encrypted rather than hashed.
- Credentials are stored AES-256-GCM encrypted in a
widget_secretstable, separate from widget configuration. - They are never sent to the browser. The widget config API returns a placeholder for fields that have a stored value, so the settings form can show that a key is configured without being able to read it.
- When a widget refreshes, the browser sends only the widget's id. The server looks the credential up and adds it to the upstream request.
- Credentials are excluded from backup exports. Restoring a backup brings your layout back and asks you to re-enter keys.
The encryption key
By default the key is generated into your data directory as .encryption-key (mode 0600). That protects credentials in API responses, backups and git, but not against someone who can already read files on the host — the key sits next to the database it decrypts.
For a publicly reachable instance, set the key in the environment instead:
openssl rand -hex 32environment:
- HOMEPAGE_SECRET_KEY=<the 64-character hex string>With that set, a leaked database file is not enough on its own. Keep a copy — losing the key means re-entering every widget credential.
Exposing Homepage publicly
Enable password protection. Beyond that:
- Put it behind HTTPS. HSTS is sent in production builds, but only TLS at the proxy makes it mean anything.
- Set
HOMEPAGE_SECRET_KEYso the credential encryption key is not stored beside the database. - Leave
CORS_ORIGINSempty unless you genuinely serve the frontend from a different origin. Empty means same-origin only. - Keep
TRUST_PROXYon (the default) when behind a reverse proxy, so rate limiting sees real client addresses rather than bucketing everyone under the proxy's IP. - Use the Docker socket proxy shipped in
docker-compose.ymlrather than mounting/var/run/docker.sockinto the app. Socket access is equivalent to root on the host, and mounting it:rodoes not restrict what the daemon will accept — the flag applies to the filesystem entry, not to the API.
No-auth installs on public URLs
If you chose no password at first-run setup and your instance is publicly reachable, any visitor could go to Settings → Security and enable a password themselves, locking you out. The no-auth mode is intended for use behind a firewall or VPN where you trust all visitors.
If your instance is on a public URL, always set a password.
Hardening notes
- Security headers (CSP, HSTS,
X-Frame-Options,X-Content-Type-Options,Referrer-Policy) are applied byhelmet. - Remote HTML — Mastodon posts, RSS article bodies — is sanitised with DOMPurify before rendering.
- The host probe used by the PROBEd widget can reach private addresses, which is the point of the feature, but refuses cloud instance-metadata endpoints and is rate limited.
- Uploaded app icons are restricted to raster formats. SVG is rejected because it can carry script and is served from the app's own origin.