Skip to main content

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-session block 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_TOKEN from sv0-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

CredentialLogin commandToken lifetimeDevice-bound?CategoryFailure 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 phoneMorning routine for default 8 h IIC configaws 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 profile12 h (permission set SessionDuration: PT12H; that's the max for any permission set)NoMorning routineCascades 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 windowBrowser only on first loginFire-and-forgetTransparent refresh — no observed mid-session breakage
GitHub CLI (gh)gh auth login (one-time web flow → macOS keychain)gho_ OAuth token: no expiry until revokedNoFire-and-forgetNever breaks
GitHub SSH (id_ed25519_mini1_agentic)Auto-loaded into ssh-agent at login (AddKeysToAgent yes)No passphrase; persists until rebootNoFire-and-forgetReboot-only
GHCR pulls (CI)Workflow GITHUB_TOKEN passed to deploy-instance.sh as GHCR_TOKENPer-workflow-runNoFire-and-forgetNone — 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 rotatedNoFire-and-forgetNone until rotation. Treat as TODO: confirm the consumer, or remove from .env if dead.
Wrangler / Cloudflare OAuthwrangler login (browser)~1 h (expiration_time field in ~/Library/Preferences/.wrangler/config/default.toml)BrowserMid-session interruptWrangler CLI auth errors; re-run wrangler login
Cloudflare static API token (CLOUDFLARE_API_TOKEN in sv0-platform/.env)n/a — permanent tokenNever (revoke-only)NoFire-and-forgetAlready 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.comSSH cert 4 min (auto-regenerated per-connection); CF app_token 24 hBrowser on first daily loginMorning routineSSH 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 expiresNoFire-and-forgetAuto-restart via launchd KeepAlive
Terraform Cloud tokenStored in ~/.terraform.d/credentials.tfrc.jsonNever (user-created API token)NoFire-and-forgetManual rotation only
TailscalemacOS app loginNever while app is runningNoFire-and-forgetReboot-only; node 100.86.92.75 mcmini4p
SSH to Hetznerssh deploy@178.156.245.75 etc. (uses id_ed25519_mini1_agentic)Persistent while ssh-agent aliveNoFire-and-forgetReboot-only
Connector static creds (AWS IAM user keys in sv0-connectors/integrations/aws/.env)Pre-configured AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEYNever (IAM user long-term creds)NoFire-and-forgetNone until rotation; connector path is SSO-independent
Azure SP creds (connectors — AZURE_CLIENT_ID/SECRET)Static SP client secret in .envLong-lived (1–2 y typical)NoFire-and-forget for nowMid-session break only if secret expires — not a daily issue
WorkOS API keys (.env)Static WORKOS_API_KEY, PROD_WORKOS_API_KEYNever (revoke-only)NoFire-and-forgetNone
Grafana tokens (GRAFANA_API_TOKEN, GRAFANA_ALLOY_TOKEN)Static service-account tokensNever (service-account tokens don't expire)NoFire-and-forgetNone
Better Stack token (BETTER_STACK_CLAUDECODE_GLOBAL_API_TOKEN in sv0-platform/.env)Static API token for Better Stack log shippingNever (revoke-only)NoFire-and-forgetNone 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)NoFire-and-forgetNone 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 tokenUntil admin resets passwordNoFire-and-forget for nowAn 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 / rotationNoFire-and-forget for stable envsIf 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 credentialsNeverNoFire-and-forgetNone
1Password CLI (op)op signin — macOS biometrics / password~30 min idle timeoutBiometric / passwordTruly device-boundOut of agent scope per workspace memory rule — agents never invoke op
Notion MCPBrowser OAuth via MCP authenticate~30 d (Notion default)BrowserMorning routine (rare)MCP calls fail; re-auth
Docker / ColimaColima running; docker.sock healthyPersistentNoFire-and-forgetReboot-only
saml2awsInstalled (2.36.19), no ~/.saml2aws confign/an/aUnusedNot 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-session block 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_TOKEN already 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 show succeeded 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:

  1. AWS SSOaws sso login on both sso-sessions (IF-securityv0-mgmt, IF-sv0-demo-lab-1). Auto-switches to --use-device-code when invoked over SSH (so tmux-from-iPhone works). Then warms the 12 h role-credential cache via aws sts get-caller-identity so the clock starts at intent.
  2. Azure CLI — silent az account show check; only prompts if the 90 d refresh window expired.
  3. Cloudflare Access SSH — silent cloudflared access ssh-gen against dev-azure-ssh.securityv0.com; warns if the 24 h app_token is expired.
  4. Wrangler — skipped if CLOUDFLARE_API_TOKEN is exported (see the Wrangler permanent fix); otherwise wrangler whoami check.
  5. GitHub CLIgh auth status check.
  6. SSH agent — confirms mini1-agentic key is loaded; re-adds if not.
  7. Tailscale — confirms the mcmini4p node 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

StepWall timeDevice interaction?
AWS SSO mgmt login~60 sYes — browser or iPhone Safari (device-code)
AWS SSO demo-lab login~15 sOften no — shared portal session
Azure check~2 sNo
CF Access ssh-gen~5 sBrowser only if 24 h app_token expired
Wrangler check~3 sNone if CLOUDFLARE_API_TOKEN exported; otherwise browser
GH / SSH / Tailscale checks~3 sNo
Total~90 s1 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:

  1. 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.
  2. 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-code from 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:

  1. To re-authenticate to AWS SSO, the broker still needs the operator's browser session — it cannot bypass the OIDC flow.
  2. 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

PathFeasibilitySetup costSolves the problem?
tmux + Tailscale SSH from iPhoneYes~5 min (enable SSH or Tailscale SSH + reconnect iPhone)Yes
aws sso login --use-device-codeYes (already supported)ZeroRemoves Mac-mini browser dependency
Telegram bot relayLow valueHighNo net gain over device-code
Cloud auth-broker VMOverkillMediumNo net gain over local cache
Hardware-key passthroughNot possiblen/aNo

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:

  1. Verify the prerequisites. AWS CLI ≥ 2.9 (aws --version) + sso-session blocks present in ~/.aws/config. Both are checked at the top of daily-auth.sh.
  2. 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.
  3. 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.
  4. Use --use-device-code from remote sessions. URL + code go to terminal output, openable from any device. The script auto-selects this when $SSH_CONNECTION is set OR when DAILY_AUTH_DEVICE_CODE=1 is 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

ChangeEffortOutcome
Verify AWS CLI ≥ 2.9 + sso-session blocks present1 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 short1 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 Tailscale1 min (open app)Enables iPhone remote access
Export CLOUDFLARE_API_TOKEN in ~/.zshrc2 min (source the helper)Eliminates Wrangler OAuth entirely
Use DAILY_AUTH_DEVICE_CODE=1 when away from the Mac mini0 min — already supportedForces 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 — both sso-session entries 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 set SessionDuration, max 12 h; IF-AdministratorAccess is 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)