fix(harnesses): stage tokens for agent UID, fix three broken auth paths
Mirrors agent-runtimes audit + fix. All three M22 Phase 9 wrapper-script
auth paths were broken since the cutover: each one ran as the agent UID
trying to `cat` a 0400 root-only ESO mount. Same root cause we hit on
minimax/z-ai earlier today.
Changes:
1. anthropic-cloud-paul-oauth/v1 (NEW in framework)
- Mirrors agent-runtimes — was previously only present there.
- init.sh stages oauth_token into ~/.claude/.credentials.json (Claude
Code's native subscription-OAuth schema). No env, no apiKeyHelper,
no wrapper. Restores the equivalent of what harness_init.py used to
do for the legacy SOPS path.
- The legacy `bin/anthropic-wrapper.sh` was dead code (never wired).
2. gitea-https/v1
- init.sh stages the token to $HOME/.config/git/gitea-https-token
(0600 agent:agent) and points the per-host git credential helper at
the staged copy. Previously the helper `cat`d the ESO mount path
and silently failed at every git invocation.
3. gitea-admin/v1
- init.sh stages the token to $HOME/.config/gitea-admin/token
(0600 agent:agent). Wrapper updated to read from the staged copy.
- Removes stale `requires: anthropic-cloud/v1` (the only anthropic
harness in agent-runtimes is anthropic-cloud-paul-oauth/v1).
Pattern matches gitea-ssh / minimax / z-ai: ESO mount stays root-only,
init.sh runs as root and `install -m 0600 -o agent -g agent`s a single
explicit copy. Per-secret enumeration; future ESO Secret keys remain
inaccessible by default.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,18 @@
|
|||||||
|
kind: context
|
||||||
|
name: anthropic-cloud-paul-oauth
|
||||||
|
version: 1
|
||||||
|
description: "Anthropic cloud — subscription OAuth token for paul (personal account)"
|
||||||
|
requires: []
|
||||||
|
provides: [claude-code]
|
||||||
|
|
||||||
|
env:
|
||||||
|
ENFORCE_SUBSCRIPTION_PRICING: "true"
|
||||||
|
|
||||||
|
scripts:
|
||||||
|
init: "./init.sh"
|
||||||
|
|
||||||
|
secrets_required:
|
||||||
|
- name: anthropic-cloud-paul-oauth
|
||||||
|
account_ref: "anthropic-cloud-paul-oauth.user:paul"
|
||||||
|
mount_path: /run/agent/secrets/anthropic-cloud-paul-oauth
|
||||||
|
mode: "0400"
|
||||||
82
harnesses/contexts/anthropic-cloud-paul-oauth/v1/init.sh
Executable file
82
harnesses/contexts/anthropic-cloud-paul-oauth/v1/init.sh
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# anthropic-cloud-paul-oauth init — write Claude Code's native credentials
|
||||||
|
# file from the ESO-mounted oauth_token.
|
||||||
|
#
|
||||||
|
# Threat model: keep the ESO mount root-only (mode 0400) so the agent user
|
||||||
|
# cannot directly `cat` /run/agent/secrets/anthropic-cloud-paul-oauth/*.
|
||||||
|
# init.sh runs as root (in uid-wrapper.sh, before the gosu drop) and writes
|
||||||
|
# a per-secret artefact at $AGENT_HOME/.claude/.credentials.json with mode
|
||||||
|
# 0600 owned by agent. Claude Code reads that file natively for OAuth-based
|
||||||
|
# subscription auth — no env vars, no apiKeyHelper, no wrapper script.
|
||||||
|
#
|
||||||
|
# Why .credentials.json (not apiKeyHelper):
|
||||||
|
# - This is the SUBSCRIPTION (Pro/Max) path: ENFORCE_SUBSCRIPTION_PRICING=true
|
||||||
|
# on this harness causes the runner to strip ANTHROPIC_API_KEY and
|
||||||
|
# ANTHROPIC_AUTH_TOKEN from the subprocess env so an accidental API key
|
||||||
|
# can't fall through to per-token billing. Subscription auth flows
|
||||||
|
# through CLAUDE_CODE_OAUTH_TOKEN — Claude Code's native storage for
|
||||||
|
# that is .credentials.json with the claudeAiOauth shape.
|
||||||
|
# - apiKeyHelper would still work, but routes through the api_key path,
|
||||||
|
# which the runner's subscription-pricing enforcement is specifically
|
||||||
|
# designed to block. .credentials.json is the canonical OAuth path.
|
||||||
|
#
|
||||||
|
# This mirrors what entrypoint/harness_init.py used to do for the legacy
|
||||||
|
# SOPS-decrypted CLAUDE_CODE_OAUTH_TOKEN path. Phase 9 ESO-cutover removed
|
||||||
|
# the SOPS files; this init.sh restores the equivalent behaviour for the
|
||||||
|
# ESO-mounted token.
|
||||||
|
#
|
||||||
|
# Rotation handling: per-container init. Ephemeral container agents (one
|
||||||
|
# task = one container) always pick up the latest mounted oauth_token.
|
||||||
|
# Long-running sessions need a future scripts.control_loop hook to
|
||||||
|
# re-stage between agent CLI invocations.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ESO_OAUTH="/run/agent/secrets/anthropic-cloud-paul-oauth/oauth_token"
|
||||||
|
|
||||||
|
if [ ! -r "$ESO_OAUTH" ]; then
|
||||||
|
echo "ERROR: $ESO_OAUTH not readable. Check ESO ExternalSecret acct-<anthropic-cloud-paul-oauth-id>." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
AGENT_USER="${AGENT_USER:-agent}"
|
||||||
|
AGENT_HOME=$(getent passwd "$AGENT_USER" | cut -d: -f6)
|
||||||
|
if [ -z "$AGENT_HOME" ] || [ ! -d "$AGENT_HOME" ]; then
|
||||||
|
AGENT_HOME="/home/$AGENT_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
CONFIG_DIR="${CLAUDE_CONFIG_DIR:-$AGENT_HOME/.claude}"
|
||||||
|
mkdir -p "$CONFIG_DIR"
|
||||||
|
chown "$AGENT_USER:" "$CONFIG_DIR" 2>/dev/null || true
|
||||||
|
chmod 0700 "$CONFIG_DIR"
|
||||||
|
|
||||||
|
# Build the .credentials.json file. The token is piped via stdin so it
|
||||||
|
# never lands in argv (visible in /proc/<pid>/cmdline) or env. The schema
|
||||||
|
# matches what `claude setup-token` produces locally and what the legacy
|
||||||
|
# SOPS path in harness_init.py wrote.
|
||||||
|
CRED_FILE="$CONFIG_DIR/.credentials.json"
|
||||||
|
cat "$ESO_OAUTH" | python3 -c "
|
||||||
|
import json, sys
|
||||||
|
token = sys.stdin.read().strip()
|
||||||
|
print(json.dumps({
|
||||||
|
'claudeAiOauth': {
|
||||||
|
'accessToken': token,
|
||||||
|
'refreshToken': None,
|
||||||
|
'expiresAt': 4102444800000,
|
||||||
|
'scopes': [
|
||||||
|
'user:file_upload',
|
||||||
|
'user:inference',
|
||||||
|
'user:mcp_servers',
|
||||||
|
'user:profile',
|
||||||
|
'user:sessions:claude_code',
|
||||||
|
],
|
||||||
|
'subscriptionType': 'max',
|
||||||
|
'rateLimitTier': 'default_claude_max_5x',
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
" > "$CRED_FILE"
|
||||||
|
|
||||||
|
chown "$AGENT_USER:" "$CRED_FILE" 2>/dev/null || true
|
||||||
|
chmod 0600 "$CRED_FILE"
|
||||||
|
|
||||||
|
echo "anthropic-cloud-paul-oauth: oauth_token staged into $CRED_FILE (0600 $AGENT_USER:$AGENT_USER)"
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# gitea-admin-wrapper.sh — reads the ESO-mounted gitea-admin token at exec time
|
# gitea-admin-wrapper.sh — reads the agent-staged gitea-admin token at exec
|
||||||
# and prefixes it as a transient env var to the underlying tool.
|
# time and prefixes it as a transient env var to the underlying tool.
|
||||||
#
|
#
|
||||||
# M22 Phase 9: token mounted at /run/agent/secrets/gitea-admin/token (read-only,
|
# M22 Phase 9 staging pattern: init.sh runs as root and `install`s a 0600
|
||||||
# tmpfs, mode 0400). The token NEVER enters the wrapper's parent shell — only
|
# agent-owned copy of the ESO-mounted token into $HOME/.config/gitea-admin/
|
||||||
# the exec'd tool's environment via `exec env VAR=value cmd`. The brief
|
# token. This wrapper reads the staged copy, not the ESO mount path, so
|
||||||
# presence in /proc/<tool_pid>/environ of the tool process is the accepted
|
# the `cat` succeeds under the agent UID. The brief presence of the token
|
||||||
# floor (per spec/secrets-runtime.md SR-DISP-7 / H-SECRET-4).
|
# in /proc/<tool_pid>/environ of the exec'd tool is the accepted floor
|
||||||
|
# (per H-SECRET-4 / SR-DISP-7); the wrapper's parent shell never sees it.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
TOKEN_FILE="/run/agent/secrets/gitea-admin/token"
|
TOKEN_FILE="$HOME/.config/gitea-admin/token"
|
||||||
|
|
||||||
# git-credential-helper subcommand: read token and emit git credential format.
|
# git-credential-helper subcommand: read token and emit git credential format.
|
||||||
# Git invokes us as `gitea-admin-wrapper.sh git-credential-helper get` and
|
# Git invokes us as `gitea-admin-wrapper.sh git-credential-helper get` and
|
||||||
# expects key=value lines on stdout terminated by a blank line.
|
# expects key=value lines on stdout terminated by a blank line.
|
||||||
git_credential_helper() {
|
git_credential_helper() {
|
||||||
if [[ ! -r "$TOKEN_FILE" ]]; then
|
if [[ ! -r "$TOKEN_FILE" ]]; then
|
||||||
echo "gitea-admin-wrapper: token file not readable at $TOKEN_FILE" >&2
|
echo "gitea-admin-wrapper: staged token not readable at $TOKEN_FILE" >&2
|
||||||
|
echo "gitea-admin-wrapper: did init.sh run? (it stages the token from the ESO mount)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
local base_url="${GITEA_BASE_URL:-https://gitea.oreillyit.nz}"
|
local base_url="${GITEA_BASE_URL:-https://gitea.oreillyit.nz}"
|
||||||
@@ -33,7 +36,8 @@ case "${1:-}" in
|
|||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
if [[ ! -r "$TOKEN_FILE" ]]; then
|
if [[ ! -r "$TOKEN_FILE" ]]; then
|
||||||
echo "gitea-admin-wrapper: token file not readable at $TOKEN_FILE" >&2
|
echo "gitea-admin-wrapper: staged token not readable at $TOKEN_FILE" >&2
|
||||||
|
echo "gitea-admin-wrapper: did init.sh run? (it stages the token from the ESO mount)" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
# `exec env VAR=...` keeps the secret out of the wrapper's parent shell
|
# `exec env VAR=...` keeps the secret out of the wrapper's parent shell
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ kind: context
|
|||||||
name: gitea-admin
|
name: gitea-admin
|
||||||
version: 1
|
version: 1
|
||||||
description: "Gitea admin: SSH, git identity, API token for skynet org"
|
description: "Gitea admin: SSH, git identity, API token for skynet org"
|
||||||
requires:
|
requires: []
|
||||||
- anthropic-cloud/v1
|
|
||||||
provides: [gitea-admin]
|
provides: [gitea-admin]
|
||||||
|
|
||||||
git_identity:
|
git_identity:
|
||||||
|
|||||||
@@ -1,19 +1,60 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# init.sh for gitea-admin harness (M22 Phase 9)
|
# gitea-admin init — stage the API token for the agent user and configure
|
||||||
# Sets up non-secret env vars and configures git credential helper.
|
# the git credential helper.
|
||||||
# Credential token is NOT exported here — only read at exec time by the wrapper.
|
#
|
||||||
|
# Threat model: keep the ESO mount root-only (mode 0400). init.sh runs as
|
||||||
|
# root and `install`s a per-secret 0600 agent-owned copy at
|
||||||
|
# $AGENT_HOME/.config/gitea-admin/token. The wrapper at
|
||||||
|
# bin/gitea-admin-wrapper.sh reads the staged copy (not the ESO mount) at
|
||||||
|
# exec time. Without staging, every wrapper invocation would fail at the
|
||||||
|
# `cat` step because the wrapper runs under the agent UID and the ESO file
|
||||||
|
# is root:root mode 0400.
|
||||||
|
#
|
||||||
|
# This was previously broken: the wrapper `cat`d the ESO mount path which
|
||||||
|
# the agent could not read. Discovered during the M22 Phase 9 audit; the
|
||||||
|
# wrapper-script approach was never re-validated post-cutover. Stage-and-
|
||||||
|
# wrapper-points-at-stage matches gitea-ssh / gitea-https / minimax / z-ai.
|
||||||
|
#
|
||||||
|
# Rotation handling: per-container init. Long-running sessions need a
|
||||||
|
# future scripts.control_loop hook to re-stage between operations.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
# Non-secret configuration
|
ESO_TOKEN="/run/agent/secrets/gitea-admin/token"
|
||||||
export GITEA_BASE_URL="${GITEA_BASE_URL:-https://gitea.oreillyit.nz}"
|
|
||||||
|
|
||||||
# Ensure SSH directory exists with correct permissions
|
if [ ! -r "$ESO_TOKEN" ]; then
|
||||||
mkdir -p /home/agent/.ssh
|
echo "ERROR: $ESO_TOKEN not readable. Check ESO ExternalSecret acct-<gitea-admin-id>." >&2
|
||||||
chmod 700 /home/agent/.ssh
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Configure git to use the gitea-admin credential helper wrapper
|
AGENT_USER="${AGENT_USER:-agent}"
|
||||||
# The wrapper reads the ESO-mounted token at exec time
|
AGENT_HOME=$(getent passwd "$AGENT_USER" | cut -d: -f6)
|
||||||
git config --global credential.helper "!/opt/harness/contexts/gitea-admin/v1/bin/gitea-admin-wrapper.sh git-credential-helper"
|
if [ -z "$AGENT_HOME" ] || [ ! -d "$AGENT_HOME" ]; then
|
||||||
|
AGENT_HOME="/home/$AGENT_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "[gitea-admin] init complete"
|
# Stage the token into a per-secret path owned by agent, mode 0600.
|
||||||
|
# Wrapper reads from this path; ESO mount is never accessed at runtime by
|
||||||
|
# the agent UID.
|
||||||
|
STAGED_DIR="$AGENT_HOME/.config/gitea-admin"
|
||||||
|
STAGED_TOKEN="$STAGED_DIR/token"
|
||||||
|
mkdir -p "$STAGED_DIR"
|
||||||
|
chown "$AGENT_USER:" "$STAGED_DIR" 2>/dev/null || true
|
||||||
|
chmod 0700 "$STAGED_DIR"
|
||||||
|
install -m 0600 -o "$AGENT_USER" -g "$AGENT_USER" "$ESO_TOKEN" "$STAGED_TOKEN"
|
||||||
|
|
||||||
|
# Ensure SSH directory exists with correct permissions (legacy SSH path).
|
||||||
|
mkdir -p "$AGENT_HOME/.ssh"
|
||||||
|
chown "$AGENT_USER:" "$AGENT_HOME/.ssh" 2>/dev/null || true
|
||||||
|
chmod 0700 "$AGENT_HOME/.ssh"
|
||||||
|
|
||||||
|
# Non-secret configuration: GITEA_BASE_URL is set in harness env.
|
||||||
|
|
||||||
|
# Configure git to use the gitea-admin credential helper wrapper. Run as
|
||||||
|
# the agent user so ~/.gitconfig is owned correctly; otherwise root would
|
||||||
|
# write into /root/.gitconfig and the agent's git wouldn't see the helper.
|
||||||
|
WRAPPER_PATH="/opt/harness/contexts/gitea-admin/v1/bin/gitea-admin-wrapper.sh"
|
||||||
|
su - "$AGENT_USER" -c "git config --global credential.helper '!$WRAPPER_PATH git-credential-helper'"
|
||||||
|
|
||||||
|
echo "gitea-admin: token staged at $STAGED_TOKEN (0600 $AGENT_USER:$AGENT_USER)"
|
||||||
|
echo "gitea-admin: git credential helper configured"
|
||||||
|
|||||||
@@ -1,31 +1,69 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Configure git to use git credential helper for gitea.oreillyit.nz via HTTPS.
|
# gitea-https init — stage the token for the agent user and configure the
|
||||||
|
# git credential helper to read from the staged copy.
|
||||||
#
|
#
|
||||||
# The token is read from /run/agent/secrets/gitea-https/token at every git
|
# Threat model: keep the ESO mount root-only (mode 0400). init.sh runs as
|
||||||
# invocation (not at init time). This avoids the token appearing in logs.
|
# root and `install`s a per-secret 0600 agent-owned copy at
|
||||||
|
# $AGENT_HOME/.config/git/gitea-https-token. The git credential helper
|
||||||
|
# reads from the staged copy at every git invocation; the ESO mount path
|
||||||
|
# is never accessed by the agent.
|
||||||
|
#
|
||||||
|
# This was previously a `cat /run/agent/secrets/gitea-https/token` from
|
||||||
|
# inside an init.sh-generated helper script, which silently failed because
|
||||||
|
# the helper runs as agent and the ESO file is root:root mode 0400. The
|
||||||
|
# stage-and-helper-points-at-stage pattern matches gitea-ssh / minimax /
|
||||||
|
# z-ai / anthropic-cloud-paul-oauth.
|
||||||
|
#
|
||||||
|
# Rotation handling: per-container init. Ephemeral container agents
|
||||||
|
# always pick up the latest mounted token. Long-running sessions need a
|
||||||
|
# future scripts.control_loop hook to re-stage between git invocations.
|
||||||
|
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
ESO_TOKEN="/run/agent/secrets/gitea-https/token"
|
||||||
|
|
||||||
|
if [ ! -r "$ESO_TOKEN" ]; then
|
||||||
|
echo "ERROR: $ESO_TOKEN not readable. Check ESO ExternalSecret acct-<gitea-https-id>." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
AGENT_USER="${AGENT_USER:-agent}"
|
||||||
|
AGENT_HOME=$(getent passwd "$AGENT_USER" | cut -d: -f6)
|
||||||
|
if [ -z "$AGENT_HOME" ] || [ ! -d "$AGENT_HOME" ]; then
|
||||||
|
AGENT_HOME="/home/$AGENT_USER"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Stage the token into a per-secret path owned by agent, mode 0600.
|
||||||
|
STAGED_DIR="$AGENT_HOME/.config/git"
|
||||||
|
STAGED_TOKEN="$STAGED_DIR/gitea-https-token"
|
||||||
|
mkdir -p "$STAGED_DIR"
|
||||||
|
chown "$AGENT_USER:" "$STAGED_DIR" 2>/dev/null || true
|
||||||
|
chmod 0700 "$STAGED_DIR"
|
||||||
|
install -m 0600 -o "$AGENT_USER" -g "$AGENT_USER" "$ESO_TOKEN" "$STAGED_TOKEN"
|
||||||
|
|
||||||
|
# Generate the credential helper. It reads from the STAGED copy, not the
|
||||||
|
# ESO mount, so it works under the agent's UID.
|
||||||
CRED_HELPER="/opt/harness/contexts/gitea-https/v1/git-credential-gitea.sh"
|
CRED_HELPER="/opt/harness/contexts/gitea-https/v1/git-credential-gitea.sh"
|
||||||
|
cat > "$CRED_HELPER" <<HELPER_EOF
|
||||||
# Create the credential helper script
|
|
||||||
cat > "$CRED_HELPER" << 'HELPER_EOF'
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Git credential helper that provides the token from a mounted file
|
# Git credential helper for gitea.oreillyit.nz — reads the agent-staged
|
||||||
# for gitea.oreillyit.nz.
|
# token. The ESO mount itself is root-only (0400); this helper would fail
|
||||||
# This reads the file at EVERY git invocation, not at init time.
|
# if pointed at it directly.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
SECRET_FILE=/run/agent/secrets/gitea-https/token
|
TOKEN_FILE=$STAGED_TOKEN
|
||||||
[ -r "$SECRET_FILE" ] || exit 1
|
[ -r "\$TOKEN_FILE" ] || exit 1
|
||||||
echo "protocol=https"
|
echo "protocol=https"
|
||||||
echo "host=gitea.oreillyit.nz"
|
echo "host=gitea.oreillyit.nz"
|
||||||
echo "username=token"
|
echo "username=token"
|
||||||
echo "password=$(cat $SECRET_FILE)"
|
echo "password=\$(cat "\$TOKEN_FILE")"
|
||||||
HELPER_EOF
|
HELPER_EOF
|
||||||
|
|
||||||
chmod +x "$CRED_HELPER"
|
chmod +x "$CRED_HELPER"
|
||||||
|
|
||||||
# Configure git to use this credential helper for gitea.oreillyit.nz
|
# Configure git globally (per-host) to use the helper for gitea.oreillyit.nz.
|
||||||
git config --global credential.https://gitea.oreillyit.nz.helper "$CRED_HELPER"
|
# Run as agent so ~/.gitconfig is owned correctly; otherwise root would
|
||||||
|
# write into /root/.gitconfig and the agent's git wouldn't see it.
|
||||||
|
su - "$AGENT_USER" -c "git config --global credential.https://gitea.oreillyit.nz.helper '$CRED_HELPER'"
|
||||||
|
|
||||||
echo "Git HTTPS credential helper configured for gitea.oreillyit.nz"
|
echo "gitea-https: token staged at $STAGED_TOKEN (0600 $AGENT_USER:$AGENT_USER)"
|
||||||
|
echo "gitea-https: credential helper configured for gitea.oreillyit.nz"
|
||||||
|
|||||||
Reference in New Issue
Block a user