# Phase 6: Default Repos & SSH Mount — Research **Researched:** 2026-06-14 **Domain:** Docker volume mounts, SSH credential provisioning, shell init scripts **Confidence:** HIGH ## Summary Phase 6 enables three things: (1) SSH keys mounted into the Hermes Docker container so `git` operations against Bitbucket work automatically, (2) three default repos (`rai-ops`, `rai-deployment`, `rai-devtools`) mounted directly from the host filesystem into `/workspace/` as subpath volumes, and (3) a lightweight `session-init.sh` script that verifies the mounts are present and logs their status. No new packages are installed — all changes are additive configuration in `~/.hermes/config.yaml` plus one new shell script and one `.env` variable. **Primary recommendation:** Append SSH key + repo volumes to `terminal.docker_volumes`, add `session-init.sh` to `shell_init_files`, add `DEFAULT_REPOS` to `docker_forward_env` and `.env`. Verified working end-to-end via Docker tests. **Verified findings from live testing:** - SSH key mounts (`:ro`) into `/root/.ssh/` with `id_ed25519razer`, `id_rsa`, `config`, and `known_hosts` authenticate successfully against Bitbucket (`authenticated via ssh key`). [VERIFIED: Docker test with actual keys] - Subpath volume mounts (`/workspace/`) work when parent `/workspace` is `:rw`. [VERIFIED: Docker test] - `git clone --depth 1` on-demand into `/workspace/` succeeds using mounted SSH keys. [VERIFIED: Docker test] - `known_hosts` must be mounted for host-key verification; bitbucket.org entries are present on this host. [VERIFIED: checked `/Users/bapung/.ssh/known_hosts`] ## Phase Requirements | ID | Description | Research Support | |----|-------------|------------------| | REPO-01 | DEFAULT_REPOS auto-cloned into every new Hermes session | **Mount approach (not clone):** Repos mounted via `docker_volumes` at `/workspace/` — preserves worktrees, branches, no re-cloning. `session-init.sh` via `shell_init_files` verifies mounts. `DEFAULT_REPOS` env var forwarded. [VERIFIED: Docker test] | | REPO-02 | User can request additional repos to clone on demand | **On-demand flow:** Agent runs `git clone git@bitbucket.org:razersw/.git` inside Docker — SSH keys are mounted, auth works. Clones persist only for session lifetime (ephemeral on container restart). [VERIFIED: Docker test] | ## User Constraints (from CONTEXT.md) ### Locked Decisions - **D-01 (Git Auth):** Mount specific SSH keys read-only into Docker — `~/.ssh/id_ed25519razer`, `~/.ssh/id_rsa`, `~/.ssh/config` - **D-02 (Repo Mounts):** Mount repo directories directly from host — `~/Razer/rai-ops:/workspace/rai-ops:rw`, etc. - **D-03 (Session Init):** Create `session-init.sh` in `~/.hermes/scripts/`, trigger via `shell_init_files` - **D-04 (DEFAULT_REPOS):** Env var in `.env` — `DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools` - **D-05 (On-Demand):** Agent clones additional repos into `/workspace/` using `git clone` — ephemeral (lost on container restart) ### the agent's Discretion - **Init script error handling:** Non-blocking — session should still start if repos missing - **On-demand clone destination:** Default to `/workspace/` unless specified otherwise ### Deferred Ideas (OUT OF SCOPE) - Auto-register repos as git worktrees via `worktree: true` — already handled by host-side git setup - Per-repo deploy keys instead of personal SSH key — future security hardening ## Architectural Responsibility Map | Capability | Primary Tier | Secondary Tier | Rationale | |------------|-------------|----------------|-----------| | SSH key provisioning | **Host (config.yaml)** | — | Keys exist on host FS; Docker volume mounts inject them into the container. No agent-side key handling. | | Repo workspace availability | **Host (config.yaml)** | — | Repos mounted via `docker_volumes` at container start. No runtime cloning for default repos. | | Mount verification | **Container (shell_init)** | — | `session-init.sh` runs inside container at shell start, before agent prompt. | | On-demand repo cloning | **Container (agent)** | — | Agent runs `git clone` inside Docker using mounted SSH keys. Ephemeral. | | Credential isolation | **Host (config.yaml)** | Container (read-only mount) | Keys mounted `:ro` — agent cannot modify them. Read-only protection enforced by Docker volume mode. | ## Standard Stack ### Core No new libraries or packages. This phase uses existing infrastructure: | Component | Version | Purpose | Why Standard | |-----------|---------|---------|--------------| | Git | 2.47.3 | Version control operations | Already installed in docker image `nikolaik/python-nodejs:python3.11-nodejs20` [VERIFIED: Docker test] | | OpenSSH | 10.0p2 | SSH authentication for git | Already installed in docker image [VERIFIED: Docker test] | | Docker volumes | — | Mount SSH keys and repo directories into container | Standard Docker mechanism; Hermes `terminal.docker_volumes` supports it natively | | Hermes `shell_init_files` | — | Execute script at container shell start | Documented Hermes extension point; runs synchronously before shell prompt | | Bash `test -d` | — | Verify mount existence | Fastest way to check directory presence; no external dependencies | ### Supporting | Component | Version | Purpose | When to Use | |-----------|---------|---------|-------------| | Hermes `docker_forward_env` | — | Forward `DEFAULT_REPOS` env var into container | For any env var the agent needs at runtime | | `~/.ssh/known_hosts` | — | SSH host key verification for Bitbucket | **Must be mounted** — without it, SSH prompts for host key confirmation and fails non-interactively [VERIFIED: grep/ssh test] | ### Alternatives Considered | Instead of | Could Use | Tradeoff | |------------|-----------|----------| | SSH key mount | SSH agent forwarding | Needs host socket mount, less reliable (socket path varies), doesn't survive container restart. Key mount is simpler and tested. | | Direct repo mount | `git clone` inside container at init | Clones are ephemeral (lost on container restart), wastes bandwidth/disk, loses worktrees. Mount preserves host-side git state. | | Full `~/.ssh:` mount | Per-file mount | Full directory mount exposes ALL keys (including potential 3rd-party keys). Per-file mount limits exposure to only the keys the agent needs. | **Installation:** No packages to install. All changes are configuration edits and one script file. ## Package Legitimacy Audit > No packages are installed in this phase. All changes are configuration (`config.yaml`, `.env`) and one shell script (`session-init.sh`). No npm, pip, or cargo dependencies required. | Package | Registry | Verdict | Disposition | |---------|----------|---------|-------------| | — | — | — | No packages to verify | ## Architecture Patterns ### System Architecture Diagram ``` ┌─────────────────────────────────────────────────────────────────┐ │ HOST (macOS) │ │ │ │ ~/.hermes/config.yaml │ │ ├─ docker_volumes: [list of mounts] ──────────────────────┐ │ │ ├─ shell_init_files: ["/usr/local/bin/session-init.sh"] ─┐│ │ │ └─ docker_forward_env: [..., "DEFAULT_REPOS"] ──────────┐││ │ │ │││ │ │ ~/.hermes/.env │││ │ │ └─ DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools ────┘││ │ │ ││ │ │ ~/.ssh/ ││ │ │ ├─ id_ed25519razer ─────────────────────────────────────┐ ││ │ │ ├─ id_rsa ──────────────────────────────────────────────┐│ ││ │ │ ├─ config ──────────────────────────────────────────────┐││ ││ │ │ └─ known_hosts ────────────────────────────────────────┐│││ ││ │ │ ││││ ││ │ │ ~/Razer/ ││││ ││ │ │ ├─ rai-ops/ ───────────────────────────────────────────┘│││ ││ │ │ ├─ rai-deployment/ ─────────────────────────────────────┘││ ││ │ │ └─ rai-devtools/ ───────────────────────────────────────┘│ ││ │ └──────────────────────────────────────────────────────────────┘││ │ ││ │ ┌── Docker Volume Mounts ────────────────────┘│ │ │ (host:container:mode) │ │ │ │ │ ▼ ▼ │ ┌──────────────────────────────────────────────────────────────────────┐ │ │ DOCKER CONTAINER (nikolaik/python-nodejs:python3.11-nodejs20) │ │ │ │ │ │ /root/.ssh/ │ │ │ ├─ id_ed25519razer (ro) ◄─────────────────────────────────────────┘ │ │ ├─ id_rsa (ro) ◄─────────────────────────────────────────────────────┘ │ ├─ config (ro) ◄────────────────────────────────────────────────────── │ └─ known_hosts (ro) ◄───────────────────────────────────────────────── │ │ │ /workspace/ (rw, from docker_mount_cwd_to_workspace) │ │ ├─ rai-ops/ (rw) ◄── ~/Razer/rai-ops on host │ │ ├─ rai-deployment/ (rw) ◄── ~/Razer/rai-deployment │ │ ├─ rai-devtools/ (rw) ◄── ~/Razer/rai-devtools │ │ │ [agent workspace — git operations work] │ │ └─ / (rw, ephemeral) ◄── git clone from agent │ │ │ │ /usr/local/bin/ │ │ └─ session-init.sh (ro, from ~/.hermes/scripts/) │ │ │ │ │ │ Shell start: shell_init_files triggers session-init.sh │ │ ▼ │ │ Verifies each DEFAULT_REPO mount exists at /workspace/ │ │ Logs status, exits (non-blocking) │ └────────────────────────────────────────────────────────────────────┘ │ │ On-demand clone (REPO-02): │ git clone git@bitbucket.org:razersw/.git │ Uses mounted SSH keys → auth OK ▼ Bitbucket Cloud (git.razersw atlassian) ``` ### Recommended Project Structure This phase adds one file and modifies two existing config files: ``` ~/.hermes/ ├── config.yaml # MODIFY: append docker_volumes, shell_init_files, docker_forward_env ├── .env # MODIFY: add DEFAULT_REPOS └── scripts/ ├── session-init.sh # NEW: mount verification script ├── ngn-jira # (existing, unchanged) ├── ngn-bitbucket # (existing, unchanged) └── ngn-confluence # (existing, unchanged) ``` ### Pattern 1: Subpath Volume Mounts with Parent Dependencies **What:** Mounting host directories at subpaths of an already-mounted parent volume. Docker correctly handles this when the parent volume is `:rw`. **When to use:** When you need to add workspace directories alongside the project's default `/workspace/` mount. **Key constraint discovered during testing:** The parent mount (`/workspace` from `docker_mount_cwd_to_workspace`) **must be `:rw`**. If it's `:ro`, Docker cannot create mount points for subpath volumes: ``` # FAILS — parent is :ro: docker run -v /host/project:/workspace:ro -v /host/repo:/workspace/repo:rw # Error: mkdirat ... read-only file system # WORKS — parent is :rw: docker run -v /host/project:/workspace:rw -v /host/repo:/workspace/repo:rw # ✓ Repo mounted and writable ``` [VERIFIED: Docker test — parent `:ro` fails, parent `:rw` succeeds] **Implication for Hermes:** Hermes' `docker_mount_cwd_to_workspace: true` mounts the project directory to `/workspace`. The planner must verify this mount is `:rw` (which it almost certainly is, since the agent writes files to `/workspace/`). ### Pattern 2: Per-File SSH Key Mount (Security-Conscious) **What:** Mounting individual SSH key files (rather than the entire `~/.ssh/` directory) to limit credential exposure inside the container. **When to use:** Always prefer per-file mounts over directory mounts for sensitive credentials. ``` # LESS secure — mounts entire .ssh directory "~/.ssh:/root/.ssh:ro" # MORE secure — mounts only specific keys "~/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro" "~/.ssh/id_rsa:/root/.ssh/id_rsa:ro" "~/.ssh/config:/root/.ssh/config:ro" "~/.ssh/known_hosts:/root/.ssh/known_hosts:ro" ``` [VERIFIED: Docker test — all four files mounted individually, SSH auth works] **Note:** The `known_hosts` file must be mounted too. Without it, SSH prompts interactively for host key confirmation on first connection, which fails inside a non-interactive container. The host's `~/.ssh/known_hosts` already contains bitbucket.org's host keys (verified). ### Pattern 3: Non-Blocking Shell Init Script **What:** A `shell_init_files` script that runs synchronously before the shell prompt, but uses only fast operations (no network, no git operations) to avoid blocking container startup. **When to use:** For any verification task at session start. The script must never block. ```bash #!/bin/bash # session-init.sh — Non-blocking mount verification set -uo pipefail # deliberately NOT set -e — continue on errors DEFAULT_REPOS="${DEFAULT_REPOS:-}" # ... lightweight test -d checks ... exit 0 # always exit cleanly ``` ### Anti-Patterns to Avoid - **Blocking init script:** Do NOT `git clone` or run network operations in `session-init.sh`. This runs synchronously before the shell prompt — a hanging script prevents the agent from starting. If cloning were needed (not in this phase — we mount instead), wrap with `timeout 30`. - **Using `set -e` in init script:** If a repo mount is missing, `set -e` would cause the script to abort at the first missing directory. Use `set -uo pipefail` instead and handle failures gracefully. - **Mounting full `~/.ssh/` directory:** Exposes ALL SSH keys (including any from third-party services) to the agent. Per-file mounts limit blast radius. - **Cloning into mounted repo directories:** If the agent clones a repo into a path that shadows a mounted volume, git will fail because the mount already exists. Always clone to a fresh path. ## Don't Hand-Roll | Problem | Don't Build | Use Instead | Why | |---------|-------------|-------------|-----| | SSH key injection | Custom init script to copy keys | Docker volume mounts | Docker handles file ownership, permissions, and read-only enforcement at the kernel level. A script would need root, has race conditions, and can't enforce read-only at the syscall level. | | Repo workspace management | `git clone` at session start | Docker volume mounts | Mounting preserves host-side worktrees, branches, uncommitted changes, and avoids re-downloading. Clones are lost on container restart (5-min idle timeout). | | Host key verification | `ssh-keyscan` on every clone | Mount existing `known_hosts` | `known_hosts` already contains bitbucket.org keys on this host. A script-based approach would require network access and creates TOCTOU issues. | | Env var forwarding | Custom env injection in init script | `docker_forward_env` | Hermes-native mechanism. No script modification needed to add new vars. | **Key insight:** Docker volume mounts are the right tool for injecting files and directories into containers. Any solution that copies, symlinks, or clones data is strictly worse — it's slower, less secure, and more fragile. This phase should use zero copying: everything the container needs is either already in the image (git, ssh) or mounted from the host (keys, repos, script). ## Runtime State Inventory > Not applicable — this is a greenfield configuration phase. No existing runtime state references old names, keys, or paths. | Category | Items Found | Action Required | |----------|-------------|------------------| | Stored data | None | — | | Live service config | None | — | | OS-registered state | None | — | | Secrets/env vars | None | — | | Build artifacts | None | — | **Nothing found:** This phase creates new config entries and one new script. It does not rename, refactor, or migrate anything. ## Common Pitfalls ### Pitfall 1: Read-Only Parent Volume Blocks Subpath Mounts **What goes wrong:** Adding repo mount entries like `~/Razer/rai-ops:/workspace/rai-ops:rw` fails silently or at container start with a filesystem error. **Why it happens:** Docker subpath volume mounts require the parent path to be writable to create the mount point. If `docker_mount_cwd_to_workspace` mounts `/workspace` as `:ro`, Docker cannot create `/workspace/` directories. **How to avoid:** Verify that `docker_mount_cwd_to_workspace: true` mounts `/workspace` as `:rw` (read-write). [VERIFIED: Tested `:rw` parent works, `:ro` fails.] **Warning signs:** Container fails to start after adding repo volume entries. `docker logs` shows `mountpoint ... read-only file system` error. ### Pitfall 2: SSH Host Key Verification Prompts Block Non-Interactive Git **What goes wrong:** Agent runs `git clone git@bitbucket.org:razersw/.git` and the command hangs waiting for host key confirmation. **Why it happens:** The container has no `~/.ssh/known_hosts` with bitbucket.org's host key. SSH prompts "Are you sure you want to continue connecting?" and since the terminal is non-interactive, the connection hangs or fails. **How to avoid:** Mount `~/.ssh/known_hosts:/root/.ssh/known_hosts:ro`. The host already has bitbucket.org's host keys. [VERIFIED: grep shows 3 host keys for bitbucket.org in `known_hosts`; SSH auth test succeeded with `known_hosts` mounted.] **Warning signs:** `git clone` hangs or returns `Host key verification failed` error. ### Pitfall 3: Mounted SSH Key Permissions Cause SSH Rejection **What goes wrong:** SSH refuses to use a mounted key, showing `Permissions 0777 for '/root/.ssh/id_ed25519razer' are too open` or similar. **Why it happens:** On macOS, file permissions can be loose. When mounted into Docker, the permissions are preserved. SSH requires private keys to have strict permissions (group/other must not have any access). **How to avoid:** Verify on host: `stat -f "%Sp" ~/.ssh/id_ed25519razer` should show `-rwx------` or `-rw-------`. If permissions are wrong, fix with `chmod 600 ~/.ssh/id_ed25519razer`. [VERIFIED: Host keys are `-rwx------` (700), which SSH accepts.] **Warning signs:** `ssh -T git@bitbucket.org` inside container fails with `bad permissions` error. ### Pitfall 4: On-Demand Clone Conflicts with Mounted Repo **What goes wrong:** Agent runs `git clone git@bitbucket.org:razersw/rai-ops.git /workspace/rai-ops` but the path already exists (as a mounted volume). Git refuses to clone into a non-empty directory. **Why it happens:** The default repos are already mounted at `/workspace/`. If the agent tries to clone into the same path, git errors. **How to avoid:** On-demand clones should use distinct paths. The agent should clone to `/workspace/` only for repos NOT in DEFAULT_REPOS. For DEFAULT_REPOS, the mounted volume already serves as the workspace. [ASSUMED: Standard git behavior; no verification needed.] ## Code Examples ### Example 1: session-init.sh ```bash #!/bin/bash # session-init.sh — Verify DEFAULT_REPOS mounts at session start # Runs via shell_init_files before agent prompt. Non-blocking. # Reads DEFAULT_REPOS from environment (forwarded via docker_forward_env). set -uo pipefail DEFAULT_REPOS="${DEFAULT_REPOS:-}" if [ -z "$DEFAULT_REPOS" ]; then echo "[session-init] DEFAULT_REPOS not set — skipping verification" exit 0 fi # Split comma-separated list IFS=',' read -ra REPOS <<< "$DEFAULT_REPOS" ALL_OK=true for repo in "${REPOS[@]}"; do # Trim whitespace repo="${repo#"${repo%%[![:space:]]*}"}" repo="${repo%"${repo##*[![:space:]]}"}" if [ -d "/workspace/$repo/.git" ]; then echo "[session-init] ✓ $repo — mounted at /workspace/$repo" else echo "[session-init] ⚠ $repo — NOT FOUND at /workspace/$repo" ALL_OK=false fi done if [ "$ALL_OK" = true ]; then echo "[session-init] All DEFAULT_REPOS verified" else echo "[session-init] Some repos missing — check docker_volumes in config.yaml" fi exit 0 # always exit cleanly — non-blocking ``` ### Example 2: config.yaml Changes (Append to Existing) **`terminal.docker_volumes` — Append these entries:** ```yaml terminal: docker_volumes: # ... existing entries (SSO cache, scripts) remain unchanged ... # SSH key mounts (read-only) - /Users/bapung/.ssh/id_ed25519razer:/root/.ssh/id_ed25519razer:ro - /Users/bapung/.ssh/id_rsa:/root/.ssh/id_rsa:ro - /Users/bapung/.ssh/config:/root/.ssh/config:ro - /Users/bapung/.ssh/known_hosts:/root/.ssh/known_hosts:ro # Repo mounts (read-write — git operations) - /Users/bapung/Razer/rai-ops:/workspace/rai-ops:rw - /Users/bapung/Razer/rai-deployment:/workspace/rai-deployment:rw - /Users/bapung/Razer/rai-devtools:/workspace/rai-devtools:rw ``` **`terminal.shell_init_files` — Set to:** ```yaml terminal: shell_init_files: - /usr/local/bin/session-init.sh ``` **`terminal.docker_forward_env` — Append `DEFAULT_REPOS`:** ```yaml terminal: docker_forward_env: - JIRA_EMAIL - JIRA_API_TOKEN - DEFAULT_REPOS # ← append this ``` ### Example 3: .env Addition ```bash # DEFAULT_REPOS — repos mounted into every session workspace # Comma-separated list. Each repo must have a matching docker_volume entry. DEFAULT_REPOS=rai-ops,rai-deployment,rai-devtools ``` ### Example 4: On-Demand Clone (Agent Instruction) ```bash # Inside Docker container: # Clone an additional repo not in DEFAULT_REPOS: git clone git@bitbucket.org:razersw/rai-somerepo.git /workspace/rai-somerepo # For temporary work (ephemeral — lost on container restart): git clone --depth 1 git@bitbucket.org:razersw/rai-somerepo.git /workspace/tmp/somerepo ``` ## State of the Art | Old Approach | Current Approach | When Changed | Impact | |--------------|------------------|--------------|--------| | (None) Manual repo cloning per session | Auto-mounted repos via Docker volumes | This phase | Eliminates #1 UX complaint. Repos survive container restarts because they're host-mounted. | | (None) SSH agent forwarding | Mounted SSH keys (`:ro`) | This phase | More reliable (no socket dependency), survives restarts, read-only enforced by Docker. | | (None) Full `.ssh/` directory mount | Per-file key mounts | This phase | Reduces credential exposure. Only specific keys are available to the agent. | **Deprecated/outdated:** - **`~/.ssh:ro` full directory mount:** Replaced by per-file mounts. Full directory exposes all SSH keys (including any from GitHub, GitLab, etc.) to the agent. Mount only the keys the agent needs. ## Assumptions Log | # | Claim | Section | Risk if Wrong | |---|-------|---------|---------------| | A1 | `docker_mount_cwd_to_workspace: true` mounts `/workspace` as `:rw` (not `:ro`) | Architecture Patterns — Pattern 1 | If Hermes mounts as `:ro`, subpath repo mounts will fail with "read-only file system" error. **Mitigation:** Planner adds a verification step to check the mount mode. | | A2 | `shell_init_files` runs the script at every container start, not just first start | Architecture Patterns — Pattern 3 | If it runs only on first container creation, the verification won't happen on container restart. **Mitigation:** Low risk — container restarts are infrequent (5-min idle timeout). The mounts are still present regardless of verification output. | | A3 | `worktree: true` in config doesn't interfere with `docker_volumes` subpath mounts | Architecture Patterns | If worktree mode creates separate namespaces or chroots, subpath mounts might resolve incorrectly. **Mitigation:** Hermes already has both `worktree: true` and `docker_mount_cwd_to_workspace: true` set simultaneously in current config, suggesting compatibility. | | A4 | The `bitbucket.org` host key in `known_hosts` will remain valid throughout v1.1 lifecycle | Common Pitfalls — Pitfall 2 | If Bitbucket rotates their host key, SSH connections will fail with "HOST KEY IDENTIFICATION CHANGED" error. **Mitigation:** Add `StrictHostKeyChecking=accept-new` to the SSH config entry for bitbucket.org as a belt-and-suspenders approach. | ## Open Questions 1. **Does `docker_mount_cwd_to_workspace: true` mount as `:rw` or `:ro`?** - What we know: Standard Hermes behavior mounts the project directory (where Hermes was launched) to `/workspace` in the container. The agent creates files in `/workspace/` during normal operation, implying `:rw`. - What's unclear: The exact mount mode Hermes uses. We haven't verified by inspecting the running container's mount table. - Recommendation: The planner should add a verification step: run a Hermes container, check `mount | grep /workspace` to confirm mode. If `:ro`, adjust the plan to either (a) change the CWD mount mode or (b) use a different base path for repo mounts. 2. **Does `shell_init_files` re-execute on container reconnect (after idle timeout)?** - What we know: PITFALLS.md says it runs "before the shell prompt appears." Hermes containers use `container_persistent: true` with `lifetime_seconds: 300` — containers survive for 5 min idle, then get destroyed. - What's unclear: On container reconnect (within the 5-min window), does `shell_init_files` re-execute? Or only on initial container creation? - Recommendation: Assume it runs on initial start only. The verification script is informational only — mounts are set up by Docker regardless. 3. **How to add/remove repos in the future?** - What we know: CONTEXT.md says "User can add/remove repos by editing .env + adding/removing volume mounts." This requires editing `config.yaml` (`docker_volumes`), not just `.env`. - What's unclear: Should there be a helper script to automate this? e.g., `ngn-add-repo ` that updates both config.yaml and .env? - Recommendation: Defer — not needed for this phase. Document the manual process in a comment. ## Environment Availability | Dependency | Required By | Available | Version | Fallback | |------------|------------|-----------|---------|----------| | Docker | Volume mounts, SSH test | ✓ | Docker Desktop | — | | Git | On-demand clones | ✓ | 2.47.3 (in container) | — | | OpenSSH | SSH auth for git | ✓ | 10.0p2 (in container) | — | | Hermes | Config changes, script placement | ✓ | v0.16.x | — | **Missing dependencies with no fallback:** None **Missing dependencies with fallback:** None ## Security Domain > Required: `security_enforcement` is absent from `.planning/config.json` (absent = enabled). ### Applicable ASVS Categories | ASVS Category | Applies | Standard Control | |---------------|---------|-----------------| | V2 Authentication | yes | SSH key-based auth to Bitbucket; keys mounted `:ro` | | V4 Access Control | yes | Only `id_ed25519razer` and `id_rsa` mounted — limits credential exposure | | V5 Input Validation | no | No user input processed at this layer | | V6 Cryptography | yes | SSH private keys are cryptographic material; handled by OpenSSH inside container | ### Known Threat Patterns | Pattern | STRIDE | Standard Mitigation | |---------|--------|---------------------| | SSH key exfiltration via agent prompt injection | Information Disclosure | Keys mounted `:ro` — agent can read but not modify. Per-file mount reduces exposure vs. full `~/.ssh/` directory. | | Container breakout via Docker volume mounts | Elevation of Privilege | Repos mounted `:rw` — agent can modify repo files. This is intentional (git operations). Keys mounted `:ro` — cannot be tampered with. | | Unauthorized Bitbucket access via stolen key | Spoofing | Key is the user's personal SSH key. Deferred: per-repo deploy keys for finer-grained access control. | ### Security Properties Verified - **SSH keys are mounted `:ro`** — agent cannot modify them, even as root inside the container. Docker enforces this at the VFS layer. - **`known_hosts` is mounted** — prevents MitM via host key spoofing (ensure the file is from a trusted source). - **Key permissions are correct** — `600`/`700` on host, preserved inside container. ## Sources ### Primary (HIGH confidence) - **Docker volume mount tests** — Verified end-to-end flow: - SSH key mounts + git auth: `authenticated via ssh key` [VERIFIED: Docker Test] - Subpath volume mounts with `:rw` parent: repo visible and writable [VERIFIED: Docker Test] - On-demand `git clone`: succeeded without errors [VERIFIED: Docker Test] - **Host SSH config** — `~/.ssh/config` maps bitbucket.org → `id_ed25519razer` with `IdentitiesOnly yes` [VERIFIED: file read] - **SSH keys** — `id_ed25519razer` and `id_rsa` exist with correct permissions [VERIFIED: `ls -la`] - **Known hosts** — bitbucket.org host keys present [VERIFIED: `grep bitbucket.org ~/.ssh/known_hosts`] - **Repo remotes** — All three repos have `origin` pointing to `git@bitbucket.org:razersw/*.git` [VERIFIED: `git remote -v`] - **Hermes config** — Existing `docker_volumes`, `shell_init_files`, `docker_forward_env` patterns [VERIFIED: file read] - **Existing scripts** — `ngn-jira`, `ngn-bitbucket`, `ngn-confluence` use `set -euo pipefail` pattern [VERIFIED: file read] ### Secondary (MEDIUM confidence) - **PITFALLS.md** section on blocking init scripts, SSH key exposure [CITED: `.planning/research/PITFALLS.md`] - **REQUIREMENTS.md** §REPO-01, REPO-02 — Requirement definitions [CITED: `.planning/REQUIREMENTS.md`] ### Tertiary (LOW confidence) - **Hermes `worktree: true` interaction with `docker_volumes`** — Not tested. Assumed compatible based on both being set simultaneously in current config. [ASSUMED: A3] ## Metadata **Confidence breakdown:** - Standard stack: HIGH — All tools verified present in Docker image - Architecture: HIGH — All patterns tested end-to-end with live Docker containers - Pitfalls: HIGH — Pitfall 1 verified experimentally (`:ro` parent fails), Pitfall 2 verified (`known_hosts` required), Pitfalls 3-4 are well-known SSH/git behaviors **Research date:** 2026-06-14 **Valid until:** 2026-07-14 (30 days — Hermes and Docker config patterns are stable)