Protect (PR) · Identity
Identity at the client side layer
deterministic · Outside the model's reasoning
If the agent decides to violate this concern, what stops it at this layer?
What this cell does
Per-agent credentials, no shared keys, filesystem ACLs.
Artifacts (3)
claude-code-prod.serviceview on GitHub# ABOUTME: systemd unit binding the Claude Code agent to a dedicated user with a per-agent credential.
# ABOUTME: The credential file in EnvironmentFile is read by systemd as root; the agent process never reads it at rest.
[Unit]
Description=Claude Code agent (production)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=agent-runner
Group=agent-runner
# Read the per-agent credentials. systemd reads this as root before the
# User= switch; the agent process inherits the env vars but never sees the
# file directly (the filesystem ACL from provision-credential.sh denies the
# agent's own user read access).
EnvironmentFile=/etc/agents/claude-code-prod/env
# Hardening
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=true
PrivateDevices=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
RestrictNamespaces=true
RestrictRealtime=true
RestrictSUIDSGID=true
LockPersonality=true
MemoryDenyWriteExecute=true
SystemCallArchitectures=native
ReadWritePaths=/var/lib/agents/claude-code-prod /workspace
# The agent gets a working directory it can write to; everything else is
# read-only or hidden via ProtectSystem and ProtectHome.
WorkingDirectory=/workspace
ExecStart=/usr/local/bin/claude --config /etc/agents/claude-code-prod/config.json
# Restart on crash, but rate-limit so a crash loop doesn't burn API quota.
Restart=on-failure
RestartSec=30s
StartLimitIntervalSec=300
StartLimitBurst=3
[Install]
WantedBy=multi-user.target
provision-credential.shview on GitHub#!/usr/bin/env bash
# ABOUTME: Provisions a per-agent credential file with operator-owned ACLs.
# ABOUTME: The agent's own user cannot read the file at rest; the operator launches with it in env.
set -euo pipefail
AGENT_NAME="${1:-}"
AGENT_USER="${2:-agent-runner}"
OPERATOR_GROUP="${3:-operators}"
if [[ -z "$AGENT_NAME" ]]; then
cat <<'USAGE' >&2
Usage: provision-credential.sh <agent-name> [agent-user] [operator-group]
Example:
AGENT_TOKEN="$(vault read -field=token agents/claude-code-prod)" \
provision-credential.sh claude-code-prod agent-runner operators
USAGE
exit 64
fi
if [[ "$EUID" -ne 0 ]]; then
echo "Must run as root (uses chown and setfacl)." >&2
exit 1
fi
if [[ -z "${AGENT_TOKEN:-}" ]]; then
echo "AGENT_TOKEN must be set in the environment." >&2
exit 1
fi
CRED_DIR="/etc/agents/${AGENT_NAME}"
CRED_FILE="${CRED_DIR}/token"
ENV_FILE="${CRED_DIR}/env"
mkdir -p "$CRED_DIR"
chown "root:${OPERATOR_GROUP}" "$CRED_DIR"
chmod 0750 "$CRED_DIR"
umask 0177
printf '%s\n' "$AGENT_TOKEN" > "$CRED_FILE"
chown "root:${OPERATOR_GROUP}" "$CRED_FILE"
chmod 0640 "$CRED_FILE"
# Render env file consumed by the systemd unit. The systemd unit reads this
# at agent launch and exports ANTHROPIC_API_KEY into the agent's environment.
# The agent process itself cannot read /etc/agents/<name>/env at rest.
cat > "$ENV_FILE" <<EOF
ANTHROPIC_API_KEY=${AGENT_TOKEN}
AGENT_NAME=${AGENT_NAME}
EOF
chown "root:${OPERATOR_GROUP}" "$ENV_FILE"
chmod 0640 "$ENV_FILE"
# ACL: the agent's own user is denied read on the credential file. The
# operator (or systemd, running as root) reads it at launch. A compromised
# agent process cannot re-read the file from disk.
if command -v setfacl >/dev/null 2>&1; then
setfacl -m "u:${AGENT_USER}:---" "$CRED_FILE"
setfacl -m "u:${AGENT_USER}:---" "$ENV_FILE"
else
echo "WARN: setfacl not available; relying on group-based denial only." >&2
fi
echo "Provisioned ${CRED_DIR}"
echo "Credential fingerprint: $(printf '%s' "$AGENT_TOKEN" | sha256sum | cut -d' ' -f1 | head -c 16)"
unset AGENT_TOKEN
verify.shview on GitHub#!/usr/bin/env bash
# ABOUTME: Verifies per-agent credential hygiene: not in process listing, not readable by agent, unique per agent.
# ABOUTME: Run as root. Exits non-zero on any failed check.
set -euo pipefail
AGENT_USER="${AGENT_USER:-agent-runner}"
AGENTS_DIR="${AGENTS_DIR:-/etc/agents}"
PASS=0
FAIL=0
check() {
local name="$1"
local result="$2"
if [[ "$result" == "ok" ]]; then
echo "PASS: $name"
PASS=$((PASS + 1))
else
echo "FAIL: $name -- $result"
FAIL=$((FAIL + 1))
fi
}
# 1. Credential not in process listing
if ps -eo args | grep -E 'ANTHROPIC_API_KEY=[^ ]+' | grep -v grep >/dev/null; then
check "credential not in process listing" "found ANTHROPIC_API_KEY=... in args"
else
check "credential not in process listing" "ok"
fi
# 2. Agent cannot read its own credential file at rest
shopt -s nullglob
for cred in "$AGENTS_DIR"/*/token; do
if sudo -u "$AGENT_USER" cat "$cred" >/dev/null 2>&1; then
check "agent cannot read $cred" "agent user $AGENT_USER read it successfully"
else
check "agent cannot read $cred" "ok"
fi
done
# 3. Unique credential per agent
TOKENS=$(find "$AGENTS_DIR" -maxdepth 2 -name token -type f 2>/dev/null)
if [[ -z "$TOKENS" ]]; then
check "per-agent credentials provisioned" "no tokens found in $AGENTS_DIR"
else
HASHES=$(echo "$TOKENS" | xargs -r md5sum | awk '{print $1}' | sort)
UNIQUE=$(echo "$HASHES" | uniq | wc -l)
TOTAL=$(echo "$HASHES" | wc -l)
if [[ "$UNIQUE" -eq "$TOTAL" ]]; then
check "per-agent credentials are unique ($TOTAL agents)" "ok"
else
check "per-agent credentials are unique" "$((TOTAL - UNIQUE)) duplicate(s) detected"
fi
fi
echo ""
echo "Summary: $PASS passed, $FAIL failed"
exit "$FAIL"
Cell notes
Identity / Client-side
Control. Per-agent credentials in operator-owned config; no shared keys; filesystem ACLs preventing cross-agent credential access.
Strength. Deterministic when the operator-owned config is uncompromised. Bypassable through token theft from logs, process listings, or world-readable env files; through credential reuse if the operator copies the config; through agent processes that run as a privileged user.
Tooling
- - A secrets manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, 1Password CLI, or
passfor solo work). - -
setfacl(Linux) orchmodplus dedicated user accounts (macOS). - - An OIDC-issuing IdP (Okta, Auth0, Azure AD, Keycloak) for token-based credentials.
- -
systemd(Linux) orlaunchd(macOS) to bind the credential to the agent process.
Files in this directory
- -
provision-credential.sh, creates a per-agent credential directory, locks down ownership, applies an ACL so the agent's own user cannot read the file at rest. Run as root. - -
claude-code-prod.service, systemd unit that loads the per-agent env file and runs the agent as a dedicated user. Drop in/etc/systemd/system/. - -
verify.sh, three checks: credential not in process listing, agent cannot read its own credential file at rest, every agent has a unique credential hash.
Verification
Run verify.sh. All three checks must pass.
sudo ./verify.sh
Common mistakes
- - Using a single
~/.anthropic/credentialsfor every agent on the box. - - Putting the token in a
.envfile checked into the repo. - - Setting the env var in
/etc/profile, where every process inherits it. - - Logging the credential into the agent's own log file at startup.
- - Granting the agent's own user write permission on
/etc/agents/so the agent can rotate its own key, that ability lets the agent change its identity at will.
Citation
NIST CSF 2.0 PR.AA-01, PR.AA-03. NIST SP 800-207 (Zero Trust Architecture). NIST SP 800-63 Rev. 4 (Digital Identity Guidelines).
Primary bypasses
Documented, not hypothetical. A control whose bypass is undocumented is worse than no control, because somebody trusted it.
- token theft
- credential leakage in logs
Crosswalk
| NIST CSF 2 0 | PR.AA-01, PR.AA-03 |
|---|---|
| NIST AI RMF | MANAGE 2.4 |
| OWASP LLM | LLM02 |
| OWASP AGENTIC | ASI03 |
| OTHER | NIST SP 800-207, NIST SP 800-63 |
Cite this cell:
https://agenticcovenants.com/protect/identity/client-side/