SSD Nodes Learn Hosting plans →
How to do am Matt ConnorBy Matt Connor

How to Keep API Keys Comot from AI Agents

AI agent fit leak API key with one tool call. Use scoped, short-lived tokens behind credential gateway, instead of real keys inside environment.

Wetín e mean to keep secrets comot from AI agents

An AI agent na normal Linux process wey dey run commands. Every environment variable wey that process get, the code wey e run fit read am. So, if API key dey inside agent environment, agent fit send the key go any host wey e fit reach. To keep secrets comot from agent mean say you give am a handle instead of the key: a short lived scoped token, or placeholder wey another thing go replace with the real value for network boundary.

This no be story about model turning hostile. The mechanism dey more ordinary. Agent dey read web page, README, or issue comment wey get instructions, and e follow dem, because language model no fit tell difference between text wey you write and text wey e fetch. Na prompt injection be this. Once e happen, exactly one thing determine how much damage fit happen: wetin the process fit read. If you never set boundary yet, how to run coding agent safely for server explain the isolation levels wey this guide dey build on.

The threat model for simple terms

Run this as the user wey your agent dey run as.

tr '\0' '\n' < /proc/self/environ | grep -iE 'key|token|secret|password'

Every line wey e print na one HTTP request away from stranger server. Now check wetin dey for disk near the agent.

grep -rIl --exclude-dir=.git -e 'API_KEY' -e 'SECRET' ~/projects
find ~ -maxdepth 3 -name '.env' -o -name 'credentials' -o -name '*.pem'

Agent wey get shell no need clever exploit to move that data comot. Four ordinary paths fit do the work, and all four go look like normal work for the log:

  • One outbound curl or fetch go any host, with the value for query string.
  • One git commit and git push go repository wey agent fit write to.
  • Package install script, wey dey run arbitrary code as agent user.
  • DNS lookup of hostname wey contain the value, wey still go comot even when HTTP egress dey blocked.

You no fit review your way out of this. The fix na to make sure say nothing valuable dey within reach.

Secret wey dey inside working tree na secret wey dey inside context window

Agent dey read files. If .env file dey inside repository wey e dey work on, e go read am. Once e read am, e don enter context window. This means say e dey inside transcript, any log wey you keep, and anything wey agent write afterward.

Before, when key dey inside tree wey agent dey work on:

cd ~/projects/billing
cat .env
# STRIPE_SECRET_KEY=sk_live_...
# DATABASE_URL=postgres://app:hunter2@db.internal:5432/billing

After, when file don move reach where agent no fit access:

sudo install -d -m 750 -o root -g agent-review /etc/agent-review
sudo install -m 640 -o root -g agent-review ~/projects/billing/.env /etc/agent-review/billing.env
rm ~/projects/billing/.env

Agent user no fit open the file again, because working tree no longer get am. Deny rules for agent own config na second layer, no be the first one. Claude Code dey read permission rules from .claude/settings.json for the project:

{
  "permissions": {
    "deny": ["Read(./.env)", "Read(./secrets/**)", "Read(./**/*.pem)"]
  }
}

This stop honest mistake wey agent fit open file while e dey explore. E also pass file tools: Claude Code dey check these rules against file commands wey e recognize for Bash, like cat and head, and against targets of redirections like < .env. But e no stop injected instruction from running grep -r KEY . inside project directory, or small Python script wey open .env by itself. This na because permission documentation talk say deny rules no apply to commands wey read files without naming dem, or to subprocesses wey open files by themselves. For those cases, docs point to Claude Code sandbox, wey enforce the block for operating system level. Whether dem go ask you before such command run depend on session permission mode. auto mode na now the built-in starting mode for interactive Claude Code sessions, so server wey you no dey watch fit run more of these commands without asking.

The same limit apply to anything wey dey shape agent habits instead of permissions: skill wey make agent stick to the smallest change wey work fit stop one run from entering files wey e no suppose open, but na still advice wey model fit talk its way out of. Treat config like guardrail, and filesystem permission like wall. The same separation apply inside containers too: env files and secrets for Docker Compose cover the version of this problem wey dey one layer down.

Give every agent im own unprivileged user

If agent dey run as you, e inherit your SSH keys, your cloud credentials, and your shell history. Separate user need only one command and e remove all those things.

sudo adduser --disabled-password --gecos "" agent-review
sudo chmod 700 /home/agent-review
sudo -u agent-review cat ~/.ssh/id_ed25519

The last line must fail with cat: /home/you/.ssh/id_ed25519: Permission denied. If e print a key instead, agent user fit reach the file. To open file by name, e need search (x) permission for every directory above am, not read permission. So, home directory wey get mode 711 and nobody else fit list still fit allow the key pass if the key file itself readable. chmod 700 ~ close the path, and chmod 600 ~/.ssh/id_ed25519 fix the key. No add the agent user to sudo, and no give am NOPASSWD rule wey wider than the one command e truly need. Users with least privilege for VPS explain the group and sudoers details. Remember this separation when you run more than one session for the box, because one Claude Code session fit send text straight to another, and anything wey the first session hold fit cross that channel inside one message.

One more boundary good to add for cloud VPS. Instance metadata service dey answer for fixed link local address, and e often hand out role credentials to anything wey ask. Nothing for the endpoint check which local user dey make the call: for EC2, AWS document say IMDSv1 calls no need token, and its own advice for keeping one user away from the service na local firewall rule keyed to that user's ID.

sudo iptables -I OUTPUT 1 -p tcp -m owner --uid-owner agent-review -d 169.254.169.254 -j REJECT

Insert the rule for the top with -I instead of appending am with -A. For host wey dey run ufw with its default allow outgoing policy, ufw last OUTPUT chain accepts every new TCP connection, so ufw no go reach an appended rule. iptables rules also disappear after reboot unless you persist dem. If you enable metadata service IPv6 endpoint, [fd00:ec2::254], add the same rule with ip6tables.

Check am from the agent side. sudo -u agent-review curl -s --max-time 3 http://169.254.169.254/ suppose print nothing and exit non zero, because packet dey rejected before e leave the box. If e print a listing, the rule no dey reached.

Credential boundary for inject

The pattern wey really solve this na credential injection. Agent no dey ever hold real key. E dey send request through local gateway, and gateway go replace placeholder with the real secret as e dey go out. The secret dey inside gateway storage, for another process, and different user own am.

OneCLI na one open source implementation of this. E get Apache-2.0 license apart from im ee/ directories, wey get separate enterprise license. You need subscription to use those directories for production. E dey run inside containers beside the agent. As of October 2026, the project document this Docker Compose setup. The setup dey read three generated secrets from one .env file wey dey beside the compose file:

git clone https://github.com/onecli/onecli.git && cd onecli/docker
cat > .env <<EOF
SECRET_ENCRYPTION_KEY=$(head -c 32 /dev/urandom | base64)
GATEWAY_INTERNAL_SECRET=$(head -c 32 /dev/urandom | base64)
BETTER_AUTH_SECRET=$(head -c 32 /dev/urandom | base64)
COMPOSE_PROFILES=runner
EOF
chmod 600 .env
docker compose up -d --wait

Back up that .env file, because if you lose SECRET_ENCRYPTION_KEY, every secret wey dem store with am go become orphan. Dashboard dey listen on port 10254, while gateway dey listen on 10255. Both dey bind to 127.0.0.1 by default. You store the real credential once. Then you give each agent placeholder value instead of the key, plus im own scoped access token. Agent dey send the token inside Proxy-Authorization header. Gateway go match the outbound request by host and path, decrypt the credential wey match, then substitute am. Agent environment no hold anything wey person fit steal.

The important thing here no be the encryption. Na say the question “which thing this agent use, and when” don turn to log query. You go read one audit trail instead of guessing which one among six environments get copy of the key.

Secret na for process, no be for environment

If you dey run the agent under systemd, you no need environment variables at all. LoadCredential= dey put the secret for private directory wey only the service user fit read, e go show as %d for the unit file and as $CREDENTIALS_DIRECTORY inside the process. This restriction dey based on user, no be service. So give each agent service its own user: any other process wey dey run as the same user fit open the directory unless the unit also use filesystem namespacing, like PrivateMounts=yes, wey hide am from other services. The value no ever show for /proc/<pid>/environ, so ps eww no fit show am, and the directory go disappear when the service stop.

First encrypt the credential to the machine. These commands come from systemd documentation and dem work for systemd 250 or newer, wey cover Ubuntu 24.04 and Debian 13:

echo -n 'sk-example-value' > /tmp/plain.txt
sudo systemd-creds encrypt --name=api_key /tmp/plain.txt /etc/credstore/api_key.cred
shred -u /tmp/plain.txt
sudo systemd-run -P --wait -p LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred \
  systemd-creds cat api_key

The last command go print sk-example-value. This prove say the encrypted file fit decrypt for this host. Then reference am from the unit:

[Service]
User=agent-review
LoadCredentialEncrypted=api_key:/etc/credstore/api_key.cred
Environment=AGENT_KEY_FILE=%d/api_key
ExecStart=/usr/local/bin/agent-worker

Your agent code go open the file for $AGENT_KEY_FILE when e need the value. Reading a file na one moment. An environment variable dey last for the whole life of the process, including every child process wey e start.

Prefer tokens wey get short lifetime over keys wey get long lifetime

Key wey no dey expire still dey valid whenever e show up months later for log or transcript. If the service get session token, use the session token and set the shortest lifetime wey the work fit allow.

aws sts assume-role \
  --role-arn arn:aws:iam::123456789012:role/agent-readonly \
  --role-session-name agent-review \
  --duration-seconds 900

Fifteen minutes na the minimum wey AWS STS (security token service) dey accept, and e usually enough for one agent task. For GitHub, give the agent user its own gh login with fine grained token wey scope to the single repository wey e dey work on, so gh auth token inside that session go return something wey no fit touch anything else. First scope am by resource, then by time.

Check, then keep checking

Three checks dey worth running after any change to agent setup. Run dem as the agent user, no be as yourself.

sudo -u agent-review env | grep -iE 'key|token|secret'
sudo -u agent-review ls -la /home/you/ 2>&1 | head -3
sudo -u agent-review curl -s -o /dev/null -w '%{http_code}\n' --max-time 5 https://api.github.com/user

The first one suppose print nothing at all. The second one suppose print ls: cannot open directory '/home/you/': Permission denied, but that only show say you no fit list the directory; na the cat test earlier prove say file inside am no fit read. The third one tell you which identity the agent network path present, and na this question the gateway pattern dey answer: 401 mean say request reach GitHub without credential wey GitHub accept, either no credential at all or one wey invalid or expired. 200 mean say e carry valid credential, so you suppose know which token e be. If you dey run agents unattended, how to control AI agent costs for VPS cover the budget limits wey go with these access limits.

FAQ

I fit just trust the model make e no leak my keys?

No, because for this threat model, na not the model be the attacker. The agent dey read text from web pages, repositories, and issue trackers, and that text fit contain instructions. The model no get reliable way to know which one na your instructions and which one na text e fetch. Any control wey depend on the model to choose correctly go fail the first time an injected instruction sound convincing. So, the control suppose dey inside the operating system or the network instead.

Environment variables really bad for agent secrets?

Dem bad for one specific reason: dem dey inherit. Every child process wey the agent start gets a copy, including build script, test runner, and any package install hook. The same user fit also read the variables through /proc/<pid>/environ, so anything wey the agent run fit read dem without the agent passing dem along. If you read a file only when you need am, with LoadCredential= or a gateway, you limit the exposure to that moment.

If I put secrets for vault, e solve this by itself?

Only partly. Vault solves the storage problem. If you self host the vault, you need harden am separately, because people usually breach a Vaultwarden server through its admin token or backup file, not through the encrypted items wey e hold. E no solve the last step, where something pulls the secret from the vault and gives am to the agent as an environment variable. That one puts you back for the same problem. The important thing na who performs the substitution. If the agent fetch the secret, the agent get the secret. If gateway or init system performs the substitution outside the agent process, the agent never hold am.

How I fit know whether agent don leak something already?

Most times, you no fit know after the event, and na this be the reason to use gateway. Without gateway, your evidence dey scattered across shell history, the agent transcript, and outbound connection logs wey you probably no dey keep. With credential gateway, every credential use na one line with agent identity and timestamp. If you suspect say leak happen, rotate the key first and investigate after. Rotation cheap, but certainty no be.

Wetin be the minimum wey I suppose do today?

Move every .env file comot from the directories wey your agents dey work in, and create one unprivileged user for each agent. These two changes fit take about ten minutes and close the commonest path: agent reading credential file wey no reason to dey beside the code. Gateway and short lived tokens na the next step, no be the first one. This same starting point apply to any agent runtime, including running an autonomous agent safely on a VPS.

Dis article help you?