BoxBox is built for private homelab use. It can modify real host files, so treat deployment configuration as part of the security boundary.
Before exposing BoxBox beyond your own machine:
- Set
BOXBOX_JWT_SECRETto a long random value. - Set
BOXBOX_USERS_adminto a bcrypt hash or configure a bcrypt hash inconfig.yaml. - Use a reverse proxy with HTTPS.
- Mount only the directories BoxBox needs.
- Use
read_only: truefor backups and sensitive locations. - Do not expose
/host_root; whole-host browsing is intentionally absent from the default deployment. - Keep the default same-origin WebSocket policy or use a narrow
allowed_originslist. - Put the service behind your normal VPN, Tailscale, WireGuard, or trusted reverse proxy access controls when possible.
- Access tokens expire after 15 minutes. Refresh tokens expire after 7 days and are rotated in an HttpOnly, SameSite=Strict cookie.
- Both token types carry a stable session ID. Every authenticated HTTP request checks that the session exists in the active registry and belongs to a configured user. WebSocket authentication is checked at handshake. Refresh also checks that the token is the session's current refresh token.
- Logout needs only the current refresh cookie, not an access-token header. It removes the whole session, so all access tokens from that login stop working, including tokens issued before refresh. Other logins for the same user remain active.
- The version 1
auth-sessions.jsonregistry underdata_dirstores session IDs, usernames, SHA-256 hashes of current refresh tokens, and deadlines. It never stores raw JWTs. Authentication is stateful: login, refresh, and logout each write the registry. - On Linux, the service restricts the registry directory to
0700and the file to0600. A missing registry file or a valid version 1 snapshot with an emptysessionsmap at startup creates an empty active registry, so old access and refresh tokens fail after restart. A zero-byte file is invalid. Corrupt, unreadable, non-regular, unsupported, or future-version stores stop startup instead of resetting. - A write failure before rename returns
500. The service restores only the session changed by that request. Login, refresh, and logout do not publish a token pair orSet-Cookie; refresh and logout preserve the existing cookie for retry. Login does not clear an unrelated existing cookie. - Rename publishes the new registry. Success also requires the temporary file to be synced and closed and the parent directory to be synced. If a durability step fails after rename, the service keeps the installed in-memory state and returns
401 SESSION_RECOVERY_REQUIREDwithout a token pair. Refresh and logout clear the cookie. Login does not clear an unrelated cookie. - Keep
data_diron persistent storage and run one server process per directory. The service does not coordinate processes that share a directory. Deleting the registry while the server runs does not clear its in-memory sessions, and a later successful write can persist them again. This is not immediate disk-loss revalidation. - Browser refresh coalescing applies within one tab. A replaced refresh token for an active session returns
409 REFRESH_SUPERSEDEDwithout changing the shared cookie or issuing tokens. Refresh and logout retry once with the browser's current cookie. Repeated conflicts remain retryable and do not end the session. Expired tokens and removed sessions still return401. auth-revocations.jsonis ignored and is not deleted. Its denylist cannot identify sessions to load. Legacy tokens without a session ID are rejected on first use and never auto-enrolled; users sign in again. A missing registry file or valid empty registry after restart rejects old tokens without requiring JWT-secret rotation solely because the registry was lost.- Restoring an older valid
auth-sessions.jsonbackup can revive sessions, access tokens, and their current refresh tokens. RotateBOXBOX_JWT_SECRETafter restoring a backup. An older validator that ignores session IDs and the registry may accept tokens after downgrade; rotate the secret if you downgrade. - After startup with a user removed from configuration, that user's tokens fail validation. Session deadlines preserve the existing sliding refresh lifetime and retain the longest existing deadline when configured token TTLs shrink.
- Auth endpoints are rate-limited per client IP.
- Repeated failures trigger exponential per-account lockouts.
Generate bcrypt hashes with htpasswd (from apache2-utils on Debian/Ubuntu):
htpasswd -bnBC 12 admin 'your-password' | cut -d: -f2Store only the resulting hash:
users:
admin: "$2b$12$..."BOXBOX_USERS_admin='$2b$12$...'Plaintext configured passwords and JWT secrets shorter than 32 bytes are rejected at startup. If no users are configured, the server also refuses to start.
Mount points define what the authenticated UI and API can access.
Prefer narrow paths:
mount_points:
- name: "media"
path: "/srv/media"
read_only: false
- name: "backups"
path: "/srv/backups"
read_only: trueThe default compose file does not mount /. A mount that resolves to / is rejected unless allow_root_mount: true is explicitly configured after reviewing the risk.
The backend validates requested paths against configured mount points and blocks traversal outside those roots. Examples of blocked paths include:
../secret
..%2Fsecret
media/../../etc/passwd
Read-only mount points also block write operations after path resolution.
With no allowed_origins, BoxBox accepts browser WebSocket upgrades only when Origin matches the request host. A literal * is supported as an explicit, risky opt-out.
Restrict origins for browser-exposed deployments:
allowed_origins:
- "https://boxbox.example.com"
- "*.internal.example.com"Or through env:
BOXBOX_ALLOWED_ORIGINS="https://boxbox.example.com,*.internal.example.com"Use your proxy to provide:
- HTTPS certificates.
- HTTP to HTTPS redirect.
- WebSocket forwarding for
/api/v1/ws. - Request size limits compatible with your upload size.
- Optional extra auth, IP allow-listing, or VPN-only access.
If you use Traefik or another reverse proxy, add routing and TLS configuration according to that proxy's setup. The default compose file uses normal host port binding.
Forwarded client-IP headers are ignored unless the direct proxy IP/CIDR is configured in trusted_proxies or BOXBOX_TRUSTED_PROXIES.
Chunked uploads are assembled in /tmp/boxbox by default and moved into the final destination when complete. Configure enough disk space for temporary chunks:
volumes:
- boxbox-temp:/tmp/boxboxUse max_upload_mb to cap accepted upload sizes.
Share links expose one file or a folder tree to anyone holding the token URL:
- Tokens are 256-bit random values (43 URL-safe characters); possession of the token is the only credential for recipient endpoints.
- Recipient endpoints are public and rate-limited per client IP, separately from the stricter auth budget.
- Unknown, expired, and revoked tokens all return the same
404, so recipients cannot probe why a link stopped working. - Share management is scoped to the creating account: users can list, update, and revoke only their own links.
- Owners can revoke a link at any time; expiry is optional (
expiresInSecondsat creation). A revocation that completes during an upload prevents that upload from being published. - Shared targets are re-resolved against the current mount configuration on every recipient access. Removed or renamed mounts invalidate the link, and a mount that became read-only still serves reads while denying uploads or deletes.
- Recipient downloads and previews carry the same sandboxed
Content-Security-Policyas the app's streaming endpoints, and active document formats (HTML, SVG, XML) are forced to download instead of rendering inline. - Folder uploads are capped by both the per-link
maxUploadBytesand servermax_upload_mblimits. Upload-only links cannot replace existing items; delete permission also permits replacement and deletion below (never at) the shared root. - Folder ZIP archives validate traversal and resolved paths against the share root before streaming.
- Full-access links also allow folder creation and empty file edits. Folder creation uses an opened filesystem root to confine mutations if another writer swaps a parent directory for a symlink.
- Anyone holding an active share link can view the owner's stored wallpaper. The browser keeps the wallpaper owner marker and cancels pending synchronization when the authenticated session ends.
- Recipient metadata responses never include mount names or internal paths.
- The share store directory is restricted to owner access (
0700) and its token-bearingshares.jsonfile to0600, including existing stores when the service initializes. - Before downgrading to a version predating split upload/delete permissions, back up
shares.json; older versions cannot preserve the new per-link permissions and upload caps if they rewrite the store.
- Rotate
BOXBOX_JWT_SECRETif it was ever committed or shared. - Legacy tokens without a session ID are rejected on first use. Users sign in again; no automatic enrollment or JWT-secret rotation is needed solely for that one-time sign-out.
- Rotate admin passwords after test deployments.
- Review mounted paths after adding new host disks.
- Check logs for repeated failed logins or path validation errors.
- Keep the image rebuilt from current dependencies.
- Back up
/dataif custom drive names matter to you.