Security
A complete description of how ParallelPath protects data - in transit, at rest, in use - and what it can't protect against. Everything here can be checked against the source code running on this server. For how it has been tested and what was found, see the Security testing report.
- Security
- Summary
- 1. Encryption in transit
- 2. Encryption at rest
- 2b. What administrators can and can't see
- 3. Accounts and sign-in
- 4. Access control
- 5. Integrity: nothing disappears
- 6. Isolation ("jailing")
- 7. Web application hardening
- 8. Server hardening
- 9. AI Tone Check and privacy
- 10. Limits - what this can't protect against
- Reporting a vulnerability
Summary
| Layer | Protection |
|---|---|
| In transit (browser <-> server) | HTTPS only: TLS 1.2/1.3, modern ciphers, HSTS, HTTP redirected to HTTPS |
| In transit (inside the server) | Unix sockets / loopback only - nothing crosses a network in clear; remote AI providers must use HTTPS; email uses STARTTLS/TLS; backup copies use SFTP |
| In transit (calls) | WebRTC DTLS-SRTP, end-to-end between the two devices; the relay can't decrypt |
| At rest (content) | AES-256-GCM with a separate key per family; per-file keys; every sensitive field encrypted |
| At rest (personal details) | Usernames, emails, names, IP addresses, devices, notifications - all encrypted columns (keyed blind index for sign-in lookups) |
| At rest (whole store) | Required LUKS2 volume (argon2id) holding the database, file store and backups work area; the app refuses to start without it |
| Backups | Encrypted data inside an encrypted archive; keys only in a passphrase-protected recovery bundle |
| Administrators | Separate portal; can't read family content or personal details; can't take over accounts without the person's consent |
| Keys | systemd encrypted credentials (TPM2-sealed when available); never in the database or on disk in plain form |
| Accounts | argon2id passwords, mandatory two-step verification, lockout, rate limiting, short sessions |
| Integrity | Messages immutable; append-only evidence tables enforced by the database; hash-chained audit trail |
| Isolation | Dedicated system user, systemd sandbox, AppArmor, least-privilege database roles, localhost-only app |
| Web hardening | Strict Content-Security-Policy, cross-site request blocking, secure cookies, no third-party content |
| Server | Firewall, fail2ban, automatic security updates, encrypted backups |
1. Encryption in transit
- HTTPS everywhere. nginx serves only TLS 1.2 and 1.3 with forward-secret ECDHE ciphers (AES-GCM, ChaCha20-Poly1305). Port 80 only serves Let's Encrypt challenges and redirects to HTTPS.
- HSTS (
max-age2 years) is enabled once a real certificate is installed, so browsers refuse plain-HTTP connections. - Certificates come from Let's Encrypt (auto-renewed), an existing certificate you choose, or your own. The installer checks expiry, name coverage and key match.
- Live updates use a WebSocket over the same TLS connection. The server checks the
Originheader and the session before accepting it. - Behind nginx, the app listens only on
127.0.0.1; it is never directly reachable from the internet. - Database traffic never touches the network: PostgreSQL is used over a local Unix socket.
- Email (optional) is sent with STARTTLS/TLS and never contains family content - only "you have a new message, sign in to read it".
Calls
- Audio and video are WebRTC, encrypted with DTLS-SRTP between the two browsers. Keys are negotiated by the browsers themselves.
- ParallelPath only carries signalling (connection setup) over the authenticated WebSocket, and only between the two people on that call.
- When a direct connection isn't possible, media is relayed by the server's own coturn relay. The relay forwards encrypted packets and cannot decrypt them. Relay credentials are short-lived HMAC tokens (6 hours) issued only to signed-in users. The relay refuses to forward into private/internal networks.
- No third-party call service is involved.
- Recordings only happen if the caller requests it and the other person consents. Each browser records what it saw and uploads 5-second chunks, which are encrypted on arrival and combined into one encrypted file in shared documents.
2. Encryption at rest
Envelope encryption
master key (32 bytes) -- systemd encrypted credential, TPM2-sealed if available; never in the database
|-- family data key (one per family, random) -- stored only in wrapped (encrypted) form
| |-- every sensitive database field AES-256-GCM, bound to table.column:row-id
| |-- per-file keys (one per file version) -- wrapped by the family key
| |-- file blobs on disk AES-256-GCM in authenticated 1 MiB chunks
|-- system key (random) -- for data belonging to no family (2FA secrets, settings secrets, feedback)
- AES-256-GCM (authenticated encryption) everywhere, with a fresh random nonce per value.
- Associated data binds each ciphertext to its exact place (
family | table.column:row-id). Copying an encrypted value into another row or family makes it fail to decrypt. - Files are encrypted in 1 MiB chunks, each authenticated with its position and a "final chunk" flag, so truncation, re-ordering or tampering is detected. A SHA-256 of the original file is recorded for integrity and reports.
What is encrypted
Message subjects and bodies, amendments and reasons; event titles, notes and locations; file and folder names and contents; every file version; expense descriptions, notes and payment references; info-bank entries; journal entries; check-in locations and notes; child names and dates of birth; family names; swap notes; restriction notes; audit-trail reasons and details; report files; call recordings; 2FA secrets and recovery codes; AI provider API keys; problem reports.
Personal details are encrypted too
Usernames, email addresses, display names, IP addresses, device/browser strings, notification texts, membership titles, invitation emails and problem-report subjects are stored as AES-256-GCM encrypted columns (system key). Sign-in still works because usernames and emails also have a keyed blind index (HMAC-SHA256 with a key derived from the master key): the database can match an exact value without ever holding it.
An automated test writes known values through the app and then scans the raw database file and the file store byte-by-byte to prove none of them appear in clear.
What is left as plain values - inside the encrypted volume
Timestamps, record types and states (e.g. "expense: approved"), amounts and dates (so balances and calendars can be computed) and category labels are stored as plain values so the database can sort and total them. They are still encrypted at rest by the mandatory LUKS volume below, and hold no names or content.
Full-volume encryption (required)
Every installation (direct or Docker) creates a LUKS2 encrypted volume (argon2id key derivation) that holds the PostgreSQL data and the file store. Its key is a systemd encrypted credential (TPM2-sealed when available) and it unlocks automatically at boot. A separate LUKS recovery passphrase goes into the encrypted recovery bundle. Encryption can't be switched off: on start-up the app checks that its data folder is on a dm-crypt device and refuses to run if it isn't.
Logs
The web server's access log keeps only an anonymised address (last IPv4 octet / IPv6 host part removed), no query strings and no referrers, and URLs that carry secret tokens (invitations, password resets, calendar feeds) are not logged at all. Full IP addresses are only used in memory for rate limiting and briefly in the system journal for fail2ban.
Keys
- The master key, backup key, call-relay secret, vault key and SMTP password are systemd encrypted credentials in
/etc/parallelpath/creds(root-only). They are sealed with the host credential key and, where a TPM2 chip exists, with the TPM - so the files are useless if copied to another machine. - systemd decrypts them into memory only for the specific service that needs them (
LoadCredentialEncrypted). The app's AppArmor profile explicitly denies access to the credential files themselves. - Admin commands (
ppctl) run inside a transient sandboxed unit so the key is never exposed to your shell. - Rotation:
sudo ppctl rotate-master-keyre-wraps every family key and the system key under a new master key without re-encrypting data.
Backups and recovery keys
Each backup (database dump + encrypted file store) is encrypted again as a whole with AES-256-GCM (chunked, authenticated) using the backup key - so a backup is encrypted data inside an encrypted archive. Backups can be copied to a NAS/USB folder or another server over SFTP, verified and downloaded by admins, and are useless without the keys.
The keys (master, backup, LUKS recovery) are kept in a recovery bundle encrypted with the administrator's passphrase (PBKDF2-SHA256, 600,000 iterations + AES-256-GCM). The installer creates it from keys held only in memory and copies it to the backup destinations. It can be opened offline in the browser at /decrypt-export.
Parents' own exports
Parents can download everything that's theirs (Account > Your data). With a passphrase, the download is encrypted the same way (PBKDF2 + AES-256-GCM, .ppexport), so the copy on their device is encrypted at rest too; it opens in the browser at /decrypt-export - nothing is uploaded.
2b. What administrators can and can't see
Server administrators run the server; they are not part of any family.
| Admins can | Admins can't |
|---|---|
| See counts, server health, analytics (anonymised) | Open, list or decrypt any message, file, journal, event, expense, info-bank entry, check-in, call recording or report |
| See accounts as numbers with masked username/email, status and dates | See people's names, usernames or emails in clear (only confirm an exact value someone gives them) |
| Disable/enable accounts (the person is notified, with the reason) | Reset someone's 2FA, or receive a password-reset link, unless that person opted in to admin-assisted recovery |
| See that events happened in the audit trail (for integrity checks) | See who in a family did what, to which record, from which IP, or why |
| Create, schedule, verify, copy and download encrypted backups | Read anything inside a backup without the offline recovery bundle and its passphrase |
| Read problem reports | See who sent a report, unless the sender chose to share their username |
These rules are enforced in code and covered by automated tests that load every admin page and export and check that no family content, username or email appears.
3. Accounts and sign-in
- Passwords: argon2id (64 MiB memory, 3 passes). 12+ characters, common and username-based passwords rejected. Comparisons take constant time and unknown usernames are checked against a dummy hash so timing doesn't reveal which accounts exist.
- Two-step verification: TOTP (RFC 6238), required by default for everyone and always for admins. Codes can't be replayed. Ten single-use recovery codes are stored hashed.
- Brute-force protection: per-IP rate limits in the app and in nginx; accounts lock for 15 minutes after 8 failures; failures are logged for fail2ban, which bans repeat offenders (3 failures for the admin portal).
- No account enumeration: sign-in, registration and reset responses are identical whether or not an account exists.
- Sessions: 256-bit random tokens, stored only as SHA-256 hashes. Cookies are
Secure,HttpOnly,__Host-prefixed,SameSite=Lax(family) /Strict(admin). Idle timeout 30 min (admin 20), absolute 12 h (admin 4). Tokens rotate after two-step verification. Password changes sign out other devices. Users can see and revoke sessions. - Invitations and resets are single-use, expire (7 days / 24 hours) and are stored hashed.
4. Access control
- Every request is checked against the person's membership of the family and their role: parent, family member, child, or professional (read-only, limited to chosen areas, optionally time-limited).
- Private spaces (private files, private info-bank entries, private journal entries, private events) are only ever returned to their owner.
- Families are isolated: every query is scoped to the active family, and encryption keys differ per family, so even a logic bug couldn't decrypt another family's data with the wrong key.
- Server admins have no route that decrypts family content.
- Professionals' views of messages and journals are recorded in the audit trail.
5. Integrity: nothing disappears
- Messages are immutable. The database refuses changes to a message's body, sender, thread or timestamp, and refuses deletion. Amendments and retractions are separate records with reasons.
- Append-only evidence tables: audit events, revisions, message revisions, read receipts, file versions, sign-in events, call recordings and reports. The runtime database role isn't granted UPDATE/DELETE on them, and database triggers reject it anyway.
- No hard deletes of family records - deletion means "moved to the recycle bin" with a reason; every version remains in the history.
- Every change requires a reason and stores a full encrypted snapshot (revision), so you can see exactly what changed, when, by whom and why.
- Hash-chained audit trail: each event stores
SHA-256(previous hash + this event). Changing, removing or re-ordering any event breaks the chain. Verify any time in the admin portal or withsudo ppctl verify-audit. A daily anchor of the chain head is written to the system journal and an anchor file, so even a full re-chain is detectable. - Reports are fingerprinted (SHA-256) and verifiable at /verify without revealing content.
6. Isolation ("jailing")
The web application:
- runs as a dedicated
parallelpathsystem user with no shell and no home; - has its code installed read-only (owned by root);
- is confined by a systemd sandbox: read-only filesystem except its data folder, private
/tmp, no devices, no new privileges, no capabilities, restricted system calls (@system-serviceminus privileged groups), restricted address families, protected kernel/clock/hostname/cgroups, hidden other processes, memory and task limits; - is confined by an AppArmor profile that only allows its code, Python runtime, data folder, the PostgreSQL socket and its own credentials - and explicitly denies shells, network tools,
/root,/home, SSH keys, TLS private keys and the credential store; - connects to PostgreSQL as
pp_app, which can't change the schema or rewrite evidence. Schema changes use a separatepp_ownerrole reachable only by root. Authentication is peer (OS user to role mapping) over the local socket - there are no database passwords to steal; - listens only on 127.0.0.1.
Privileged tasks (metrics, fail2ban, backups) are done by a separate root helper that only accepts small, re-validated JSON requests from a one-way drop folder, never uses a shell, refuses to ban protected addresses and writes its outputs without following symlinks.
7. Web application hardening
- Content-Security-Policy: scripts only from this site (no inline scripts), no plugins, no framing (
frame-ancestors 'none'),base-uri 'none', forms only to this site. - Cross-site request forgery: state-changing requests must come from this site (checked via
Origin/RefererandSec-Fetch-Site) in addition to SameSite cookies. - Other headers:
X-Content-Type-Options: nosniff,X-Frame-Options: DENY,Referrer-Policy: same-origin,Cross-Origin-Opener-Policy,Cross-Origin-Resource-Policy, a restrictivePermissions-Policy, andCache-Control: no-storeon private pages. - Output escaping: all templates auto-escape; JSON embedded in pages is escaped.
- Uploads: names sanitised, size-limited (nginx and app), stored encrypted under random IDs, served with
nosniff, a download disposition by default, and a sandboxing CSP for inline previews (only for images, PDF, audio/video and plain text). - CSV reports are protected against spreadsheet formula injection.
- No third-party content: no CDNs, fonts, analytics or trackers. The service worker caches only public static files.
8. Server hardening
- Firewall (ufw): only SSH, 80, 443 and (if calls are on) the TURN ports.
- fail2ban: separate jails for family sign-in, admin sign-in, nginx rate-limit violations and SSH.
- Automatic security updates (unattended-upgrades).
- Admin portal IP allow-list (optional) enforced by nginx.
- The installer never weakens existing services when installed alongside other sites.
9. AI Tone Check and privacy
- The built-in checker runs on the server with word lists and simple rules. Nothing leaves the server.
- AI providers (Ollama, OpenAI-compatible servers, Anthropic Claude) are optional and off until an admin adds one and accepts a privacy acknowledgement. They receive the draft text only, and only over HTTPS (plain HTTP is refused except to the server itself). A self-hosted Ollama/LM Studio server keeps everything on hardware you control.
- Results are bound to the exact text checked (HMAC), so a tampered score can't be attached to a message.
- Users can see which provider is used and can switch Tone Check off.
10. Limits - what this can't protect against
Being honest about the threat model matters:
- This is not end-to-end encryption between parents. The server holds the keys so it can show your records, generate reports and run reminders. Someone with root access to the running server could, with effort, extract keys from memory. Choose a trustworthy operator - or run it yourself.
- Compromised devices. If a phone or computer signed in to ParallelPath has malware, it can see what that person sees.
- People you share with. Anyone you give access to (or who you show your screen) can take copies.
- The admin portal is need-to-know, not the server's root account. The portal hides content and personal details (section 2b), but someone with root on the running server could inspect memory or the running database. The protections against that are operational: choose a trustworthy operator, restrict root access, and keep the recovery bundle and passphrase separate from the server.
- Beta software. It has been tested, but no software is bug-free. See the disclaimer.
Reporting a vulnerability
Please report security issues privately to the server's administrator and to the project at darkciphersystems.com. Please don't publish details until a fix is available.