Operations & security

Day-two life with a Sitebin instance: the operator CLI, backups, health, and what you should know before hosting strangers' content on the public internet.

Operator commands

All operator commands run inside the container, e.g. docker exec sitebin sitebin <cmd>:

CommandPurpose
sitebin listList all sites (id, size, files, mode, created, owner/domains).
sitebin reportsList filed abuse reports.
sitebin delete <id|domain>Take down a site by view id, edit id, or domain.
sitebin backup [file]Write a gzip tar of /data (stdout if no file).
sitebin restore <file>Restore /data from a backup.
sitebin caddyfilePrint the generated Caddyfile.
sitebin healthcheckProbe the internal health endpoint.

Backup

The /data volume is everything — sites, indexes, and certificates. Back up that path and you have backed up the whole instance. Alternatively, sitebin backup streams a snapshot:

docker exec sitebin sitebin backup - > sitebin-$(date +%F).tar.gz

Restore a snapshot with sitebin restore <file>.

Health, freeze, and logs

  • Health: the image ships a HEALTHCHECK, so docker ps shows the instance's health out of the box.
  • Freeze: SITEBIN_READONLY=true disables new-site creation — existing sites keep serving.
  • Logs: structured request + lifecycle logs on stdout (docker logs).

Availability & failover

Sitebin is a single-writer system: writes are serialized by in-process locks, so exactly one instance may run against a given /data at any time.

Never run two containers on the same, shared, or bidirectionally synced volume — multi-step operations (uploads with quota enforcement, index updates, replace-all) can interleave and corrupt state. Active–active is not supported.

Everything else about the design makes failover easy: the container is disposable and /data is the entire instance — sites, indexes, accounts, certificates, and the .secret that keeps sessions valid across a move.

Baseline: restore to a fresh server

With streaming backups and a low DNS TTL, this alone gives minutes-level recovery — and it's the plan every deployment should have and test:

# continuously (cron) on the primary:
docker exec sitebin sitebin backup - | ssh backup-host 'cat > sitebin-latest.tar.gz'

# disaster: on any fresh server with Docker
docker run -d --name sitebin -v sitebin-data:/data … sitebin:latest  # same env as before
cat sitebin-latest.tar.gz | docker exec -i sitebin sitebin restore /dev/stdin
docker restart sitebin
# point DNS (base domain, wildcard, custom domains) at the new server;
# certificates re-issue automatically if missing.

Keep the compose/env file in version control — server + compose file + backup is the complete instance.

Active–passive standby

When minutes of downtime are too many: replicate the volume block-level to a second server — DRBD (synchronous, RPO ≈ 0) or ZFS send/recv on a tight interval — with the container stopped on the standby. On failure, promote the replica, start the container, and move the floating IP (or flip low-TTL DNS). The one inviolable rule is the single-writer rule: make sure the old primary is down (fencing) before the standby starts.

Read replicas (one writer, many readers for view traffic) are architecturally feasible and on the enterprise roadmap, but not implemented today.

Security notes

  • User content is only served on random subdomains and custom domains — never on the main domain. Each site gets its own origin.
  • Passwords are stored as Argon2id hashes; password attempts (API, gate, and WebDAV) are rate limited per IP and per site.
  • Uploads are sanitized against path traversal; symlinks in zips are rejected; per-site size/count quotas are enforced during streaming.
  • The authz/tls-check/health endpoints live on a separate listener that is never proxied publicly.
Open, no-login file hosting attracts phishing and malware. As the operator you are responsible for what your instance serves: keep the takedown command handy, consider tight SITEBIN_MAX_* limits and SITEBIN_MAX_EXPIRY_DAYS, and put the instance behind abuse monitoring if it is exposed to strangers.

Anyone can flag a site through the public abuse-report endpoint (POST /api/report, no auth required); filed reports show up in sitebin reports.

Content security headers

Sitebin serves HTML that strangers uploaded, so it sets response headers on every site it serves. There are two levels, and which one a site gets depends on whether the plan behind it is one you can hold accountable.

Every site, both editions — hygiene that breaks nothing:

X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), camera=(), microphone=(),
                    payment=(), usb=(), midi=()
Content-Security-Policy: object-src 'none'; base-uri 'self'

Untrusted content — anonymous drops, and sites on any tier not marked "trusted" — gets the exfiltration blocks folded in:

Content-Security-Policy: object-src 'none'; base-uri 'self';
    form-action 'none'; connect-src 'self';
    frame-ancestors 'none'; frame-src 'none';
    report-uri /_sitebin/csp-report; report-to csp
Referrer-Policy: no-referrer

The aim is narrow and deliberate: stop the exfiltration, not the rendering. Scripts and images still load from anywhere, because blocking them would equally break every legitimate page that pulls a library from a CDN. What a phishing kit loses is the ability to ship what it captures: form-action 'none' kills the credential POST, and connect-src 'self' kills fetch, XHR, sendBeacon and WebSockets. Those are two separate directives because connect-src does not cover form submissions — omitting form-action would leave the oldest trick in the book wide open.

An uploaded page cannot loosen any of this. A <meta> policy of its own can only narrow what the header already set.

What this does not stop. An image beacon (new Image().src = "https://…/?p=" + password) and top-level navigation still leak. CSP has no answer for the second at all — the navigate-to directive was dropped from the standard and is implemented nowhere. Closing it would mean sandbox, which also breaks every external link. Treat these headers as raising the cost of the common kit and making the attempt visible, not as a guarantee.

Trusted tiers

A tier in tiers.json may set "trusted": true. Sites owned on it skip the strict layer — useful when the plan's holders are identifiable and their apps legitimately call an API on another domain. An anonymous site never qualifies, whatever its tier says. The community edition registers no account extension, so every site there is trusted and the strict layer never applies.

The decision is stored per site as a marker file, and the matcher keys on its absence: a site whose marker is missing is served more strictly, never less. A security control has to fail on the safe side.

Violation reports as an abuse signal

Untrusted sites report violations to /_sitebin/csp-report. Sitebin counts them per site and keeps the distinct destinations that were blocked, capped and flushed on a timer so a hostile page cannot turn the endpoint into a write amplifier. A site whose first visitor trips form-action against a foreign host is almost always phishing — the counts and destinations show up in the instance register at /account/admin, with a filter, next to the delete button.

Separating user content from the app

Browsers draw most of their boundaries at the registrable domain, not the origin. If user sites live under the same registrable domain as the dashboard, an uploaded page can write cookies upward onto the app, SameSite treats navigations between them as same-site, and a phishing takedown against one site can put the app's own domain at risk. Set SITEBIN_VIEW_DOMAIN to a separate registrable domain and list that domain in the Public Suffix List. The hosted service does exactly this: the app is on app.sitebin.io, user sites are on sitebin.app.