-
- Stop Storing API Keys in Plaintext Config Files (KeepassXC + Secret Service)
-
- ___ _
- / __| | |_ ___ _ __
- \__ \ | _| / _ \ | '_ \
- |___/ \__| \___/ | .__/
- |_|
-
- ___ _ _
- / __| | |_ ___ _ _ (_) _ _ __ _
- \__ \ | _| / _ \ | '_| | | | ' \ / _` |
- |___/ \__| \___/ |_| |_| |_||_| \__, |
- |___/
-
- _ ___ ___
- /_\ | _ \ |_ _|
- / _ \ | _/ | |
- /_/ \_\ |_| |___|
-
-
- _ __
- | |/ / ___ _ _ ___
- | ' < / -_) | || | (_-<
- |_|\_\ \___| \_, | /__/
- |__/
-
- _
- (_) _ _
- | | | ' \
- |_| |_||_|
-
-
- ___ _ _ _ _
- | _ \ | | __ _ (_) _ _ | |_ ___ __ __ | |_
- | _/ | | / _` | | | | ' \ | _| / -_) \ \ / | _|
- |_| |_| \__,_| |_| |_||_| \__| \___| /_\_\ \__|
-
-
- ___ __ _
- / __| ___ _ _ / _| (_) __ _
- | (__ / _ \ | ' \ | _| | | / _` |
- \___| \___/ |_||_| |_| |_| \__, |
- |___/
-
- ___ _ _
- | __| (_) | | ___ ___
- | _| | | | | / -_) (_-<
- |_| |_| |_| \___| /__/
-
-
- __ _ __ __ __ ___
- / / | |/ / ___ ___ _ __ __ _ ___ ___ \ \/ / / __|
- | | | ' < / -_) / -_) | '_ \ / _` | (_-< (_-< > < | (__
- | | |_|\_\ \___| \___| | .__/ \__,_| /__/ /__/ /_/\_\ \___|
- \_\ |_|
-
- _
- _| |_
- |_ _|
- |_|
-
-
- ___ _
- / __| ___ __ _ _ ___ | |_
- \__ \ / -_) / _| | '_| / -_) | _|
- |___/ \___| \__| |_| \___| \__|
-
-
- ___ _ __
- / __| ___ _ _ __ __ (_) __ ___ \ \
- \__ \ / -_) | '_| \ V / | | / _| / -_) | |
- |___/ \___| |_| \_/ |_| \__| \___| | |
- /_/
-
- ╔─*──*──*──*──*──*──*──*──*──*──*──*──*──*──*──*─╗
- ║1 ........................................ 1║
- ║2* ........................................ *2║
- ║3 ........................................ 3║
- ║1 ...........Posted: 2026-03-31........... 1║
- ║2* .Tags: sysadmin linux security debian .. *2║
- ║3 ........................................ 3║
- ║1 ........................................ 1║
- ╚────────────────────────────────────────────────╝
-
- I found API keys and tokens sitting in plaintext in ~/.claude.json -- the config
- file for Claude Code's MCP servers. Trello tokens, Habitica API keys, an Otter
- password. Just sitting there in a user file that could end up in a backup, a
- dotfile repo, a screen share.
-
- This is how I moved them into KeepassXC via the freedesktop Secret Service API,
- so the config file only contains variable references.
-
- This post assumes you already have KeepassXC set up as your Secret Service
- provider. If not, see my earlier post on KeepassXC as a keyring manager.
-
- ## The problem
-
- Tools that use config files with env blocks store secrets in plaintext:
-
- ```
- "env": {
- "TRELLO_API_KEY": "04e633dc273440d1df0d5fa6a0e7d873",
- "TRELLO_TOKEN": "ATTA598f072ba685622a606..."
- }
- ```
-
- These are user files. They get backed up, synced, shared, grepped, accidentally
- committed.
-
- ## The fix
-
- Three pieces:
-
- 1. Store secrets in KeepassXC via secret-tool
- 1. Export them in .zshrc at shell startup
- 1. Replace plaintext values with ${VAR} references in the config
-
- ## 1. Install secret-tool
-
- ```
- sudo apt install libsecret-tools
- ```
-
- This is the CLI for the freedesktop Secret Service D-Bus API. It talks to
- whatever is registered as the Secret Service provider -- in our case, KeepassXC.
-
- ## 2. Disable gnome-keyring as Secret Service provider
-
- If gnome-keyring is installed, it will race KeepassXC for the
- org.freedesktop.secrets D-Bus name. When KeepassXC is locked or hasn't started
- yet, gnome-keyring silently wins and your secrets go into its own keyring file
- instead.
-
- Neuter gnome-keyring's secrets component without uninstalling it (other things
- depend on it for SSH, PKCS#11, etc.):
-
- ```
- # Stop D-Bus auto-activation for secrets
- sudo bash -c 'cat > /usr/share/dbus-1/services/org.freedesktop.secrets.service << EOF
- [D-BUS Service]
- Name=org.freedesktop.secrets
- Exec=/usr/bin/false
- EOF'
-
- # Hide the autostart entry
- mkdir -p ~/.config/autostart
- cp /etc/xdg/autostart/gnome-keyring-secrets.desktop ~/.config/autostart/
- echo "Hidden=true" >> ~/.config/autostart/gnome-keyring-secrets.desktop
-
- # Kill any running instance
- killall gnome-keyring-daemon
- ```
-
- Then delete the old gnome-keyring data if you don't need it:
-
- ```
- rm ~/.local/share/keyrings/login.keyring
- ```
-
- ## 3. Enable KeepassXC Secret Service
-
- In KeepassXC:
-
- - Tools > Settings > Secret Service Integration > Enable
- - Database Settings > Secret Service Integration > Expose a group
-
- Verify it's working:
-
- ```
- dbus-send --session --dest=org.freedesktop.secrets \
- --type=method_call --print-reply \
- /org/freedesktop/secrets \
- org.freedesktop.Secret.Service.ReadAlias string:default
- ```
-
- You should see a path like `/org/freedesktop/secrets/collection/Passwords`, not
- `/collection/login` (that's gnome-keyring).
-
- ## 4. Store secrets
-
- KeepassXC must be unlocked. Each secret gets a label, a service name for
- grouping, and an env_var attribute for the export name:
-
- ```
- echo -n "YOUR_API_KEY" | secret-tool store \
- --label="MCP Trello API Key" \
- service trello-drex \
- env_var TRELLO_API_KEY \
- usage mcp
- ```
-
- Repeat for each secret. The attributes are arbitrary key-value pairs used for
- lookup. I used `service` to group by MCP server and `env_var` for the
- environment variable name.
-
- ## 5. Export in .zshrc
-
- At the bottom of ~/.zshrc:
-
- ```
- if command -v secret-tool &>/dev/null; then
- export HABITICA_USER_ID=$(secret-tool lookup service habitica env_var HABITICA_USER_ID 2>/dev/null)
- export HABITICA_API_TOKEN=$(secret-tool lookup service habitica env_var HABITICA_API_TOKEN 2>/dev/null)
- export TRELLO_DREX_API_KEY=$(secret-tool lookup service trello-drex env_var TRELLO_API_KEY 2>/dev/null)
- export TRELLO_DREX_TOKEN=$(secret-tool lookup service trello-drex env_var TRELLO_TOKEN 2>/dev/null)
- export OTTER_EMAIL=$(secret-tool lookup service otter env_var OTTER_EMAIL 2>/dev/null)
- export OTTER_PASSWORD=$(secret-tool lookup service otter env_var OTTER_PASSWORD 2>/dev/null)
- fi
- ```
-
- ## 6. Replace plaintext in config
-
- Claude Code's MCP config supports ${VAR} expansion. Replace:
-
- ```
- "TRELLO_API_KEY": "04e633dc273440d1df0d5fa6a0e7d873"
- ```
-
- With:
-
- ```
- "TRELLO_API_KEY": "${TRELLO_DREX_API_KEY}"
- ```
-
- The variable is resolved from the process environment at MCP server launch time.
-
- ## Security: what this does and doesn't do
-
- ### What it fixes
-
- - No plaintext secrets in user files (backups, dotfile repos, screen shares,
- grep results)
- - Single source of truth for secrets (KeepassXC database, which is encrypted)
- - Lock KeepassXC and new shells can't retrieve secrets
-
- ### What it doesn't fix
-
- - Secrets are exported as env vars in every shell session. Any process running
- as your user can read them via /proc/PID/environ.
- - If KeepassXC is unlocked, any user-level process can query the Secret Service
- D-Bus API directly.
- - This is not meaningfully better against malware running as your user. If you
- have that problem, env vars and plaintext files are equally compromised.
-
- ### Env vars vs. plaintext files: the tradeoff
-
- This setup trades one attack surface for another.
-
- Plaintext files are worse for storage exposure: they persist on disk, get backed
- up, synced, committed, grepped, indexed, and shared. The secret survives the
- session and exists in a form that's easy to accidentally leak.
-
- Env vars are worse for runtime exposure: every child process inherits them (not
- just the ones that need them), they're visible via /proc/PID/environ to anything
- running as your user, and they can show up in crash dumps or logging frameworks
- that dump the environment.
-
- The core difference: env vars have a broader runtime attack surface, plaintext
- files have a broader storage attack surface.
-
- For a single-user desktop where the realistic threat is accidental file sharing
- (not local malware), the env var approach wins. If you wanted to eliminate both,
- you could use a wrapper script that fetches secrets on-demand and only exports
- them into the MCP server process -- but that's more complexity than most setups
- warrant.
-
- ### The honest assessment
-
- On a single-user machine with full disk encryption, the practical security gain
- is protection against accidental exposure, not against a determined attacker
- with local access. That's still worth doing -- accidental exposure is far more
- likely than targeted local compromise for most people.
-
- ## Better: on-demand secrets with a wrapper script
-
- The .zshrc approach above works, but exports secrets into every shell session. A
- tighter approach: a wrapper script that fetches secrets on-demand, only in the
- MCP server process.
-
- Save as ~/.local/bin/mcp-secret-launch:
-
- ```
- #!/usr/bin/env python3
- """Fetch secrets from Secret Service and exec an MCP server."""
- import os, sys, secretstorage
-
- service, command, args = sys.argv[1], sys.argv[2], sys.argv[2:]
-
- conn = secretstorage.dbus_init()
- collection = secretstorage.get_default_collection(conn)
-
- if collection.is_locked():
- print("KeepassXC is locked.", file=sys.stderr)
- sys.exit(1)
-
- for item in collection.search_items({"service": service, "usage": "mcp"}):
- env_var = item.get_attributes().get("env_var")
- if env_var:
- os.environ[env_var] = item.get_secret().decode("utf-8")
-
- os.execvp(command, args)
- ```
-
- Make it executable:
-
- ```
- chmod +x ~/.local/bin/mcp-secret-launch
- ```
-
- Then change your MCP config from:
-
- ```
- "habitica": {
- "command": "npx",
- "args": ["-y", "habitica-mcp-server"],
- "env": {
- "HABITICA_USER_ID": "${HABITICA_USER_ID}",
- "HABITICA_API_TOKEN": "${HABITICA_API_TOKEN}",
- "MCP_LANG": "en"
- }
- }
- ```
-
- To:
-
- ```
- "habitica": {
- "command": "/home/you/.local/bin/mcp-secret-launch",
- "args": ["habitica", "npx", "-y", "habitica-mcp-server"],
- "env": {
- "MCP_LANG": "en"
- }
- }
- ```
-
- The first arg ("habitica") tells the wrapper which secrets to fetch. It looks up
- all Secret Service items with service=habitica and usage=mcp, exports them as
- env vars using the env_var attribute, then execs the real command. Non-secret
- env vars like MCP_LANG stay in the config as normal.
-
- Remove the secret-tool exports from .zshrc -- they're no longer needed.
-
- ### What this buys you
-
- - Secrets only exist in the MCP server process memory
- - Nothing in /proc/PID/environ of your shells
- - No shell startup latency from D-Bus lookups
- - grep -r on your homedir finds nothing
-
- ### What it costs
-
- - One extra script to maintain (~15 lines)
- - MCP launch errors are slightly harder to debug
- - Python + secretstorage must be available (standard on most desktops)
-
- ## Caveats
-
- ### KeepassXC must be unlocked
-
- If it's locked when an MCP server tries to start, the wrapper exits with an
- error. With the .zshrc approach, the lookups silently return empty strings --
- arguably worse since you get no error, just broken auth.
-
- ### gnome-keyring will silently intercept your secrets
-
- This is the biggest gotcha. If gnome-keyring is running when you call
- secret-tool store, your secrets go into ~/.local/share/keyrings/ instead of
- KeepassXC. You won't get an error. You have to check which collection owns the
- default alias (see the dbus-send verify step above).
-
- ### apt remove gnome-keyring will break things
-
- Don't do it. Other packages depend on gnome-keyring for non-secrets
- functionality. Just disable the secrets component as shown above.
-