π§ Listen to this article: ΰ€Ήΰ€Ώΰ€ΰ€¦ΰ₯ Β· English Β· ΰ¬ΰ¬‘ΰ¬Όΰ¬Ώΰ¬
Most credential leaks are not dramatic. There is no hacker, no breach report. Someone simply keeps an account-wide API key in a plaintext file in their home directory, and one day a backup, a screen-share, a pasted command, or an AI coding session quietly carries it somewhere it should never have gone.
As of August 2026, that last vector is the one that has changed the math. Nearly every developer now runs an AI coding assistant that reads files and runs commands on their behalf. Everything such a tool reads, and everything a command prints, becomes part of a session transcript that is transmitted to the model provider on every turn. If a plaintext key is ever displayed, decrypted to the screen, or echoed during a session, it is now sitting in that transcript, outside your control. This is not hypothetical: it is exactly how a perfectly careful engineer can leak a key without ever doing anything obviously wrong.
This post is a complete, generic runbook for fixing that. The worked example is a Cloudflare Global API Key, but the technique applies to any high-privilege credential. Part one uses nothing but gpg and bash and works on any Linux or macOS machine. Part two is only for people who use an AI coding assistant and want a hard, enforced guard on top.
Why an account-wide key deserves special treatment
A Cloudflare Global API Key is account-wide and unscoped. It can touch every zone, worker, and DNS record on every account tied to that email. That creates two distinct risks:
- At rest. A plaintext key in a home-directory file can be read by any process, script, or backup that touches your filesystem. It is one careless
chmod, one over-broad backup job, one borrowed laptop away from exposure. - In an AI session. Anything a coding assistant reads, or any command that prints the key, lands in the transcript sent to the provider.
cat ~/.cf,echo $CLOUDFLARE_API_KEY, or agpg --decryptthat dumps to the screen all leak it instantly.
The fix has three parts: keep the key encrypted at rest, decrypt it only inside a short-lived subprocess that never prints it, and β if an AI assistant is in the loop β add a hard block enforced by code, not by remembering to be careful.
A note before you start: if the tool you are using supports a scoped API token instead of the Global API Key, use the token. A token can be revoked individually and cannot reach anything outside its assigned scope, so the blast radius of a leak is a fraction of the Global Key's. The steps below apply equally to a token; you simply store a single token value instead of an email plus key pair.
Step 1: Retrieve the credential and stage it with tight permissions
Get the key from your provider's dashboard. For Cloudflare, that is My Profile, then the API Tokens tab, then View next to the Global API Key.
Write it to a staging file in a project directory, creating it locked-down from the first byte:
mkdir -p ~/vault
umask 077 # new files are created 600, never world-readable
printf '%s\n' "[email protected] REPLACE_WITH_GLOBAL_API_KEY main-account" > ~/vault/.cf
chmod 600 ~/vault/.cf
The umask 077 matters: it guarantees the plaintext is never briefly readable by other users during creation, which a later chmod cannot undo.
Step 2: Encrypt it with GPG symmetric AES-256
Symmetric, passphrase-based encryption is the right choice here. Only one person ever needs to decrypt this file, so a public/private keypair would add complexity for no benefit.
gpg --symmetric --cipher-algo AES256 -o ~/vault/.cf.gpg ~/vault/.cf
GPG prompts you to enter and confirm a passphrase. Choose something strong and unique and record it in a password manager, because there is no recovery if you forget it. From this point, that passphrase is the only thing standing between filesystem access and the key.
Step 3: Verify the encrypted copy before deleting anything
Never delete the plaintext until you have proven the ciphertext decrypts back byte-for-byte:
diff <(gpg --quiet --batch --decrypt ~/vault/.cf.gpg) ~/vault/.cf
No output means the two are identical and you are safe to proceed. Any output means something is wrong β wrong passphrase, wrong cipher, corrupted file β and you must not delete the plaintext yet.
Step 4: Securely delete the plaintext
shred -u ~/vault/.cf
chmod 600 ~/vault/.cf.gpg
ls -la ~/vault/.cf* # only .cf.gpg should remain
shred overwrites the bytes before unlinking, which is stronger than rm. Be honest about its limits: on SSDs, copy-on-write filesystems, or any file that was ever snapshotted or backed up, the overwrite guarantee weakens. The only real guarantee against a truly compromised credential is rotation, not deletion of one copy.
Step 5: Shorten the passphrase cache (optional but recommended)
By default gpg-agent caches your passphrase for ten minutes or more after you type it. To narrow that window:
mkdir -p ~/.gnupg && chmod 700 ~/.gnupg
cat >> ~/.gnupg/gpg-agent.conf <<'EOF'
default-cache-ttl 60
max-cache-ttl 120
EOF
chmod 600 ~/.gnupg/gpg-agent.conf
gpg-connect-agent reloadagent /bye
This caps caching at 60 to 120 seconds β long enough for a script to run right after you decrypt, short enough that a walk-away does not leave the vault effectively unlocked.
Step 6: Create a decrypt-and-exec wrapper that never prints the secret
This is the step that makes the vault usable day to day without ever exposing the key. Save this as ~/vault/cf-run.sh:
#!/usr/bin/env bash
# cf-run.sh β decrypt .cf.gpg and exec a command with CLOUDFLARE_EMAIL /
# CLOUDFLARE_API_KEY set for that one child process only. The decrypted value
# is never printed, written to disk, or returned as this script's output.
# Usage: ./cf-run.sh npx wrangler deploy
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CREDS_FILE="$SCRIPT_DIR/.cf.gpg"
[ -f "$CREDS_FILE" ] || { echo "cf-run.sh: $CREDS_FILE not found" >&2; exit 1; }
[ "$#" -gt 0 ] || { echo "usage: cf-run.sh <command> [args...]" >&2; exit 1; }
CREDS="$(gpg --quiet --batch --decrypt "$CREDS_FILE")" || { echo "cf-run.sh: decryption failed" >&2; exit 1; }
CF_EMAIL="$(awk '{print $1}' <<<"$CREDS")"
CF_KEY="$(awk '{print $2}' <<<"$CREDS")"
unset CREDS
exec env CLOUDFLARE_EMAIL="$CF_EMAIL" CLOUDFLARE_API_KEY="$CF_KEY" "$@"
chmod +x ~/vault/cf-run.sh
Why it stays safe, line by line:
- The decrypted text goes straight into a shell variable via command substitution β never to the terminal or stdout.
awkextracts the two fields from that variable; still nothing is printed.unset CREDSdrops the combined line from memory as soon as the pieces are pulled out.exec env VAR=... "$@"replaces the wrapper's process with your target command, handing it the values only in its own environment. Nothing is written to disk, logged, or echoed at any point.
Step 7: Use the wrapper for everything
Never do CLOUDFLARE_API_KEY=$(cat ...) wrangler deploy again. Route every command through the wrapper, and let the target tool read the variables from its own environment:
~/vault/cf-run.sh npx wrangler deploy
~/vault/cf-run.sh curl -s "https://api.cloudflare.com/client/v4/user" \
-H "X-Auth-Email: $CLOUDFLARE_EMAIL" -H "X-Auth-Key: $CLOUDFLARE_API_KEY"
Step 8 (AI-assistant users only): add a hard guard hook
If a coding assistant runs commands on your machine, "remembering not to print the key" is not a control. Many assistants expose a hook system that can inspect a command before it runs and refuse it. The idea is a pre-execution guard that blocks two things: reading the plaintext form of a vault directly, and decrypting the vault to stdout or a pipe rather than into a captured variable.
The guard, in plain terms, inspects the pending shell command and:
- Blocks a read command (
cat,less,head,strings, and so on) pointed at the vault's plaintext filename. - Blocks a
gpg --decryptof the vault unless its output is captured in a command substitution such asX="$(gpg -d ... )". - Allows everything else, including the legitimate
cf-run.shinvocation.
A minimal Python guard returns exit code 0 to allow and exit code 2 to block, printing the reason to standard error so the assistant sees why:
#!/usr/bin/env python3
import json, re, sys
VAULT_HINTS = ("vault/.cf",) # add every vault path you protect
READ_CMDS = r"(cat|less|more|head|tail|strings|xxd|od)"
def main() -> int:
try:
cmd = (json.load(sys.stdin).get("tool_input") or {}).get("command", "") or ""
except Exception:
return 0 # never break tooling on a parse error
# Block reading the plaintext vault directly (matches .cf but not .cf.gpg)
if re.search(rf"(^|[;&|]\s*){READ_CMDS}\s+[^\n]*vault/\.cf(\s|$)", cmd):
sys.stderr.write("BLOCKED: reading the plaintext vault would leak it. Use cf-run.sh.\n")
return 2
# Only care about decrypting a protected vault
if re.search(r"gpg\b.{0,60}?(-d\b|--decrypt\b)", cmd) and any(h in cmd for h in VAULT_HINTS):
if re.search(r"\$\(\s*gpg\b", cmd) or re.search(r"`\s*gpg\b", cmd):
return 0 # captured into a variable β safe
sys.stderr.write("BLOCKED: decrypting to stdout leaks secrets. Capture into a variable.\n")
return 2
return 0
if __name__ == "__main__":
sys.exit(main())
Wire it into your assistant's pre-tool hook configuration, then test it against a decoy file β never the real vault, because a bug in the rule during testing could print the actual secret. Create a harmless vault/.cf under a throwaway directory, confirm the assistant refuses to read it, and separately confirm the legitimate cf-run.sh command still passes.
Conclusion
Encrypting a credential at rest is not enough on its own. The value only stays safe if it is also decrypted in a way that never surfaces it, and β where an AI assistant is involved β if unsafe access is blocked by code rather than by discipline. GPG symmetric encryption, a decrypt-and-exec wrapper, and a pre-execution guard hook together give you a vault that is genuinely usable and genuinely hard to leak from. It takes fifteen minutes to set up and removes an entire class of quiet, embarrassing exposures.
Merits
- Portable and dependency-free: the core setup needs only
gpgandbash, present on nearly every Unix machine. - The secret is never printed, written to disk, or logged during normal use.
- Symmetric encryption keeps the model simple for a single operator.
- The optional guard hook turns "be careful" into an enforced, code-level control.
- The same pattern works for any high-privilege credential, not just Cloudflare.
Demerits
- A forgotten passphrase means permanent loss; there is no recovery path.
shredcannot fully guarantee deletion on SSDs, copy-on-write filesystems, or previously backed-up files.- Encryption protects the key going forward but does nothing to undo a past exposure.
- The guard hook is assistant-specific and must be kept in sync with your actual vault paths.
Caution
Every value in this article β [email protected], REPLACE_WITH_GLOBAL_API_KEY, main-account, and the ~/vault path β is a placeholder. Substitute your own and never commit a plaintext credential to version control. Test the wrapper and the guard against decoy files before trusting them with a real key, and rotate any credential the moment you suspect it was exposed. Proceed at your own risk; you are responsible for your own credentials and infrastructure.
Frequently asked questions
Why use GPG symmetric encryption instead of a public/private keypair? For a single operator who is both the encryptor and the only decryptor, a passphrase-based symmetric cipher is simpler and just as strong. A keypair adds key-management overhead with no security gain in this case.
Is it safe to store the encrypted .cf.gpg file in git? The ciphertext is technically safe to commit, but there is no upside to it leaving your machine. Keep it local, and never, ever commit the plaintext.
Should I use a Cloudflare API Token instead of the Global API Key? Yes, whenever the tool supports it. A scoped token can be revoked individually and cannot reach resources outside its scope, so a leak is far less damaging than a leaked Global API Key.
What does the decrypt-and-exec wrapper actually protect against? It ensures the decrypted secret only ever lives in the environment of one child process. It is never printed to a terminal, captured in shell history, or written to disk, which are the common accidental-leak paths.
How does a secret leak into an AI coding assistant?
Everything the assistant reads or a command prints is added to the session transcript sent to the model provider. A single cat of a plaintext key, or a decrypt to stdout, places the key in that transcript permanently.
Does encrypting the key help if it already leaked once? No. Encryption only protects the key from future exposure. If a key was ever printed, pasted, or seen in a transcript, you must rotate it; encrypting the old value changes nothing about the exposed one.
How often should I rotate a high-privilege key? Rotate on a schedule that matches your risk tolerance, and immediately on any suspected exposure. For an account-wide key, err on the side of rotating more often, since its blast radius is the entire account.
Can I adapt this to AWS, database, or other credentials? Yes. The same three ideas β encrypt at rest, decrypt only into a throwaway subprocess, and block unsafe reads β apply to any secret. Adjust the field parsing and the environment variable names in the wrapper to match.
Tags
#Security #GPG #Encryption #SecretsManagement #Cloudflare #APIKeys #DevOps #DevSecOps #AICoding #Linux
Kubernetes Security Checklist
Harden cluster access, workload identity, pod security, network boundaries, software supply chain, secrets, and operational monitoring.
Free. No spam β unsubscribe in one click.


Responses
Sign in to leave a response.