Privacy & Security
What's sent off-site, libsodium-encrypted secrets, hashed-only logging, SSRF allowlist, and the AUTH_KEY rotation contract.
Compliance buyers read this page first. Tradeoffs are stated bluntly, defaults err on the side of "store nothing if you can avoid it," and the privacy posture is enforced by code rather than policy where possible.
What's sent off-site
Without AI: zero outbound calls
With no AI provider key configured and Turnstile disabled, OpenTrust makes zero outbound HTTP calls. No telemetry, no analytics, no licence checks, no font loads. The Inter variable font (SIL OFL 1.1) ships in assets/fonts/ so even Google Fonts is not contacted.
With AI: only on visitor questions
When a visitor submits a question on /trust-center/ask/, OpenTrust calls the configured AI provider's chat endpoint with:
- The visitor's question text (capped at 1000 chars).
- A slim corpus index of your published trust-center content.
- The specific documents the model retrieves via tool calls.
- The conversation history within that session.
No visitor PII is forwarded. OpenTrust does not collect IPs, user agents, referers, or session IDs in plaintext, so it cannot transmit them.
With Turnstile: only on the first message of a session
When Turnstile is enabled, the chat page loads https://challenges.cloudflare.com/turnstile/v0/api.js to render the challenge widget, and OpenTrust calls https://challenges.cloudflare.com/turnstile/v0/siteverify server-side on the first message of each visitor session to verify the token. Successful verification grants a 1-hour bypass transient.
The token itself is opaque and contains no personal data.
Encrypted secrets
API keys (Anthropic, OpenAI, OpenRouter) and the Turnstile secret key are encrypted at rest with libsodium secretbox. The encryption key is derived from wp_salt('auth'), so it is never stored in the database alongside the ciphertext.
| Where | What |
|---|---|
opentrust_provider_keys option (autoload off) | Map of {provider: ot_enc_v1:base64-ciphertext}. |
opentrust_settings.turnstile_secret_key | Single ot_enc_v1: ciphertext blob. |
The plaintext exists only in memory during the request that decrypts it. The settings UI never shows the plaintext after save, only a masked fingerprint.
AUTH_KEY rotation
Rotating the WordPress AUTH_KEY constant invalidates every encrypted secret OpenTrust has stored. After a rotation, the AI chat refuses to start until you re-enter every provider key and the Turnstile secret. This is the intended contract.
The reasoning: a database-only leak (a stolen wp_options dump, a stolen wp_postmeta dump) does not leak AI keys, because AUTH_KEY lives in wp-config.php outside the database. Rotating AUTH_KEY is the recommended response to a database compromise, and OpenTrust's secrets follow that contract by becoming unreadable.
After a rotation:
- Open OpenTrust → Settings → AI Chat.
- Paste each provider key again. Save.
- Re-enter the Turnstile secret key if you use Turnstile. Save.
OpenTrust will surface admin notices indicating which keys are unreadable until you re-enter them.
Hashed-only logging
The wp_opentrust_chat_log table has no column capable of holding a raw IP, email, session ID, user agent, or referer. The schema is:
id BIGINT UNSIGNED PRIMARY KEY
created_at DATETIME
session_hash CHAR(16) -- truncated salted hash
ip_hash CHAR(16) -- truncated salted hash
question TEXT -- visitor text, capped at 1000 chars
model VARCHAR(100)
provider VARCHAR(32)
tokens_in INT
tokens_out INT
citation_count INT
response_ms INT
refused TINYINT(1)
tool_turns TINYINT
tool_names VARCHAR(255)Hashes use the per-site opentrust_site_salt (auto-generated on first need). A hash is a 16-character prefix; collisions are tolerable because the column is for analytics, not authentication.
A daily cron (opentrust_chat_log_purge) drops rows older than 90 days. The retention period is fixed in v1.x; if you need shorter retention, disable logging and rely on your provider-side dashboards for cost monitoring.
The privacy posture is enforced by the schema itself, not by good intentions.
SSRF allowlist
Every outbound HTTP call goes through wp_safe_remote_* wrappers in OpenTrust_Chat_Provider, gated by a per-provider allowlist of permitted hosts:
| Provider | Allowed hosts |
|---|---|
| Anthropic | api.anthropic.com |
| OpenAI | api.openai.com |
| OpenRouter | openrouter.ai |
| Turnstile (always) | challenges.cloudflare.com |
Citations emitted by the model are validated against your corpus's URL allowlist before being shown. The model cannot fabricate a citation pointing at an arbitrary external URL.
Capability and nonce checks
Every admin action requires manage_options and verifies a _wpnonce. Every CPT meta save handler calls current_user_can('edit_post', $post_id) and verifies a per-CPT save nonce.
The chat REST endpoint requires:
- A valid
X-WP-Nonceof actionwp_rest. - A valid Turnstile token if Turnstile is enabled and the session is not yet bypass-verified.
- A passing per-IP sliding-window rate limit.
- A passing per-session sliding-window rate limit.
Token-budget reservation happens inside the handler after all four gates pass.
Bot defence
Three independent layers, in order:
- Per-IP and per-session rate limits. Hashed identifiers, sliding windows, configured in AI Chat → Settings.
- Cloudflare Turnstile. Optional. Verifies the first message of each session, then grants a 1-hour bypass.
- Token budgets. Hard daily and monthly ceilings. Even an unlimited-bot scenario stops at the daily cap.
The combination is intentionally redundant. A single misconfigured layer cannot blow up the AI bill.
What's stored about visitors
In summary, OpenTrust stores the following about a visitor who uses the AI chat:
- A truncated salted hash of their session token (
session_hash). - A truncated salted hash of their IP (
ip_hash). - The text of their question (
question, capped at 1000 chars). - Aggregate token counts and the model that answered.
It does not store the raw IP, the user agent, the referer, the session token, the cookie, or any plaintext identifier. The hashed columns are not reversible.
If you disable logging, even those hashed rows are not written.
What's stored about admins
A single user-meta flag per admin: _opentrust_review_notice_dismissed. Records that an admin dismissed the one-time wp.org review prompt. No other admin-side data is stored beyond the standard WordPress user record.