{"id":49218435,"url":"https://github.com/pavlov-net/ssh-agent-proxy","last_synced_at":"2026-04-24T01:10:06.519Z","repository":{"id":350294508,"uuid":"1206195821","full_name":"pavlov-net/ssh-agent-proxy","owner":"pavlov-net","description":"Localhost HTTP signing proxy backed by any SSH agent — single Rust binary, no runtime dependencies","archived":false,"fork":false,"pushed_at":"2026-04-20T02:42:04.000Z","size":121,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-20T04:32:34.429Z","etag":null,"topics":["sandbox","ssh-agent"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pavlov-net.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-09T17:11:48.000Z","updated_at":"2026-04-20T02:42:07.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/pavlov-net/ssh-agent-proxy","commit_stats":null,"previous_names":["stuartparmenter/op-sign-proxy","stuartparmenter/ssh-agent-proxy","pavlov-net/ssh-agent-proxy"],"tags_count":3,"template":false,"template_full_name":null,"purl":"pkg:github/pavlov-net/ssh-agent-proxy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pavlov-net%2Fssh-agent-proxy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pavlov-net%2Fssh-agent-proxy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pavlov-net%2Fssh-agent-proxy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pavlov-net%2Fssh-agent-proxy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pavlov-net","download_url":"https://codeload.github.com/pavlov-net/ssh-agent-proxy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pavlov-net%2Fssh-agent-proxy/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32204725,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-24T00:06:41.111Z","status":"ssl_error","status_checked_at":"2026-04-24T00:06:35.224Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["sandbox","ssh-agent"],"created_at":"2026-04-24T01:10:05.468Z","updated_at":"2026-04-24T01:10:06.508Z","avatar_url":"https://github.com/pavlov-net.png","language":"Rust","readme":"# ssh-agent-proxy\n\nA tiny localhost HTTP proxy that forwards SSHSIG sign requests to a\nlocal ssh-agent. It exists so you can sign git commits from inside a\ncontainer (or any sandbox that can't bind-mount Unix sockets) while\nthe private key stays in whatever agent holds it on the host —\n1Password Desktop, the stock OpenSSH agent, gpg-agent with SSH\nsupport, yubikey-agent, anything that speaks the agent protocol.\n\n## Why\n\nTwo problems compose badly:\n\n1. **SSH keys in 1Password Desktop on Windows live behind a named\n   pipe** (`\\\\.\\pipe\\openssh-ssh-agent`), not a Unix socket. A WSL2\n   process can't open the pipe directly without a Windows-side\n   helper.\n2. **Sandboxed container runtimes like `docker sbx` can't bind-mount\n   arbitrary Unix sockets** from the host into the workload. Even on\n   a pure Linux box with a normal ssh-agent socket, you can't just\n   forward it into a sandbox the usual way.\n\nThe common answer is \"run a signing HTTP oracle on the host that\ntalks to the agent, and have the container hit it over HTTP\" —\nevery sandbox leaves outbound network open. This repo is that oracle.\n\n```\n          ┌──────────────────────┐           ┌────────────────────────────┐\n          │       host side      │           │     container / sandbox    │\n          │                      │           │                            │\n agent ◄──┤  ssh-agent-proxy     │  HTTP     │  git commit -S             │\n          │   :7221 /sign        │◄──────────┤   gpg.ssh.program =        │\n          │        /publickey    │           │     ssh-agent-proxy-sign   │\n          │        /healthz      │           │                            │\n          └──────────────────────┘           └────────────────────────────┘\n```\n\nThe proxy holds **no private key material** of its own. Every `/sign`\nand `/publickey` request opens a fresh connection to the configured\nagent, lets the agent do the cryptographic work, then closes the\nconnection. Key rotation in the upstream agent takes effect on the\nvery next request, with no proxy restart.\n\n## Endpoints\n\n- **`POST /sign`** — body is raw bytes to sign, response is an armored\n  `-----BEGIN SSH SIGNATURE-----` block with namespace `git`.\n  Byte-identical to `ssh-keygen -Y sign -n git` for deterministic\n  signature schemes (Ed25519 and RSA `rsa-sha2-512`).\n- **`GET /publickey`** — OpenSSH authorized_keys-format line for the\n  key the proxy will sign with. The container-side shim uses this to\n  auto-populate `user.signingkey` so you don't have to bake a specific\n  key into the container image.\n- **`GET /healthz`** — liveness probe.\n\n## Backend / agent paths\n\nThe proxy dials whichever ssh-agent you point it at. Defaults per\nplatform:\n\n| Platform | Default agent path | Override env var |\n|---|---|---|\n| Linux / macOS | `$SSH_AUTH_SOCK` | `SSH_AGENT_PROXY_UPSTREAM` |\n| Windows | `\\\\.\\pipe\\openssh-ssh-agent` | `SSH_AGENT_PROXY_UPSTREAM` |\n\nOn Linux 1Password Desktop typically exposes its socket at\n`~/.1password/agent.sock`; on macOS\n`~/Library/Group Containers/2BUA8C4S2C.com.1password/t/agent.sock`;\non Windows the standard OpenSSH agent named pipe is where 1Password\n(and the Windows OpenSSH service) both listen. If you're happy with\nwhatever `SSH_AUTH_SOCK` already points at, leave\n`SSH_AGENT_PROXY_UPSTREAM` unset and the proxy honors it.\n\n## Environment variables\n\n| Var | Default | Purpose |\n|---|---|---|\n| `SSH_AGENT_PROXY_ADDR` | `127.0.0.1:7221` | HTTP listen address |\n| `SSH_AGENT_PROXY_NAMESPACE` | `git` | SSHSIG namespace |\n| `SSH_AGENT_PROXY_UPSTREAM` | (see above) | Upstream agent path |\n| `SSH_AGENT_PROXY_PUBKEY` | unset | Literal authorized_keys line; if set, pin signing to this specific key from the agent |\n| `SSH_AGENT_PROXY_PUBKEY_FILE` | unset | Path to a file containing the pubkey line (ignored if `SSH_AGENT_PROXY_PUBKEY` is set) |\n\nIf neither `SSH_AGENT_PROXY_PUBKEY` nor `SSH_AGENT_PROXY_PUBKEY_FILE`\nis set, the proxy uses the first key the agent advertises.\n\n## Build\n\nRequires Rust 1.85+ (edition 2024).\n\nWith make (Linux / macOS / WSL2):\n\n```sh\nmake build                 # target/release/ssh-agent-proxy\nmake build-windows         # cross-compile to x86_64-pc-windows-gnu\nmake build-darwin          # cross-compile to aarch64-apple-darwin\nmake build-all             # all three\n\nmake install               # install to ~/.local/bin (override BINDIR=…)\nmake check                 # cargo clippy + cargo test\n```\n\nOn Windows (native, with Rust installed):\n\n```powershell\ncargo build --release\n# binary at target\\release\\ssh-agent-proxy.exe\n```\n\nFrom WSL2 targeting Windows (requires `rustup target add x86_64-pc-windows-gnu`\nand `apt install gcc-mingw-w64-x86-64`):\n\n```sh\nmake build-windows\n```\n\n## Run it interactively\n\n```sh\n# Point at your local ssh-agent and start the proxy\nexport SSH_AUTH_SOCK=$HOME/.1password/agent.sock   # or whatever\n./target/release/ssh-agent-proxy\n# listening on 127.0.0.1:7221 (namespace \"git\")\n```\n\nQuick smoke test from another shell:\n\n```sh\ncurl -s http://127.0.0.1:7221/publickey\n# ssh-ed25519 AAAA… user@host\n\nprintf 'hello\\n' | curl -s --data-binary @- http://127.0.0.1:7221/sign\n# -----BEGIN SSH SIGNATURE-----\n# …\n# -----END SSH SIGNATURE-----\n```\n\n## Run as a systemd user service (Linux / WSL2 / macOS)\n\n### WSL2 one-time prerequisites\n\nSkip if you're not on WSL2.\n\n1. Enable systemd in `/etc/wsl.conf`:\n   ```ini\n   [boot]\n   systemd=true\n   ```\n   Then `wsl --shutdown` from PowerShell / cmd and re-open your shell.\n\n2. Enable lingering so user services survive closing your last WSL\n   terminal:\n   ```sh\n   sudo loginctl enable-linger \"$USER\"\n   ```\n\n### Install\n\n```sh\nmake install-systemd       # build + install + drop unit + drop env template\n$EDITOR ~/.config/ssh-agent-proxy/env        # point SSH_AUTH_SOCK or SSH_AGENT_PROXY_UPSTREAM\nsystemctl --user enable --now ssh-agent-proxy.service\nmake status                # or: make logs\n```\n\n`install-systemd` is idempotent and preserves the existing env file\non re-runs. The shipped unit enables a comprehensive systemd sandbox\n(`ProtectSystem=strict`, `ProtectHome=read-only`, `NoNewPrivileges`,\n`LockPersonality`, `MemoryDenyWriteExecute`, `SystemCallFilter=@system-service`,\n`LimitMEMLOCK=infinity`, `LimitCORE=0`, and the rest of the usual\nhardening set).\n\nTo remove:\n\n```sh\nmake uninstall-systemd     # preserves ~/.config/ssh-agent-proxy/env\n```\n\n## Run on Windows\n\nInstall the MSI from the latest release. The installer drops the\nsigned binary in `%ProgramFiles%\\ssh-agent-proxy\\` and adds a Start\nMenu shortcut.\n\nThe binary is a tray app. Double-click it (or let it auto-start after\ninstall) and a tray icon appears. Right-click gives you:\n\n- **Start at login** (checked by default) — toggles an\n  `HKCU\\Software\\Microsoft\\Windows\\CurrentVersion\\Run` entry so the\n  proxy launches in your user session at logon.\n- **Exit** — shuts the HTTP server down and quits.\n\nConfiguration is via environment variables, same as Linux/macOS. Set\nthem in your user environment (System Properties → Environment\nVariables → User variables) before the proxy starts:\n\n- `SSH_AGENT_PROXY_ADDR` (default `127.0.0.1:7221`)\n- `SSH_AGENT_PROXY_UPSTREAM` (e.g. `\\\\.\\pipe\\openssh-ssh-agent`)\n- `SSH_AGENT_PROXY_PUBKEY_FILE` (e.g. `%USERPROFILE%\\.ssh\\git_signing.pub`)\n- `SSH_AGENT_PROXY_NAMESPACE` (default `git`)\n\nLogs go to `%LOCALAPPDATA%\\ssh-agent-proxy\\tray.log`.\n\nFor debugging, run `ssh-agent-proxy.exe --console` from a terminal;\nlog output appears on stderr and the tray icon is suppressed.\n\n## Use it from a container\n\n### Install the shim\n\n```dockerfile\nCOPY scripts/ssh-agent-proxy-sign.sh /usr/local/bin/ssh-agent-proxy-sign\nRUN chmod +x /usr/local/bin/ssh-agent-proxy-sign \u0026\u0026 \\\n    apt-get update \u0026\u0026 apt-get install -y --no-install-recommends \\\n        curl openssh-client ca-certificates \u0026\u0026 \\\n    rm -rf /var/lib/apt/lists/*\n```\n\n`openssh-client` is needed only if you also want to *verify*\nsignatures inside the container — the shim delegates `-Y verify` /\n`-Y check-novalidate` to the real `ssh-keygen`. If you only sign, you\ncan drop it.\n\n### Git config\n\n```sh\ngit config --global gpg.format      ssh\ngit config --global gpg.ssh.program /usr/local/bin/ssh-agent-proxy-sign\ngit config --global user.signingkey ~/.cache/ssh-agent-proxy-sign/signing.pub\ngit config --global commit.gpgsign  true\ngit config --global tag.gpgsign     true\n```\n\nNote that `user.signingkey` points at a path that **doesn't exist\nyet**. The shim auto-populates it from the proxy's `/publickey`\nendpoint on first use, so the container never needs to bake in a\nspecific public key. To pick up a rotated key, `rm` the cache file\nand it refreshes on the next commit.\n\n### Networking\n\nThe proxy binds `127.0.0.1:7221` on the host. Simplest container\nnetworking:\n\n```sh\ndocker run --network host …\n```\n\nIf your runtime can't do `--network host` (e.g. `docker sbx`), bind\nthe proxy to `0.0.0.0:7221` with\n`SSH_AGENT_PROXY_ADDR=0.0.0.0:7221` and give the container a\nhost-gateway hop:\n\n```sh\ndocker run --add-host=host.docker.internal:host-gateway \\\n    -e SSH_AGENT_PROXY_URL=http://host.docker.internal:7221/sign \\\n    …\n```\n\nBe aware that binding `0.0.0.0` exposes the signing endpoint to\nanything that can reach the host interface. The trust boundary is\n\"any local process as your user can request signatures\", same as\n`ssh-agent`.\n\n### Shim environment variables (container side)\n\n| Var | Default | Purpose |\n|---|---|---|\n| `SSH_AGENT_PROXY_URL` | `http://127.0.0.1:7221/sign` | Sign endpoint URL |\n| `SSH_AGENT_PROXY_PUBKEY_URL` | derived from `SSH_AGENT_PROXY_URL` | Public-key endpoint URL |\n| `SSH_AGENT_PROXY_CURL` | `curl` | Override the curl binary |\n\n## How the signing works under the hood\n\n`src/sshsig.rs` is a from-scratch implementation of OpenSSH's\n[SSHSIG wire format](https://github.com/openssh/openssh-portable/blob/master/PROTOCOL.sshsig)\nplus the 70-column PEM-like armor. Given any `sshsig::Signer`, it\nproduces an armored signature byte-identical to what `ssh-keygen -Y\nsign -n git` would have produced for deterministic signature schemes\n(Ed25519, and RSA with `rsa-sha2-512` / PKCS#1 v1.5). There are\nbyte-equality tests against `ssh-keygen` in the test suite.\n\n`src/agent.rs` implements a minimal SSH agent protocol client\n(REQUEST_IDENTITIES + SIGN_REQUEST only). `src/agent_source.rs`\nwraps it into an `AgentSource` that dials the agent fresh per\nrequest, selects the configured key, and returns a `Signer`. For RSA\nkeys we force the `rsa-sha2-512` flag and verify the agent honored\nit — a misbehaving agent that tried to downgrade to SHA-1 would be\nrejected rather than returning a signature that modern verifiers\nwon't accept. This check applies to all key types.\n\n## Security notes\n\n### Threat model\n\n**Defends against:**\n\n- Another unprivileged process on the same host scraping\n  `/proc/$pid/mem` or attaching via `ptrace` (Linux:\n  `prctl(PR_SET_DUMPABLE, 0)`; macOS: `ptrace(PT_DENY_ATTACH)`;\n  Windows: process mitigation policies + strict handle checks).\n- Transient buffers ending up in a swap file (Linux and macOS:\n  `mlockall(MCL_CURRENT|MCL_FUTURE)`; the systemd user unit sets\n  `LimitMEMLOCK=infinity` so this doesn't silently fall back to\n  \"swap protection off\").\n- Transient buffers ending up in a core dump (Linux and macOS:\n  `RLIMIT_CORE=0` + `PR_SET_DUMPABLE=0`; systemd unit: `LimitCORE=0`;\n  Windows: `SetErrorMode` + crash-dump suppression).\n- Re-gaining privileges on exec (`PR_SET_NO_NEW_PRIVS=1` on Linux,\n  `NoNewPrivileges=true` in the systemd unit, no setuid-alike on the\n  other platforms).\n- Rotation drift. The proxy does not cache the signer across\n  requests. Rotate the key in the upstream agent and the very next\n  sign uses the new key.\n- The container seeing the private key. It doesn't, ever. The\n  container sees only signatures and (optionally) the public key.\n\n**Does NOT defend against:**\n\n- Root on the same host. Root can read `/proc/$pid/mem`, load a\n  kernel module, use EndpointSecurity on macOS, or enable\n  `SeDebugPrivilege` on Windows. Userspace mitigations don't hold\n  against the kernel.\n- A compromised upstream agent. The proxy trusts the agent to return\n  honest signatures; we sanity-check the returned signature format\n  against what we requested, but we can't tell whether the agent is\n  signing with the right private key.\n- Other processes running as your own user. Any of them can already\n  call `/sign` or talk to the agent directly. Same trust boundary as\n  `ssh-agent`.\n- Hardware attacks (cold boot, DMA, physical access).\n- The internals of the ssh-agent process, wherever it is.\n\n### Config hygiene\n\n- On Linux/macOS, `~/.config/ssh-agent-proxy/env` should be 0600 and\n  under `ProtectHome=read-only` in the systemd unit. Don't check it\n  into dotfiles git.\n- On Windows, configure the proxy via per-user environment variables\n  (System Properties → Environment Variables → User variables). The\n  tray app inherits them at launch. Machine-wide environment\n  variables work too but expose the config to every account on the\n  host.\n- Tray logs land in `%LOCALAPPDATA%\\ssh-agent-proxy\\tray.log`, which\n  keeps them out of the world-readable `%ProgramData%`.\n\n### HTTP authentication\n\nThere is none. Any local process running as your user can call\n`/sign` and get signatures, just like any local process can use\n`ssh-agent`. For stronger isolation, bind the proxy to a Unix socket\nyou bind-mount selectively into containers (requires a small patch —\n`SSH_AGENT_PROXY_ADDR` is TCP-only today) or put a bearer token in\nfront of `/sign` and `/publickey`.\n\n## Repo layout\n\n| Path | What |\n|---|---|\n| `src/main.rs` | Config loading, HTTP server (axum), signal handling |\n| `src/agent.rs` | Minimal SSH agent protocol client (LIST + SIGN) |\n| `src/agent_source.rs` | `AgentSource` + `AgentBackedSigner` |\n| `src/sshsig.rs` | SSHSIG wire format + OpenSSH armor |\n| `src/wire.rs` | Shared SSH wire-format primitives |\n| `src/config.rs` | Environment variable configuration |\n| `src/server.rs` | axum HTTP handlers (`/sign`, `/publickey`, `/healthz`) |\n| `src/dialer_unix.rs` | Unix domain socket dialer |\n| `src/dialer_windows.rs` | Windows named-pipe dialer |\n| `src/hardening_{linux,macos,windows}.rs` | Per-platform process hardening |\n| `src/tray_windows.rs` | Windows tray icon + menu + message loop |\n| `src/autostart_windows.rs` | HKCU Run-key management for \"Start at login\" |\n| `wix/ssh-agent-proxy.wxs` | WiX v4+ installer definition |\n| `assets/icon.ico` | Windows application icon (embedded via `build.rs`) |\n| `scripts/ssh-agent-proxy-sign.sh` | Container-side `gpg.ssh.program` shim |\n| `contrib/systemd/ssh-agent-proxy.service` | systemd **user** unit |\n| `contrib/systemd/env.example` | `EnvironmentFile=` template |\n\n## Tests\n\n```sh\nmake check       # cargo clippy + cargo test\n```\n\n21 tests cover the SSH wire-format primitives, SSHSIG byte-equality\nagainst real `ssh-keygen` (Ed25519 and RSA), agent protocol parsing,\nkey selection logic, and `check-novalidate` verification. Tests that\nshell out to `ssh-keygen` skip themselves when it's not installed.\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpavlov-net%2Fssh-agent-proxy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpavlov-net%2Fssh-agent-proxy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpavlov-net%2Fssh-agent-proxy/lists"}