Outline mit Token-Größe pro Abschnitt, dann gezielt den Teilbaum lesen — statt head oder grep auf Überschriftenzeilen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014Jmx1FL5pssYBKsCjj2pJr |
||
|---|---|---|
| .. | ||
| .env.agents | ||
| agent-env.zsh | ||
| AGENTS-claude.md | ||
| AGENTS-opencode.md | ||
| AGENTS.md | ||
| ensure-machine-agent | ||
| README.md | ||
Design: SSH-Key- und Git-Signing-Architektur (Arbeitsrechner mwiegand)
Stand 2026-08-04 — umgesetzt und verifiziert. Beschreibt das Warum hinter
dem Code in diesem Verzeichnis (agent-env.zsh, ensure-machine-agent,
.env.agents) plus ~/.ssh/config und ~/.gitconfig.
Umsetzungsstand, Historie und die offenen Schritte auf ISAC-Seite stehen im
ai-Repo: tasks/2026-07-31-agent-key-isolation/HANDOFF.md, Vorarbeit in
tasks/2026-06-13-isac-infra-agent-isolation/.
Problem
Auf dem Rechner laufen zwei Arten von Akteuren mit unterschiedlichem Vertrauensniveau:
- Ich (interaktive Shell) — brauche den persönlichen SSH-Key, der über LDAP auch auf Kundensystemen autorisiert ist.
- Agents (Claude/OpenCode via
agent-env) — sollen interne Systeme erreichen, aber strukturell nicht unbemerkt an den kundenfähigen Key kommen.
Vor dem Umbau war die Trennung rein konventioniert: der private Haupt-Key lag
unverschlüsselt als ~/.ssh/id_mwiegand auf der Platte (für jede Agent-Session
lesbar), der 1Password-SSH-Agent bot alle Keys jedem Prozess an, und ein
Login-Script lud den Haupt-Key zusätzlich in den Apple-Agent. Git-Signing hing
am Datei-Key.
Dazu die Komfort-Anforderung: Signieren darf im Alltag nicht nerven — keine Fingerabdruck-Freigabe pro Commit.
Anforderungen
- Agents verwenden einen eigenen SSH-Key (nur interne Systeme); der kundenfähige Haupt-Key ist für sie nicht still nutzbar.
- Jede Nutzung des Haupt-Keys erzeugt eine sichtbare, bewusste Freigabe (Fingerabdruck) — der Prompt ist die Schranke.
- Kein privater Schlüssel liegt als Datei auf der Platte.
- Git-Commits sind signiert, prompt-frei — Herkunft „diese Maschine" ist nachweisbar (Mensch/Agent unterscheiden die Commit-Trailer).
- Kein Autoload beim Login (explizite Entscheidung): was in einem Agent liegt, wurde bewusst dorthin gelegt.
Architektur: drei Schlüssel, drei Wege
| Schlüssel (1Password-Item, Vault Employee) | Fingerprint | Lebt in | Zweck | Prompt-Verhalten |
|---|---|---|---|---|
| SSH Key mwiegand (Haupt-Key) | SHA256:lIx1Na… |
nur 1Password | SSH überall, inkl. Kundensysteme | 1Password-Freigabe pro Key und Terminal-Session — bewusst spürbar |
| SSH isac agent (Agent-Key) | SHA256:77FiJM… |
geteilter Maschinen-ssh-agent ~/.ssh/isac-agent.sock |
SSH auf interne Systeme (User mwiegand-agent) |
einmal op read pro Boot, danach still |
| SSH Signing Key mwiegand (Signing-Key) | SHA256:SvHSJQ… |
geteilter Maschinen-ssh-agent ~/.ssh/isac-agent.sock |
Git-Signing aller Commits der Maschine — meine und Agent-Commits | einmal ensure-machine-agent pro Boot (passiert automatisch mit der ersten Agent-Session), danach still |
Die Auswahl des Schlüssels erfolgt über den Agent-Socket, nicht über Key-Dateien:
~/.ssh/configHost *→IdentityAgent ~/.1password/agent.sock(Haupt-Key, promptet pro Nutzung).Match exec USE_AGENT_SSH_USER=1→User mwiegand-agent+IdentityAgent ~/.ssh/isac-agent.sock(Ein-Key-Agent; steht vorHost *, nach den ckn-Host-Blöcken).- Git-Signing läuft über
SSH_AUTH_SOCK, der überall (interaktive Shell viaagent-env.zsh, Agent-Sessions zusätzlich via.env.agents) auf den Maschinen-Agent zeigt — signiert wird immer mit dem Signing-Key. Fürs interaktive SSH istSSH_AUTH_SOCKirrelevant, weil diessh_configIdentityAgentexplizit setzt.
Der Sicherheitskern: die Prompt-Semantik bleibt sauber. Ein 1Password-Prompt bedeutet immer „jemand will den kundenfähigen Key" — er kommt selten und ist ernst zu nehmen. Signing und Agent-SSH erzeugen nie Prompts, also gibt es keine Gewöhnung an reflexhaftes Bestätigen.
Komponenten
Alles Agent-bezogene liegt versioniert in diesem Verzeichnis; ~/.zshrc und
~/.local/bin enthalten nur noch Verweise darauf.
| Datei | Rolle |
|---|---|
agents/agent-env.zsh (gesourced von dotfiles/.zshrc) |
exportiert SSH_AUTH_SOCK fest auf ~/.ssh/isac-agent.sock; agent-env() ruft ensure-machine-agent und dann op run --env-file ~/.env.agents |
agents/ensure-machine-agent (Symlink in ~/.local/bin/) |
idempotent: startet den Maschinen-Agent bei Bedarf und stellt beide Keys sicher (Signing + Agent). Wird von agent-env gerufen; manuell nur nötig, wenn man nach einem Reboot committen will, bevor die erste Agent-Session lief |
agents/.env.agents (Symlink ~/.env.agents) |
rein deklarativ — op run parst die Datei nur, führt keinen Code aus: USE_AGENT_SSH_USER=1, SSH_AUTH_SOCK (Robustheit: Konsumenten ohne agent-env finden den richtigen Socket), BW_SSH_ARGS, GIT_CONFIG_* → user.signingkey = Signing-Pubkey (Env schlägt alle git-Configs, auch lokale), Secret-Referenzen |
~/.ssh/config |
Socket-Routing s. o.; keine IdentityFile-Einträge für persönliche Keys |
~/.gitconfig |
gpg.format=ssh, commit.gpgsign=true, user.signingkey = Signing-Pubkey-String (kein Pfad), gpg.ssh.allowedSignersFile |
~/.ssh/allowed_signers |
beide Pubkeys (Signing + Agent) für lokale Verifikation (git log --format=%G?) |
Gelöscht: ~/.ssh/id_mwiegand (unverschlüsselter Datei-Key),
add-ssh-key-to-agent (Login-Autoload des Haupt-Keys), lokale
user.signingkey-Overrides in 8 Repos.
Entscheidungen und verworfene Alternativen
Signing-Key als eigener Schlüssel statt Haupt-Key. Die Prompt-Frequenz hängt nicht am Schlüssel, sondern am Mechanismus — aber ein gemeinsamer Schlüssel hätte Haupt-Key-Prompts beim Committen alltäglich gemacht und die Schranke durch Gewöhnung entwertet. Außerdem: Forges kennen den Signing-Key nur als Signaturschlüssel; ein Leak ist kein Systemzugriff.
Ein gemeinsamer Signing-Key für Mensch und Agents (statt Agent-Commits
mit dem Agent-SSH-Key zu signieren, wie zuerst gebaut). Die Key-Trennung
hätte nur ehrliche Agent-Commits von meinen unterschieden: die
Agent-Sockets sind für jeden lokalen Prozess erreichbar, ein böswilliger
Agent könnte ohnehin als ich signieren. Für ehrliche Commits leistet der
Co-Authored-By-Trailer die Unterscheidung. Die Signatur bedeutet ehrlich
„kam von meiner Maschine"; dafür halbiert sich die Forge-Pflege (ein Key
registrieren/verifizieren statt zwei) und der Boot-Load läuft automatisch
über die erste Agent-Session mit. Ältere Commits (2026-08-04) sind noch mit
dem Agent-Key signiert — er bleibt dafür in allowed_signers.
op-ssh-sign erwogen und verworfen. Zuerst umgesetzt (Key verlässt
1Password nie), dann zurückgebaut: die 1Password-Autorisierung gilt global
pro App(+Terminal-Session) für alle Keys. Das Signing-Nerv-Problem hätte
sich nur durch Lockern des Modells lösen lassen — was die Haupt-Key-Schranke
mitgelockert hätte. Der Agent-Weg entkoppelt beides: Signing prompt-frei,
1Password-Modell bleibt streng. (Zwischenschritt Apple-Agent als
Signing-Halter wurde durch den Maschinen-Agent ersetzt — ein Agent weniger.)
macOS-Keychain als Signer verworfen. Die Keychain speichert Bytes, kann
aber keine SSH-Signaturen erzeugen (security bietet nur cms). Jeder
Keychain-Weg endet in einer Klartext-Tempdatei pro Commit — schlechter als
jeder Agent. (ssh-add --apple-use-keychain speichert nur Passphrasen und
setzt eine Key-Datei voraus.)
Kein Login-Autoload. Bewusste Entscheidung: Keys werden nicht beim Login
geladen, sondern lazy — mit der ersten Agent-Session (automatisch) bzw. per
ensure-machine-agent von Hand. Symptom eines fehlenden Loads ist ein
Commit-Fehler Couldn't find key in agent? — eindeutig und folgenlos.
Geteilter Agent statt Agent pro Session. Ein fester Socket
(~/.ssh/isac-agent.sock) statt mktemp pro Session: ein op read pro Key
und Boot statt pro Session, kein Cleanup verwaister Agents, und der
Match-Block kann auf einen stabilen Pfad zeigen (wirkt auch für Subprozesse
mit bereinigtem Env). Beide Bewohner (Agent-Key, Signing-Key) sind per
Definition die Schlüssel, die Agents zugänglich sein dürfen — Langlebigkeit
weicht nichts auf.
SSH_AUTH_SOCK zusätzlich fest in .env.agents. Lektion aus dem Umbau:
eine Shell mit veralteter agent-env-Funktion lud die neue .env.agents
(Signing-Config auf Agent-Key), zeigte aber noch auf den leeren Apple-Agent →
Couldn't find key in agent?. Signing-Config und Socket dürfen nicht
auseinanderfallen; beide stehen jetzt in derselben Datei.
Grenzen (bewusst offen)
Die Architektur ist eine Zustimmungs- und Erkennungsschranke, keine technische Unerreichbarkeit:
- Der 1Password-Socket bleibt für Agent-Prozesse erreichbar
(
allowAllUnixSockets: truein der Claude-Sandbox-Config; 1Password kann Keys nicht pro Client filtern). Ein Agent, der den Haupt-Key anfordert, löst aber einen sichtbaren Prompt aus, den ich ablehnen kann. op-CLI-Pfad (empirisch geprüft 2026-08-04): ein Agent kannop readauf beliebige Items versuchen — u. a. den Haupt-Key — aber nicht unbemerkt. Vier Versuche aus einer Agent-Session ergaben vier 1Password-Dialoge; die Freigabe für dasop runbeimagent-env-Start deckt nachgelagerteop-Aufrufe nicht mit ab (ob eine bestätigte Freigabe danach ein 10-Minuten-Fenster öffnet, ist unverifiziert — der Dialog behauptet das, gemessen wurde es nicht). Die verbleibende Schwäche ist die Blindheit des Dialogs: er sagt „full access to your account", nennt aber nicht das angefragte Item. Beim Bestätigen ist nicht unterscheidbar, ob ein Agent ein harmloses API-Passwort oder den kundenfähigen SSH-Key liest. Das ist systematisch, nicht nur eine Eigenschaft offener Abfragen: auchop readmit einer punktgenauenop://Employee/<item>/<feld>-Referenz zeigt denselben Dialog (geprüft 2026-08-04) — 1Password autorisiert das Werkzeug, nie den Zugriff, selbst wenn dieser vollständig spezifiziert ist. Gerade die Secrets in.env.agentssind ausschließlich solche Referenzen — und weil legitime Agent-Starts denselben Dialog erzeugen, trainiert der Alltag reflexhaftes Bestätigen. Genau das macht der gescopte Service-Account (Schritt 1) überflüssig: was nicht anfragbar ist, muss nicht beurteilt werden.- Vollständige Schließung = HANDOFF-Schritte 1/3: gescopter Service-Account mit eigenem Vault für den Agent-Key, Socket-Sperre in der Sandbox, Seatbelt-Variante für unsandboxte Kommandos.
Betrieb
- Nach Reboot: die erste
agent-env-Session lädt beide Keys automatisch; nur wer vorher committen will, ruft einmalensure-machine-agent(Symptom sonst:Couldn't find key in agent?). - Verifikation:
git log --format='%h %G? %GK'—G+SvHSJQ…für alle Maschinen-Commits (Commits vom 2026-08-04 noch mit77FiJM…).ssh-add -lin einer Agent-Session zeigt genau die zwei Maschinen-Keys. - Forges: der Signing-Pubkey muss bei Bitbucket DC/Forgejo als Signing Key registriert sein; bei Forgejo zusätzlich ownership-verifiziert (Challenge signieren, nur via Web-UI), sonst „unverified" (offen).
- Rotation: neuer Key = neues 1Password-Item + Pubkey in
~/.gitconfig/allowed_signers/.env.agentsbzw. ISAC (admin_agent_users) nachziehen; es gibt keine Key-Dateien zu tauschen.