Skip to content

Secrets & API Key Storage

Pinchy handles your LLM provider API keys — Anthropic, OpenAI, Ollama Cloud, and others. Here’s exactly where they live, how they move, and what the limits of that protection are.

API keys are stored in PostgreSQL, encrypted with AES-256-GCM. The encryption key normally comes from the ENCRYPTION_KEY environment variable. If you leave it unset, Pinchy auto-generates one at first start and persists it in the pinchy-secrets Docker volume so it survives restarts — production deployments should still set ENCRYPTION_KEY explicitly so the key is part of your backup/restore story rather than tied to a single volume.

Each row is encrypted with its own nonce and authenticated separately. A single decryption failure (e.g., a corrupted row) does not hide every other integration’s credentials — the others stay readable.

When Pinchy reads a key from the database, it decrypts it in memory to reconstruct the OpenClaw config. The plaintext key is never written back to disk by Pinchy itself.

Your database contains ciphertext. Without the ENCRYPTION_KEY, the stored values are unreadable.

openclaw.json — the config file that OpenClaw reads — doesn’t contain any actual keys. Instead, it uses SecretRef pointers:

{
"models": {
"providers": {
"anthropic": {
"apiKey": {
"source": "file",
"provider": "pinchy",
"id": "/providers/anthropic/apiKey"
}
}
}
}
}

OpenClaw resolves these pointers at runtime by reading from the secrets file. The config file itself can be inspected, backed up, or committed to version control without exposing any secrets.

At startup — and every time you change a provider or integration — Pinchy calls regenerateOpenClawConfig(), which:

  1. Reads all relevant rows from PostgreSQL
  2. Decrypts each value in memory
  3. Writes the decrypted keys to /openclaw-secrets/secrets.json

That path is a Docker tmpfs mount. tmpfs is RAM-based storage — it’s never written to disk, never included in Docker volume exports, and disappears on container restart.

# docker-compose.yml (excerpt)
openclaw-secrets:
driver: local
driver_opts:
type: tmpfs
device: tmpfs
o: "mode=0770,uid=999,gid=999"

The directory is owned by uid/gid 999 (the pinchy system user inside the Pinchy container) with mode 0770. Pinchy writes secrets.json as the owner; OpenClaw runs as root in its own container and reads the file regardless of ownership. Inside, secrets.json is written with mode 0600 (owner read/write only) as defense-in-depth: even a same-uid process that obtained directory access cannot read another tenant’s file.

  • Root filesystem analysis — an attacker with read access to your Docker volume storage won’t find plaintext keys in /var/lib/docker/volumes/
  • Docker volume exportsdocker run --volumes-from or a volume backup contains no secrets because tmpfs doesn’t back up
  • Container image inspection — keys are never baked into the image layer
  • openclaw.json leaks — the config file is safe to share or inspect

tmpfs is RAM. If someone has access to the running process or the host, they can reach the data:

  • Root access to the host — a host root user can read any container’s memory via /proc/<pid>/mem or ptrace
  • Memory dumps — a core dump or crash report may contain key material
  • Container escape — if the OpenClaw container is compromised, secrets.json is readable from inside
  • docker exec access — anyone who can docker exec into the container can read the file

These are infrastructure-level threats. Protect against them at the host level: locked-down SSH, minimal docker exec permissions, and host-level disk encryption for swap (RAM can spill to swap). See the Hardening Guide for recommendations.

How credentials reach plugins: three patterns

Section titled “How credentials reach plugins: three patterns”

Different consumers reach their secrets through different paths. Knowing which pattern applies is useful when something looks wrong in openclaw.json.

Pattern A — OpenClaw resolves SecretRef pointers. Used for models.providers.<name>.apiKey and any env.<VAR> template. The flow above describes this pattern in full: ciphertext lives in PostgreSQL, plaintext lands in the tmpfs secrets.json, OpenClaw walks the pointer at call time. Provider API keys (Anthropic, OpenAI, Google, etc.) all use this path.

Pattern B — Pinchy plugins fetch credentials at call time. Used by every Pinchy-built plugin that talks to a third-party SaaS: pinchy-odoo, pinchy-email, pinchy-web. openclaw.json only carries the plugin’s apiBaseUrl, the gateway auth token, and an opaque connectionId. The plugin calls GET /api/internal/integrations/:connectionId/credentials with the gateway token as Bearer auth when it actually needs the credential, caches it in-process with a 5-minute TTL, and invalidates the cache on a 401 so credential rotation works without restarts. The credential itself never appears in openclaw.json.

Pattern C — Bootstrap credentials in openclaw.json. Used only for gateway.auth.token and the matching plugins.entries.pinchy-*.config.gatewayToken. These are the trust root for the OpenClaw container and cannot be fetched through Pinchy’s API by definition — they are written in plaintext into openclaw.json on purpose. The openclaw-plaintext-scanner validates that no other provider key prefixes have leaked into the config file. Rotate by regenerating the config (any change in Settings → Providers) and restarting the OpenClaw container.

The defense-in-depth scan that runs at config-write time recognises known provider-key prefixes (sk-ant-, sk-, etc.) and refuses to write the config if any appear outside the legitimate Pattern A SecretRef slots.

If you suspect an API key has been exposed, rotate it at the provider and update it in Pinchy via Settings → Providers. Pinchy will write the new encrypted value to PostgreSQL and regenerate the secrets file immediately.

If you find a security issue, please report it via the process described in SECURITY.md. We take security reports seriously and aim to respond within 48 hours.