Git & SSH Agent Setup for GitH
Quick Commands
# edit + deploy
git status
git add -A
git commit -m "docs: update"
git push
# rebuild static blog output (local)
cd site
npm ci --no-audit --no-fund
npm run build
# VPS: pull only
# (on server)
git pull --ff-only
GitHub SSH Agent â Final Architecture & Runbook (from your chat dump)
Final model (verified): One host user is the Git identity. A single shared agent socket at
**/run/gitshared/ssh-agent.sock**is used by other local users via the**gitshare**group and two wrappers. Keys live under the host user; no perâuser key sprawl.
1) Goals & Design
- One identity, many users: Centralize GitHub auth on the host user.
- Shared, not copied: Expose the agent socket (not private keys) to other users.
- Strict SSH posture: Enforce
IdentitiesOnly,IdentityAgent, andStrictHostKeyChecking. - Auditability: Systemâwide
known_hosts, predictable socket path, consistent wrappers.
Trust boundary
[Other local users] --SSH_AUTH_SOCK--> [/run/gitshared/ssh-agent.sock] --keys--> [Host user's keys]
2) Prerequisites
- Linux host with
ssh,git,sudo. - A host user (the Git identity) with a private key for GitHub.
- A local group
**gitshare**used to authorize socket access for other users.
3) Key Setup (host user)
# Create an Ed25519 key if you donât have one
ssh-keygen -t ed25519 -C "your_email@example.com" -f ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
chmod 644 ~/.ssh/id_ed25519.pub
# Add PUBLIC key to GitHub â Settings â SSH and GPG keys â New SSH key
cat ~/.ssh/id_ed25519.pub
# Trust GitHub host key systemâwide (nonâinteractive jobs wonât stall)
ssh-keyscan github.com | sudo tee -a /etc/ssh/ssh_known_hosts > /dev/null
4) Shared Socket & Group
# Create group (once)
sudo groupadd -f gitshare
# Create shared run dir with restrictive perms
sudo install -d -m 0770 -o "$USER" -g gitshare /run/gitshared
# Add any collaborating users to the group (reâlogin required)
sudo usermod -aG gitshare <otheruser>
Why: Permissions on /run/gitshared are what gate access; only group members can talk to the agent.
5) Start the Agent (host user)
# Bind the agent to a predictable, shared socket
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock
rm -f "$SSH_AUTH_SOCK"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK")"
# Load your key into the agent
ssh-add ~/.ssh/id_ed25519
ssh-add -l # sanity check
The agent runs in the current session. To persist across logouts, see §10 (optional systemd).
6) Zsh functions (final, enforced usage)
Final state uses zsh functions in your shell config, not standalone scripts. Both functions enforce the shared agent and strict SSH options.
6.1 run_as â run any command as another user via the shared agent
# Any command as another user with your SSH agent
run_as() {
local user="$1"; shift
sudo -u "$user" env SSH_AUTH_SOCK="$GIT_SHARED_SOCK" /bin/bash -lc "$*"
}
Usage:
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.sock
run_as build 'ssh -T git@github.com'
6.2 git_as â run git as another user in a target directory (with path rewriting)
# git_as <user> <dir> <git-args...>
git_as() {
local user="$1"; local dir_in="$2"; shift 2
# Home directories
local host_home="$HOME"
local target_home
target_home="$(getent passwd "$user" | cut -d: -f6)"
# Rewrite input path into target user's path
local cd_cmd
case "$dir_in" in
"~") cd_cmd='cd ~' ;;
"~/"*) cd_cmd="cd ~ && cd \"${dir_in#~/}\"" ;;
"$host_home"/*) cd_cmd="cd \"$target_home\" && cd \"${dir_in#${host_home}/}\"" ;;
/*) cd_cmd="cd \"$dir_in\"" ;;
*) cd_cmd="cd ~ && cd \"$dir_in\"" ;;
esac
# One-line, zsh-friendly GIT_SSH_COMMAND
local gsc="ssh -o IdentitiesOnly=yes -o IdentityAgent=$GIT_SHARED_SOCK -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts"
sudo -u "$user" env \
SSH_AUTH_SOCK="$GIT_SHARED_SOCK" \
GIT_SSH_COMMAND="$gsc" \
/bin/bash -lc "$cd_cmd && git $*"
}
Usage:
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.sock
git_as build ~/repo 'fetch --all --prune'
Why functions? They ensure everyone uses the same agent/socket and strict SSH flags, while keeping configs in one place (
~/.zshrc).
7) Verifications
# Host user â GitHub (uses your agent)
ssh -T git@github.com
# Other user via shared agent (simple)
run_as build 'ssh -T git@github.com'
# Git read-only test as other user inside a repo
git_as build ~/repo 'ls-remote git@github.com:org/repo.git | head'
If any step prompts for host key, re-append GitHub to
/etc/ssh/ssh_known_hostsand retry.
8) Containers (optional, if needed)
If a container must use the agent, bindâmount the socket dir and export the env.
docker run --rm -it \
-v /run/gitshared:/run/gitshared \
-e SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock \
-e GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o IdentityAgent=/run/gitshared/ssh-agent.sock -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts" \
-v /etc/ssh/ssh_known_hosts:/etc/ssh/ssh_known_hosts:ro \
alpine:3.20 sh
Keep keys out of images. Access is gated by the
gitsharegroup on the host.
9) Troubleshooting
**Permission denied (publickey)**ssh-add -lshows no keys â reâadd:ssh-add ~/.ssh/id_ed25519.- Socket perms:
ls -ld /run/gitsharedâ group must begitshare, mode0770. - Ensure the other user is in
gitshareand reâlogged in.
**Agent admitted failure to sign**- Key not loaded in current agent â
ssh-add -D && ssh-add ~/.ssh/id_ed25519.
- Key not loaded in current agent â
- Stale/broken socket
rm -f /run/gitshared/ssh-agent.sock && eval "$(ssh-agent -a /run/gitshared/ssh-agent.sock)".
- Wrong known_hosts
- Reâappend:
ssh-keyscan github.com | sudo tee -a /etc/ssh/ssh_known_hosts.
- Reâappend:
- Accidental second agent
- Check with
pgrep -a ssh-agent; keep only the one bound to/run/gitshared/ssh-agent.sock.
- Check with
10) Optional: Persistent Agent via systemd (single, clean unit)
Only if you want loginâindependent uptime. Do not use the earlier
gitshared-ssh-agent@.serviceor genericssh-agent.servicevariants; disable/mask them first.
systemctl --user disable --now gitshared-ssh-agent@.service 2>/dev/null || true
systemctl --user disable --now ssh-agent.service 2>/dev/null || true
Create ~/.config/systemd/user/gitshare-ssh-agent.service:
[Unit]
Description=ssh-agent bound to /run/gitshared/ssh-agent.sock
[Service]
Type=simple
ExecStart=/usr/bin/env bash -lc 'install -d -m 0770 -o $USER -g gitshare /run/gitshared; rm -f /run/gitshared/ssh-agent.sock; exec ssh-agent -D -a /run/gitshared/ssh-agent.sock'
[Install]
WantedBy=default.target
Activate:
systemctl --user daemon-reload
systemctl --user enable --now gitshare-ssh-agent.service
You still load keys with
ssh-addper login, unless you adopt a secure keychain.
11) Deprecations (explicit)
- Not used anymore:
gitshared-ssh-agent@.service,ssh-agent.service(older explorations). Keep them disabled/masked to avoid agent conflicts.
12) Runbook (Ops)
- Start/verify agent â
ssh-add -l. - Add collaborators to
**gitshare**â reâlogin. - Ensure zsh functions (
run_as,git_as) are present in~/.zshrcandGIT_SHARED_SOCK=/run/gitshared/ssh-agent.sockis exported. - Verify with
ssh -T git@github.com(host &run_as <user>); usegit_asfor repo operations. - Audit CI/local logs: confirm no PATs, no perâuser keys.
- Rotate keys on schedule or incident; reâadd to agent.
13) Onboarding a New Local User (5âminute guide)
sudo usermod -aG gitshare <user>â user reâlogs in.- Verify access:
run_as <user> 'ssh -T git@github.com'(should greet your GitHub account). - Provide
run_as/git_assnippets in a shared dotfiles repo or paste into the userâs shell rc.
14) Minimal Directory Snapshot
/run/gitshared/ssh-agent.sock # shared agent socket (0770, group=gitshare)
/etc/ssh/ssh_known_hosts # contains github.com
/home/<hostuser>/.ssh/id_ed25519 # host user key (private)
~/.zshrc # contains run_as & git_as; exports GIT_SHARED_SOCK
~/.config/systemd/user/gitshare-ssh-agent.service # (optional) single persistent unit
Change log (this doc vs. earlier draft)
- Du hast alles richtig gebaut â Agent lĂ€uft, Key geladen, Socket-Rechte passen â aber der letzte Fehler ist âcommunication with agent failedâ nur bei proxyuser. Das ist kein Rechteproblem mehr, sondern ein Sicherheits-Guard in modernen OpenSSH-Versionen: ssh-agent akzeptiert standardmĂ€Ăig keine Anfragen von anderen UIDs, selbst wenn der Socket gruppen-schreibbar ist. Ergebnis: andere User können verbinden, der Agent lehnt aber das Protokoll ab â genau diese Meldung.
- Kurz: Cross-UID-Sharing eines ssh-agent ist (in aktuellen OpenSSH) nicht mehr supported.
Workarounds:Replaced standalone wrapper scripts with zsh functionsrun_asandgit_as(your latest state). - Kept final socket model:
/run/gitshared/ssh-agent.sock+gitsharegroup. - Kept note to disable earlier units (
gitshared-ssh-agent@.service,ssh-agent.service).
Du hast alles richtig gebaut â Agent lĂ€uft, Key geladen, Socket-Rechte passen â aber der letzte Fehler ist âcommunication with agent failedâ nur bei
proxyuser.
Das ist kein Rechteproblem mehr, sondern ein Sicherheits-Guard in modernen OpenSSH-Versionen: ssh-agent akzeptiert standardmĂ€Ăig keine Anfragen von
anderen UIDs, selbst wenn der Socket gruppen-schreibbar ist. Ergebnis: andere User können verbinden, der Agent lehnt aber das Protokoll ab â genau
diese Meldung.
Kurz: Cross-UID-Sharing eines ssh-agent ist (in aktuellen OpenSSH) nicht mehr supported.
Workarounds:
Got it. Below are additions to append to your existing doc (nothing removed). They explain why we changed, what changed, and how to run it with the new agent relay. You can paste these sections after your current content.
15) Architecture Update: Agent Relay (what & why)
What changed
We keep one host user as the GitHub identity and the real ssh-agent bound to /run/gitshared/ssh-agent.sock (the âREALâ socket). We add a tiny
relay (via socat) that exposes a second socket: /run/gitshared/ssh-agent.relay (the âRELAYâ socket).
Local collaborators use the RELAY socket. The relay connects requests to the REAL socket.
[other local users] --SSH_AUTH_SOCK--> /run/gitshared/ssh-agent.relay
â (relay, same UID as host user)
âââ> /run/gitshared/ssh-agent.sock --keys--> [host user keys]
Why we changed (root cause)
Modern OpenSSH ssh-agent enforces a cross-UID guard: it will refuse requests from a different UID, even if the UNIX socket has group write
permissions. Thatâs why you saw:
error fetching identities: communication with agent failed(from other users)- followed by
Permission denied (publickey)to GitHub.
So: group permissions alone are not enough. The relay runs under the host userâs UID, so the agent sees requests as same-UID and accepts themâno key sprawl, no container changes.
What stays the same
- Single identity (host user), keys live under the host user.
- Strict SSH posture (IdentitiesOnly, StrictHostKeyChecking, system-wide known_hosts).
- gitshare group still gates who can touch the shared sockets/relay.
16) Operational Change (one-liner)
Wherever the doc previously told you to use:
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.sock
use this instead:
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.relay
Your run_as / git_as functions continue to work; theyâll now point at the RELAY.
17) Implementation: Start/Verify (manual quick path)
As the host user (the Git identity):
# Real agent (only once per boot/session)
install -d -m 2770 -o "$USER" -g gitshare /run/gitshared
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock
rm -f "$SSH_AUTH_SOCK"
eval "$(ssh-agent -a "$SSH_AUTH_SOCK")"
ssh-add -D
ssh-add ~/.ssh/id_ed25519
ssh-add -l
# Relay (tiny proxy) â same user
sudo apt-get update -y && sudo apt-get install -y socat
rm -f /run/gitshared/ssh-agent.relay
socat UNIX-LISTEN:/run/gitshared/ssh-agent.relay,fork,mode=0660 \
UNIX-CONNECT:/run/gitshared/ssh-agent.sock >/dev/null 2>&1 &
chgrp gitshare /run/gitshared/ssh-agent.relay
ls -l /run/gitshared/ssh-agent.sock /run/gitshared/ssh-agent.relay
As another local user (e.g., **proxyuser**):
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relay
ssh-add -l # should list the host userâs ED25519 key
ssh -T git@github.com # âHi <your_account>!â
18) Systemd (user) Unit for the Relay (robust & persistent)
Create ~/.config/systemd/user/gitshare-agent-relay.service under the host user:
[Unit]
Description=ssh-agent relay -> /run/gitshared/ssh-agent.relay
ConditionPathExists=/run/gitshared/ssh-agent.sock
[Service]
Type=simple
# Wait for REAL socket, then relay
ExecStart=/usr/bin/env bash -lc '\
REAL=/run/gitshared/ssh-agent.sock; RELAY=/run/gitshared/ssh-agent.relay; \
umask 007; \
while [ ! -S "$REAL" ]; do sleep 0.5; done; \
rm -f "$RELAY"; \
exec socat UNIX-LISTEN:"$RELAY",fork,mode=0660 UNIX-CONNECT:"$REAL" \
'
ExecStartPost=/bin/chgrp gitshare /run/gitshared/ssh-agent.relay
Restart=always
RestartSec=1
[Install]
WantedBy=default.target
Activate:
systemctl --user daemon-reload
systemctl --user enable --now gitshare-agent-relay.service
systemctl --user status gitshare-agent-relay.service --no-pager
The REAL agent itself can stay managed by your existing
gitshare-ssh-agent.service(§10 of your doc).
19) Wrappers (minor update)
Keep your functions exactly as before but export the relay path:
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.relay
run_as() {
local user="$1"; shift
sudo -u "$user" env SSH_AUTH_SOCK="$GIT_SHARED_SOCK" /bin/bash -lc "$*"
}
git_as() {
local user="$1"; local dir_in="$2"; shift 2
local gsc="ssh -o IdentitiesOnly=yes -o IdentityAgent=$GIT_SHARED_SOCK -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts"
sudo -u "$user" env SSH_AUTH_SOCK="$GIT_SHARED_SOCK" GIT_SSH_COMMAND="$gsc" /bin/bash -lc "cd \"$dir_in\" && git $*"
}
20) Security Model (unchanged principles, clarified)
- Keys never leave the host user. The relay only forwards the agent protocol; it cannot read private keys.
- Access remains gated by UNIX permissions and the gitshare group on
/run/gitshared. - Keep StrictHostKeyChecking=yes and a system-wide
/etc/ssh/ssh_known_hostswithgithub.com. - Treat membership in
gitshareas sensitiveâmembers can sign SSH handshakes via the agent.
21) Containers (if needed)
Bind-mount the relay socket (not the real one):
docker run --rm -it \
-v /run/gitshared:/run/gitshared \
-e SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relay \
-e GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o IdentityAgent=/run/gitshared/ssh-agent.relay -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts" \
-v /etc/ssh/ssh_known_hosts:/etc/ssh/ssh_known_hosts:ro \
alpine:3.20 sh
22) Troubleshooting (relay-aware)
**ssh-add -l**(other user) â âcommunication with agent failedâ †Ensure youâre pointing at the relay:echo $SSH_AUTH_SOCKâ/run/gitshared/ssh-agent.relayâ€systemctl --user status gitshare-agent-relay.service(host user) must be active †REAL socket exists:ls -l /run/gitshared/ssh-agent.sock†Relay socket perms:srw-rw---- : gitshare- Relay restarts repeatedly †REAL socket not readyâuse the systemd unit above (it waits), or start the real agent first.
**Permission denied (publickey)**still †On host user:SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -lmust list the key †Ensuregithub.comis in/etc/ssh/ssh_known_hosts†Make sure Git actually uses SSH (not HTTPS remote).- File permission errors when pushing as a different user †Grant group write to the repo tree:
chgrp -R gitshare âŠ; chmod -R g+rwX âŠ; find ⊠-type d -exec chmod g+s {} +
23) FAQ
Why not just share the REAL socket with group write? Because modern ssh-agent rejects requests from different UIDs even if the socket is writable by a
group. The relay runs under the host userâs UID, so the agent accepts.
Is the relay secure? It doesnât expose private keys; it only forwards agent RPC. Security hinges on UNIX ownership & gitshare membershipâkeep that group
small.
Performance impact? Negligibleâlocal UNIX domain socket hop.
Why not deploy keys or PATs? Relay preserves a single identity and avoids token/key sprawl while keeping strict host-side control.
24) Change Log (addendum)
- Switched to agent relay at
/run/gitshared/ssh-agent.relayto overcome ssh-agentâs cross-UID restriction. - No content removed from prior model; REAL agent remains at
/run/gitshared/ssh-agent.sock. - Added systemd user unit
gitshare-agent-relay.servicethat waits for the REAL socket. - Updated guidance to point wrappers and containers at the RELAY socket.
Awesomeâhereâs an extra section to append (nothing removed), spelling out exactly how to use the setup on the host and how collaborators like
**proxyuser** use it day-to-day.
25) Practical Usage â Host vs. Other Local Users
This section gives copy-paste-ready workflows for both the host user (the Git identity) and other local users (e.g., proxyuser). It assumes the REAL
agent is at /run/gitshared/ssh-agent.sock and the RELAY is at /run/gitshared/ssh-agent.relay (see §§15â18).
25.1 Host User Workflow (Git identity owner)
You can point at either socket. For consistency with everyone else, we recommend using the RELAY everywhere.
One-time (per boot / after login)
# Ensure the agent (REAL) and relay are running per §§10 & 18.
systemctl --user status gitshare-ssh-agent.service
systemctl --user status gitshare-agent-relay.service
Per-shell convenience
Add to your ~/.zshrc (host user):
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.relay
export SSH_AUTH_SOCK=$GIT_SHARED_SOCK
Verify identity
ssh-add -l # should show your ED25519 key
ssh -T git@github.com # should greet your account
Typical git tasks (host user)
# Ensure SSH remote (not HTTPS)
git remote -v
git remote set-url origin git@github.com:<owner>/<repo>.git
# Normal workflow
git fetch --all --prune
git pull --rebase
git push
Operating on someone elseâs repo directory (host user)
If the repo lives under another user (e.g., /home/proxyuser/repo), ensure group write once:
sudo chgrp -R gitshare /home/proxyuser/repo
sudo chmod -R g+rwX /home/proxyuser/repo
sudo find /home/proxyuser/repo -type d -exec chmod g+s {} +
Then either run directly:
git -C /home/proxyuser/repo push
or use your wrapper (§19):
git_as proxyuser ~/repo 'push'
25.2 Collaborator Workflow (e.g., proxyuser and others)
One-time on each collaborator
-
Ensure theyâre in the gitshare group and re-login:
id | grep gitshare || echo "Re-login required or add to group." -
The host user controls the REAL agent & RELAY; collaborators do not need keys.
Per-shell convenience
Add to collaboratorâs ~/.zshrc:
# Use the RELAY (not the REAL socket)
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relay
No extra SSH config is required, but the remote must be SSH-style.
Verify access (collaborator)
ssh-add -l # should list the host userâs ED25519 key via relay
ssh -T git@github.com # should greet the host userâs GitHub account
Use in any repo (collaborator)
# Ensure SSH remote (not HTTPS)
git remote -v
git remote set-url origin git@github.com:<owner>/<repo>.git
git fetch --all --prune
git pull --rebase
git push
If you see
Permission denied (publickey), confirm youâre pointing at the relay:
echo $SSH_AUTH_SOCKâ/run/gitshared/ssh-agent.relay.
If not,export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relayand retry.
25.3 Quick Commands (cheat sheet)
Host user (once):
# REAL agent + key (handled by §10 service in steady state)
ssh-add -l
Host user (relay health):
systemctl --user status gitshare-agent-relay.service --no-pager
ls -l /run/gitshared/ssh-agent.sock /run/gitshared/ssh-agent.relay
Any collaborator (every shell):
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relay
ssh-add -l
ssh -T git@github.com
Git remote must be SSH:
git remote set-url origin git@github.com:<owner>/<repo>.git
25.4 Troubleshooting (host vs. collaborator)
- Collaborator:
ssh-add -lâ âcommunication with agent failedâ- Cause: using REAL socket or relay not running.
- Fix:
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relay - Host user:
systemctl --user status gitshare-agent-relay.service
- Collaborator:
Permission denied (publickey)-
Check relay variable, then verify REAL agent has a key (host user):
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l -
Ensure
/etc/ssh/ssh_known_hostscontainsgithub.com.
-
- Host user pushing into collaboratorâs repo path fails with FS perms
- Apply group write & setgid once (see §25.1).
25.5 Minimal .zshrc snippets (copy-paste)
Host user:
# Shared agent relay for everyone (consistent path)
export GIT_SHARED_SOCK=/run/gitshared/ssh-agent.relay
export SSH_AUTH_SOCK=$GIT_SHARED_SOCK
# (Optional) strict SSH defaults used by wrappers
export GIT_SSH_COMMAND='ssh -o IdentitiesOnly=yes -o IdentityAgent=/run/gitshared/ssh-agent.relay -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts'
Collaborator (e.g., proxyuser):
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relay
# (Optional) enforce strict SSH for all git
export GIT_SSH_COMMAND='ssh -o IdentitiesOnly=yes -o IdentityAgent=/run/gitshared/ssh-agent.relay -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts'
Net effect: Host continues to own the GitHub identity and the REAL agent. Everyoneâhost and collaboratorsâpoints to the RELAY for a uniform developer experience. No key sprawl, no container changes, and the cross-UID restriction is cleanly sidestepped.
Perfect â here are append-only sections you can paste into your doc. They explain why we moved to the brokered approach, how the final solution works, and a cleanup/hardening plan after all the experiments.
27) Host-Brokered Git via Ephemeral Relay (final)
What we need:
- Use the host userâs GitHub identity (private key stays on host).
- Run
gitinside other usersâ repo directories (e.g.,proxyuser). - No key sprawl, no changing their ownership/permissions, no agent access for them.
How it works (overview):
-
The REAL ssh-agent runs under the host user, at:
/run/gitshared/ssh-agent.sock -
For each brokered command, the host spins up a short-lived relay socket:
/run/gitshared/relay.<user>.<pid>.<rand>.sockThe relay forwards to the REAL agent. It is owned by the host, group=gitshare,
0660. -
The host executes a tiny sudo-whitelisted runner as the target user.
That runner receives the relay path and setsSSH_AUTH_SOCK+GIT_SSH_COMMANDitself, so we donât rely onsudo env_keep. -
gitruns with the target userâs UID (so the repo is accessible) but signs/authenticates via the host userâs key through the relay.
Why this wins:
- Works around OpenSSHâs cross-UID agent restriction (agent rejects foreign UIDs).
- Keeps private keys only under the host.
- No ownership changes in other usersâ repo trees.
- No password prompts (tight sudoers whitelist).
- Relay is ephemeral â minimal attack surface, automatic cleanup.
27.1 Components (final, minimal)
Sudoers (NOPASSWD, exact path):
# /etc/sudoers.d/git-broker
blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run *
Runner (sets env from the relay path):
# /usr/local/bin/git-broker-run (root:root, 0755)
#!/usr/bin/env bash
set -euo pipefail
dir="${1:?need repo dir}"; shift
relay="${1:?need relay socket}"; shift || true
cd "$dir"
export SSH_AUTH_SOCK="$relay"
export GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes -o IdentityAgent=$relay -o StrictHostKeyChecking=yes -o UserKnownHostsFile=/etc/ssh/ssh_known_hosts"
exec /usr/bin/git "$@"
Host helper (**git_broker** function, uses group=gitshare relay + nohup/setsid):
# in host's ~/.zshrc (source it afterwards)
# git_broker <targetUser> <repoDir> <git args...>
git_broker() {
set -euo pipefail
local tgt="${1:?target user}"; local repo="${2:?repo dir}"; shift 2
local REAL="/run/gitshared/ssh-agent.sock"
local RELAY="/run/gitshared/relay.${tgt}.$$.$RANDOM.sock"
# ensure base dir & start ephemeral relay (TTY-detached)
install -d -m 2770 -o "$USER" -g gitshare /run/gitshared
rm -f "$RELAY"
nohup setsid socat UNIX-LISTEN:"$RELAY",fork,group=gitshare,mode=0660 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1 &
# wait for relay to appear
for _ in {1..100}; do [[ -S "$RELAY" ]] && break; sleep 0.05; done
[[ -S "$RELAY" ]] || { echo "relay did not appear"; return 1; }
# run as target user, passing <repoDir> and <relay> before git args
sudo -n -u "$tgt" /usr/local/bin/git-broker-run "$repo" "$RELAY" "$@"
# best-effort cleanup
rm -f "$RELAY" || true
}
Usage (host only):
# Read-only
git_broker proxyuser /home/proxyuser status
git_broker proxyuser /home/proxyuser fetch --all --prune
# Push / tags
git_broker proxyuser /home/proxyuser push -u origin HEAD
git_broker proxyuser /home/proxyuser push --tags
28) Rationale: Why we changed & why this design
- Cross-UID agent restriction: Modern OpenSSH
ssh-agentrefuses requests from other UIDs even if the socket is group-writable. That producederror fetching identities: communication with agent failedfor non-host users. - Relay vs. direct sharing: A local relay connects to the REAL agent as the host user, so the agent sees same UID and accepts. The relay itself is just a UNIX socket forwarder; it never exposes private key material.
- No env fragility: Earlier attempts relied on
sudo env_keepto transportSSH_AUTH_SOCK/GIT_SSH_COMMAND. The new runner sets them internally from the relay path, making the flow robust against sudo environment filtering. - No repo ownership changes: We execute
gitas the target user (via sudo), so we donât need tochgrp/chmodtheir working tree. - Tight control surface: The relay is ephemeral per command and
0660forgitshare. Only members ofgitshare(host + explicitly allowed users) can open it; and we can shrink the group to just the host if we want host-only operations.
29) Security model (focused)
- Private key stays with host: The REAL agent + key live under the host account. Other users never touch a private key.
- Signing capability = socket access: Anyone who can open the relay can sign as the host identity (cannot extract the key, but can act on behalf).
Keep
gitsharemembership strict. - Auditability:
sudologs show each brokered call (runner + arguments).githistory author/committer is set per repo (git config user.*).
- Optional stronger approval: Replace host key with FIDO2/ed25519-sk to require a physical touch for each signature, even via relay.
30) Ops quick checks
# Host: REAL agent healthy and has key?
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l
# Relay needed only for brokered calls; no long-lived relay required.
# End-to-end:
git_broker proxyuser /home/proxyuser ls-remote git@github.com:<owner>/<repo>.git | head
31) Cleanup & Hardening after the transition
Remove/disable old experiments and stale state:
-
Disable any obsolete services that started extra agents/relays:
# As host user (user services) systemctl --user disable --now gitshared-ssh-agent@.service 2>/dev/null || true systemctl --user disable --now ssh-agent.service 2>/dev/null || true systemctl --user disable --now gitshare-agent-relay.service 2>/dev/null || true(Keep your clean REAL agent service from §10 if you use it; you donât need a persistent relay anymore.)
-
Purge stale sockets / temp relays:
sudo find /run/gitshared -maxdepth 1 -type s -name 'relay.*.sock' -delete sudo rm -f /run/gitshared/ssh-agent.relay # if you used a persistent relay earlier -
Tighten
**gitshare**membership (principle of least privilege):# Keep only host + users you explicitly want to broker for getent group gitshare # Remove extras if any: sudo gpasswd -d <user> gitshare -
Lock sudoers to expected paths (optional but recommended):
-
Narrow the runnerâs allowed path (matches first argument) if you only broker in one base:
# Only allow repos under /home/proxyuser blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run /home/proxyuser * -
If you broker for multiple users, add one line per target user with their base path.
-
-
Delete legacy wrapper scripts (we standardized on the single
git_broker):
Remove any olderrun_as/git_asscripts that assume direct agent sharing. Keep them only if you still need them for non-Git commands. -
Known_hosts baseline:
Ensuregithub.comlives in/etc/ssh/ssh_known_hosts:ssh-keyscan github.com | sudo tee -a /etc/ssh/ssh_known_hosts >/dev/null -
Secret hygiene:
- Confirm
.gitignoreblocksnginx/secrets/**,nginx/logs/**,**/*.key,**/*.pem,.env*, etc. - If any secret was briefly staged, unstage & purge:
git rm -r --cached <path>and (if ever committed) usegit filter-repo/BFG.
- Confirm
-
Optional key refresh:
If the host key was touched during debugging and you want a clean slate, rotate:- generate a new
ed25519key, - upload public part to GitHub,
- remove old public key from GitHub,
ssh-add -D && ssh-add ~/.ssh/id_ed25519.
- generate a new
32) FAQ (brokered mode)
- Why not just group-share the REAL agent socket?
OpenSSHssh-agentrejects foreign UIDs even with group write â you get âcommunication with agent failed.â - Why set env in the runner?
Sudo filters env; relying onenv_keepis brittle. Passing the relay path and setting env inside the runner is robust and auditable. - Can collaborators push by themselves now?
Not with this design. Only the host launches relays and calls the runner. If you ever want a collaborator to push directly, use a repo-scoped deploy key (outside of this flow). - Does this change Git author/committer?
NoâSSH identity is the network signature; author/committer fields come from the repoâsuser.name/user.email.
33) Who Can Push When (Host-Only Policy Explained)
This section documents, crisply and unambiguously, who can push under the final brokered design, and how to verify/enforce it. It complements §§27â32 and does not replace them.
33.1 Local vs. Remote Git (why status works but push doesnât)
- Local commands (no network):
git status,git diff,git log
â Always work for any UNIX user in that working tree. No SSH key/agent needed. - Remote commands (network, require SSH signing):
git fetch,git pull,git push,git ls-remote
â Require a valid SSH identity (agent/key) or HTTPS token.
With our model, the only usable SSH identity is the host userâs key, reachable via ephemeral relays created by the host for a single command (§27).
Therefore: proxyuser can run local commands anytime, but cannot push unless the host brokers the operation for that command.
33.2 Effective Policy (final)
- The REAL agent lives at
/run/gitshared/ssh-agent.sockunder the host user and holds the private key. - A relay socket is created ephemerally per command by the host (
git_broker âŠ). It is deleted right after. - The sudo-whitelisted runner (
/usr/local/bin/git-broker-run) executesgitas the target user but injects the relay path so authentication/signing still uses the hostâs key.
Net effect:
- Only the host can create a usable path to the key (by starting the relay).
- Other users cannot reach a key/agent unless the host actively brokers that command.
33.3 When could proxyuser push? (only if you intentionally allow it)
proxyuser can push only in one of these deliberate cases:
- Host-brokered call (current/approved path):
Host runsgit_broker proxyuser /path/to/repo push âŠâ allowed for that one command. - Persistent relay exposed (not recommended):
A long-lived relayâŠ/ssh-agent.relaywith0660group access exists andproxyuserpointsSSH_AUTH_SOCKto it.
â Do not run such a persistent relay if you want host-only control. - Own credentials for
proxyuser(not your host identity):- Repo-scoped Deploy Key (write) on GitHub, or
- SSH account key on
proxyuser, or - HTTPS remote + personal/access token.
â Different identity; outside host-only model.
- Temporary access window (explicit):
Host creates a relay and leaves it available (or grants an ACL) for a time-boxed period.
â Only if you intentionally open such a window.
If none of the above is true, proxyuser cannot push.
33.4 Enforcement Checklist (host-only)
Keep these in place to guarantee that only the host can push:
-
No persistent relay service running
systemctl --user disable --now gitshare-agent-relay.service 2>/dev/null || true sudo rm -f /run/gitshared/ssh-agent.relay -
Only ephemeral relays via
**git_broker**- Use the §27 function that creates
relay.<user>.<pid>.<rand>.sockand removes it after the command.
- Use the §27 function that creates
-
Minimal sudoers
# /etc/sudoers.d/git-broker blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run *No rules that let
proxyuserexecute the runner or set env for themselves. -
Runner owns env (not env_keep):
/usr/local/bin/git-broker-runsetsSSH_AUTH_SOCK/GIT_SSH_COMMANDfrom the relay argument. -
Tight group membership
getent group gitshare # Keep membership minimal; removing users prevents them from opening 0660 relays directly.(You may keep only the host in
gitshareand still broker, because the relay is used by the target process launched via sudo.) -
Single REAL agent
pgrep -a ssh-agent ls -l /run/gitshared/ssh-agent.sock SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l
33.5 Negative/Positive Verification (copy-paste tests)
Negative (prove **proxyuser** cannot push alone):
sudo -iu proxyuser bash -lc \
'GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes" git ls-remote git@github.com:<owner>/<repo>.git || echo "EXPECTED: denied"'
# EXPECTED: Permission denied (publickey) / failure
Positive (host-brokered, proves push path works):
git_broker proxyuser /home/proxyuser ls-remote git@github.com:<owner>/<repo>.git | head
# EXPECTED: refs listing (works because host created the relay for this command)
33.6 Post-Migration Cleanup Recap (safe to run)
-
Disable/remove any legacy agent/relay units:
systemctl --user disable --now gitshared-ssh-agent@.service 2>/dev/null || true systemctl --user disable --now ssh-agent.service 2>/dev/null || true systemctl --user disable --now gitshare-agent-relay.service 2>/dev/null || true -
Purge stale sockets:
sudo find /run/gitshared -maxdepth 1 -type s -name 'relay.*.sock' -delete -
Shell RCs: remove any global
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.relayor old wrappers that assume shared agents. -
**.gitignore**& secrets: confirm excludes (nginx/secrets/**, keys, tokens, logs).
If anything was staged by mistake:git rm -r --cached <path>. -
Known hosts baseline:
ssh-keyscan github.com | sudo tee -a /etc/ssh/ssh_known_hosts >/dev/null
33.7 TL;DR
proxyusercan always run local Git (e.g.,status), but cannot push unless the host brokers that single command.- Thatâs by design: the only signing path is the hostâs key via an ephemeral relay the host controls.
- Keep the enforcement checklist to ensure this stays true, and use the verification snippets anytime to prove it.
34) Onboarding a New Local User (Brokered Mode)
This section shows exactly how to enable a new local user (e.g., alice) so that you (host) can run git in their directory using your host SSH
identity, without changing their file ownership and without giving them agent access.
Assumes you already have §27 in place (REAL agent,
git_broker, runner).
34.1 Create / confirm the UNIX user (if not existing)
# If needed (root):
sudo adduser alice
34.2 Allow the broker to work for this user
A) Sudoers (host may run the brokered runner as the new user)
sudo visudo -f /etc/sudoers.d/git-broker
Append one line (keep existing ones for other users):
blade34242 ALL = (alice) NOPASSWD: /usr/local/bin/git-broker-run *
This does not give
aliceany agent access. It only allows you to call the whitelisted runner as alice.
B) Relay access model
Use one of these (pick the one matching your current design):
-
Group-based (your current setup): ensure
aliceis ingitshareso hergitprocess can open the ephemeral relay socket (0660, group=gitshare).sudo groupadd -f gitshare sudo usermod -aG gitshare alice # IMPORTANT: new group takes effect after a new login: sudo -iu alice id | grep gitshare -
ACL-based (no group membership): keep §27âs ACL variant of
git_broker(gives rw to exactly one user on the ephemeral socket).
(If you go ACL-only, you do not need to add_alice_to_gitshare_.)
Either way, there is no persistent relay. Only your
git_brokercreates a per-command socket and removes it afterwards.
34.3 âGit the userâs directoryâ (initialize a repo in their home)
As the new user (local-only steps; no agent needed):
# become the user
sudo -iu alice
# in their home (or any path you want to be the repo root)
cd ~
git init
git branch -M main
# (optional) set author metadata for this repo (affects commit headers, not SSH identity)
git config user.name "Alice Local"
git config user.email "alice@example.local"
# add a remote (SSH form â your brokered identity will authenticate)
git remote add origin git@github.com:<owner>/<repo>.git
# (optional) first content & .gitignore
echo "# Alice's working tree" > README.md
printf '%s\n' \
'.env*' '*.key' '*.pem' 'nginx/secrets/**' 'nginx/logs/**' \
> .gitignore
git add .
git commit -m "Initial commit (by alice)"
exit
34.4 First remote test & push (host-brokered)
From your host account (blade34242):
# quick remote handshake test (lists refs if auth works)
git_broker alice /home/alice ls-remote git@github.com:<owner>/<repo>.git | head
# push the initial branch using your host SSH identity
git_broker alice /home/alice push -u origin main
git status/git logetc. always work locally foralice.
Remote operations (fetch/pull/push) go through your SSH identity only when you broker a command.
34.5 Day-to-day use (for you)
Examples you can run anytime as host:
# read-only ops in alice's repo
git_broker alice /home/alice status
git_broker alice /home/alice fetch --all --prune
# typical update
git_broker alice /home/alice pull --rebase
# push current branch / tags
git_broker alice /home/alice push
git_broker alice /home/alice push --tags
34.6 Security & policy (what this does not do)
alicecannot push on their own with your identity. There is no persistent relay; only yourgit_brokercreates a per-call socket.- If you used the group-based model,
alicebeing ingitsharealone still doesnât help them: without an active relay you started, thereâs nothing to connect to. - If you ever want
aliceto push independently, you must intentionally provide:- a deploy key (repo-scoped) or
- their own SSH key on GitHub or
- HTTPS remote with a token.
(All outside the host-only policy.)
34.7 Quick verification & troubleshooting
# Host: real agent has key
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l
# Host-brokered remote listing (should print refs)
git_broker alice /home/alice ls-remote git@github.com:<owner>/<repo>.git | head
# Negative proof: as alice (no broker), remote should fail
sudo -iu alice bash -lc \
'GIT_SSH_COMMAND="ssh -o IdentitiesOnly=yes" git ls-remote git@github.com:<owner>/<repo>.git || echo "EXPECTED: denied"'
If remote tests fail even via git_broker:
-
Ensure
/etc/ssh/ssh_known_hostscontainsgithub.com:ssh-keyscan github.com | sudo tee -a /etc/ssh/ssh_known_hosts >/dev/null -
Confirm your sudoers line exists for
aliceand no password is prompted:sudo -l -U blade34242 | sed -n '1,200p' | grep -A1 git-broker-run -
If group-based: confirm
aliceis ingitshareand re-logged in:sudo -iu alice id | grep gitshare
34.8 De-onboarding (remove a user later)
-
Remove the sudo permission:
sudo edit /etc/sudoers.d/git-broker # delete the (alice) line -
(Group-based only) Remove from
gitshare:sudo gpasswd -d alice gitshare -
No need to touch their repo; they keep full local control, but cannot brokered-push via your identity anymore.
Danke fĂŒr den klaren Befund. Das âConnection closedâ kommt sehr typisch von Job-Control/HUP-Interaktionen mit dem im Hintergrund gestarteten socat. Auch
nohup+setsid helfen nicht immerâzsh schickt dir trotzdem Job-Events, und manche Setups (TTYS/HUP) reiĂen dann die SSH-Session mit runter.
Hier ist eine robuste Variante deiner git_broker-Funktion, die das zuverlÀssig verhindert:
- startet den Relay vollstÀndig entkoppelt (bevorzugt via
systemd-run --userals transienter Dienst), - oder (Fallback) nutzt zsh-spezifisch
&!(sofort disowned, kein Job-Output, keine Bindung an dein TTY), - kein
**set -e**im Top-Level (kein versehentliches Shell-Exit).
Drop-in Fix: neue git_broker-Funktion
FĂŒge das bei **blade34242** in ~/.zshrc ein (alte Funktion komplett ersetzen) und source ~/.zshrc:
# git_broker <targetUser> <repoDir> <git args...>
git_broker() {
emulate -L zsh # lokale, sichere zsh-Umgebung
setopt localoptions no_monitor # keine Job-Control Meldungen
local tgt="${1:?target user}"
local repo="${2:?repo dir}"
shift 2
local REAL="/run/gitshared/ssh-agent.sock"
local RELAY="/run/gitshared/relay.${tgt}.$$.$RANDOM.sock"
# Basisdir hart absichern: owner rwx, group r-x, others ---
install -d -m 2750 -o "$USER" -g gitshare /run/gitshared
# sauberes Starten: alten Socket weg
rm -f "$RELAY"
local used_unit=""
if command -v systemd-run >/dev/null 2>&1; then
# VollstÀndig vom TTY getrennt als transienter User-Unit
used_unit="git-relay-${tgt}-$$-$RANDOM"
systemd-run --user --unit="$used_unit" --collect \
-p RuntimeMaxSec=300 -p KillMode=process \
-p StandardOutput=null -p StandardError=null \
/usr/bin/socat UNIX-LISTEN:"$RELAY",fork,group=gitshare,mode=0660 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1
else
# Fallback: &! = sofort disown (keine Job-Msgs, kein HUP an TTY)
setsid /usr/bin/socat UNIX-LISTEN:"$RELAY",fork,group=gitshare,mode=0660 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1 &!
fi
# auf Socket warten
for _ in {1..200}; do [[ -S "$RELAY" ]] && break; sleep 0.05; done
[[ -S "$RELAY" ]] || { [[ -n "$used_unit" ]] && systemctl --user stop "$used_unit" >/dev/null 2>&1 || true; echo "relay did not appear"; return 1; }
# als Zieluser ausfĂŒhren: Runner setzt SSH_AUTH_SOCK/GIT_SSH_COMMAND selbst
sudo -n -u "$tgt" /usr/local/bin/git-broker-run "$repo" "$RELAY" "$@"
local rc=$?
# aufrÀumen
rm -f "$RELAY" || true
[[ -n "$used_unit" ]] && systemctl --user stop "$used_unit" >/dev/null 2>&1 || true
return $rc
}
Warum das dein Disconnect-Problem löst
**systemd-run --user**startetsocatauĂerhalb deiner Shell/TTY â keine Job-Events, kein HUP, keine Session-Bindung.- Fallback
**&!**(zsh): Job wird disowned, zsh schreibt keine[2] done âŠ-Zeilen mehr und hĂ€ngt nicht am TTY. - Kein
**set -e**global â keine versehentlichen Shell-Exits, wenn irgendwo ein Nicht-Null-RC auftaucht.
30-Sek-Checks
# 1) REAL-Agent + Key
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l
# 2) Test ohne Disconnect
git_broker proxyuser /home/proxyuser status
# 3) Remote-Handshake
git_broker proxyuser /home/proxyuser ls-remote git@github.com:blade34242/nginx-reverse-proxy.git | head
Optional: kleines Hardening, das hilft
# /run/gitshared: nur Traverse fĂŒr Gruppe, kein group-write (Race minimiert)
sudo chown blade34242:gitshare /run/gitshared
sudo chmod 2750 /run/gitshared
Der Relay-Socket selbst bleibt
0660(group=gitshare), damit der alsproxyuserlaufende Git-Prozess ihn öffnen kann â aber nur wĂ€hrend dein Broker-Call lĂ€uft.
Wenn danach immer noch ein Disconnect kommt, sag mir bitte kurz:
- Ausgabe von
echo $ZSH_VERSION; set -o(zsh-Optionen), - ob
systemd-run --userverfĂŒgbar ist (command -v systemd-run), - und den Timestamp aus
journalctl --user -n 50direkt nach dem Abbruch.
35) Security Posture â Attack Surfaces, Hardening, Residual Risk
This section documents what we changed, what it protects, and where risk remains, so ops and reviewers can reason about the model.
35.1 Threat Model (what we protect)
- Private key never leaves the host user. Only the hostâs REAL
ssh-agentholds the key (/run/gitshared/ssh-agent.sock). - Other users never get an agent (no agent forwarding, no shared long-lived socket).
- Brokered model: Host creates a short-lived relay socket per command and runs
gitas the target user via a whitelisted runner (sudoers). The relay is deleted after the command.
35.2 Attack Surfaces (introduced by the design)
- Ephemeral relay socket (UNIX socket file)
- Path:
/run/gitshared/relay.<user>.<pid>.<rand>.sock - Risk: another local user in the same group could connect while it exists and request signatures.
- Path:
**sudo**runner (/usr/local/bin/git-broker-run)- Risk: misuse if sudoers is overly broad, or if the runner executes arbitrary programs.
- Shared directory (
/run/gitshared)- Risk: if group-writable, others could plant files or race the relay filename.
- OpenSSH agent behavior
- We intentionally bypass the cross-UID restriction via a relay. If relay is exposed, itâs equivalent to exposing signing capability while it exists.
35.3 Hardening in Place (what we did)
-
No persistent relay. Only
git_brokercreates a relay per command and removes it immediately after. -
Strict sudoers (NOPASSWD, exact path):
blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run *- The runner only execs
/usr/bin/git; no shell; no PATH tricks. - The runner sets
SSH_AUTH_SOCKandGIT_SSH_COMMANDitself (no reliance onenv_keep).
- The runner only execs
-
Strict SSH posture inside the runner:
IdentitiesOnly=yes,IdentityAgent=<relay>,StrictHostKeyChecking=yes,UserKnownHostsFile=/etc/ssh/ssh_known_hosts(system-pinned GitHub host key; no interactive prompts).
-
REAL agent isolation:
- Only under host user; single socket (
/run/gitshared/ssh-agent.sock). - No
ForwardAgentusage; no agent sockets leaked to other sessions.
- Only under host user; single socket (
-
Directory & socket permissions:
/run/gitsharedcreated by host, not group-writable (recommendchmod 2750): traverse for group, no group write.- Relay sockets:
0660,group=gitshare(or ACL to exactly one user in ACL mode).
-
Randomized relay names to mitigate race/guessing (
<pid>.<rand>). -
Known-hosts baseline:
ssh-keyscan github.com >> /etc/ssh/ssh_known_hosts.
35.4 Residual Risks (and how to mitigate)
-
Relay window abuse (local)
Residual: A user ingitsharecould connect to a relay during the brief window it exists.
Mitigate:- Keep
gitsharemembership minimal (ideally: only host; use ACL variant to grant per-call access to a specific user). - Short relay lifetime (already ephemeral).
- Use FIDO2/ed25519-sk keys or
ssh-add -c(confirm required) to require human presence for signatures.
- Keep
-
Denial of Service on relay
Residual: A local user could hold relay connections open during the window.
Mitigate: Keepgitsharetight; prefer ACL variant for single-user operations; keep relay per-command only. -
Over-broad sudoers
Residual: If sudoers allowed arbitrary commands or any path, host could unintentionally run more thangit.
Mitigate: Keep exactly:blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run *Optional: constrain first arg (path prefix):
blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run /home/proxyuser * -
Group-writable
**/run/gitshared**
Residual: If mode2770, group members can create files there (relay-name race).
Mitigate (recommended):sudo chown blade34242:gitshare /run/gitshared sudo chmod 2750 /run/gitshared # owner rwx, group r-x, others ---The host still creates sockets; group can traverse to connect but cannot create files.
-
Compromised host account
Residual: If host is compromised, attacker can sign as the GitHub identity.
Mitigate:- Protect host account; use hardware-backed key (FIDO2) with touch; consider
ssh-add -t <ttl>for key lifetimes. - Monitor sudo/journal logs for
git-broker-runcalls.
- Protect host account; use hardware-backed key (FIDO2) with touch; consider
35.5 Operational Guidance (do this)
-
Keep
**gitshare**minimal. Prefer ACL variant for single-user brokering. Review membership:getent group gitshare -
Harden shared dir:
sudo chown blade34242:gitshare /run/gitshared sudo chmod 2750 /run/gitshared -
Verify no persistent relay:
systemctl --user disable --now gitshare-agent-relay.service 2>/dev/null || true sudo rm -f /run/gitshared/ssh-agent.relay -
Audit sudoers & runner:
sudo visudo -c sudo -l -U blade34242 | grep -A1 git-broker-run ls -l /usr/local/bin/git-broker-run # root:root, 0755 -
Disable agent forwarding globally (defense-in-depth):
# /etc/ssh/ssh_config.d/99-no-agent-forwarding.conf Host * ForwardAgent no -
Key hygiene:
- Host private key
~/.ssh/id_ed25519mode600, dir~/.sshmode700. - Consider rotating keys after the migration/testing phase.
- Host private key
35.6 Quick Audit (copy/paste)
# Only one REAL agent?
pgrep -a ssh-agent
ls -l /run/gitshared/ssh-agent.sock
# No persistent relay?
ls -l /run/gitshared/ssh-agent.relay || echo "OK: none"
# Shared dir hardened?
namei -mo /run/gitshared
# Sudoers scope OK?
sudo -l -U blade34242 | sed -n '1,200p' | grep -A1 git-broker-run
# GitHub handshake works only when brokered:
git_broker proxyuser /home/proxyuser ls-remote git@github.com:<owner>/<repo>.git | head
sudo -iu proxyuser bash -lc 'git ls-remote git@github.com:<owner>/<repo>.git || echo "EXPECTED: denied"'
Bottom line
- Attack surface minimized: no persistent agent exposure; relay is short-lived and controlled by the host.
- Weaknesses acknowledged: short relay window, local DoS, host compromise risk.
- Mitigations applied: strict sudoers, runner with fixed binary & in-runner env, hardened dir perms, minimal group, strict SSH options, known_hosts pinning, optional hardware key / confirm.
hier ist eine knackige Namens-Taxonomie fĂŒr alle Varianten, die wir durchgespielt haben â mit Kurzbeschreibung, Synonymen und Status. Nutze diese Labels in deiner Doku / beim Reden darĂŒber.
â Aktuelles Ziel-Setup (empfohlen)
- TTY-safe Ephemeral Relay (per-Command)
- Kurz: pro Git-Befehl kurzlebiger
socat-Relay, gestartet TTY-sicher viasystemd-run --user(Fallback:&!disowned). - Warum: kein Session-Drop, kein Dauer-Socket, minimales Angriffsfenster.
- Synonyme: Per-Command Relay, Transient Relay, TTY-safe Relay.
- Status: FINAL (verwenden).
- Kurz: pro Git-Befehl kurzlebiger
- Brokered Runner (sudo-whitelist)
- Kurz: Host ruft als Ziel-User den fixen Runner
/usr/local/bin/git-broker-runauf; der Runner setzt selbstSSH_AUTH_SOCK/GIT_SSH_COMMAND(aus Relay-Pfad) und fĂŒhrt/usr/bin/gitaus. - Warum: unabhĂ€ngig von
env_keep, auditierbar, prÀzise Sudo-Kontrolle. - Synonyme: Runner-based Brokering, Self-setting Runner.
- Status: FINAL (verwenden).
- Kurz: Host ruft als Ziel-User den fixen Runner
- REAL Agent (host-only)
- Kurz: Ein einzelner echter
ssh-agentunter dem Host-User, Socket/run/gitshared/ssh-agent.sock, Key nur dort geladen. - Synonyme: Host REAL Agent, Primary Agent.
- Status: FINAL (verwenden; optional per systemd user service).
- Kurz: Ein einzelner echter
â Varianten (je nach Policy)
- ACL-Relay (per User)
- Kurz: Relay
0600+setfacl u:<user>:rwpro Call; keine Gruppen nötig. - Synonyme: Per-User ACL Relay.
- Status: Optional (max. Least-Privilege).
- Kurz: Relay
- Group-Relay (gitshare)
- Kurz: Relay
0660,group=gitshare; Ziel-User ist ingitshare. - Synonyme: Group-gated Relay.
- Status: Optional (einfach, kleines Race-Fenster innerhalb der Gruppe).
- Kurz: Relay
- Host-Approval Mode
- Kurz: Signieren nur nach Freigabe:
ssh-add -x/-X(Lock/Unlock) oder FIDO2/ed25519-sk mit Touch. - Synonyme: Agent Lock, Hardware-touch Required.
- Status: Optional (fĂŒr stĂ€rkere Freigabe-Kontrolle).
- Kurz: Signieren nur nach Freigabe:
- Host-only Mode
- Kurz: Keine Users in
gitshare+ ACL-Relay; nur gezielt adressierter User erhÀlt rw auf diesen Socket. - Synonyme: Strict Host-only.
- Status: Optional (hÀrteste Policy).
- Kurz: Keine Users in
đïž Veraltet / vermeiden
- Direct Shared Agent (group-shared socket)
- Kurz: Andere User verbinden direkt auf den REAL-Agent via Gruppen-Rechte.
- Problem: OpenSSH Cross-UID-Guard â âcommunication with agent failedâ.
- Synonyme: Direct Share, Shared SSH_AUTH_SOCK.
- Status: DEPRECATED (funktioniert nicht mehr sinnvoll).
- Wrappers mit
**SSH_AUTH_SOCK**(run_as/git_as)- Kurz: Zsh-Funktionen, die direkt den REAL-Agent setzen.
- Problem: Scheitert am Cross-UID-Guard; inkonsistent.
- Status: DEPRECATED (nur noch fĂŒr non-Git Kommandos ok).
- Persistent Relay Service
- Kurz: Dauerhaftes
âŠ/ssh-agent.relaymit0660. - Problem: Langes Angriffsfenster, ungewollte Selbst-Pushes möglich.
- Status: DEPRECATED (abschalten).
- Broker mit
**env_keep**-AbhÀngigkeit
- Kurz: VerlieĂ sich auf
Defaults! ⊠env_keepfĂŒrSSH_AUTH_SOCK/GIT_SSH_COMMAND. - Problem: Fragil, distro/sudo-abhĂ€ngig.
- Status: DEPRECATED (ersetzt durch Runner-setzt-Env).
đ Alternative Auth-Wege (bewusst anders)
- Deploy-Key Mode
- Kurz: Repo-scoped Key (read/write) auf GitHub, liegt beim Ziel-User.
- Status: Alternative (eigene IdentitÀt, nicht Host-Key).
- HTTPS + Token
- Kurz: Remote via HTTPS mit Personal/Access Token.
- Status: Alternative (nicht unser Host-Key-Modell).
- Container Agent Mount
- Kurz:
/run/gitsharedin Container binden +SSH_AUTH_SOCK/GIT_SSH_COMMANDsetzen. - Status: Optional (nur falls Container Git machen muss; sonst vermeiden).
đ§ âWelches Label nutze ich wann?â (Kurz-Navigation)
- Wir fahren âTTY-safe Ephemeral Relay + Brokered Runner + REAL Agent (host-only)â.
- Policy feinjustieren:
- Mehr Sicherheit: Host-only Mode + ACL-Relay.
- Mehr Convenience: Group-Relay (gitshare).
- Explizite Freigabe: Host-Approval Mode (Lock/Unlock oder FIDO2).
Wenn du willst, packe ich dir diese Liste als Block direkt in deine Doku â sag nur, ob deutsch/englisch und ob mit kurzen Pros/Cons je Eintrag.
Hereâs a crisp, security-first analysis (with ratings + recommendations) of every approach we touched, in clean English you can reuse with stakeholders.
Security Analysis & Recommendations
1) Threat model (assumptions)
- Adversary: other local users on the same host (or inside the same VM/container) with normal user privileges; accidental misuse; misconfiguration.
- Crown jewels: the private SSH key backing your GitHub identity.
- Goal: allow the host user to push from other usersâ working trees without distributing keys or granting durable signing capability.
2) Terminology (use these terms consistently)
- REAL agent: the only ssh-agent that actually holds the private key (host user).
Path:/run/gitshared/ssh-agent.sock - Relay: a UNIX socket forwarder (via
socat) that connects a client process to the REAL agent. No keys flow through; only signing requests. - Brokered runner:
/usr/local/bin/git-broker-runâ a tiny root-owned script you invoke viasudo (target user). It setsSSH_AUTH_SOCK/GIT_SSH_COMMANDfrom the relay argument and execs/usr/bin/git. - git_broker: the hostâs helper function that spins up an ephemeral relay and calls the runner as the target user for one Git command.
3) Approach-by-approach rating (Risk | Misuse | Complexity)
Legend: Risk = residual security risk; Misuse = chance users can push unintentionally; Complexity = operational cost.
(1 = Low/Best, 5 = High/Worst)
| # | Approach | Risk | Misuse | Complexity | Verdict |
|---|---|---|---|---|---|
| A | Direct Shared Agent (group-shared REAL socket) | 5 | 5 | 2 | Deprecated (blocked by OpenSSH cross-UID; insecure if bypassed) |
| B | Wrapper with shared SSH_AUTH_SOCK (run_as/git_as) | 5 | 4 | 2 | Deprecated (fails on modern OpenSSH; fragile) |
| C | Persistent Relay Service (âŠ/ssh-agent.relay always on) |
4 | 4 | 2 | Avoid (long exposure window; easy misuse) |
| D | Broker relying on sudo env_keep | 3 | 3 | 3 | Avoid (env filtering is distro/sudo dependent; brittle) |
| E | Group-Relay per command (relay 0660, group=gitshare) |
2 | 2â3 | 3 | OK (small race window inside group) |
| F | ACL-Relay per command (0600 + setfacl u:<user>:rw) |
1â2 | 1â2 | 3â4 | Good (least privilege, per-user scoping) |
| G | Host-Approval (agent lock, or FIDO2 touch required) | 1 | 1 | 3 | Great add-on (human presence gating) |
| H | Host-only (strict) (only host in gitshare, ACL relay) |
1 | 1 | 3â4 | Strong (no group race; precise access) |
| I | TTY-safe Ephemeral Relay + Brokered Runner (FINAL) | 1â2 | 1â2 | 3 | Recommended (short exposure; robust; no disconnects) |
What youâre running now (I): TTY-safe Ephemeral Relay + Brokered Runner + REAL agent â this scores low risk, low misuse, medium complexity (acceptable), and is our recommended operational baseline.
4) Deep dive â final recommended design (what makes it secure)
- Key isolation: The private key lives only in the REAL agent under the host user; no copies, no agent forwarding.
- Short exposure: The relay exists only for one command, then itâs removed. No persistent socket to abuse.
- No env fragility: The runner sets
SSH_AUTH_SOCK/GIT_SSH_COMMANDfrom the relay path; we do not depend onsudopassing environment through. - TTY-safe: Relay launched as a transient user service (
systemd-run --user) or as a disowned job (&!). No SSH session drops. - Policy enforcement: Only the host can create a usable signing path. Other users can run local Git (
status,log) but cannot push unless you broker that command.
Residual risk (and mitigations):
- Relay race within
**gitshare**: another member could touch the relay during the brief window.
â Keepgitshareminimal or use ACL-Relay per user (best). - Local DoS: someone holds the relay open while it exists.
â Minimal group; prefer ACL mode; keep relay lifetime tiny. - Host compromise: attacker controlling host account can sign.
â Use FIDO2 keys (touch required), shortssh-add -tlifetimes; monitorsudo/journal logs.
5) When to use each approach (pragmatic mapping)
- I: TTY-safe Ephemeral Relay + Runner (Recommended)
Multi-user hosts; you need to push from other usersâ working trees under your identity; strict control; auditability. - F/H: ACL-Relay + Host-only
Highest assurance. Use when group sharing is too broad. Each command gives exactly one user temporary rw on one socket. - G: Host-Approval (Lock / FIDO2)
Sensitive repos / production branches. Adds human presence control even if a relay is reachable. - E: Group-Relay per command
Small, trusted teams; convenience > strict least-privilege. Keepgitsharetiny, relay lifetime short. - C: Persistent Relay
Generally avoid. If you must (e.g., tightly controlled CI VM without other users), lock down ownership, firewall the machine, and rotate keys often. - A/B/D: Direct share, old wrappers, env_keep broker
Do not use in modern environments. Either broken by OpenSSH or too fragile/insecure. - Alternatives (not host identity)
- Deploy keys: service accounts, per-repo automation with non-personal identity.
- HTTPS + token: environments that block SSH or need narrow scopes.
- Container mount of agent: only when containers must push under host identity; bind-mount socket read-only, keep container users controlled.
6) Concrete recommendations (priority order)
-
Stay on the final model (I): TTY-safe Ephemeral Relay + Brokered Runner + REAL agent.
-
Tighten group: Prefer ACL-Relay per user or keep
gitshareto host + exactly the users you broker for.
(Best: host-only ingitshareand ACL-Relay; zero group race.) -
Harden
**/run/gitshared**:sudo chown blade34242:gitshare /run/gitshared sudo chmod 2750 /run/gitshared # owner rwx, group r-x, others --- -
No persistent relay services:
systemctl --user disable --now gitshare-agent-relay.service 2>/dev/null || true sudo rm -f /run/gitshared/ssh-agent.relay -
Keep sudoers precise (one line per target user, exact path):
blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run *If desired, restrict first arg (path prefix) for extra safety.
-
Consider FIDO2 (ed25519-sk) for production branches to enforce touch-per-signature.
-
Logging & review: watch
journalctl --userandsudologs forgit-broker-runcalls; rotate keys on schedule.
7) One-liners to verify posture (copy/paste)
# REAL agent present and key loaded
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l
# No persistent relay
ls -l /run/gitshared/ssh-agent.relay || echo "OK: no persistent relay"
# Shared dir hardened
namei -mo /run/gitshared
# Sudoers precise
sudo -l -U blade34242 | sed -n '1,200p' | grep -A1 git-broker-run
# Negative: target user alone cannot push
sudo -iu proxyuser bash -lc 'git ls-remote git@github.com:<owner>/<repo>.git || echo "EXPECTED: denied"'
# Positive: brokered command works
git_broker proxyuser /home/proxyuser ls-remote git@github.com:<owner>/<repo>.git | head
8) Executive summary (for slides/tickets)
- We centralized SSH identity in the hostâs REAL agent; other users never see the private key.
- We broker per-command Git operations via a tty-safe ephemeral relay and a sudo-whitelisted runner.
- Risk is low: no persistent socket, minimal group exposure (or ACL), strict SSH options, known-hosts pinned.
- For high-sensitivity repos, add FIDO2 touch or agent lock.
- Avoid legacy patterns (direct shared agent, persistent relay, env_keep brokers).
This gives you the vocabulary, the scoring, and the âwhen to use whatâ so you can defend the design and operate it confidently.
37) Current Architecture (final) â What it is now & why we switched
37.1 At a glance (what runs, when, and who can do what)
- REAL ssh-agent (host-only):
Runs under the host user (your Git identity), holds the private key. Socket:/run/gitshared/ssh-agent.sock. - Ephemeral relay (per command):
A short-livedsocatsocket is created only for the duration of a brokered Git command. Launched TTY-safe viasystemd-run --user(fallback: disowned&!), so your SSH session never drops. - Brokered runner (sudo-whitelisted helper):
/usr/local/bin/git-broker-runis invoked automatically by**git_broker**for each command, as the target user.
It forces Git to use the relay by setting per-callcore.sshCommandwithIdentityAgent=<relay>(and keepsStrictHostKeyChecking+ systemknown_hosts). We explicitly do not useIdentitiesOnlyhere, so the agent key can be offered across UIDs via the relay. - Access gate:
Groupâbased (gitshare) or ACL variant controls which local users can open the ephemeral relay socket during that brief window. - Sudoers:
A precise NOPASSWD rule allows the host to run only the runner as specific target users; target users themselves cannot use your identity.
Result: Only the host can âbrokerâ pushes/pulls in other usersâ directories. Target users keep full local Git (status/log), but cannot reach GitHub with your key unless you broker a command.
37.2 Why we switched (lessons learned from earlier attempts)
- Direct shared agent (group-shared REAL socket) â blocked by OpenSSH cross-UID guard â âcommunication with agent failedâ.
- Background relay tied to TTY (plain
nohup/setsid) â random SSH session disconnects due to job control / SIGHUP. - ENV-dependent broker (relying on
SSH_AUTH_SOCKpassed via sudo) â fragile across distros/sudo policy. **IdentitiesOnly=yes**under target UID â agent key not offered (no matching IdentityFile), ending with**Permission denied (publickey)**.- Persistent relay service â unnecessary exposure window (not least-privilege).
The final model fixes all of that:
- TTY-safe transient relay (no disconnects, no long exposure),
- runner enforces
IdentityAgent=<relay>per call (not fragile to ENV), - no
IdentitiesOnlyunder target UID (agent key can be offered), - strict SSH options + system
known_hosts, - precise sudoers scope.
37.3 Exact components (final, âknown goodâ)
- REAL agent (host):
/run/gitshared/ssh-agent.sock, key loaded (ssh-add -lshows your ED25519). - Shared dir perms:
/run/gitsharedis 2750 (owner=rwx,group=rx,others=---) â group can traverse; cannot plant files. - Relay (per command):
/run/gitshared/relay.<user>.<pid>.<rand>.sock, mode 0660, groupgitshare(or ACL to the single target user). - Runner:
/usr/local/bin/git-broker-run(root:root, 0755) setsgit -c core.sshCommand="ssh ⊠-o IdentityAgent=<relay> âŠ"and keepsStrictHostKeyChecking=yes+/etc/ssh/ssh_known_hosts. - Sudoers (host â target): one line per allowed user, e.g.
blade34242 ALL = (proxytest) NOPASSWD: /usr/local/bin/git-broker-run * - Broker function (host shell):
git_broker <user> <repo> <git-argsâŠ>creates relay, calls runner, cleans relay; nosudoin front (itsudos internally).
37.4 Operational roles & guarantees
- Host user can:
- broker
pull/push/fetchin any enabled target userâs repo, - keep the key centralized and audited,
- add/remove target users via sudoers (who you may broker for) and group/ACL (who may open the ephemeral relay).
- broker
- Target user can:
- do local Git freely,
- cannot push with your identity unless you broker that command (no persistent relay, no agent share).
- Security posture:
- minimal exposure window (relay exists only per command),
- strict SSH (no TOFU prompts, host key pinned),
- least privilege via group or ACL, and precise sudoers.
37.5 Onboarding a new local user (quick recipe)
- Sudoers: add one NOPASSWD line for that user (hostârunner as target).
- Access to relay: either put the user in
gitshareor use the ACL relay variant. New login required for group changes. - Repo state: target userâs directory is a Git repo with a correct SSH remote (
git@github.com:owner/repo.git). If unborn, make an initial commit. - Broker usage: from host, run e.g.
git_broker <user> /home/<user> ls-remote origin(read test), thenpull/push.
37.6 Common symptoms â cause â one-line fix
**Permission denied (publickey)**(brokered):
Cause: target user canât open relay, or Git not using relay.
Fix: ensure/run/gitsharedis 2750 and user has access (group/ACL); runner must setcore.sshCommandwithIdentityAgent=<relay>.- SSH session drops on broker call:
Cause: relay tied to your TTY.
Fix: usesystemd-run --user(or disowned&!) in broker. **sudo: a password is required**:
Cause: no sudoers line for that target user.
Fix: add the NOPASSWD runner rule for that user.**src refspec main does not match any**:
Cause: unborn branch.
Fix: create an initial (even empty) commit, then push with-u.
37.7 Why this design is the keeper
- Resilient: no dependency on volatile env; per-call enforcement ensures the right agent path every time.
- Safe by default: short-lived relay, strict SSH, host-only key custody.
- Operable: clear knobs (sudoers + group/ACL), deterministic logs (runner prints what it uses), and no surprise disconnects.
Mental model:
You rungit_broker âŠ. It briefly builds a bridge (relay), flips a switch (runner) that makes Git use that bridge, does the work, and then tears the bridge down. Your key never leaves the host agent.
38) Appendix â All Functions, Scripts & Config (final, copy-paste ready)
Use these exact blocks in your doc. They reflect the current, working setup (TTY-safe ephemeral relay + brokered runner, no IdentitiesOnly).
38.1 Host shell function â git_broker (group mode, TTY-safe)
Where: host userâs ~/.zshrc
What it does: creates a short-lived relay socket, calls the runner as the target user, then cleans up. Uses systemd-run --user when available to
avoid SSH disconnects.
# git_broker <targetUser> <repoDir> <git args...>
git_broker() {
emulate -L zsh
setopt localoptions no_monitor
local tgt="${1:?target user}"
local repo="${2:?repo dir}"
shift 2
local REAL="/run/gitshared/ssh-agent.sock"
local RELAY="/run/gitshared/relay.${tgt}.$$.$RANDOM.sock"
# shared dir: owner rwx, group r-x (no group write)
install -d -m 2750 -o "$USER" -g gitshare /run/gitshared
rm -f "$RELAY"
local used_unit=""
if command -v systemd-run >/dev/null 2>&1; then
used_unit="git-relay-${tgt}-$$-$RANDOM"
systemd-run --user --unit="$used_unit" --collect \
-p RuntimeMaxSec=300 -p KillMode=process \
-p StandardOutput=null -p StandardError=null \
/usr/bin/socat UNIX-LISTEN:"$RELAY",fork,group=gitshare,mode=0660 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1
else
setsid /usr/bin/socat UNIX-LISTEN:"$RELAY",fork,group=gitshare,mode=0660 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1 &!
fi
# wait for relay
for _ in {1..200}; do [[ -S "$RELAY" ]] && break; sleep 0.05; done
[[ -S "$RELAY" ]] || { [[ -n "$used_unit" ]] && systemctl --user stop "$used_unit" >/dev/null 2>&1 || true; echo "relay did not appear"; return 1; }
# call runner AS target user (first two args must be repo, relay)
sudo -n -u "$tgt" /usr/local/bin/git-broker-run "$repo" "$RELAY" "$@"
local rc=$?
rm -f "$RELAY" || true
[[ -n "$used_unit" ]] && systemctl --user stop "$used_unit" >/dev/null 2>&1 || true
return $rc
}
Usage (examples):
git_broker proxytest /home/proxytest status
git_broker proxytest /home/proxytest ls-remote origin | head
git_broker proxytest /home/proxytest pull
git_broker proxytest /home/proxytest push -u origin main
38.2 Runner â /usr/local/bin/git-broker-run (root:root, 0755)
Where: /usr/local/bin/git-broker-run
What it does: runs one Git command as the target user, forcing Git to use the relay (-c core.sshCommand="ssh ⊠-o IdentityAgent=<relay> âŠ").
Note: we intentionally do not set IdentitiesOnly.
#!/usr/bin/env bash
set -euo pipefail
dir="${1:?need repo dir}"; shift
relay="${1:?need relay socket}"; shift || true
# visible debug so you can see what arrives
echo "[runner] EUID=$(id -un) dir=$dir" >&2
echo "[runner] relay=$relay args=$*" >&2
# show relay visibility
if [[ -S "$relay" ]]; then
ls -l "$relay" >&2 || true
(command -v getfacl >/dev/null 2>&1 && getfacl -p "$relay" >&2) || true
else
echo "[runner] relay socket NOT FOUND at start" >&2
fi
cd "$dir"
# allow agent keys via relay (env path)
export SSH_AUTH_SOCK="$relay"
# per-call SSH override (no IdentitiesOnly)
sshcmd=(ssh
-o IdentityAgent="$relay"
-o StrictHostKeyChecking=yes
-o UserKnownHostsFile=/etc/ssh/ssh_known_hosts)
# one quick probe (non-interactive) for diagnostics
echo "[runner] ssh probe via relay ..." >&2
/usr/bin/ssh -T "${sshcmd[@]:1}" git@github.com </dev/null || true
echo "[runner] ssh probe done" >&2
# force Git to use the relay for THIS call
exec /usr/bin/git -c core.sshCommand="${sshcmd[*]}" "$@"
38.3 Sudoers (host â target users)
Where: /etc/sudoers.d/git-broker (edit via visudo -f /etc/sudoers.d/git-broker)
What it does: allows the host user to run only the runner as specific target users without password.
# Host may run only this one program as these users, no password
blade34242 ALL = (proxyuser) NOPASSWD: /usr/local/bin/git-broker-run *
blade34242 ALL = (proxytest) NOPASSWD: /usr/local/bin/git-broker-run *
# add more target users as needed:
# blade34242 ALL = (proxy) NOPASSWD: /usr/local/bin/git-broker-run *
38.4 One-time system prep (idempotent)
Run as host user (may prompt for sudo password once):
# group for relay access (if not present)
sudo groupadd -f gitshare
# secure shared dir: traverse for group, no group-write
sudo install -d -m 2750 -o "$USER" -g gitshare /run/gitshared
# add target users to group (re-login required for them)
sudo usermod -aG gitshare proxyuser
sudo usermod -aG gitshare proxytest
# pin GitHub host key system-wide (avoids TOFU prompts)
ssh-keyscan github.com | sudo tee -a /etc/ssh/ssh_known_hosts >/dev/null
# start REAL agent (once per boot or via your own login script)
export SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock
pgrep -a ssh-agent >/dev/null || eval "$(ssh-agent -a "$SSH_AUTH_SOCK")"
ssh-add -l >/dev/null 2>&1 || ssh-add ~/.ssh/id_ed25519
38.5 (Optional) ACL mode variant (per-user relay, no group membership)
If you prefer least privilege per call (no gitshare group), keep the runner as-is and add a second broker function that grants access with an ACL to the
specific target user:
# git_broker_acl <targetUser> <repoDir> <git args...>
git_broker_acl() {
emulate -L zsh
setopt localoptions no_monitor
local tgt="${1:?target user}"
local repo="${2:?repo dir}"
shift 2
local REAL="/run/gitshared/ssh-agent.sock"
local RELAY="/run/gitshared/relay.${tgt}.$$.$RANDOM.sock"
install -d -m 2750 -o "$USER" -g gitshare /run/gitshared
rm -f "$RELAY"
local used_unit=""
if command -v systemd-run >/dev/null 2>&1; then
used_unit="git-relay-${tgt}-$$-$RANDOM"
systemd-run --user --unit="$used_unit" --collect \
-p RuntimeMaxSec=300 -p KillMode=process \
-p StandardOutput=null -p StandardError=null \
/usr/bin/socat UNIX-LISTEN:"$RELAY",fork,mode=0600 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1
else
setsid /usr/bin/socat UNIX-LISTEN:"$RELAY",fork,mode=0600 \
UNIX-CONNECT:"$REAL" >/dev/null 2>&1 &!
fi
for _ in {1..200}; do [[ -S "$RELAY" ]] && break; sleep 0.05; done
[[ -S "$RELAY" ]] || { [[ -n "$used_unit" ]] && systemctl --user stop "$used_unit" >/dev/null 2>&1 || true; echo "relay did not appear"; return 1; }
# grant only this target user rw to the relay
command -v setfacl >/dev/null 2>&1 && setfacl -m u:"$tgt":rw "$RELAY" || true
sudo -n -u "$tgt" /usr/local/bin/git-broker-run "$repo" "$RELAY" "$@"
local rc=$?
rm -f "$RELAY" || true
[[ -n "$used_unit" ]] && systemctl --user stop "$used_unit" >/dev/null 2>&1 || true
return $rc
}
Sudoers bleibt identisch. In ACL-Mode brauchst du die Ziel-User nicht in gitshare.
38.6 Quick health checks (paste into your doc)
# REAL agent & key
SSH_AUTH_SOCK=/run/gitshared/ssh-agent.sock ssh-add -l
ssh -T -o IdentityAgent=/run/gitshared/ssh-agent.sock git@github.com
# Directory perms
namei -mo /run/gitshared # expect: drwxr-sr-x (2750) owner=host, group=gitshare
# Sudoers scope
sudo -l -U blade34242 | grep -A1 git-broker-run
# Broker read test (should print refs; runner logs visible at top)
git_broker proxytest /home/proxytest ls-remote origin | head
# First push (if unborn)
git_broker proxytest /home/proxytest checkout -B main
git_broker proxytest /home/proxytest commit --allow-empty -m "Initial commit"
git_broker proxytest /home/proxytest push -u origin main
38.7 What changed vs. early attempts (one paragraph for your doc)
We switched from a direct shared agent (blocked by OpenSSH cross-UID restrictions) and a persistent relay (unnecessary exposure + TTY disconnect issues)
to a per-command TTY-safe relay launched by the host and a runner that forces Git to use the relay via core.sshCommand (no reliance on
environment). We also removed IdentitiesOnly for target users so the agent key is actually offered via the relay. The result is stable pushes, no
session drops, and least-privilege access: the key stays with the host; the relay exists only for the duration of a single brokered command.