Skip to main content

Security

AreaWhat is in place
Sign-inPasswords hashed with bcrypt (cost 10). Sessions are JWTs signed with JWT_SECRET, valid 30 days, sent as Authorization: Bearer.
Proving the phoneEither an OTP from Twilio Verify (SMS channel only, never calls), or an incoming JOIN text whose sender number Twilio reports.
JOIN codes6 characters from an alphabet without look-alikes, single use, 10-minute expiry, optionally tied to the number typed on the page.
Twilio webhookEvery request must carry a valid X-Twilio-Signature (HMAC-SHA1 over the public URL and sorted form fields with the auth token), compared in constant time. Unsigned requests get 403. At most 5 JOIN texts per number per hour.
Internal callsThe mail service is reachable only on the compose network. Every call needs X-Internal-Token (constant-time compare) and names the user in X-User-Id; only the api knows the token. nginx returns 404 for /api/internal/.
ExposureThe api listens on 127.0.0.1 in production; PostgreSQL publishes no ports; the mail service's SMTP-in port is on 127.0.0.1 only. Only nginx (80/443) and Postfix (25) are public.
TransportTLS by Let's Encrypt; HSTS; X-Content-Type-Options: nosniff; Referrer-Policy: same-origin.
Privacy in the dataBcc recipients are visible only to the sender and to themselves; reads are always filtered through the reader's own pointers.
AliasesUnique across users; names of ordinary mailboxes on the same server are reserved.
Password changeNeeds the current password, unless the account has none yet or the person signed in with an SMS code in the last 15 minutes (the "forgot password" path).
EmailPostfix refuses unknown recipients during the SMTP conversation (no backscatter). Mail claiming to come from a PhoneMail number but arriving from outside is rejected. Outgoing mail is DKIM-signed; SPF and DMARC (p=quarantine) are published.
Webmail portsIMAP, submission and the password endpoint listen only on the Docker bridge address, for Roundcube; failed logins wait one second. Submission only sends as the signed-in address.
Secrets.env (600) and /etc/phonemail/twilio.env (640) stay on the Pi, never in the repository or these docs.
Brute forceThe Pi's fail2ban protects SSH, IMAP, SMTP login and webmail; PhoneMail's own login does not yet have a per-number lockout (see Roadmap).

Known gaps​

  • No rate limit yet on password login or OTP start per number or IP.
  • Tokens cannot be revoked before they expire (no server-side session list).
  • The Twilio auth token should be rotated (Twilio Console → API keys & tokens) before a public launch, and the file on the Pi updated.