Dale Hagglund hat es genau getroffen. Also werde ich einfach das Gleiche sagen, aber auf eine andere Art und Weise, mit einigen Besonderheiten und Beispielen. ☺
Das Richtige in der Unix- und Linux-Welt ist:
- ein kleines, einfaches, leicht überprüfbares Programm zu haben, das als Superuser läuft und den lauschenden Socket bindet;
- ein weiteres kleines, einfaches, leicht überprüfbares Programm zu haben, das Privilegien abgibt und vom ersten Programm gestartet wird;
- den Kern des Dienstes in einem separaten dritten Programm zu haben, das unter einem Nicht-Superuser-Account läuft und vom zweiten Programm in Kettenschaltung geladen wird, in der Erwartung, einfach einen offenen Dateideskriptor für den Socket zu erben.
Sie haben eine falsche Vorstellung davon, wo das hohe Risiko liegt. Das hohe Risiko liegt im Lesen aus dem Netzwerk und im Handeln auf das Gelesene, nicht im einfachen Öffnen eines Sockets, dem Binden an einen Port und dem Aufruf von listen()
. Es ist der Teil eines Dienstes, der die eigentliche Kommunikation durchführt, der das hohe Risiko darstellt. Die Teile, die öffnen, bind()
, und sogar (bis zu einem gewissen Grad) der Teil, der listen()
aufruft, sind nicht das hohe Risiko und können unter der Ägide des Superusers ausgeführt werden. Sie verwenden und handeln nicht mit (mit Ausnahme der Quell-IP-Adressen im Fall von accepts()
) Daten, die unter der Kontrolle von nicht vertrauenswürdigen Fremden über das Netzwerk stehen.
Es gibt viele Möglichkeiten, dies zu tun.
accept()
Wie Dale Hagglund sagt, tut dies der alte “Netzwerk-Superserver” inetd
. Das Konto, unter dem der Dienstprozess ausgeführt wird, ist eine der Spalten in inetd
. Es trennt nicht den Teil des Abhörens und den Teil des Ablegens von Privilegien in zwei separate Programme, die klein und leicht überprüfbar sind, aber es trennt den Hauptdienstcode in ein separates Programm, inetd.conf
ed in einem Dienstprozess, den es mit einem offenen Dateideskriptor für den Socket erzeugt.
Die Schwierigkeit des Auditierens ist nicht so sehr ein Problem, da man nur das eine Programm auditieren muss. Das Hauptproblem von exec()
ist nicht so sehr das Auditing, sondern eher, dass es im Vergleich zu neueren Tools keine einfache, feinkörnige Kontrolle der Laufzeitdienste bietet.
UCSPI-TCP und daemontools
Die Pakete UCSPI-TCP und daemontools von Daniel J. Bernstein wurden entwickelt, um dies in Verbindung zu tun. Alternativ kann man auch Bruce Guenters weitgehend äquivalentes daemontoolss-encore Toolset verwenden.
Das Programm zum Öffnen des Socket-Dateideskriptors und zum Binden an den privilegierten lokalen Port ist inetd
, von UCSPI-TCP. Es erledigt sowohl das tcpserver
als auch das listen()
.
accept()
erzeugt dann entweder ein Dienstprogramm, das selbst Root-Rechte abgibt (weil das Protokoll, das bedient wird, beinhaltet, als Superuser zu starten und sich dann “anzumelden”, wie es z. B. der Fall ist bei, einem FTP- oder SSH-Daemon) oder tcpserver
, das ein eigenständiges kleines und leicht zu überprüfendes Programm ist, das nur die Rechte abgibt und dann zum eigentlichen Dienstprogramm weiterleitet (von dem also kein Teil mit Superuser-Rechten läuft, wie es z. B. bei setuidgid
der Fall ist).
Ein Dienst qmail-smtpd
-Skript wäre also zum Beispiel (dieses für dummyidentd zur Bereitstellung des Null-IDENT-Dienstes):
#!/bin/sh -e
exec 2>&1
exec \
tcpserver 0 113 \
setuidgid nobody \
dummyidentd.pl
nosh
Mein nosh-Paket ist dafür gedacht. Es hat ein kleines run
Dienstprogramm, genau wie die anderen. Ein kleiner Unterschied ist, dass es sowohl mit setuidgid
-artigen “LISTEN_FDS”-Diensten als auch mit UCSPI-TCP-Diensten verwendet werden kann, so dass das traditionelle systemd
-Programm durch zwei separate Programme ersetzt wird: tcpserver
und tcp-socket-listen
.
Wiederum spawnen Einzweck-Dienstprogramme und laden sich gegenseitig in Ketten. Eine interessante Eigenheit des Designs ist, dass man die Superuser-Rechte nach tcp-socket-accept
, aber noch vor listen()
abgeben kann. Hier ist ein Skript für accept()
, das tatsächlich genau das tut:
#!/bin/nosh
fdmove -c 2 1
clearenv --keep-path --keep-locale
envdir env/
softlimit -m 70000000
tcp-socket-listen --combine4and6 --backlog 2 ::0 smtp
setuidgid qmaild
sh -c 'exec \
tcp-socket-accept -v -l "${LOCAL:-0}" -c "${MAXSMTPD:-1}" \
ucspi-socket-rules-check \
qmail-smtpd \
'
Die Programme, die unter der Ägide des Superusers laufen, sind die kleinen, dienstagnostischen Kettenladeprogramme run
, qmail-smtpd
, fdmove
, clearenv
, envdir
und softlimit
. Zu dem Zeitpunkt, zu dem tcp-socket-listen
gestartet wird, ist der Socket geöffnet und an den Port setuidgid
gebunden, und der Prozess hat keine Superuser-Rechte mehr.
s6, s6-networking und execline
Die Pakete s6 und s6-networking von Laurent Bercot wurden entwickelt, um dies in Verbindung zu tun. Die Befehle sind strukturell sehr ähnlich zu denen von sh
und UCSPI-TCP.
smtp
-Skripte wären weitgehend identisch, bis auf die Ersetzung von daemontools
für run
und s6-tcpserver
für tcpserver
. Man könnte aber auch gleichzeitig das Toolset execline von M. Bercot verwenden.
Hier ist ein Beispiel für einen FTP-Dienst, leicht modifiziert von Wayne Marshalls Original , der execline, s6, s6-networking und das FTP-Server-Programm aus publicfile verwendet:
#!/command/execlineb -PW
multisubstitute {
define CONLIMIT 41
define FTP_ARCHIVE "/var/public/ftp"
}
fdmove -c 2 1
s6-envuidgid pubftp
s6-softlimit -o25 -d250000
s6-tcpserver -vDRH -l0 -b50 -c ${CONLIMIT} -B '220 Features: a p .' 0 21
ftpd ${FTP_ARCHIVE}
ipsvd
Gerrit Pape’s ipsvd ist ein weiteres Toolset, das nach dem gleichen Prinzip wie ucspi-tcp und s6-networking funktioniert. Die Werkzeuge heißen dieses Mal s6-setuidgid
und setuidgid
, aber sie tun dasselbe, und der risikoreiche Code, der das Lesen, Verarbeiten und Schreiben von Dingen erledigt, die über das Netzwerk von nicht vertrauenswürdigen Clients ist immer noch in einem separaten Programm.
Hier ist das Beispiel von M. Pape für die Ausführung von chpst
in einem tcpsvd
-Skript:
#!/bin/sh
exec 2>&1
cd /public/10.0.5.4
exec \
chpst -m300000 -Uwwwuser \
tcpsvd -v 10.0.5.4 443 sslio -v -unobody -//etc/fnord/jail -C./cert.pem \
fnord
fnord
run
, das neue Dienstüberwachungs- und Init-System, das in einigen Linux-Distributionen zu finden ist, ist dazu gedacht, das zu tun, was systemd
tun kann. Allerdings verwendet es keine Suite von kleinen, in sich geschlossenen Programmen. Man muss systemd
leider in seiner Gesamtheit prüfen.
Mit inetd
erstellt man Konfigurationsdateien, um einen Socket zu definieren, an dem systemd
lauscht, und einen Dienst, den systemd
startet. Die “unit”-Datei des Dienstes enthält Einstellungen, die eine weitgehende Kontrolle über den Dienstprozess ermöglichen, einschließlich des Benutzers, unter dem er läuft.
Wenn dieser Benutzer auf einen Nicht-Superuser eingestellt ist, erledigt systemd
die gesamte Arbeit des Öffnens des Sockets, des Bindens an einen Port und des Aufrufs von systemd
(und, falls erforderlich, von systemd
) im Prozess Nr. 1 als Superuser, und der von ihm gestartete Dienstprozess läuft ohne Superuser-Rechte.