Securing OpenClaw with 1Password: No More Plaintext API Keys
I’ve been running OpenClaw on my home server for a few weeks now. It connects to Discord, Telegram, ElevenLabs for TTS, various LLM providers — and every single API key was sitting in plaintext in openclaw.json.
Not great. I knew it was there, I just hadn’t gotten around to fixing it. Today I did.
The problem
Run openclaw secrets audit --check and you’ll see exactly how bad it is:
Secrets audit: findings. plaintext=5, unresolved=0, shadowed=0, legacy=0.
- [PLAINTEXT_FOUND] openclaw.json:channels.discord.token
- [PLAINTEXT_FOUND] openclaw.json:channels.telegram.botToken
- [PLAINTEXT_FOUND] openclaw.json:messages.tts.elevenlabs.apiKey
- [PLAINTEXT_FOUND] auth-profiles.json:profiles.anthropic:default.token
- [PLAINTEXT_FOUND] auth-profiles.json:profiles.google:default.token
Five secrets in plain sight. Anyone with read access to the config directory gets everything.
Why 1Password over environment variables
OpenClaw supports three secret sources: environment variables, files, and exec (running a command to fetch the value). I started with env vars — just export the keys in .bashrc and point SecretRefs at them. That works, but it has issues:
- Secrets still live on disk in
.bashrcor.envfiles - Every process on the system can read them via
/proc - No audit trail, no rotation, no access control
1Password with a service account gives you all of that. The secrets live in 1Password’s vault, the service account token is the only thing on disk, and you get access logs for free.
If you already pay for 1Password (I do), the cost is zero.
Setup
1. Create a service account
Go to my.1password.com → Developer → Infrastructure Secrets → Service Accounts.
Create one called something like openclaw-nexus. Grant it read access to a vault — I created a dedicated “OpenClaw” vault to keep things isolated.
Copy the token. It starts with ops_.
2. Store your secrets
Create API Credential items in the vault for each secret. I ended up with:
- Discord Bot token
- Telegram Bot Token
- ElevenLabs API Key
- LiteLLM Gemini API Key
- Google API Key
The field name matters — use credential (the default for API Credential items) so the op:// references work cleanly.
3. Set the service account token
On your server:
echo 'export OP_SERVICE_ACCOUNT_TOKEN="ops_your_token_here"' >> ~/.bashrc
source ~/.bashrc
Verify it works:
op vault list
op read "op://OpenClaw/Discord Bot token/credential"
4. Configure OpenClaw providers
Here’s where it gets slightly awkward. OpenClaw’s exec SecretRef id field doesn’t allow spaces in the pattern (^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$), so you can’t use op:// URIs with spaces directly as the ref ID.
The workaround: create one provider per secret, with the op:// path baked into the args:
{
"secrets": {
"providers": {
"default": { "source": "env" },
"op-discord": {
"source": "exec",
"command": "/usr/bin/op",
"args": ["read", "--no-newline", "op://OpenClaw/Discord Bot token/credential"],
"passEnv": ["OP_SERVICE_ACCOUNT_TOKEN"],
"jsonOnly": false
},
"op-telegram": {
"source": "exec",
"command": "/usr/bin/op",
"args": ["read", "--no-newline", "op://OpenClaw/Telegram Bot Token/credential"],
"passEnv": ["OP_SERVICE_ACCOUNT_TOKEN"],
"jsonOnly": false
}
}
}
}
Then reference them in the config:
{
"channels": {
"discord": {
"token": { "source": "exec", "provider": "op-discord", "id": "value" }
},
"telegram": {
"botToken": { "source": "exec", "provider": "op-telegram", "id": "value" }
}
}
}
The "id": "value" is just a placeholder — with jsonOnly: false, OpenClaw runs the command and uses stdout as the secret value directly.
Repeat for each secret. Yes, it’s one provider per secret. Not the most elegant thing, but it works reliably and each provider is self-contained.
5. Apply and verify
Use openclaw secrets audit --check again:
Secrets audit: findings. plaintext=2, unresolved=0, shadowed=0, legacy=0.
- [PLAINTEXT_FOUND] auth-profiles.json:profiles.anthropic:default.token
- [PLAINTEXT_FOUND] auth-profiles.json:profiles.google:default.token
Down from 5 to 2. The remaining Anthropic token is an OAuth token from the Max plan — it auto-rotates, so it needs to stay writable in auth-profiles.json. That’s expected.
What about HashiCorp Vault?
I considered it. Vault is proper infrastructure — unsealing, token renewal, policies, the works. For a team managing hundreds of secrets across services, it’s the right tool.
For five API keys on a home server? Overkill. 1Password gives me encryption at rest, access logs, and a nice UI to manage everything. No extra server to run.
The gotcha: headless servers
If you’re running op CLI with desktop app integration (the default), you need the 1Password desktop app unlocked. That’s fine on a Mac. On a headless Linux server, it’s not.
Service accounts solve this. The OP_SERVICE_ACCOUNT_TOKEN env var lets op authenticate without the desktop app. The tradeoff is that you can’t write back to the vault from the CLI (read-only), but for secret consumption that’s exactly what you want.
Bonus: securing the gateway service token
There’s one more secret that’s easy to miss — the gateway auth token in your systemd service file.
If you’re running OpenClaw as a systemd user service, you probably have something like this in a drop-in override:
[Service]
Environment="OPENCLAW_GATEWAY_TOKEN=your-token-here"
The problem: anyone who can run systemctl --user cat openclaw-gateway.service sees the token. It shows up in systemctl show, journal logs, and terminal recordings.
The fix is EnvironmentFile. Move the token to a dedicated secrets file:
mkdir -p ~/.config/openclaw
cat > ~/.config/openclaw/secrets.env << 'EOF'
OPENCLAW_GATEWAY_TOKEN=your-token-here
OP_SERVICE_ACCOUNT_TOKEN=ops_your_sa_token_here
EOF
chmod 600 ~/.config/openclaw/secrets.env
Then update the systemd drop-in:
[Service]
EnvironmentFile=%h/.config/openclaw/secrets.env
Reload and restart:
systemctl --user daemon-reload
systemctl --user restart openclaw-gateway
Now systemctl --user cat openclaw-gateway.service shows EnvironmentFile= but not the actual values. The secrets file is 600 — only your user can read it.
This also gives you a single place for the 1Password service account token. Instead of putting OP_SERVICE_ACCOUNT_TOKEN in .bashrc (where it’s available to every shell session), it only exists in the gateway’s environment. Tighter scope.
Final state
All channel tokens and API keys resolve from 1Password at gateway startup. If 1Password is unreachable, the gateway refuses to start (fail-fast). If a reload fails mid-runtime, it keeps the last known good snapshot. No partial states.
The only thing on disk is ~/.config/openclaw/secrets.env — a 600 file with the service account token and gateway auth token. Everything else resolves from 1Password at startup. One file to protect instead of secrets scattered across config files, .bashrc, and systemd drop-ins.
openclaw secrets audit --check
# Exit code 0: clean (or expected findings only)
Good enough.