Idee aus einer Denkpause, später umgesetzt
Letztes Jahr nach dem Sommerurlaub hatte ich – nicht ganz freiwillig – genug Abstand vom Tagesgeschäft, um grundlegende Fragen wieder ernsthaft zu betrachten, ohne den Druck, sofort eine Lösung bauen zu müssen.
Eine dieser Fragen war:
Wie trennt man auf NetBSD Prozesse sinnvoll voneinander, ohne UID-Mapping, mit mehr Sicherheit als ein reines
chroot, aber ohne gleich die großen Geschütze wie Xen-Virtualisierung aufzufahren?
Für Linux ist die Antwort meist klar: Docker, Podman, nerdctl – sauber gesprochen: OCI-Container auf Basis von Namespaces und cgroups.
Für NetBSD wollte ich etwas anderes: einen Ansatz, der sich stärker aus einem Guss anfühlt und zur bestehenden Architektur passt.
Der Vergleich mit FreeBSD Jails lag nahe: keine UID-Umschreibung, stattdessen eine explizite Trennung über eine zusätzliche Jail-ID im Prozesskontext.
FreeBSD Jails sind dabei nicht nur eine lose Inspiration, sondern der naheliegende Referenzrahmen: Sie zeigen seit vielen Jahren, dass glaubwürdige Prozess-Isolation ohne UID-Mapping und ohne Vollvirtualisierung möglich ist.
Gleichzeitig sind FreeBSD Jails tief mit der dortigen Kernel-Architektur verwoben (Prison-Strukturen, globale Jail-Kontexte, spezifische Prozessmodelle). Genau diese enge Kopplung macht sie für NetBSD nicht ohne Weiteres übertragbar.
Die Idee blieb zunächst liegen. Einige Monate später habe ich sie mit mehr Abstand und neuen Werkzeugen wieder aufgegriffen. In dieser Zeit habe ich mich intensiver mit KI-gestützter Entwicklung beschäftigt – privat für Prototypen, beruflich für spezialisierte Automatisierung. Das Ergebnis war spürbare Beschleunigung, ohne den Anspruch an Review und technische Sorgfalt aufzugeben.
Ein zusätzlicher Impuls kam, als ich gesehen habe, dass Jonathan Perkin mit Claude Code einen innovativen pkgsrc-Package-Builder namens „bob“ in Rust vorantreibt (Repository). Die Idee dahinter hatte ich selbst vor vielen Jahren schon einmal – sie war bei mir jedoch immer wieder liegengeblieben. Nicht, weil sie falsch oder zu groß gewesen wäre, sondern weil Zeit und Fokus begrenzt sind.
Zu sehen, dass solche Vorhaben heute mit KI-Unterstützung realistisch umsetzbar werden, war ein entscheidender Motivationsschub.
Bevor wir tiefer in Kernel-Hooks und Werkzeuge einsteigen, lohnt sich ein kurzer Schritt zurück: Was soll diese Lösung leisten – und was bewusst nicht?
Mir ging es nicht darum, einen weiteren Container-Stack zu bauen, sondern um eine Form von Prozess-Isolation, die:
- ohne UID-Mapping auskommt
- stärker ist als ein reines
chroot - sich in NetBSD architektonisch natürlich anfühlt
Dieses Zielbild bestimmt alle weiteren Designentscheidungen.
Das Ergebnis ist ein prototypischer Stack aus secmodel_jail, jailctl und jailmgr.
Der Ansatz ist bewusst keine 1:1-Portierung der FreeBSD Jails.
Nicht, weil deren Design grundsätzlich problematisch wäre, sondern weil es auf Annahmen beruht, die nicht zur NetBSD-Kernelarchitektur passen: globale Jail-Kontexte, eigene Prison-Hierarchien und ein stärker monolithischer Kontrollpfad.
Ein direkter Port würde entweder fremde Kernel-Strukturen etablieren oder das bestehende kauth/secmodel-Modell umgehen. Beides wäre architektonisch inkonsistent.
Motivation aus der Praxis: ein kleiner Host, viele Dienste
In der Praxis sah das bei mir auf NetBSD lange so aus:
Sobald auf einem kleinen, sparsamen Host mehrere Dienste sauber voneinander getrennt werden sollten, führte an Xen kaum ein Weg vorbei.
Der Grund ist banal, aber entscheidend: Ein klassisches chroot isoliert das Dateisystem, nicht jedoch die Prozesse. Sie sehen sich gegenseitig, können sich signalisieren (kill -9) und sich im schlimmsten Fall direkt beeinflussen. Für ernsthaften Betrieb ist das ein No-Go.
Die Konsequenz war ein vertrautes Bild: ein Zoo aus virtuellen Maschinen.
Eine VM für den Reverse Proxy, weitere für Mail-, Web-, Kalender-, Speicher- oder Git-Dienste – jeweils sauber isoliert, aber mit eigenem Init, Netzwerk-Stack und entsprechendem Overhead.
Mit dem richtigen Tooling ist das handhabbar.
Es bleibt jedoch schwergewichtig: höherer Ressourcenverbrauch, mehr operative Komplexität und Isolation nur über vollständige Virtualisierung.
Geht das auf NetBSD auch leichter?
Mehr Isolation als ein reines chroot, klare Prozessgrenzen – aber ohne für jeden Dienst eine eigene virtuelle Maschine betreiben zu müssen.
Verwandte Arbeiten: Isolation auf NetBSD
NetBSD hat bereits früh mit verschiedenen Isolationsansätzen experimentiert: gaols, sysjail oder Systrace haben gezeigt,dass Prozess-Isolation prinzipiell möglich ist, sind jedoch entweder sehr invasiv, schwer wartbar oder konzeptionell an ihre Grenzen gestoßen.
secmodel_jail versteht sich ausdrücklich nicht als Neuanfang auf leerer Fläche, sondern als Reduktion: weniger Mechanik, klarere Zuständigkeiten, und eine konsequente Nutzung des heutigen kauth-Modells.
| Arbeit / Projekt | Quelle | Kurzbeschreibung | Status |
|---|---|---|---|
| gaols (P3A, 2008) | https://2008.asiabsdcon.org/papers/P3A-paper.pdf | Jails-ähnliche Prozess-Isolation über kauth(9) (funktionale Hooks). |
Forschungsprototyp; nie integriert. |
| mult (P5A, 2008) | https://2008.asiabsdcon.org/papers/P5A-paper.pdf | Ressourcen-Isolation durch Instanziierung ganzer Kernel-Subsysteme. | Sehr invasiver Forschungsprototyp. |
| netbsd-sandbox | https://github.com/smherwig/netbsd-sandbox | Userland-Sandboxing via chroot und secmodel_sandbox, rlimits, Capabilities, Lua policies |
Hardening-Werkzeug, kein Jail-Modell. |
| Systrace / sysjail | https://man.netbsd.org/NetBSD-2.0/systrace.4 | Syscall-Interposition zur Prozess-Isolation; Basis für sysjail. | Deprecated / entfernt. |
Zielbild: Was die drei Bausteine gemeinsam liefern
Um den folgenden Stack gut einordnen zu können, hilft es, ihn nicht als lose Tool-Sammlung zu sehen, sondern als durchgängige Kette von Verantwortung – vom Kernel bis zum Betrieb.
Jede Ebene hat dabei eine klar abgegrenzte Rolle.
Kernel-Welt Userland Betrieb
─────────── ──────── ───────
[ secmodel_jail ] ──▶ [ jailctl ] ────▶ [ jailmgr ]
Regeln Hebel Ablauf
| Baustein | Ebene | Aufgabe |
|---|---|---|
secmodel_jail |
Kernel (secmodel) |
Jail-ID im Credential-Kontext, Policy-Hooks (Prozess/Netz), Sysctl-API |
jailctl |
Userland (usr.sbin) |
Erzeugen/Betreten/Listen/Zerstören von Jails, Attach/Exec-Workflows |
jailmgr |
Betrieb (Shell-Orchestrierung) | Base-Layer, Mount-Layout, Host-IP-Aliase, Multi-Jail-Start/Stop, optional NPF-RDR |
secmodel_jail liefert die Sicherheitssemantik,
jailctl die direkte Bedienoberfläche,
jailmgr die operative Standardisierung für den Alltag.
Damit dieses Zusammenspiel verständlich wird, müssen wir kurz einen Schritt tiefer gehen.
Der entscheidende Punkt ist nämlich nicht das Tooling, sondern wie NetBSD Sicherheitsentscheidungen im Kernel grundsätzlich trifft.
Exkurs: Was sind secmodel und kauth in NetBSD?
Für viele OS-Nutzer ist das der wichtigste Kontext:
kauthist NetBSDs allgemeines Framework für Autorisierungsentscheidungen im Kernel. Subsysteme fragen dort an („darf dieser Prozess X tun?“), und Listener beantworten/filtern das.secmodelist das modulare Sicherheitsmodell darüber: einzelne Security-Modelle können sich inkautheinklinken und Regeln definieren.secmodel_jailist in diesem Bild ein konkretes Sicherheitsmodell, das Prozess-Isolation über eine Jail-ID ergänzt.
Praktisch heißt das: Nicht ein einzelner Monolith entscheidet alles, sondern klar getrennte Policies je Scope (Prozess, Credential, Netzwerk).
1) Kernel-Fundament: secmodel_jail (Prototyp-Stand)
Der zentrale Designpunkt ist extrem klar: Alternativen wie globale Jail-Listen, prozessweite Flags oder separate Kernel-Subtrees wurden bewusst verworfen. Sie hätten zusätzliche Sonderpfade eingeführt und die Nachvollziehbarkeit von Sicherheitsentscheidungen verschlechtert.
Die Bindung an den Credential-Kontext hält die Semantik lokal, vererbbar und konsistent mit bestehenden Kauth-Entscheidungen.
Das heißt: Die Zugehörigkeit zur Jail hängt nicht an Dateisystemtricks, sondern am Credential-Kontext. Für Anwender übersetzt: Entscheidend ist nicht nur „wo der Prozess im Dateisystem startet“, sondern ein zusätzliches Sicherheitsattribut, das der Kernel bei Prüfungen berücksichtigt.
Bevor wir uns den ersten Code ansehen, ein kurzer Hinweis zum Lesemodus:
Sie müssen diesen Code nicht im Detail verstehen. Wichtig ist nur, wo die Jail-Zugehörigkeit gespeichert wird und wann der Kernel sie abfragt.
static jailid_t
secmodel_jail_cred_id(kauth_cred_t cred)
{
void *data;
data = kauth_cred_getdata(cred, jail_key);
return (jailid_t)(uintptr_t)data;
}
Und genauso direkt wird diese ID gesetzt:
static void
secmodel_jail_cred_setid(kauth_cred_t cred, jailid_t id)
{
kauth_cred_setdata(cred, jail_key, (void *)(uintptr_t)id);
}
Warum das wichtig ist
- Keine UID/GID-Umschreibungen nötig
- Policy-Entscheidungen bleiben im Kernel-konformen Kauth-Modell
- Vererbung über Credential-Copy ist explizit und nachvollziehbar
Und ganz praktisch: Prozesse in unterschiedlichen Jails sehen sich weniger und greifen weniger ineinander ein, obwohl sie auf demselben Host laufen.
euid 0, Jail-ID 0) ist bewusst als Ausnahme modelliert.
Das ist wichtig für Administration und Recovery.
Der Match für Prozess-Interaktionen zeigt die Semantik glasklar:
if (secmodel_jail_is_host_root(cred))
return true;
return secmodel_jail_cred_id(cred) ==
secmodel_jail_cred_id(p->p_cred);
2) Kontrollpfad im Kernel: Sysctl als API
Bis hierhin ging es ausschließlich um Kernel-Logik und Policy. Die nächste, ganz praktische Frage lautet daher: Wie wird diese Mechanik kontrolliert und benutzbar, ohne neue Spezial-ABIs einzuführen?
secmodel_jail stellt eine konkrete Steuerfläche unter security.models.jail bereit:
id(lesen/schreiben)create(schreiben)destroy(schreiben)list(lesen)
sysctl_createv(... CTLTYPE_INT, "id", ... secmodel_jail_sysctl_id ...);
sysctl_createv(... CTLTYPE_INT, "create", ... secmodel_jail_sysctl_create ...);
sysctl_createv(... CTLTYPE_INT, "destroy", ... secmodel_jail_sysctl_destroy ...);
sysctl_createv(... CTLTYPE_STRUCT, "list", ... secmodel_jail_sysctl_list ...);
Damit wird Userland-Integration einfach: ein Tool wie jailctl muss keine proprietäre Kernel-ABI sprechen, sondern nutzt den standardisierten Sysctl-Pfad.
Für Betriebsteams ist das angenehm, weil Sysctl ein bekanntes Interface ist: lesbar, skriptbar, gut debugbar.
EPERM.
3) Policy: Was tatsächlich isoliert wird
Nachdem klar ist, wie Jails erzeugt und verwaltet werden, schauen wir uns nun an, was diese Jail-ID im Alltag tatsächlich bewirkt. Erst hier wird sichtbar, welche Grenzen der Kernel wirklich zieht.
Prozesssicht / Signals
Im Kauth-Listener für KAUTH_SCOPE_PROCESS wird Sichtbarkeit und Signalzustellung auf gleiche Jail-ID begrenzt:
case KAUTH_PROCESS_CANSEE:
case KAUTH_PROCESS_SIGNAL:
p = arg0;
if (!secmodel_jail_match(cred, p))
return KAUTH_RESULT_DENY;
return KAUTH_RESULT_DEFER;
Konsequenz im Alltag: Ein Prozess in Jail A kann nicht mehr selbstverständlich Prozesse in Jail B sehen oder signalisieren.
Netzwerk-Bind auf Jail-IP
Ein besonders praxisnaher Isolationspunkt ist das Netzwerk.
Viele Sicherheitsprobleme entstehen nicht durch ausgefeilte Angriffe,
sondern durch einen unscheinbaren Fehler im Default-Verhalten:
Ein Dienst lauscht unbeabsichtigt auf allen Interfaces (0.0.0.0).
Gerade auf Hosts mit mehreren Jails oder Diensten kann das fatale Folgen haben: Ein Prozess wird plötzlich von Netzen erreichbar, für die er nie vorgesehen war.
Aus diesem Grund kann pro Jail optional eine explizit erlaubte IPv4-Bind-Adresse konfiguriert werden. Sobald diese gesetzt ist, gilt eine einfache Regel:
Ein Prozess darf nur auf genau diese Adresse binden – sonst nicht.
Technisch wird das über den Netzwerk-Kauth-Hook durchgesetzt: „bind any“ wird explizit verboten, und auch konkrete Bind-Requests werden gegen die konfigurierte Jail-IP geprüft.
if (req == KAUTH_REQ_NETWORK_BIND_ANYADDR)
return KAUTH_RESULT_DENY;
...
if (sin->sin_addr.s_addr != config.jc_bind4)
return KAUTH_RESULT_DENY;
Die Wirkung im Alltag ist bewusst unspektakulär, aber enorm wirksam: Selbst ein falsch konfigurierter Dienst kann nicht „aus Versehen“ auf allen Interfaces lauschen.
Damit wird Netzwerk-Exposition zu einer expliziten Entscheidung statt eines impliziten Defaults.
Ressourcengrenzen beim Eintritt
Beim Eintritt in die Jail können Limits gesetzt werden (RLIMIT_CPU, RLIMIT_AS), sofern in der Jail-Konfiguration hinterlegt:
error = dosetrlimit(l, p, RLIMIT_CPU, &lim);
...
error = dosetrlimit(l, p, RLIMIT_AS, &lim);
Das ist kein „Container-Orchestrator-Magie“, sondern bewusst ein nachvollziehbarer, kernelnaher Mechanismus.
Als Werkstatt-Stand ist wichtig: Die Mechanik ist absichtlich simpel gehalten, um Verhalten zuerst korrekt und prüfbar zu machen.
4) Userland-Werkzeug: jailctl
jailctl ist die direkte Bedienoberfläche auf die Sysctl-Mechanik.
Für Nutzer bedeutet das: kein großer Daemon nötig, keine komplexe Runtime-Schicht — ein klassisches Admin-Werkzeug mit klaren Subcommands.
Die Sysctl-Aufrufe sind 1:1 im Code sichtbar:
sysctlbyname("security.models.jail.create", &req, &len, &req, sizeof(req));
sysctlbyname("security.models.jail.destroy", NULL, 0, &id, sizeof(id));
sysctlbyname("security.models.jail.id", NULL, 0, &id, sizeof(id));
sysctlbyname("security.models.jail.list", entries, &len, NULL, 0);
UX-Details, die im Alltag zählen
create -n <name> -i <ipv4> -m <bytes> -c <cpu-ms>listzeigt ID, Prozessanzahl, Name, Rootexecundattachadressieren Jail via ID oder Name
printf("%-8s %-8s %-16s %s\n", "ID", "PROCS", "NAME", "ROOT");
Zusätzlich verwaltet jailctl einen Kontext unter /var/run/jailctl und koppelt Attach-Sessions über Unix-Domain-Sockets.
Das ergibt einen robusten „detach/attach“-Workflow ohne zusätzliche Daemons.
Gerade beim Testen ist das wertvoll: Jail starten, später wieder anhängen, Prozesszustand nachvollziehen.
openpty + Manager-Prozess + Unix-Socket = saubere Interaktivität,
auch wenn die Jail detached gestartet wurde.
5) Betriebslayer: jailmgr
Bis hierhin ging es vor allem um Mechanik: Jails erstellen, betreten, auflisten, zerstören. Im Betrieb stellt sich jedoch schnell eine andere Frage: Wie betreibt man mehrere Jails reproduzierbar und konsistent?
Während jailctl bewusst low-level bleibt, liefert jailmgr den operativen Rahmen:
Auch das ist absichtlich pragmatisch: eine Shell-Orchestrierung, die bekannte Unix-Werkzeuge zusammensetzt, statt früh ein eigenes schweres Management-Framework zu bauen.
- Shared Read-only Base-Layer (
mount_null) - Jail-spezifische
etc/var/tmp/home-Overlays - Host-IP-Aliase auf z. B.
lo0 - Massenoperationen (
prepare-all,start-all,stop-all) - Optional NPF-RDR-Snippet-Generierung
Die Startlogik zeigt den Ablauf sehr klar:
add_alias "${ip}"
mount_jail_root
...
jailctl create -n "${name}" -i "${ip}" "${ROOT_DIR}" /bin/sh /etc/rc
Und die Root-Struktur ist klassisch-robust:
mount -t null -o ro "${BASE_LAYER}" "${ROOT_DIR}"
mount -t null "${ETC_DIR}" "${ROOT_DIR}/etc"
mount -t null "${VAR_DIR}" "${ROOT_DIR}/var"
Für OS-Nutzer kann man es so lesen: ähnlich wie „goldenes Basis-System + pro Instanz nur die veränderlichen Teile“.
End-to-End-Ablauf (kompakt)
Bisher haben wir die einzelnen Bausteine isoliert betrachtet. Jetzt setzen wir sie zeitlich korrekt hintereinander und verfolgen, wie ein Jail Schritt für Schritt entsteht und betrieben wird.
Der Ablauf lässt sich als klarer Übergang vom Betrieb über das Userland bis in den Kernel lesen.
[Betrieb] [Userland] [Kernel]
───────── ────────── ────────
jailmgr jailctl secmodel_jail
│ │ │
│ start-all │ │
├──────────────▶ │ create │
│ ├─────────────────────▶ │ sysctl: create
│ │ │ Jail-ID vergeben
│ │ │ Limits setzen
│ │ │
│ │ exec / attach │
│ ├─────────────────────▶ │ Credential aktiv
│ │ │ Policy greift
│ │ │
│ list │ list │
├──────────────▶ ├─────────────────────▶ │ Metadaten lesen
│ │ │
│ destroy │ destroy │
└──────────────▶ └─────────────────────▶ │ nur wenn leer
Der gleiche Ablauf, noch einmal tabellarisch zusammengefasst:
| Phase | Auslöser | Ebene | Wirkung |
|---|---|---|---|
| Vorbereitung | jailmgr start-all |
Betrieb | IP-Alias, Mount-Layout, Startlogik |
| Erzeugung | jailctl create |
Userland | Sysctl-Aufruf, chroot, Jail-ID setzen |
| Eintritt | Exec / Attach | Kernel | Credential bekommt Jail-ID, Limits aktiv |
| Betrieb | laufende Prozesse | Kernel | Signal-/Prozess-Isolation, Netz-Policy |
| Beobachtung | jailctl list |
Kernel + Userland | Sichtbare Jails inkl. Prozess-Refcount |
| Abbau | jailctl destroy |
Kernel + Userland | Nur möglich, wenn keine Prozesse laufen |
Einordnung gegenüber Linux-Containern
Dieser Ansatz ist bewusst kein Ersatz für OCI-Container und nicht für untrusted Multi-Tenant-Szenarien gedacht.
Er adressiert ein anderes Problemfeld: kleine bis mittlere NetBSD-Hosts, auf denen mehrere vertrauenswürdige Dienste mit klaren, überprüfbaren Prozessgrenzen betrieben werden sollen.
Der Ansatz versucht nicht, Linux-Namespaces/cgroups zu kopieren.
Stattdessen:
- klare Kernel-Policy über Kauth
- klare Steuerung über Sysctl
- klare Userland-Werkzeuge mit Fokus auf NetBSD-Operation
Das Ergebnis ist weniger „Ökosystem-Masse“, dafür mehr Kohärenz.
Für mich ist das der Kern des Prototyps: NetBSD-artig denken, nicht Linux-Stacks nachbauen.
Fazit
Die Kombination aus secmodel_jail, jailctl und jailmgr zeigt im aktuellen Stand, dass moderne Isolation auf NetBSD pragmatisch möglich ist:
- ohne UID-Mapping,
- stärker als reines
chroot, - deutlich schlanker als Vollvirtualisierung.
Der Ansatz steht dabei auf den Schultern früherer Arbeiten wie FreeBSD Jails, gaols und sysjail, reduziert deren Komplexität jedoch gezielt auf ein Maß, das zu NetBSDs Architektur und zum praktischen Betrieb passt.
Für mich ist das ausdrücklich noch kein fertiges Produkt, sondern ein Werkstattstand mit klarer Richtung: weniger Fragmentierung, mehr Verständlichkeit, mehr Betriebssicherheit. Der aktuelle Stand ist nicht sicherheitsauditiert und nicht für untrusted Multi-Tenant-Szenarien gedacht. KI war dabei kein Entwurfsersatz, sondern ein Beschleuniger bei Exploration, Boilerplate und Gegenlesen – die Architekturentscheidungen blieben bewusst manuell.
Ich erprobe aktuell weiter Details in Architektur und Betrieb.
Den Source Code plane ich zu veröffentlichen, sobald sich das Gesamtbild für mich konzeptionell stimmig und in sich konsistent anfühlt.
Bildnachweis: Das NetBSD-Logo ist eine eingetragene Marke der NetBSD Foundation, Inc. Verwendung im Rahmen redaktioneller Berichterstattung gemäß den Richtlinien zur Logonutzung.