- Shell 82.6%
- Nix 11%
- Jinja 6.4%
iroh — SSH ohne offenen Port, mit hartem NodeID-Filter
SSH zu einem Server ohne offenen SSH-Port / ohne öffentliche IP-Erreichbarkeit, über einen eigenen iroh-Relay (TLS), abgesichert durch eine fail-closed NodeID-Allowlist auf iroh-Ebene plus SSH-Keys. Zwei unabhängige Schichten, zwei verschiedene Geheimnisse (iroh-Key des Geräts ⟂ SSH-Key).
Build & Quellcode des Binaries: Fork
pb/iroh-ssh(iroh 1.0 + NodeID-Allowlist). Dieses Repo enthält Setup, Bootstrap-Skript und Ansible zum schnellen Reproduzieren.📖 Schritt-für-Schritt für Client / Server / Relay / Ansible: siehe SETUP.md.
TL;DR — schnellstes Reproduzieren
Binary + Identität auf einer Maschine (druckt die iroh-ID):
curl -fsSL https://f.berz.dev/aiagent/iroh-ssh/latest/iroh-bootstrap.sh | sudo -E bash -s -- --role client
Server (nimmt iroh-ssh-Verbindungen NUR von erlaubten iroh-IDs an):
curl -fsSL https://f.berz.dev/aiagent/iroh-ssh/latest/iroh-bootstrap.sh | sudo bash -s -- \
--role server --relay https://iroh.hcnbg1.berz.systems:8443 \
--allow <iroh-ID-client-1> --allow <iroh-ID-client-2>
SSH-Pubkeys legst du selbst in authorized_keys ab; --allow regelt nur die iroh-Ebene.
Server OHNE Allowlist (offen — jede iroh-ID, die die Node-ID kennt, darf rein):
curl -fsSL https://f.berz.dev/aiagent/iroh-ssh/latest/iroh-bootstrap.sh | sudo bash -s -- \
--role server --relay https://iroh.hcnbg1.berz.systems:8443 --no-allowlist
--no-allowlist lässt --allow-file im Service weg (allowlist.rs: Filter ist nur aktiv,
wenn --allow-file/--allow gesetzt ist). Dann schützt nur noch die SSH-Schicht
(authorized_keys / Passwort). Für Produktivziele die Allowlist bevorzugen.
⚠️
--role serverohne--allowund ohne--no-allowlistist fail-closed: die Allowlist ist aktiv aber leer ⇒ alle Verbindungen werden abgewiesen.
Allowlist an einem bereits laufenden Server nachträglich abschalten (ohne Re-Bootstrap):
sudo sed -i 's# --allow-file [^ ]*##' /etc/systemd/system/iroh-ssh-server.service
sudo systemctl daemon-reload && sudo systemctl restart iroh-ssh-server
Oder gezielt eine iroh-ID freischalten statt öffnen:
echo '<client-iroh-ID>' | sudo tee -a /etc/iroh-ssh/allowed_nodes
sudo systemctl restart iroh-ssh-server
OPNsense / FreeBSD (kein systemd, kein apt):
# OPNsense Shell — ohne Allowlist:
fetch -qo - https://f.berz.dev/aiagent/iroh-ssh/latest/opnsense-setup.sh | sh
# Mit Allowlist:
ALLOW="<iroh-ID>" fetch -qo - https://f.berz.dev/aiagent/iroh-ssh/latest/opnsense-setup.sh | sh
# Web-UI Tunnel (nach dem Setup):
iroh-ssh -L 8443:127.0.0.1:443 -N root@<NODE-ID> # → https://localhost:8443
Nutzt FreeBSD daemon(8) + RC-System statt systemd. Binary: native FreeBSD 14 ELF.
Details: bash/opnsense-setup.sh und pb/iroh-ssh → FREEBSD.md.
Komplettes 3-Knoten-Setup reproduzierbar (Server A + Relay B + Client C):
cd ansible
# inventory.ini (IPs/Bootstrap-Zugang) und group_vars/all.yml (Relay-Domain, E-Mail) anpassen
ansible-playbook -i inventory.ini site.yml
Architektur
C: Client/PC ──TLS/QUIC──▶ B: iroh-relay (Docker, LetsEncrypt, access = "everyone")
│ iroh (direkt ODER via Relay) ▲さんかく
└──────────────────────────────────────────────────┘
▼
A: Zielserver — iroh-ssh server (systemd, --persist, --relay-url, --allow-file)
sshd nur auf 127.0.0.1:22 (kein offener Port nach außen)
Zugriffskontrolle liegt pro Server, nicht am Relay:
- Server-Allowlist (
/etc/iroh-ssh/allowed_nodes): wer per SSH rein darf (Client-iroh-IDs). Fail-closed: aktiv + leer ⇒ alles abgewiesen, bevor ein Byte den sshd erreicht. Neuen Client freischalten = nur hier (+ SSH-Key) eintragen — kein zentraler Relay-Schritt. - SSH-Keys: der eigentliche Login-Riegel (
PasswordAuthentication no). - Relay = reiner Transport, offen (
relay_access_mode: everyone). Ein Fremder am Relay erreicht nur den Accept-Handler und wird dort vom NodeID-Filter abgewiesen (kein Zugriff). Optional härtbar:relay_access_mode: shared_tokenoderallowlist.
Repo-Inhalt
bootstrap/iroh-bootstrap.sh One-liner: Binary + persistente Identität (+ optional Server-Rolle)
ansible/ reproduzierbares 3-Knoten-Setup (A/B/C), idempotent
inventory.ini A (Zielserver), B (Relay), C (lokal)
inventory-iroh.ini A über iroh erreichen (ansible_host = A_ID)
group_vars/all.yml Versionen, Relay-Domain, Binary-URL (MinIO-Mirror)
site.yml + roles/ iroh_ssh_server / iroh_ssh_client / iroh_relay
bash/ dieselben Schritte als einfache Skripte
simulation/resilience-test.sh Ausfall-Szenarien (Reboot, Relay down, Reconnect)
KONZEPT.md ausführlicher Leitfaden + Sicherheits-Einordnung
NODEID_FILTER.md Design des NodeID-Filters (Fork)
UPDATE_RUNBOOK.md Fork aktuell halten: rebase -> tag -> rollout
Build- & Release-Kette (Fork pb/iroh-ssh)
- Code im Fork
pb/iroh-ssh(Basis:rustonbsd/iroh-ssh, gehoben auf iroh 1.0). git tag vX && git push --tags→ Woodpecker baut statisches musl-Binary → Forgejo-Release (Assetiroh-ssh.linux+.sha256).- Release wird auf MinIO gespiegelt (
f.berz.dev/aiagent/iroh-ssh/<tag>/...), damit Server/Clients es ohne mTLS percurl/get_urlladen. Build ist reproduzierbar (lokaler Build == CI-Asset, gleicher sha256). - Allowlist-Patch ist isoliert (
src/allowlist.rs+ 1 Hook) → Upstream-PR viaupstream-pr.patch/upstream-pr.mdim Fork. Updates: sieheUPDATE_RUNBOOK.md.
Relay (produktiv)
| Rolle | Wert |
|---|---|
| Relay | https://iroh.hcnbg1.berz.systems:8443 (Kubernetes hcnbg1, Wildcard-TLS, QUIC 7842/udp) |
Der Relay läuft als App im GitOps-Cluster (~/code/kubernetes, apps/iroh-relay, hcnbg1).
Der frühere Hetzner-Test-Aufbau (separater VM-Relay + Ziel-Server) wurde entfernt.
Im Test bewiesen: positiver Connect, negativer (nicht gelistete iroh-ID wird vor sshd
abgewiesen), fail-closed, Reboot-Recovery (gleiche iroh-ID), ansible -m ping über iroh.
Zweiten Relay aktivieren
DNS + Server bereitstellen, in group_vars/all.yml den zweiten relay_hosts-Eintrag
einkommentieren, ansible-playbook -i inventory.ini site.yml — der Server-Service
bekommt automatisch eine zweite --relay-url. Persistente Keys/IDs bleiben.