Security and secrets
Every credential you give CLItrail is encrypted and kept in a secret store apart from the main database, under keys that live apart from both. Tokens that only need to be recognised are stored as hashes that cannot be reversed.
#Write-only credentials
After you save an API secret, access token or key, the dashboard and API show only that it is set, its last four characters (for values of 12 characters or more), an 8-character fingerprint and when it was updated. Forms never prefill a secret. Replace saves a new value; removing a destination or log stream deletes its credentials. Webhook and log-stream signing secrets are generated by CLItrail and shown once, when created or rotated.
#A separate secret store
- Apart from the main database. Sealed credentials live in their own store: a separate SQLite-backed Durable Object class,
CLItrailVault, with its own namespace and migration history. The main database keeps only an opaque reference (sec_…) and the display details above. - Reachable only by the service. The store serves no HTTP. The service reaches it over Cloudflare’s internal RPC, and every call must carry a token derived from the master keys; anything else is refused. It stores only encrypted envelopes and refuses anything that is not one, so plaintext can never reach it.
- Encryption. Each value is sealed with AES-256-GCM under a key derived with HKDF-SHA256 from the master key, and bound as authenticated data to its reference and to its record, such as
destination:<website>:<destination>. Moved to another reference or read for another record, it fails to decrypt. - Keys apart from both. The master keys are platform secrets, never stored in either database. The service refuses to start in production without them. Keys can be rotated without downtime: envelopes under an older key are re-sealed under the new one.
- Deletion is real. Deleting or replacing a secret first overwrites its envelope with zeros, then removes it from the store.
- Used briefly. Credentials are decrypted only when needed, for one destination or stream at a time, and dropped afterwards. Google access tokens made from a service account key stay in memory until they expire and are never stored.
#Hashed or encrypted
| Value | At rest |
|---|---|
| Destination credentials: GA4 API secrets, Google Ads service account keys, Meta, TikTok and X Ads access tokens, X Ads consumer keys and token secrets | Encrypted, in the secret store |
| Webhook signing secrets, log-stream signing secrets, S3 access keys | Encrypted, in the secret store |
| Session tokens, visit receipts, handoff tokens | SHA-256 hash; the value is never stored |
| Invitation links | Keyed hash (HMAC-SHA256); the link is shown once |
| IP addresses and networks, for rate limits and reconstruction | Keyed hash, per website; the address is never stored |
| Receipts, handoff tokens and IP addresses in Logs | Keyed hash |
| The operator’s admin token | Compared in constant time, never stored in a database |
The test suite checks this: it stores known credential values, then scans every table of both stores and fails if any value appears in plaintext.
#In transit
The service refuses any request that carries a credential, or would reveal a generated one, with secrets_require_https unless it arrives over https. Platform error messages are scrubbed of known secret values and common credential shapes before they are stored or shown, and Logs redact credentials in every recorded request and response.
#No connect flows
No destination is connected by signing in to the platform: you paste credentials you created and control, and you can revoke them on the platform at any time. CLItrail holds no refresh tokens for your ad accounts.
#The web app
- Sign-in is Google or a passwordless email link, through Firebase; CLItrail sessions are
HttpOnly,SameSite=StrictandSecurecookies, stored as hashes. Dashboard API writes must come from the service’s own origin. - Every response carries
X-Frame-Options: DENY,Referrer-Policy: strict-origin-when-cross-origin, a restrictivePermissions-Policy,X-Content-Type-Options: nosniff,Cross-Origin-Opener-Policy: same-origin(same-origin-allow-popupson the dashboard, for the Google sign-in popup) andStrict-Transport-Security. Pages are served with a Content Security Policy that allows only the service’s own scripts and styles, plus, on the dashboard, the Google origins sign-in needs. - The Firebase sign-in code is served from CLItrail itself, not from a third-party script origin.
#Report a security issue
Email founders@pilotprotocol.network with what you found and the steps to reproduce it. Never include live credentials; rotate any you shared by mistake.