Local Dev Credential Bootstrap
Quick start (do these three things)
# 1. Morning routine — at the Mac mini console
./scripts/daily-auth.sh
# 2. Morning routine — away from the Mac mini (iPhone tmux, MacBook SSH, etc.)
DAILY_AUTH_DEVICE_CODE=1 ./scripts/daily-auth.sh
# 3. One-time Wrangler fix — add to ~/.zshrc
[ -f "$HOME/scripts/setup-cloudflare-env.sh" ] && source "$HOME/scripts/setup-cloudflare-env.sh"
# (the helper exports CLOUDFLARE_API_TOKEN from sv0-platform/.env so
# `wrangler` skips its short-lived OAuth flow entirely)
After step 3, wrangler whoami should report The API Token is read from the CLOUDFLARE_API_TOKEN environment variable — that confirms the OAuth bypass is active.
The remainder of this doc explains the model behind those three commands, inventories every credential the workspace touches, and lays out the remote-from-iPhone access path. Use it as reference; the three commands above are the day-to-day surface.
Why this exists
A session on 2026-05-16 burned operator time on an AWS SSO re-auth round-trip during a Terraform apply, and that triggered a read-only enumeration of every credential a Claude Code session in the SecurityV0 workspace touches: AWS SSO, Cloudflare Access, Wrangler, Azure CLI, Tailscale, SSH, GitHub, Terraform Cloud, plus ~12 static service tokens.
The first draft of this doc claimed AWS SSO had a "1 hour hard cap" on the portal access token. That is wrong. AWS Identity Center (IIC) portal sessions follow the configured user interactive session duration (default 8 h, configurable up to 90 days in the IIC console), and AWS CLI ≥ 2.9 silently refreshes access tokens via the cached refresh token within that window. So for a typical workday, AWS SSO is a once-a-morning warm-up, not a mid-session interrupt. See AWS docs: Set session duration for AWS accounts, Session duration considerations for the AWS CLI and AWS SDKs.
TL;DR
- No mid-session interrupts in the default config if AWS CLI is ≥ 2.9, the
sso-sessionblock is present in~/.aws/config, and the IIC user interactive session is at least as long as your workday (default 8 h). The script's pre-flight check warns when any of these is wrong. - Wrangler OAuth (~1 h) is the one real ongoing interrupter — fixed permanently by exporting
CLOUDFLARE_API_TOKENfromsv0-platform/.env(step 3 above). - Cloudflare Access SSH to dev-azure runs on a 24 h app_token. The daily script renews it automatically when at the Mac mini console; over SSH it warns and tells you to renew from the local console (browser flow has no headless mode).
- Remote-from-iPhone works via Tailscale → SSH → tmux +
DAILY_AUTH_DEVICE_CODE=1. Telegram bots, cloud auth brokers, and hardware-key passthrough were evaluated and rejected as net-negative. - Everything else is fire-and-forget — see the inventory appendix.
Credential inventory
| Credential | Login command | Token lifetime | Device-bound? | Category | Failure mode |
|---|---|---|---|---|---|
AWS SSO portal session (per sso-session) | aws sso login --sso-session <name> (browser) or ... --use-device-code (URL + code) | Configurable; default 8 h — set on the IIC instance under "User interactive session duration", max 90 d. Cached access token in ~/.aws/sso/cache/*.json is silently refreshed by AWS CLI ≥ 2.9 via the paired refresh token within this window. | Browser, or device code from any phone | Morning routine for default 8 h IIC config | aws CLI fails with ExpiredTokenError once the IIC session ends; re-run aws sso login |
AWS IAM role session (cached in ~/.aws/cli/cache/) | Derived on first API call against the profile | 12 h (permission set SessionDuration: PT12H; that's the max for any permission set) | No | Morning routine | Cascades from portal session end |
Azure CLI (az) | az login (browser, one-time) | Access token 1 h, auto-refreshed silently via MSAL; refresh token is 90-day sliding window | Browser only on first login | Fire-and-forget | Transparent refresh — no observed mid-session breakage |
GitHub CLI (gh) | gh auth login (one-time web flow → macOS keychain) | gho_ OAuth token: no expiry until revoked | No | Fire-and-forget | Never breaks |
GitHub SSH (id_ed25519_mini1_agentic) | Auto-loaded into ssh-agent at login (AddKeysToAgent yes) | No passphrase; persists until reboot | No | Fire-and-forget | Reboot-only |
| GHCR pulls (CI) | Workflow GITHUB_TOKEN passed to deploy-instance.sh as GHCR_TOKEN | Per-workflow-run | No | Fire-and-forget | None — scoped per run |
GH_PAT_ROUTINE (classic PAT) | Present in sv0-platform/.env but consumer unclear — grep -rn GH_PAT_ROUTINE finds zero references outside .env/.env.example. Likely consumed by an automation routine (cc-bots, pm-cleanup) rather than the platform itself. | PAT until manually rotated | No | Fire-and-forget | None until rotation. Treat as TODO: confirm the consumer, or remove from .env if dead. |
| Wrangler / Cloudflare OAuth | wrangler login (browser) | ~1 h (expiration_time field in ~/Library/Preferences/.wrangler/config/default.toml) | Browser | Mid-session interrupt | Wrangler CLI auth errors; re-run wrangler login |
Cloudflare static API token (CLOUDFLARE_API_TOKEN in sv0-platform/.env) | n/a — permanent token | Never (revoke-only) | No | Fire-and-forget | Already used by all platform automation paths |
| Cloudflare Access SSH (dev-azure VM) | First SSH connection auto-runs cloudflared access ssh-gen --hostname dev-azure-ssh.securityv0.com | SSH cert 4 min (auto-regenerated per-connection); CF app_token 24 h | Browser on first daily login | Morning routine | SSH fails; cloudflared access login dev-azure-ssh.securityv0.com renews |
| Cloudflare Tunnel (ha-tunnel) | launchctl load ~/Library/LaunchAgents/com.cloudflared.ha-tunnel.plist (on boot) | Service credential, never expires | No | Fire-and-forget | Auto-restart via launchd KeepAlive |
| Terraform Cloud token | Stored in ~/.terraform.d/credentials.tfrc.json | Never (user-created API token) | No | Fire-and-forget | Manual rotation only |
| Tailscale | macOS app login | Never while app is running | No | Fire-and-forget | Reboot-only; node 100.86.92.75 mcmini4p |
| SSH to Hetzner | ssh deploy@178.156.245.75 etc. (uses id_ed25519_mini1_agentic) | Persistent while ssh-agent alive | No | Fire-and-forget | Reboot-only |
Connector static creds (AWS IAM user keys in sv0-connectors/integrations/aws/.env) | Pre-configured AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY | Never (IAM user long-term creds) | No | Fire-and-forget | None until rotation; connector path is SSO-independent |
Azure SP creds (connectors — AZURE_CLIENT_ID/SECRET) | Static SP client secret in .env | Long-lived (1–2 y typical) | No | Fire-and-forget for now | Mid-session break only if secret expires — not a daily issue |
WorkOS API keys (.env) | Static WORKOS_API_KEY, PROD_WORKOS_API_KEY | Never (revoke-only) | No | Fire-and-forget | None |
Grafana tokens (GRAFANA_API_TOKEN, GRAFANA_ALLOY_TOKEN) | Static service-account tokens | Never (service-account tokens don't expire) | No | Fire-and-forget | None |
Better Stack token (BETTER_STACK_CLAUDECODE_GLOBAL_API_TOKEN in sv0-platform/.env) | Static API token for Better Stack log shipping | Never (revoke-only) | No | Fire-and-forget | None until rotation |
Cloudflare Zero Trust API token (CLOUDFLARE_API_TOKEN_ZERO_TRUST in sv0-platform/.env) | Static API token, scoped to Zero Trust policy management (separate from the general CLOUDFLARE_API_TOKEN) | Never (revoke-only) | No | Fire-and-forget | None until rotation |
ServiceNow basic-auth (SERVICENOW_USERNAME / SERVICENOW_PASSWORD in sv0-connectors/integrations/entra-servicenow/.env) | Username + password for the dev217540 dev instance, not an API token | Until admin resets password | No | Fire-and-forget for now | An admin password reset on the SN side is indistinguishable from a network error — failure surfaces as 401 from the SN REST API mid-scan |
Connector ingest API keys (.scratch/api-keys/*.key, format sv0_<env>_<64hex>) | Per-tenant connector tokens; mint via direct Mongo insert into connector_api_keys (admin route rejects delegated_agent callers) | Static until DB wipe / rotation | No | Fire-and-forget for stable envs | If the platform DB is wiped (e.g., deploy-azure-dev recreate), all keys become INVALID_BEARER_TOKEN — must re-mint. Hetzner dev is stable; dev-azure has had recurring wipes |
MongoDB (via MONGODB_URI in .env) | Connection string with embedded credentials | Never | No | Fire-and-forget | None |
1Password CLI (op) | op signin — macOS biometrics / password | ~30 min idle timeout | Biometric / password | Truly device-bound | Out of agent scope per workspace memory rule — agents never invoke op |
| Notion MCP | Browser OAuth via MCP authenticate | ~30 d (Notion default) | Browser | Morning routine (rare) | MCP calls fail; re-auth |
| Docker / Colima | Colima running; docker.sock healthy | Persistent | No | Fire-and-forget | Reboot-only |
| saml2aws | Installed (2.36.19), no ~/.saml2aws config | n/a | n/a | Unused | Not a risk today |
Key structural findings
- AWS SSO is a morning-routine credential, not a mid-session interrupter — provided AWS CLI ≥ 2.9, the
sso-sessionblock is in~/.aws/config, and the IIC user interactive session is set to at least your workday length. With the default 8 h IIC setting, the silent-refresh window comfortably covers one work session. If 8 h isn't enough, raise the IIC user session duration in the AWS access portal console (up to 90 days). The 2026-05-16 re-auth was a session boundary at the end of the configured window, not a hard 1 h cap. - Wrangler OAuth is the only real ~1 h interrupter. The refresh-token rotation does not fire from the CLI alone; the static
CLOUDFLARE_API_TOKENalready used by platform automation is the permanent fix. - Azure CLI is silently well-behaved. MSAL refreshes the access token transparently from a 90-day sliding refresh window. Despite three expired entries observed in
~/.azure/msal_token_cache.json,az account showsucceeded without prompt. - Cloudflare Access SSH (24 h app_token) is morning-routine only and is now auto-warmed by the script when at the local console.
Daily morning routine script
The runnable script is scripts/daily-auth.sh in this repo. Source-of-truth lives there; only excerpts are quoted here to avoid sync drift.
./scripts/daily-auth.sh
What it does, in order:
- AWS SSO —
aws sso loginon bothsso-sessions (IF-securityv0-mgmt,IF-sv0-demo-lab-1). Auto-switches to--use-device-codewhen invoked over SSH (so tmux-from-iPhone works). Then warms the 12 h role-credential cache viaaws sts get-caller-identityso the clock starts at intent. - Azure CLI — silent
az account showcheck; only prompts if the 90 d refresh window expired. - Cloudflare Access SSH — silent
cloudflared access ssh-genagainstdev-azure-ssh.securityv0.com; warns if the 24 h app_token is expired. - Wrangler — skipped if
CLOUDFLARE_API_TOKENis exported (see the Wrangler permanent fix); otherwisewrangler whoamicheck. - GitHub CLI —
gh auth statuscheck. - SSH agent — confirms
mini1-agentickey is loaded; re-adds if not. - Tailscale — confirms the
mcmini4pnode is up.
The script tolerates missing commands gracefully (each step requires its CLI and skips with a warning if absent), so it is safe to run on a fresh machine that doesn't have every tool yet.
Timing breakdown
| Step | Wall time | Device interaction? |
|---|---|---|
| AWS SSO mgmt login | ~60 s | Yes — browser or iPhone Safari (device-code) |
| AWS SSO demo-lab login | ~15 s | Often no — shared portal session |
| Azure check | ~2 s | No |
| CF Access ssh-gen | ~5 s | Browser only if 24 h app_token expired |
| Wrangler check | ~3 s | None if CLOUDFLARE_API_TOKEN exported; otherwise browser |
| GH / SSH / Tailscale checks | ~3 s | No |
| Total | ~90 s | 1 mandatory browser/device-code interaction |
Remote-from-iPhone analysis
The question: can a Claude Code session continue (or be kicked off) from an iPhone when away from the Mac mini?
Tailscale network state today
100.86.92.75 mcmini4p macOS — the Mac mini
100.82.179.22 iphone181 iOS — last seen 28 days ago (needs reconnect)
100.118.253.99 mbp14m4 macOS — MacBook Pro
iPhone needs Tailscale reconnected — open the app, no re-auth required unless the Tailscale account session itself expired.
Path A — tmux + Tailscale SSH from iPhone (VERDICT: works)
One-time setup:
- Enable SSH on the Mac mini — System Settings → General → Sharing → Remote Login → ON. Or enable Tailscale SSH in the Tailscale admin console (Mac mini currently has
RunSSH: false); Tailscale SSH handles auth via Tailscale identity, no SSH key plumbing. - The existing tmux session (
claude-tg-sv0-main) is already running on the Mac mini.
From the iPhone:
# Termius or Blink Shell → SSH to 100.86.92.75 (via Tailscale)
ssh mini1@100.86.92.75
tmux attach -t claude-tg-sv0-main
What works on this path:
- Attach to live Claude Code sessions, full terminal control
- Run
aws sso login --use-device-code→ copy URL + code → open URL in iPhone Safari → approve - Approve Claude Code tool calls
- Run
wrangler login→ copy URL → Safari → approve - Restart background jobs
What does not work:
- The browser-flow
aws sso login(without--use-device-code) — it opens a browser on the Mac mini's display, which the iPhone cannot reach. Always pass--use-device-codefrom remote sessions.
Path B — Telegram bot triggering re-auth (VERDICT: not viable)
A bot that triggers aws sso login still hits the browser flow on the Mac mini's display — invisible from the iPhone. --use-device-code removes the browser dependency, but at that point a bot relaying a URL + code is just engineering overhead on top of what tmux attach already gives you. Skip.
Path C — Tailscale SSH instead of macOS Remote Login (VERDICT: works, marginally cleaner)
Tailscale SSH uses Tailscale identity instead of ~/.ssh/authorized_keys. ACLs are managed in the Tailscale admin console. Functionally equivalent to Path A for our purpose; either is fine.
Path D — Cloud-hosted auth-broker VM (VERDICT: overkill)
The idea: a small Azure VM holds refreshed tokens and vends them to local sessions. Two problems:
- To re-authenticate to AWS SSO, the broker still needs the operator's browser session — it cannot bypass the OIDC flow.
- The 12 h role credentials it would vend are already cached locally for 12 h in
~/.aws/cli/cache/. There is no gap to fill.
Skip — the local cache + the tmux+Tailscale path solves the same problem at zero infra cost.
Path E — Hardware-key passthrough over SSH (VERDICT: not possible)
FIDO2 / WebAuthn keys cannot be forwarded over SSH. Not applicable here since the flows are browser OIDC, not hardware-key.
Summary
| Path | Feasibility | Setup cost | Solves the problem? |
|---|---|---|---|
| tmux + Tailscale SSH from iPhone | Yes | ~5 min (enable SSH or Tailscale SSH + reconnect iPhone) | Yes |
aws sso login --use-device-code | Yes (already supported) | Zero | Removes Mac-mini browser dependency |
| Telegram bot relay | Low value | High | No net gain over device-code |
| Cloud auth-broker VM | Overkill | Medium | No net gain over local cache |
| Hardware-key passthrough | Not possible | n/a | No |
Mid-session interrupt reduction
Problem 1: AWS SSO portal session
Not actually a 1 h hard cap. The IIC user interactive session follows the duration configured on the IIC instance (default 8 h, up to 90 days). AWS CLI ≥ 2.9 silently refreshes access tokens via the paired refresh token within that window. There is no mid-session interrupt unless one of: (a) the CLI version is too old, (b) the sso-session block is missing from ~/.aws/config, or (c) the configured IIC session is shorter than your workday.
The script's pre-flight check confirms (a) and (b). For (c), the fix is config — raise the IIC user interactive session duration in the AWS access portal settings (IIC console → Settings → Authentication → Configure user interactive session duration). This is the simplest fix and was the missing piece in the original draft of this doc.
Practical mitigations, ordered by leverage:
- Verify the prerequisites. AWS CLI ≥ 2.9 (
aws --version) +sso-sessionblocks present in~/.aws/config. Both are checked at the top ofdaily-auth.sh. - Set the IIC user interactive session duration to cover your workday. Default is 8 h; you can raise it. Done once in the AWS access portal console, not in the CLI.
- Warm the 12 h role cache immediately after login with
aws sts get-caller-identity --profile <name>. Starts the 12 h role clock at the moment of intent, not at the moment of first incidental use. - Use
--use-device-codefrom remote sessions. URL + code go to terminal output, openable from any device. The script auto-selects this when$SSH_CONNECTIONis set OR whenDAILY_AUTH_DEVICE_CODE=1is exported.
Force device-code mode from tmux: the script's $SSH_CONNECTION auto-detection only fires for NEW shells started after the SSH attach. If you attach from your iPhone and run daily-auth.sh in an existing tmux window, $SSH_CONNECTION is unset in that window and the script silently picks browser mode (opening a browser on the Mac mini's hidden display). The escape hatch:
DAILY_AUTH_DEVICE_CODE=1 ./scripts/daily-auth.sh
Set this whenever you are physically away from the Mac mini, regardless of how you reached the shell.
Anti-pattern to avoid: creating long-lived IAM-user credentials for AdministratorAccess as an alternative. That is a security regression and conflicts with the workspace's "fail loud over silent fallback" rule.
Problem 2: Wrangler OAuth (1 h)
Root cause: Cloudflare's OAuth access tokens are short-lived, and the refresh-token rotation in wrangler does not appear to fire reliably from the CLI alone (a static expiration_time field persists in ~/Library/Preferences/.wrangler/config/default.toml).
Fix: export the static CLOUDFLARE_API_TOKEN from sv0-platform/.env into the shell. When the environment variable is set, wrangler skips OAuth entirely.
# Add to ~/.zshrc
export CLOUDFLARE_API_TOKEN="$(grep '^CLOUDFLARE_API_TOKEN=' ~/dev/securityv0/repos/sv0-platform/.env | cut -d= -f2-)"
The static API token has no expiry, so wrangler becomes fire-and-forget. Already-running Claude Code sessions inherit the environment variable from the parent shell.
Problem 3: Cloudflare Access SSH app_token (24 h)
A morning item, not mid-session. The daily script now auto-warms it at the local console: when cloudflared access ssh-gen fails (app_token expired), the script invokes cloudflared access login automatically, then retries ssh-gen. Over SSH the script can't drive the browser; it warns and tells you to run the login command from the Mac mini.
Implementation checklist
| Change | Effort | Outcome |
|---|---|---|
Verify AWS CLI ≥ 2.9 + sso-session blocks present | 1 min (aws --version, grep ~/.aws/config) | Enables CLI silent refresh; the daily script checks both |
| Raise IIC user interactive session duration if 8 h is short | 1 min (AWS access portal → Settings) | Pushes the once-a-day re-auth further out |
| Enable macOS Remote Login (or Tailscale SSH) | 5 min (System Settings) | Enables iPhone remote access |
| Reconnect iPhone to Tailscale | 1 min (open app) | Enables iPhone remote access |
Export CLOUDFLARE_API_TOKEN in ~/.zshrc | 2 min (source the helper) | Eliminates Wrangler OAuth entirely |
Use DAILY_AUTH_DEVICE_CODE=1 when away from the Mac mini | 0 min — already supported | Forces device-code AWS SSO from any shell |
Files referenced
Real on-disk paths cited in this analysis, in case you want to verify state yourself:
~/.aws/config— bothsso-sessionentries and profiles~/.aws/sso/cache/*.json— portal access tokens + paired refresh tokens (TTL = IIC user interactive session duration, default 8 h)~/.aws/cli/cache/*.json— role credentials (TTL = permission setSessionDuration, max 12 h;IF-AdministratorAccessis at the 12 h max)~/.azure/msal_token_cache.json— Azure access + refresh tokens~/Library/Preferences/.wrangler/config/default.toml— wrangler OAuth expiry~/.cloudflared/dev-azure-ssh.securityv0.com-cf_key-cert.pub— CF Access SSH cert~/Library/LaunchAgents/com.cloudflared.ha-tunnel.plist— CF tunnel daemon~/dev/securityv0/repos/sv0-platform/.env— static CF / Grafana / WorkOS tokens~/dev/securityv0/repos/sv0-connectors/integrations/aws/.env— static IAM-user creds (connector path, SSO-independent)
Related
- Access Protection — Cloudflare Zero Trust
- CF Access Service Token Setup
- GitHub Secrets Inventory
- AWS Organization — SSO portal start URL, account-ID inventory