Skip to content

feat: add claude auth snapshot diagnostics - #90

Merged
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-11/feat/claude-auth-snapshot
Jul 1, 2026
Merged

feat: add claude auth snapshot diagnostics#90
axisrow merged 2 commits into
mainfrom
ao/clihost_af28bd122d-11/feat/claude-auth-snapshot

Conversation

@axisrow

@axisrow axisrow commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add /bin/claude-auth-snapshot.sh for in-container Claude Code OAuth metadata snapshots with snapshot, diff, list, and latest commands.
  • Add bin/claude-auth-snapshot-host.sh for host-side Docker inspect metadata plus nested container snapshots.
  • Document the baseline/failure/diff workflow in README/CLAUDE and add optional CLAUDE_AUTH_SNAPSHOT_DIR documentation.

Secret hygiene

  • Snapshots never serialize accessToken or refreshToken; they keep only metadata (expiresAt, scopes, subscription type, sha256/mtime, permissions, process/time/network state).
  • Tests and Docker smoke include grep checks proving fake token values and token field names are absent from snapshot output.

Validation

  • python -m pytest tests/unit/test_shell_scripts.py::TestShellSyntax tests/unit/test_shell_scripts.py::TestClaudeAuthSnapshotScript -q
  • python -m pytest tests/ (372 passed)
  • docker build -t clihost:claude-auth-snapshot-smoke --build-arg INSTALL_CLAUDE_CODE=false --build-arg INSTALL_CODEX=false --build-arg INSTALL_GEMINI=false --build-arg INSTALL_COPILOT=false --build-arg INSTALL_OPENCODE=false --build-arg INSTALL_DROID=false --build-arg INSTALL_HAPI=false --build-arg INSTALL_AO=false --build-arg INSTALL_HERMES=false --build-arg INSTALL_CLOUDFLARED=false --build-arg INSTALL_CHISEL=false .
  • Container smoke: docker run --rm --user hapi --entrypoint bash clihost:claude-auth-snapshot-smoke ... verified snapshot metadata and no token leakage.
  • Host-wrapper smoke: bin/claude-auth-snapshot-host.sh <container> smoke verified Docker state/mount metadata plus nested redacted snapshot.

Closes #89

axisrow and others added 2 commits July 1, 2026 14:06
…89)

Cycle-review (PR #90) — Codex flagged two criticals on this credential-adjacent
tool; both confirmed against the code and fixed.

1. Token leak via nested metadata. read_credentials only checked `scopes` was a
   list and copied `subscriptionType` unguarded, so a malformed / schema-drifted
   credentials file could smuggle a token inside a nested object under either
   field and it would land in the snapshot. Now a strict scalar allowlist: scopes
   keeps only plain strings, subscriptionType only str/None — anything else is
   dropped. The redaction guarantee no longer depends on the happy-path schema.

2. Symlink-follow write as root. The documented host flow runs the inner script
   via `docker exec` as ROOT (image sets no USER) over hapi-writable /home/hapi,
   and Path.write_text follows symlinks — a hapi-planted symlinked snapshot dir
   (or file) could steer a root write outside it. Now: refuse a symlinked
   snapshot dir, create it 0700, and write the file via
   os.open(O_WRONLY|O_CREAT|O_EXCL|O_NOFOLLOW, 0600). Mirrors the symlink
   hardening in uploads.py / ensure_claude_settings. This also removes Claude's
   minor "echo of a maybe-nonexistent path" note: a failed exclusive open aborts
   under set -e before the path is echoed.

Tests: test_snapshot_never_leaks_token_nested_in_scopes_or_subscription (plants
sk-ant-* inside scopes/subscriptionType, asserts absence) and
test_snapshot_refuses_symlinked_snapshot_dir (symlinked dir → non-zero exit,
victim dir stays empty). Verified behaviorally: snapshot lands 0600, no token;
symlinked dir refused, no write through the link.

pytest tests/ -> 374 passed, 205 subtests.

Refs #89

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv
@axisrow

axisrow commented Jul 1, 2026

Copy link
Copy Markdown
Owner Author

🔍 Local review (cycle 1)

Reviewed locally (Claude subagent + Codex companion), no bots pinged.

Verdict Reviewer Finding Location
FIX codex Token leak via nested object under scopes/subscriptionType (redaction was happy-path scalar-only) claude-auth-snapshot.sh read_credentials
FIX codex Symlink-follow write as ROOT (docker exec, no USER) over hapi-writable storage → arbitrary-path write claude-auth-snapshot.sh snapshot writer
claude CLEAN: extraction allowlist-only, no cat/echo of raw file, probe sends no token, diff logic correct, redaction test load-bearing
SKIP→moot claude echo of maybe-nonexistent path on abort resolved by symlink fix (abort before echo)
SKIP claude pure mtime-bump reads as "refresh works" acceptable diagnostic heuristic

FIX applied (e7e3a82):

  1. Strict scalar allowlist — scopes keeps only plain strings, subscriptionType only str/None; nested objects (potential token smuggling) dropped. Redaction no longer depends on happy-path schema.
  2. Symlink-safe write — refuse a symlinked snapshot dir, create it 0700, write via os.open(O_CREAT|O_EXCL|O_NOFOLLOW, 0600). Mirrors uploads.py / ensure_claude_settings hardening.

Two regression tests added (nested-token redaction; symlinked-dir refusal). Verified behaviorally: snapshot lands 0600 with no token; symlinked dir refused, victim stays empty. pytest tests/ → 374 passed, 205 subtests.

Cycle 1/3 complete: 2 FIX (both fixed), Claude minors folded in. No remaining FIX → review cycle done. Merge is yours to trigger (local mode does not auto-merge).

@axisrow
axisrow merged commit eb50c8e into main Jul 1, 2026
1 check passed
@axisrow
axisrow deleted the ao/clihost_af28bd122d-11/feat/claude-auth-snapshot branch July 1, 2026 09:12
axisrow added a commit that referenced this pull request Jul 1, 2026
Cycle-review (PR #95) — both Codex and the Claude reviewer independently flagged
(and reproduced) a critical: ensure_ssh_dir ran `ensure_dir_owned` (mkdir -p +
chown -R) then `chmod 700` on ~/.ssh with NO symlink guard. This runs as root
before the privilege drop, and /home/hapi is hapi-writable via the persistent
volume, so a hapi-planted `~/.ssh` symlink (e.g. -> /etc/ssh or another tenant's
home) is dereferenced by GNU chown -R / chmod — root re-perms and re-owns the
attacker-chosen target (arbitrary-path chmod+chown / DoS / ownership hijack).
Same vuln class already fixed in ensure_claude_settings (#90); the new helper had
reintroduced it, and the shipped happy-path tests were green over the hole.

Fix mirrors ensure_claude_settings: bail on any pre-existing `~/.ssh` symlink or
non-directory, and only chmod after re-confirming it is a real non-symlink dir.

Test: test_ssh_dir_symlink_attack_is_refused plants ~/.ssh -> victim and uses the
REAL chmod (not the fake logger) so a follow-through would actually mutate the
victim — asserts victim stays 0755, is never chowned, and the symlink is left
as-is. Fails on the old code, passes on the fix.

pytest tests/ -> 379 passed, 205 subtests.

Refs #91

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv
axisrow added a commit that referenced this pull request Jul 1, 2026
* feat: add sync rsync foundation

* fix(entrypoint): harden ensure_ssh_dir against root symlink-follow (#91)

Cycle-review (PR #95) — both Codex and the Claude reviewer independently flagged
(and reproduced) a critical: ensure_ssh_dir ran `ensure_dir_owned` (mkdir -p +
chown -R) then `chmod 700` on ~/.ssh with NO symlink guard. This runs as root
before the privilege drop, and /home/hapi is hapi-writable via the persistent
volume, so a hapi-planted `~/.ssh` symlink (e.g. -> /etc/ssh or another tenant's
home) is dereferenced by GNU chown -R / chmod — root re-perms and re-owns the
attacker-chosen target (arbitrary-path chmod+chown / DoS / ownership hijack).
Same vuln class already fixed in ensure_claude_settings (#90); the new helper had
reintroduced it, and the shipped happy-path tests were green over the hole.

Fix mirrors ensure_claude_settings: bail on any pre-existing `~/.ssh` symlink or
non-directory, and only chmod after re-confirming it is a real non-symlink dir.

Test: test_ssh_dir_symlink_attack_is_refused plants ~/.ssh -> victim and uses the
REAL chmod (not the fake logger) so a follow-through would actually mutate the
victim — asserts victim stays 0755, is never chowned, and the symlink is left
as-is. Fails on the old code, passes on the fix.

pytest tests/ -> 379 passed, 205 subtests.

Refs #91

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Vecu5xo9D9Cs2vmeohKVKv

---------

Co-authored-by: axisrow <axisrow@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
axisrow added a commit that referenced this pull request Jul 8, 2026
)

Six P1 correctness/reliability fixes from the #101 code review.

#3 (proxy.py): enable SO_KEEPALIVE on both tunnel sockets so a client that
vanishes without a FIN (laptop sleep, NAT timeout) is detected by the OS and the
idle tunnel thread + sockets are freed instead of leaking forever.

#4 (ratelimit.py): use time.monotonic() instead of wall-clock time.time(), so a
backward clock jump (NTP correction, suspend/resume, VM migration) cannot freeze
the window and lock out legitimate users past 60s/300s.

#5 (app.py): _check_auth now honours redirect=True in the removed-user branch —
a browser navigation with a valid cookie whose account was deleted/renamed lands
on /login (302) instead of a raw 403 JSON blob.

#6 (proxy.py): add "cookie" to HOP_BY_HOP_HEADERS so the client's signed
ttyd_session + csrf_token is not forwarded to the internal ttyd (defense depth).

#10 (bin/clihost-sync.sh): the remote symlink guards were byte-identical
triplicates (local pair + one copy in each of two `bash -s` heredocs), so
hardening one silently bypassed the others — the class of bug caught 3× before
(#88/#90/#95). Define them once in a shared emit_remote_prelude injected into
both remote payloads.

#11 (.env.example): document SESSION_TIMEOUT, CLEANUP_ROOT, ROOT_PASSWORD,
HAPI_USER, HERMES_AUTO_UPDATE (read by code + CLAUDE.md but missing from the
example), plus the REQUEST_TIMEOUT slowloris knob for completeness.

New/changed env vars documented in .env.example: SESSION_TIMEOUT, CLEANUP_ROOT,
ROOT_PASSWORD, HAPI_USER, HERMES_AUTO_UPDATE, REQUEST_TIMEOUT. No port or volume
changes.

Closes #101 findings #3, #4, #5, #6, #10, #11.


Claude-Session: https://claude.ai/code/session_01KgvJhzd8gEhXVE4MVu18nn

Co-authored-by: axisrow <axisrow@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Диагностика слёта Claude Code OAuth при простое: snapshot+diff инструмент

1 participant