Skip to content

Make the host ~/.ssh mount opt-in (SQUAREBOX_MOUNT_SSH) - #176

Merged
BrettKinny merged 2 commits into
mainfrom
security/ssh-files-opt-in
Oct 1, 2026
Merged

BrettKinny merged 2 commits into
mainfrom
security/ssh-files-opt-in

Conversation

@BrettKinny

Copy link
Copy Markdown
Collaborator

Summary

AI assistants can run unattended inside the Box (the *-yolo aliases), so the Box no longer gets your SSH private-key files by default.

Situation Before After
Bash/Git Bash, agent available Agent socket plus read-only config/known_hosts Unchanged
Bash/Git Bash, no agent Whole ~/.ssh mounted read-only Nothing mounted, and a note explains how to opt in
Native PowerShell, .ssh exists Whole .ssh always mounted read-only Mounted only on opt-in, otherwise a note

How to opt in: set SQUAREBOX_MOUNT_SSH=1 (works on both adapters) or pass .\install.ps1 -MountSsh. Setting SQUAREBOX_MOUNT_SSH=0 or passing -MountSsh:$false turns it back off. Any other value fails with exit 64 before anything is changed. An available agent still takes priority over the opt-in.

Install identity schema: FORMAT 2

The choice is saved as MOUNT_SSH=0|1 in install-state, so later rebuilds reuse it. ADR 0007 says adding a field requires a new format, so this PR introduces FORMAT=2 in a backward-compatible way:

  • Writers (install.sh, install.ps1) emit FORMAT=2, with MOUNT_SSH appended after HOME_VOLUME_ADOPTED.
  • All four readers (install.sh, uninstall.sh, install.ps1, uninstall.ps1) and migrate-windows-adapter.ps1 accept two formats:
    • FORMAT=2, where MOUNT_SSH is required.
    • FORMAT=1, where MOUNT_SSH must be absent and is read as 0.
  • Any other format fails closed.
  • The next rebuild or migration rewrites a FORMAT=1 file as FORMAT=2.
  • install-state-schema.json gains "format": 2 and readable_formats, and the known-formats-only rule replaces format-1-only. The verifier now also checks the format and flag handling in each adapter.
  • ADR 0010 records the decision and its migration rules.

Trade-off: releases before v1.3 reject FORMAT=2 state (fail closed). An optional field inside format 1 would have had the same effect, because old readers also reject unknown fields.

Tests

  • tests/fixtures/install-state-cases.json: 10 new cases, run against both the Bash and PowerShell adapters (27 total). They cover:

    • opt-in accepted
    • FORMAT=1 without MOUNT_SSH accepted, with LF and with CRLF line endings
    • FORMAT=1 with MOUNT_SSH rejected
    • FORMAT=2 missing MOUNT_SSH rejected
    • invalid and empty values rejected
    • FORMAT=3 and FORMAT=0 rejected
  • tests/test-lifecycle-install-state.sh: a new block checks the runtime calls recorded by the mock:

    • The default has no .ssh directory mount, prints the note, and writes MOUNT_SSH=0.
    • The opt-in adds the mount and writes MOUNT_SSH=1, and a plain rebuild keeps it.
    • With an agent available, the socket, config and known_hosts are mounted but not the directory.
    • =0 turns the opt-in off.
    • An invalid value exits 64 and leaves state and runtime untouched.
    • A legacy FORMAT=1 file is upgraded on rebuild, and uninstall.sh accepts FORMAT=1.
    • Mutation check: removing the gate or the persistence makes this test fail.
  • tests/test-lifecycle-powershell.ps1: new source-contract checks for:

    • the switch, env-var validation, and reuse of the recorded choice
    • the gated mount, with no ungated mount left
    • the note and the persisted field

    The migration test now starts from a FORMAT=1 file and checks it is published as FORMAT=2 with MOUNT_SSH=0.

Results: the full host suite passed 19/19 both with and without pwsh (PowerShell 7.6.6 for the native run).

Docs

  • SECURITY.md (mount exposure list)
  • README (new SSH access section, env-var table row, PowerShell flags, Windows boundary note)
  • CLAUDE.md (Lifecycle adapters and Windows/Podman paragraphs)
  • CONTEXT.md
  • uat-checklist.md
  • docs/releases/v1.3.0.md: migration note covering the behavior change on the next rebuild and how to keep the old behavior

CHANGELOG.md is intentionally untouched.

🤖 Generated with Claude Code

BrettKinny and others added 2 commits October 1, 2026 12:22
Without an SSH agent, install.sh mounted the host's whole ~/.ssh directory,
private keys included, read-only into the Box. Native PowerShell always
mounted it when present. AI assistants can run unattended in the Box, so
neither adapter mounts the directory by default any more.

- Bash agent forwarding, with its read-only config/known_hosts mounts, is
  unchanged.
- SQUAREBOX_MOUNT_SSH=1 (both adapters) or -MountSsh (PowerShell) restores
  the read-only directory mount when no agent is forwarded. 0 turns it off.
  Any other value fails with exit 64 before any lifecycle mutation.
- Without an agent or the opt-in, install prints how to enable the mount or
  use an agent.
- The choice is persisted as MOUNT_SSH in the Install identity, which ADR
  0007 requires to be a new format. Writers emit FORMAT=2. All four readers
  and the Windows migration script also accept FORMAT=1 without MOUNT_SSH,
  read it as 0, and republish it as FORMAT=2. ADR 0010 records the decision.
- Schema, verifier, and shared fixtures cover both formats. The lifecycle
  test asserts the default has no .ssh directory mount, the opt-in adds the
  mount and survives a rebuild, an agent still wins, =0 opts out, invalid
  values are rejected, and FORMAT=1 is upgraded.
- SECURITY.md, README, CLAUDE.md, CONTEXT.md, the UAT checklist, and the
  v1.3.0 migration guide are updated.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
# Conflicts:
#	docs/releases/v1.3.0.md
@BrettKinny
BrettKinny merged commit edda6c0 into main Oct 1, 2026
4 checks passed
@BrettKinny
BrettKinny deleted the security/ssh-files-opt-in branch October 1, 2026 04:55
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.

1 participant