Ettic Docs
OpenTrust

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.

WhereWhat
opentrust_provider_keys option (autoload off)Map of {provider: ot_enc_v1:base64-ciphertext}.
opentrust_settings.turnstile_secret_keySingle 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:

  1. Open OpenTrust → Settings → AI Chat.
  2. Paste each provider key again. Save.
  3. 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:

ProviderAllowed hosts
Anthropicapi.anthropic.com
OpenAIapi.openai.com
OpenRouteropenrouter.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:

  1. A valid X-WP-Nonce of action wp_rest.
  2. A valid Turnstile token if Turnstile is enabled and the session is not yet bypass-verified.
  3. A passing per-IP sliding-window rate limit.
  4. 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:

  1. Per-IP and per-session rate limits. Hashed identifiers, sliding windows, configured in AI Chat → Settings.
  2. Cloudflare Turnstile. Optional. Verifies the first message of each session, then grants a 1-hour bypass.
  3. 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.

On this page