Frame docs · Reference

Security

Frame is built to be secure by being small: few moving parts, few dependencies, and nothing that parses untrusted media on the server. This page is the operator's summary; the full model is in SECURITY.md.

Who is trusted

  • You, the server operator
    Trusted?:Yes, fully
    What they can see or do:Everything stored on your server: photos, videos, captions, messages, reactions, friend lists, e-mail addresses, phone numbers and IP addresses. That's the self-hosting deal.
  • Other users on the same server
    Trusted?:No
    What they can see or do:Only moments their friends addressed to them, chats with friends, and public profiles.
  • Network attackers
    Trusted?:No
    What they can see or do:With HTTPS, endpoints, timing and sizes only.
  • The push relay (Crow Tech)
    Trusted?:No
    What they can see or do:Opaque relay tokens, ciphertext sizes, timing and which server IPs push. No content, no user identities.
  • Apple and Google push services
    Trusted?:No
    What they can see or do:Device tokens, timing, ciphertext and the generic text "Frame: New activity".
  • R2 or another bucket provider
    Trusted?:No
    What they can see or do:Encrypted objects, their sizes and access times.
  • A tunnel or CDN (such as Cloudflare Tunnel)
    Trusted?:Partially
    What they can see or do:It terminates TLS, so it can technically see the traffic. Built-in HTTPS avoids this.

What the server does

  • HTTPS for anything on the internet, from Frame itself (Let's Encrypt), Caddy or a tunnel. The apps refuse plain http:// except for local network addresses.
  • Passwords are hashed with Argon2id. Sign-in is rate limited per IP and per account, and an unknown account takes as long to reject as a wrong password, so sign-in can't be used to find accounts.
  • Apple and Google sign-ins are bound to your server's name (FRAME_PUBLIC_URL) and work once, so a token someone obtained for another Frame server can't be replayed against yours.
  • Sessions are random tokens stored only as hashes, revocable per device. Changing a password signs out every other session.
  • Authorisation: every object is checked against the requesting user. Things you can't see answer 404, not 403, so their existence isn't revealed.
  • No server-side media processing. The server checks file types and sizes and stores the bytes; it never decodes images or video. Thumbnails are made on the phone, and the apps strip location and other metadata before upload.
  • Media URLs are signed and expire after 24 hours, and responses tell shared caches not to keep copies.
  • Storage encryption: with R2 or S3, every object is encrypted on the server before upload, with a key in secrets.json.
  • Push payloads are end-to-end encrypted between your server and each phone (Push notifications).
  • Logs never contain passwords, tokens or message text, and Frame's access log records route patterns, not paths or query strings. Invite codes are part of the path, so a reverse proxy's or tunnel's own access log does contain them.
  • Few dependencies: the server uses the Go standard library plus a pure-Go SQLite and golang.org/x/crypto; no web framework, ORM or JWT library. Releases ship SHA256SUMS, and the installer refuses a file whose checksum doesn't match.

Hardening

Linux installer (frame.service): a throwaway system user (DynamicUser) that can write only its data directory; ProtectSystem=strict, ProtectHome, PrivateTmp, PrivateDevices; NoNewPrivileges and no capabilities, except binding ports 80/443 when FRAME_DOMAIN needs them; MemoryDenyWriteExecute, restricted address families and namespaces, a system-call filter and UMask=0077. The config file is root-only and read by systemd before privileges are dropped.

Docker: a distroless image with no shell or package manager, running as uid 65532 on a read-only root filesystem with all capabilities dropped and no-new-privileges. In the Caddy setup, the Frame container publishes no ports.

What Frame deliberately doesn't do

  • No end-to-end encryption of photos and messages against the operator. You can read everything on your server. It keeps the system simple, fast and recoverable; your friends are trusting you.
  • Metadata is visible to the operator: who is friends with whom, who posted when and to whom, reactions, views, read receipts and IP addresses.
  • Recipients can keep what you send: screenshots, saved photos and widget caches can't be prevented.
  • A signed media URL works for anyone who has it until it expires (24 hours). Deleting a moment deletes the file, which ends access immediately.
  • Deletion is best-effort beyond the server: earlier backups still contain deleted content.
  • No automatic moderation or content scanning. Members can block people and report content to you, the operator; nothing goes to Crow Tech or anyone else.
  • E-mail addresses and phone numbers aren't verified (Frame sends no SMS). On an open server someone could register another person's number first; invite-only servers, the default, make this practically impossible.
  • A small server can be overwhelmed. Rate limits protect against abuse, not volumetric attacks.
  • Servers don't talk to each other. A user on one server can't see another server's content.

Checklist for your server

  • To do: HTTPS works: https://your-domain/healthz shows a valid lock.
  • To do: FRAME_PUBLIC_URL is set to your address(es).
  • To do: FRAME_TRUSTED_PROXIES matches your setup: unset with built-in HTTPS, only your proxy's address otherwise.
  • To do: Registration is invite (the default) or closed, unless you really want an open server.
  • To do: You created your admin account with the setup link and turned on two-factor authentication in the panel (Security).
  • To do: Consider FRAME_ADMIN=local or FRAME_ADMIN_LISTEN with an SSH tunnel if you don't need the admin panel on the internet (Keep the admin panel private). That also turns off Server admin in the apps, unless you set FRAME_ADMIN_APP=on.
  • To do: With FRAME_ADMIN=local, a reverse proxy or tunnel on the same host is listed in FRAME_TRUSTED_PROXIES.
  • To do: Only ports 22, 80 and 443 are open (or none, with a tunnel). You log in with SSH keys, not passwords.
  • To do: Automatic OS security updates are on (sudo apt install unattended-upgrades).
  • To do: Nightly backups of the database and secrets.json are copied off the server, and you've tried a restore once.
  • To do: frame.conf stays private (chmod 600 with Docker) and is never committed or shared: it can contain R2 and SMTP credentials.
  • To do: You upgrade when a release is announced.

Report a vulnerability

Please report security issues privately through GitHub private vulnerability reporting on Crow-Tech-Pte-Ltd/frame (Security tab → Report a vulnerability), or by e-mail to [email protected] with the subject "Security". Don't open public issues for vulnerabilities. We aim to acknowledge reports within 3 working days. Test only against servers you run yourself.

For AI tools: this page as Markdown, the page index and all pages in one file.

WhatsApp