4 Punkte von GN⁺ 2024-06-24 | 1 Kommentare | Auf WhatsApp teilen
  • Da sudo und doas auf setuid-Binärdateien und Privilegieneskalation angewiesen sind, handelt es sich hier um ein Experiment, bei dem ein lokaler sshd an einen Unix-Domain-Socket gebunden wird und die Ausführung von Root-Befehlen übernimmt
  • Ziel ist, dass nur autorisierte Benutzer Root-Befehle ausführen können, ohne der gesamten Benutzersitzung dauerhaft die Fähigkeit zur Privilegieneskalation zu geben
  • Die Implementierung besteht aus einer nur für Root bestimmten SSH-Schlüsseldatei, dem zugriffsbeschränkten /run/sshd/sshd.sock und einer separaten sshd-Instanz mit den Optionen AuthorizedKeysFile und PermitRootLogin=yes
  • Da ssh keine Option hat, um einen bestehenden Socket direkt zu übergeben, wurden zunächst ProxyCommand und socat verwendet; später kamen ProxyUseFdpass und ein kurzes Python-Skript hinzu, um den Socket-Dateideskriptor zu übergeben
  • Der Ansatz funktioniert, und die Sicherheitsbehandlung stützt sich hauptsächlich auf OpenSSH; für den täglichen Einsatz ist es jedoch sinnvoller, passfd.py in eine kleine ausführbare Datei zu verwandeln und den gesamten ssh-Befehl in einen Wrapper zu packen

Strukturelle Einschränkungen von sudo und doas

  • sudo und doas sind auf setuid-Binärdateien und Privilegieneskalation angewiesen, um Befehle als Root auszuführen
  • Dieses Design hat einige Grenzen
    • Die gesamte Benutzersitzung muss die Fähigkeit behalten, Privilegieneskalation durchzuführen
    • Es funktioniert nicht, wenn die komplette Benutzersitzung in einem eingeschränkten User-Namespace läuft
    • setuid-Binärdateien schränken die systemweite Sicherheitskonfiguration ein
  • s6-sudod ist eine Alternative, die Programme in einen privilegierten Server und einen unprivilegierten Client aufteilt

Ziel des Experiments

  • Lokal ssh verwenden, um eine ähnliche Rolle wie sudo zu übernehmen, ohne die betreffende sshd-Instanz dem Netzwerk auszusetzen
  • Es gibt zwei Kernbedingungen
    • Nur autorisierte Benutzer dürfen Befehle als Root ausführen
    • Es wird keine Privilegieneskalation verwendet

Konfiguration eines sshd für lokale Root-Logins

  • Einen dedizierten SSH-Schlüssel nur für die Root-Authentifizierung erzeugen und ihn nicht in authorized_keys, sondern in /root/.ssh/local_keys speichern
  • Eine separate sshd-Instanz an einen Unix-Domain-Socket binden und die Berechtigungen von /run/sshd/ so einschränken, dass unautorisierte Benutzer nicht auf den Socket zugreifen können
  • Das Ausführungsbeispiel erstellt mit s6-ipcserver /run/sshd/sshd.sock und übergibt sshd dabei die folgenden Optionen
    • AuthorizedKeysFile=/root/.ssh/local_keys
    • PermitRootLogin=yes
  • /etc/ssh/sshd_config wird nicht geändert
    • Denn beim bestehenden, ans Netzwerk gebundenen sshd soll Root-Login nicht erlaubt sein
    • In der vorhandenen Konfiguration bleibt PermitRootLogin no bestehen

Sperrung des Root-Kontos und Umgang mit Passwort-Login

  • Das Root-Konto war vollständig gesperrt, sodass keine Login-Methode funktionierte; dies geschah durch ein ! vor dem Passwort-Hash
  • sshd interpretiert dieses Präfix ! als Kontosperre und erlaubt deshalb keinen Root-Login
  • Der Root-Passwortwert in /etc/passwd wird geändert, indem ! durch * ersetzt wird
    • sshd interpretiert * nicht als speziellen Sperrwert
    • * passt zu keinem Passwort-Hash, daher bleibt Passwort-Login praktisch deaktiviert
  • Zusätzlich ist in sshd_config PasswordAuthentication no gesetzt; es ist sicher, passwortbasierte Authentifizierung in sshd separat abzuschalten

Verbindung zu einem Unix-Socket mit ssh

  • sshd hat ein -i-Flag, mit dem ein bestehender Socket übergeben werden kann, aber ssh besitzt kein entsprechendes Flag
  • Zunächst wurde mit ProxyCommand und socat eine Verbindung zu /run/sshd/sshd.sock hergestellt
  • Der Verbindungsbefehl besteht aus den folgenden Elementen
    • ProxyCommand='socat STDIO UNIX-CONNECT:/run/sshd/sshd.sock'
    • Den dedizierten Root-Schlüssel mit -i .ssh/root-key.pub angeben
    • Verbindung zu root@root
    • In das aktuelle Verzeichnis wechseln und dann eine Login-Shell starten
  • Der verwendete SSH-Schlüssel war ein hardwaregebundener Schlüssel, daher musste zur Bestätigung der Verbindung auf ein physisches Gerät getippt werden
  • Alternativ wäre auch ein ssh-agent möglich, der den Schlüssel nur nach expliziter Bestätigung freigibt, zum Beispiel hissh-agent

socat-Overhead und ProxyUseFdpass

  • socat liest alle Eingaben von ssh und schreibt sie dann an den Socket, wodurch der Verbindungs-Overhead effektiv doppelt anfällt
  • ProxyUseFdpass sorgt dafür, dass ein Befehl den Socket-Dateideskriptor über stdout an ssh sendet und ssh dann über diesen Socket verbindet
  • Auf Basis eines Beispiels zur Verwendung von OpenSSH ProxyUseFdpass aus dem Jahr 2016 wurde das Python-Skript passfd.py geschrieben
    • Verbindung zum Unix-Domain-Socket /run/sshd/sshd.sock
    • Übergabe des Dateideskriptors mit sendmsg und SCM_RIGHTS
  • Danach bestand der Verbindungsbefehl darin, passfd.py in ProxyCommand anzugeben und ProxyUseFdpass=yes hinzuzufügen
  • nc -FU /run/sshd/sshd.sock scheint ebenfalls möglich, aber laut Handbuch darf -F nicht zusammen mit -U verwendet werden

Fazit und Form für den Praxiseinsatz

  • Diese Technik funktioniert, und die sensible Sicherheitsbehandlung wird weitgehend OpenSSH überlassen
  • OpenSSH hat eine gute Bilanz und unterstützt mehrere Authentifizierungsmethoden, darunter hardwarebasierte SSH-Schlüssel
  • Die Einrichtung auf einem neuen Host erfordert keine komplexen Schritte, und der ipcserver-Befehl kann über den System-Service-Manager ausgeführt werden
  • passfd.py ist eher ein schneller Hack für das Experiment
  • Für den täglichen Einsatz ist es besser, eine kleine ausführbare Datei mit derselben Funktion in /usr/local/bin abzulegen und auch den gesamten ssh-Befehl in einen kleinen Wrapper zu packen

1 Kommentare

 
GN⁺ 2024-06-24
Meinungen auf Hacker News
  • Der wichtigste Einwand gegen diesen Ansatz ist die höhere Komplexität. Statt eines einzelnen suid-Binaries, das eine Konfigurationsdatei liest und exec() aufruft, gibt es nun ein Binary, das als root läuft und auf einem UNIX-Socket lauscht, sowie ein weiteres Binary, das mit diesem Socket spricht; beide müssen obendrein asymmetrische Kryptografie verarbeiten.
    Wenn das Kernargument gegen sudo/doas lautet: „Es gibt ein für alle Nutzer zugängliches suid-Binary, und wenn es einen Bug hat, kann es zur Rechteausweitung missbraucht werden“, dann kann man es so machen:
    chgrp wheel /usr/bin/sudo
    chmod o-rwx /usr/bin/sudo
    Wenn man sudo-Nutzer in die Gruppe wheel aufnimmt, können nur sudo-berechtigte Nutzer die Datei-Bytes von der Platte lesen und sie auch ausführen. In Bezug auf Zugriffskontrolle ist die Sicherheit fast dieselbe wie beim sshd-Ansatz, bei dem nur wheel-Nutzer auf den UNIX-Socket zugreifen können, die Komplexität ist aber deutlich geringer.
    Außerdem kann der sshd-Ansatz den root-Zugriff nicht wie sudo auf bestimmte Befehle beschränken; selbst wenn es also einen Bug gäbe, der sudos Befehlsbeschränkungen umgeht, würde er nicht mehr Rechte gewähren als der sshd-Ansatz.
    Wenn man befürchtet, dass der Paketmanager die Rechte von /usr/bin/sudo kaputtmacht, kann man sie regelmäßig per cron korrigieren lassen oder sudo komplett entfernen und aus den Quellen manuell an einem anderen Ort installieren. Dann muss man Wartung und Upgrades natürlich selbst übernehmen.

    • Persönlich verfolge ich mit etckeeper alle Änderungen an /etc. Ob Softwareinstallation/-Upgrade oder menschliche Änderung: alles wird protokolliert. Auch beim Upgrade auf ein neues Release ist es nützlich, weil man aus den lokalen Änderungen Patches erstellen, sie auf eine saubere Installation anwenden und Konflikte per 3-way Merge prüfen kann.
      https://etckeeper.branchable.com/
    • sudo-Nutzer in die Gruppe wheel aufzunehmen, sodass nur sudo-berechtigte Nutzer die Datei lesen und ausführen können, ist ziemlich vernünftig. Ich frage mich, warum das nicht überall die Standardeinstellung ist.
    • Verzeiht meine Unwissenheit. Ich würde gern erklärt bekommen, was die wheel-Gruppe ist und was sie tut. Mir ist klar, dass das eine komplexe Debatte lostreten kann.
    • Könnte man /usr/bin/sudo nicht auch immutable machen? Das würde wohl helfen, zu verhindern, dass der Paketmanager daran herumfummelt.
    • Für den Teil, dass man den root-Zugriff nicht auf bestimmte Befehle beschränken kann, gibt es die ForcedCommand-Infrastruktur.
  • Ist das nicht genau das, was systemd run0 jetzt macht? systemd hat ein neues Tool namens run0, aber eigentlich ist es nicht völlig neu: Ruft man das schon lange existierende systemd-run über einen symbolischen Link namens run0 auf, verhält es sich wie ein sudo-Ersatz.
    Der wesentliche Unterschied ist, dass es tatsächlich nicht SUID ist. Es bittet den Service-Manager, einen Befehl oder eine Shell mit der UID des Zielnutzers auszuführen, weist ein neues PTY zu und leitet Daten zwischen dem ursprünglichen TTY und diesem PTY hin und her.
    Anders gesagt: Der Zielbefehl erbt nicht den Kontext des Clients, sondern läuft in einem isolierten Ausführungskontext, der neu von PID 1 geforkt wurde. $TERM wird weitergereicht, aber das ist eine explizite Ausnahme – eher eine Allowlist als eine Blocklist.
    In vielerlei Hinsicht ähnelt run0 im Verhalten eher ssh als sudo.
    https://mastodon.social/@pid_eins/112353324518585654

    • Warum muss systemd alles neu erfinden?
  • Übersehe ich etwas? Ich sehe nicht, inwiefern ein SSH-Login als root sicherer sein soll als die Nutzung von sudo. Mir wurde immer beigebracht, das nicht zu erlauben, daher weiß ich auch nicht wirklich, wie gefährlich es tatsächlich ist.
    Hier scheint es Überlegungen zu geben, um Remote-Nutzer auszuschließen; um diesen Sicherheitsaspekt geht es mir also nicht.
    Es könnte mit Punkt 3 der Einschränkungen von sudo zusammenhängen, aber zumindest gegenüber Punkt 1 sehe ich keinen Vorteil.
    Ich verstehe, dass es ein Experiment ist, aber mir scheint das nicht weniger, sondern eher stärker verwundbar als sudo. Ein offener Socket-Proxy wirkt anfällig für Man-in-the-Middle-Angriffe.
    Trotzdem habe ich ein paar Techniken gelernt, die mit alten Tools möglich sind, und es war gut, etwas Neues gezeigt zu bekommen.

    • Das sudo-Binary ist suid root und ein privilegiertes Binary, das nicht vertrauenswürdigen Nutzern direkt ausgesetzt ist. Wenn in sudo intern etwas schiefläuft, wird die gesamte Umgebung des Nutzers zur Angriffsfläche und kann ausgenutzt werden.
      Der SSH-Ansatz exponiert kein suid-Binary, sondern nutzt die SSH-Netzwerkschicht. Er ist daher nicht weniger sicher als der Zugriff auf SSH über das Netzwerk, was als ziemlich sicher gilt.
    • Einer der großen Vorteile von sudo ist, dass man einzelne Befehle per sudo ausführen kann und dadurch die Auditierbarkeit erhöht, statt einfach so etwas wie sudo bash zu starten.
    • Mit SSH-Login als root ist im Artikel ein zusätzlicher SSH-Server gemeint, der auf einem Unix-Socket lauscht. Das übliche Threat Model beim Exponieren von root-Logins ins Internet muss hier nicht gelten.
    • Dieser Ansatz vergleicht theoretische Fehlkonfigurationen oder Schwachstellen, die es geben kann oder auch nicht, mit der neuen Angriffsfläche durch einen neuen Daemon.
      Auch dieser Daemon kann Fehlkonfigurationen oder Schwachstellen haben, und er reduziert mehrere nutzerbasierte Autorisierungsschichten auf eine einzige root-Ebene.
      Trotzdem scheint er als sicherer betrachtet zu werden. Aus einer vernünftigen Sicherheitsperspektive ist er nicht sicherer, sondern einfach nur anders.
    • Ich bin skeptisch gegenüber dem im verlinkten Beitrag beschriebenen Ansatz, aber bei der Aussage „direkter Remote-SSH-Login als root ist gefährlich“ steckt auch ein Stück Angst, Unsicherheit und Zweifel dahinter.
      Tatsächlich ist es streng genommen sicherer, sich bei Remote-SSH direkt als root anzumelden, als sich erst als Nutzer anzumelden und dann sudo zu verwenden.
      Wenn user@home per ssh zu root@server geht, ist root@server nur dann kompromittiert, wenn user@home kompromittiert wurde.
      Wenn sich user@home dagegen per ssh als user@server anmeldet und dann per sudo zu root@server wird, reicht es, wenn entweder user@home oder user@server kompromittiert ist, damit root kompromittiert wird. Insbesondere auf user@server läuft häufig andere Software, etwa Daemons oder cron-Jobs.
      Jemandem, der über so einen Weg erfolgreich infiziert hat, sollte man keine kostenlose Rechteausweitung auf root geben – und auch keine laterale Bewegung, wie sie wegen Passwortwiederverwendung häufig vorkommt.
      Das gilt natürlich nicht, wenn sudo ausschließlich im Modus für Allowlist-Befehle verwendet wird und keine Passwörter oder auf dem Remote-Host vollständig zugänglichen Zugangsdaten akzeptiert.
  • Was passiert, wenn ssh beim Booten nicht startet? In einer typischen Konfiguration hängt es, soweit ich mich erinnere, beim Start vom Netzwerk ab. Dann kann man sich auch an der Failsafe-Konsole nicht anmelden.
    Ich sehe nicht, was man gegenüber sudo oder su tatsächlich gewinnt. Statt setuid-Binaries zu vermeiden, lässt man einen Netzwerkdienst mit Root-Rechten laufen, auch wenn er nur an einen Socket gebunden ist.

    • Aus Sicht von jemandem mit einer ähnlichen Konfiguration: Auf meinem Haupt-Desktop mache ich es so. Wenn der absolute Worst Case eintritt, installiere ich das System neu, da alles gesichert ist.
      Was ist, wenn die SSD stirbt? Dann kann man sich ebenfalls nicht an der Failsafe-Konsole anmelden.
      In 30 Jahren Linux-Nutzung sind mir Festplatten sehr viel häufiger gestorben, als dass ein sshd-Daemon nicht gestartet wäre; als Verhältnis betrachtet ist das Division durch null.
      Wenn der sshd-Daemon des Betriebssystems zufällig nicht startet, würde ich das als Zeichen sehen, zu einem stabileren Betriebssystem zu wechseln.
      Was man gegenüber sudo oder su gewinnt, ist, dass lokale Privilege-Escalation-Exploits deutlich schwieriger werden.
    • Die Linux-Konsole, also das tty auf einem lokalen Display oder einem entfernten KVM sowie ttyS*-Geräte an seriellen Ports und IPMI SoL, verwenden weder sudo noch su.
      Solche Konsolen nutzen Programme wie getty oder einen Window Manager; das sind nicht-suid-Programme, die als root gestartet werden.
      Für Konsolen-Logins sollte man ein Root-Passwort gesetzt haben.
    • In meinem Fall nutze ich setuid/sudo wegen der Audit-Logs. Inzwischen betreibe ich kaum noch Multi-User-/Multi-Service-Boxen, und das meiste, was Multi-Tenant ist, läuft auf k8s, sodass man statt ssh den kubectl-Endpunkt verwenden kann.
      Wenn man sich anmelden kann, sorge ich dafür, dass man per setuid root werden kann. Bei einer k8s-Box ist das das Plattform-Infrastrukturteam, und der Zugriff auf die Services darauf läuft über den k8s-Berechtigungsanbieter.
      Aus Sicht des Plattform-Infrastrukturteams liegen Metriken und Logs ohnehin bereits außerhalb der Box, und wenn man irgendeinen Job oder Workflow auslösen muss, nutzt man eine Pipeline.
      Wenn sich trotzdem jemand einloggt und Root-Arbeiten erledigt, möchte ich ein Audit-Log haben.
      Mir fällt keine meiner Boxen ein, bei der jemand mit Login-Rechten nicht volle Root-Rechte hätte.
      Natürlich verstehe ich Fälle, in denen ein Dienst setuid nutzt; bei Diensten macht systemd setuid aber normalerweise, um Rechte abzusenken, nicht um sie zu erhöhen.
    • Wenn man Zugriff auf den Bootloader hat, kann man weiterhin systems.unit=emergency.target, init=/bin/bash, rd.break=pre-pivot setzen oder in eine Live-CD-Umgebung booten. Alle üblichen Notfall-Wiederherstellungsoptionen funktionieren.
      Für weniger gravierende Notfälle sehe ich auch keinen Grund, warum diese sshd-Instanz ans Netzwerk gebunden sein müsste.
  • Es ist etwas schade, dass nicht alle Nachteile dieses Ansatzes enthalten sind. sudo kann steuern, welche Gruppen welche Befehle ausführen dürfen, welche Argumente diese Befehle annehmen dürfen, ob das Erzeugen von Subshells erlaubt ist usw.
    Mit diesem Ansatz verliert man viel von dieser feingranularen Kontrolle und verlässt sich auf vertrauenswürdige Schlüssel, die schwerer zu verwalten sind als das Bearbeiten der sudoers-Datei.
    Wer sehen will, was sudo alles Erstaunliches kann, dem empfehle ich wirklich das Buch Sudo Mastery.

    • SSH kann mit ForceCommand ebenfalls einen Teil davon leisten, aber ich stimme zu, dass es nicht ebenso flexibel oder präzise ist.
  • Das ist eine ähnliche Idee wie run0 von Systemd: https://news.itsfoss.com/systemd-run0/.

    • Und run0 ist keine selbstgebastelte Übergangslösung. Es wurde auditiert und ist wahrscheinlich besser als etwas Selbstgebautes.
  • Eines der Probleme von ssh ist, dass Prozesserzeugung nicht Teil des Protokolls ist. Und weil es ein Remote-Protokoll ist, kann man keine lokalen Ressourcen an den Kindprozess weiterreichen.
    Deshalb kann man kein nullterminiert getrenntes Argument-Array übergeben, keine zusätzlichen File Descriptors weiterreichen und keine ausführbare Datei angeben.
    Stattdessen übergibt man der auf dem Server konfigurierten Shell einen einzelnen String. Dadurch braucht man Shell-Escaping und muss außerdem wissen, welche Shell auf der Serverseite läuft.
    Um SSH als echten sudo-Ersatz zu nutzen, bräuchte man als Erweiterung etwas, das posix_spawn nahekommt.

  • Stimme vollkommen zu. Ich mache etwas Ähnliches und habe es früher auch einmal in einem HN-Kommentar beschrieben.
    Mein Ansatz ist etwas anders. Als physische SSH-Konsole nutze ich eine dedizierte Maschine, die sich in einem privaten LAN befindet, getrennt von den anderen Maschinen im Haus. Es ist kein Managed Switch, sondern ein normaler Switch, mit Ethernet-Kabeln und ohne Trunking.
    Login ist nur per SSH möglich, und daran habe ich einen Yubikey gekoppelt.
    Der Desktop-PC hat seine eigene Firewall und erlaubt nur SSH-Traffic von der IP-/MAC-Adresse dieser SSH-Konsole. Das gilt nur in dem privaten LAN, das beide gemeinsam nutzen; über ein anderes physisches LAN kann der Desktop aufs Internet zugreifen.
    Der sshd-Daemon erlaubt nur Logins mit Public/Private Key und blockiert Passwort-Logins.
    Wenn ich root brauche, boote ich die „SSH-Konsole“. Weil auf der Maschine fast nichts läuft, ist sie sehr schnell hochgefahren. Ich logge mich ein, rufe mit Pfeil nach oben wieder die Zeile ssh root@... auf, drücke Enter und betätige den Yubikey.
    Diese SSH-Konsole und die Tastatur stehen auf dem Schreibtisch und sind immer in Reichweite.
    Ob iptables/nftables + sshd in einem privaten LAN, das zudem physisch von einem anderen privaten LAN getrennt ist, sicherer oder unsicherer ist als das sudo-Binary oder su, kann jeder selbst beurteilen.
    Wenn man nach dem „Warum“ fragt, würde ich antworten: „Weil ich es kann.“ Ich habe das vor so langer Zeit eingerichtet, dass ich mich nicht einmal mehr erinnere, wann es war. Wahrscheinlich habe ich vor etwa zwei Jahren begonnen, mit dieser Idee zu spielen, und nutze sie seitdem durchgehend. Es gab keinerlei Probleme.

    • Die konkrete Konfiguration ist zwar nicht völlig identisch, klingt aber ähnlich wie das Konzept eines Bastion Hosts.
  • Eine elegante Lösung für dieses Problem. Man muss Nutzer nicht wie Kinder behandeln, sollte aber zugleich mit vernünftigen Default-Einstellungen potenzielle Fehler vermeiden.
    Wenn man root braucht, kann man sich auf der Konsole als root anmelden, daher halte ich nicht einmal su für nötig. Dieser Ansatz kommt dem Login als root auf einem Konsolen-tty so nahe wie möglich.

    • Der Ansatz „Wenn man root braucht, soll man sich auf der Konsole als root anmelden“ hat zwei Probleme.
      Erstens müssen im Gegensatz zu sudo alle Nutzer das Root-Passwort kennen.
      Zweitens gibt es keine Möglichkeit zu auditieren, wer sich tatsächlich angemeldet und was getan hat, wenn sich alle einfach als root einloggen.
  • Ich habe vor 10 Jahren etwas Ähnliches ausprobiert. Den UNIX-Socket-Teil gab es nicht; ich ließ einen separaten sshd laufen, der nur auf localhost lauschte, und musste mich auch nicht mit SCM_RIGHTS befassen.
    Es gab weder besonders gute noch schlechte Ergebnisse; irgendwann verlor ich einfach das Interesse und übertrug die Einrichtung nicht auf den nächsten Rechner.