Skip to content

Forward the host SSH agent into project containers (1Password, Bitwarden, YubiKey)#227

Open
sbaerlocher wants to merge 3 commits into
whatwedo:v2from
sbaerlocher:feat/ssh-host-agent
Open

Forward the host SSH agent into project containers (1Password, Bitwarden, YubiKey)#227
sbaerlocher wants to merge 3 commits into
whatwedo:v2from
sbaerlocher:feat/ssh-host-agent

Conversation

@sbaerlocher

@sbaerlocher sbaerlocher commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

New host SSH-agent mode: instead of the managed dde-ssh-agent container, dde forwards the developer's existing host agent into project containers (1Password, Bitwarden, a classic ssh-agent, or a hardware token such as a YubiKey). dde holds no keys itself. The existing managed mode stays the default and is unchanged; leaving ssh.agent unset changes nothing.

This addresses two recurring pain points of the managed agent: keys missing in project containers and an awkward key-add flow (Refs #113), and keys gone after a host or agent restart (Refs #1). In host mode the host agent owns the keys, dde only forwards the socket.

Configuration is machine-global in ~/.dde/config.yml:

ssh:
  agent:
    mode: host          # managed (default) | host
    source: 1password   # null | auto | env | 1password | bitwarden | <path>

How to test

Verified locally on macOS (Docker Desktop) with 1Password, Bitwarden, and the managed fallback: ssh -T git@dev.whatwedo.ch inside the container authenticates through the forwarded agent. dde system:doctor reports the resolved socket. The provider×platform matrix and gotchas (locked vault, stale socket after an agent restart → re-up, empty agent) are in docs/guides/ssh-agent.md.

What to watch for in review

  • The host socket is mounted read-write in Compose long-syntax: Docker Desktop lands it as root:root with no UID mapping, so the container entrypoint chowns it to the dde user as root (a read-only mount would forbid that; :ro buys nothing on a socket). Long-syntax keeps a socket path containing a : from corrupting the short-form split.
  • The managed dde-ssh-agent container is suppressed at the service level in host mode (SshAgentService::start()/build()), because the lifecycle starts every global service unconditionally.
  • Security: in host mode every project container can reach the forwarded agent (same model as ssh -A) — with a hardware token's touch policy the key still stays locked until a physical tap.

The build check (website docs) is pre-existingly red on v2 (astro/starlight peer conflict) and is fixed by #228 — it goes green once that merges or after a rebase. This PR does not touch website/.

Refs #113
Refs #1

@sbaerlocher sbaerlocher force-pushed the feat/ssh-host-agent branch from 16cade5 to 7778f8d Compare June 29, 2026 19:00
@sbaerlocher sbaerlocher changed the title feat(config): forward the host SSH agent into project containers Host-SSH-Agent in Projekt-Container weiterleiten (1Password, Bitwarden, YubiKey) Jun 29, 2026
@sbaerlocher sbaerlocher changed the title Host-SSH-Agent in Projekt-Container weiterleiten (1Password, Bitwarden, YubiKey) Forward the host SSH agent into project containers (1Password, Bitwarden, YubiKey) Jun 29, 2026
The .kiro/ directory holds local feature specs and steering notes that are
scratch artifacts, not part of the distributed project. Keep them untracked.

Signed-off-by: Simon Bärlocher <s.baerlocher@sbaerlocher.ch>
Add a `host` SSH-agent mode as an alternative to the managed `dde-ssh-agent`
container. In host mode dde forwards the developer's existing host SSH agent
(1Password, Bitwarden, a classic ssh-agent, or a hardware token such as a
YubiKey) into project containers and holds no keys itself. The default
`managed` mode is unchanged; leaving `ssh.agent` unset preserves it.

Configuration is machine-global in ~/.dde/config.yml:

    ssh:
      agent:
        mode: host          # managed (default) | host
        source: 1password   # null | auto | env | 1password | bitwarden | <path>

The in-container contract is constant in both modes — the socket lives at
/tmp/ssh-agent/socket and SSH_AUTH_SOCK points at it, so a container cannot tell
which mode is active. Only the host side of the mount differs.

HostSshAgentResolver is the single source of truth for "where is the host agent
socket and is it usable", shared by the forwarding path (DockerComposeManager)
and the doctor check. Platform/provider resolution:

- macOS: 1password -> group-container socket, bitwarden ->
  ~/.bitwarden-ssh-agent.sock, both bind-mounted directly; auto/env -> Docker
  Desktop host-services bridge. Direct mounts sidestep the launchd-visibility
  trap that leaves the bridge empty.
- Linux: 1password -> ~/.1password/agent.sock; bitwarden/env/auto ->
  SSH_AUTH_SOCK (Bitwarden's Linux path is version-variable, never hardcoded).
- Unsupported OS -> reported unavailable, never crashes.

The host socket is bind-mounted read-write in Compose long-syntax: on Docker
Desktop the mount lands as root:root with no host->container UID mapping, so the
container entrypoint chowns it to the dde user while still root (a read-only
mount would forbid that, and :ro buys nothing on a socket). Long-syntax avoids a
short-form source:target split corruption when the socket path contains a ":".

The managed dde-ssh-agent container is suppressed at the service level in host
mode: the lifecycle starts every global service unconditionally, so the gate
lives in SshAgentService::start()/build() rather than at each call site. The
system:install start and system:up interactive key-add are likewise skipped.

A mode-aware system:doctor check verifies the prerequisite per mode/platform,
and project:up warns and continues when the host agent cannot be resolved
instead of aborting the bring-up.

Signed-off-by: Simon Bärlocher <s.baerlocher@sbaerlocher.ch>
Add docs/guides/ssh-agent.md covering both modes, the provider x platform
resolution matrix, the read-write/chown rationale, and the macOS launchd
caveat. Includes a troubleshooting section for the three states that pass the
doctor check yet still fail inside a container (locked vault -> Permission
denied, stale socket after the host agent restarts -> Connection refused,
empty agent -> no identities), a hardware-token/YubiKey section (touch policy
keeps the token closed even with the socket forwarded), and a security note
that host mode exposes the forwarded agent to all project container code.

Register HostSshAgentResolver and HostSshAgentResolution in the AGENTS.md
architecture list.

Signed-off-by: Simon Bärlocher <s.baerlocher@sbaerlocher.ch>
@sbaerlocher sbaerlocher force-pushed the feat/ssh-host-agent branch from 7778f8d to 58dec68 Compare June 29, 2026 20:47
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