Four findings from security audit (internal/security/credential-token-backlog.md):
1. STDERR LEAK — molecule-git-token-helper.sh:146,153 logged ${response}
on platform errors. The response body MAY contain the token in some
failure modes (alternate JSON key shape on partial success). Now:
- capture curl's stderr to a tmp file (not $response) so we can log
the curl error message without ever interpolating the response body
- on empty-token branch, log only response size (bytes) for debug
2. CHMOD 600 — already in place at lines 116, 124, 223 (verified, no change)
3. RESPAWN SUPERVISION — entrypoint.sh wrapped daemon launch in a
while-true bash loop with 30s back-off. Without this, a daemon crash
silently leaves the workspace stuck on an expired token until the
container restarts. Logs to /home/agent/.gh-token-refresh.log
(agent-writable; /var/log is root-owned).
4. JITTER — molecule-gh-token-refresh.sh: added 0..120s random offset to
each sleep so 39 containers don't synchronize their refresh requests
against the platform endpoint.
Also:
- Daemon now sends helper output to /dev/null instead of merging stderr,
belt-and-suspenders against any future helper change that might write
the token to stdout.
- Daemon log lines include rc=$? on failure for actionable triage.
Inherent risks (org-wide token blast, prompt-injection theft, bearer
in volume, no audit log) tracked in internal/security/credential-token-backlog.md
as separate roadmap items.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: molecule-ai[bot] <276602405+molecule-ai[bot]@users.noreply.github.com>
102 lines
4.9 KiB
Bash
102 lines
4.9 KiB
Bash
#!/bin/sh
|
|
# Drop privileges to the agent user before exec'ing molecule-runtime.
|
|
# claude-code refuses --dangerously-skip-permissions when running as
|
|
# root/sudo for safety. Without this entrypoint, every cron tick fails
|
|
# with `ProcessError: Command failed with exit code 1` and the agent
|
|
# logs `--dangerously-skip-permissions cannot be used with root/sudo
|
|
# privileges for security reasons`.
|
|
#
|
|
# Pattern matches the legacy monorepo workspace/entrypoint.sh:
|
|
# fix volume ownership as root, then re-exec via gosu as agent (uid 1000).
|
|
|
|
if [ "$(id -u)" = "0" ]; then
|
|
# Configs volume is created by Docker as root; agent needs write access
|
|
# for plugin installs, memory writes, .auth_token rotation, etc.
|
|
chown -R agent:agent /configs 2>/dev/null
|
|
# Strip CRLF from hook scripts — Windows Docker Desktop copies host files
|
|
# with CRLF line endings even when .gitattributes says eol=lf. The \r in
|
|
# the shebang line makes python3 try to open 'script.py\r' → ENOENT →
|
|
# claude-code swallows the hook error → "(no response generated)".
|
|
# This is the permanent fix — runs at every container start.
|
|
for f in /configs/.claude/hooks/*.sh /configs/.claude/hooks/*.py; do
|
|
[ -f "$f" ] && sed -i 's/\r$//' "$f"
|
|
done
|
|
# /workspace handling — only chown when the contents are root-owned
|
|
# (typical on Docker Desktop on Windows where host uid maps to 0).
|
|
# On Linux Docker with matching uids the recursive chown is skipped
|
|
# to keep startup fast.
|
|
chown agent:agent /workspace 2>/dev/null || true
|
|
if [ -d /workspace ]; then
|
|
first_entry=$(find /workspace -mindepth 1 -maxdepth 1 -print -quit 2>/dev/null)
|
|
if [ -n "$first_entry" ] && [ "$(stat -c '%u' "$first_entry" 2>/dev/null)" = "0" ]; then
|
|
chown -R agent:agent /workspace 2>/dev/null
|
|
fi
|
|
fi
|
|
# Claude Code session directory — mounted at /root/.claude/sessions by
|
|
# the platform provisioner. Symlink it into agent's home so the SDK
|
|
# finds it when running as agent. The provisioner's mount point is
|
|
# hardcoded to /root/.claude/sessions; we don't want to change the
|
|
# platform contract just for this template.
|
|
mkdir -p /home/agent/.claude
|
|
if [ -d /root/.claude/sessions ]; then
|
|
chown -R agent:agent /root/.claude /home/agent/.claude 2>/dev/null
|
|
ln -sfn /root/.claude/sessions /home/agent/.claude/sessions
|
|
fi
|
|
|
|
# --- GitHub credential helper setup (issue #547 / #613) ---
|
|
# Configure git to use the molecule credential helper for github.com.
|
|
# This runs as root so the global gitconfig is written before we drop
|
|
# to agent. The helper fetches fresh GitHub App installation tokens
|
|
# from the platform API, with caching and env-var fallback.
|
|
if [ -x /app/scripts/molecule-git-token-helper.sh ]; then
|
|
# Set credential helper for github.com only (not all hosts).
|
|
# The '!' prefix tells git to run the command as a shell command.
|
|
git config --global "credential.https://github.com.helper" \
|
|
"!/app/scripts/molecule-git-token-helper.sh"
|
|
# Disable other credential helpers for github.com to avoid conflicts.
|
|
git config --global "credential.https://github.com.useHttpPath" true
|
|
# Move gitconfig to agent's home so it takes effect after gosu.
|
|
if [ -f /root/.gitconfig ]; then
|
|
cp /root/.gitconfig /home/agent/.gitconfig
|
|
chown agent:agent /home/agent/.gitconfig
|
|
fi
|
|
fi
|
|
# Create the token cache directory for the agent user.
|
|
mkdir -p /home/agent/.molecule-token-cache
|
|
chown agent:agent /home/agent/.molecule-token-cache
|
|
chmod 700 /home/agent/.molecule-token-cache
|
|
|
|
exec gosu agent "$0" "$@"
|
|
fi
|
|
|
|
# Now running as agent (uid 1000)
|
|
|
|
# --- Start background token refresh daemon (with respawn supervision) ---
|
|
# Keeps gh CLI and git credentials fresh across the 60-min token TTL.
|
|
# Wrapped in a respawn loop so a daemon crash doesn't silently leave the
|
|
# workspace stuck on an expired token. Runs in the background; entrypoint
|
|
# continues to exec molecule-runtime.
|
|
if [ -x /app/scripts/molecule-gh-token-refresh.sh ]; then
|
|
nohup bash -c '
|
|
while true; do
|
|
/app/scripts/molecule-gh-token-refresh.sh
|
|
rc=$?
|
|
echo "[molecule-gh-token-refresh] daemon exited rc=$rc — respawning in 30s" >&2
|
|
sleep 30
|
|
done
|
|
' > /home/agent/.gh-token-refresh.log 2>&1 &
|
|
fi
|
|
|
|
# --- Initial gh auth setup ---
|
|
# If GITHUB_TOKEN or GH_TOKEN is set (injected at provision time),
|
|
# authenticate gh CLI with it so it works immediately (before the first
|
|
# background refresh fires). The background daemon will replace this
|
|
# with a fresh token within ~60s of boot.
|
|
if [ -n "${GITHUB_TOKEN:-}" ]; then
|
|
echo "${GITHUB_TOKEN}" | gh auth login --hostname github.com --with-token 2>/dev/null || true
|
|
elif [ -n "${GH_TOKEN:-}" ]; then
|
|
echo "${GH_TOKEN}" | gh auth login --hostname github.com --with-token 2>/dev/null || true
|
|
fi
|
|
|
|
exec molecule-runtime "$@"
|