OpenBSD entwickelt Sicherheit durch Designentscheidungen im Kernel und userland-Code. Jeder Daemon läuft mit minimalen Rechten in separaten Prozessen. Die Systemcalls pledge() und unveil() beschränken Programme auf definierte Operationen und Dateizugriffe. Speicherbereiche sind entweder beschreibbar oder ausführbar – niemals beides gleichzeitig. Der Kernel bietet Sicherheitsstufen die selbst Root-Zugriff einschränken.
Systemkonfiguration zeigt doas als Rechteeskalations-Tool und sysctl für Kernel-Parameter. Paketverwaltung erklärt Paket-Signaturen mit signify(1). Dieser Artikel beschreibt die Sicherheitsarchitektur die diese Tools schützt und wie OpenBSD Angriffsflächen systematisch reduziert.
Secure by Default – Designphilosophie
OpenBSD aktiviert nach der Installation nur SSH und Cron. Alle anderen Dienste bleiben deaktiviert. Die Firewall pf blockiert eingehende Verbindungen standardmäßig. Dieser Ansatz reduziert Angriffsvektoren ohne Konfigurationsaufwand.
Das Projekt führt kontinuierliche Code-Audits durch. Jede Zeile wird auf Sicherheitsprobleme analysiert – Buffer Overflows, Format-String-Bugs, Race Conditions. Unsicherer Code wird entfernt oder neu geschrieben. Die Entwickler bevorzugen einfache, überprüfbare Implementierungen gegenüber Feature-Reichtum.
Die Philosophie beeinflusst alle Entscheidungen. LibreSSL ersetzt OpenSSL nach Heartbleed. doas ersetzt sudo mit 400 statt 4000 Zeilen Code. httpd bietet grundlegende Webserver-Funktionen ohne komplexe Module. Weniger Code bedeutet weniger Fehler.
Privilege Separation – Prozessisolation
OpenBSD-Daemons teilen sich in mehrere Prozesse mit unterschiedlichen Privilegien. Der privilegierte Parent-Prozess läuft als Root und führt nur kritische Operationen aus:
- Ports unter 1024 binden,
- Dateien mit besonderen Rechten öffnen.
Child-Prozesse laufen als unprivilegierte Benutzer und verarbeiten Netzwerk-Traffic oder Anfragen.
Der SSH-Daemon sshd demonstriert das Konzept. Ein privilegierter Monitor-Prozess läuft als Root und authentifiziert Benutzer. Nach erfolgreicher Anmeldung startet er einen unprivilegierten Session-Prozess der als authentifizierter Benutzer läuft. Kompromittierung des Session-Prozesses betrifft nur die User-Session – nicht das gesamte System.
$ ps aux | grep sshd
root 48124 0.0 0.2 1104 2492 ?? S 8:16AM 0:00.05 sshd: /usr/sbin/sshd [listener] 0 of 10-100 startups (sshd)
root 75300 0.0 0.4 1596 3928 ?? S 8:22AM 0:00.11 sshd-session: michael [priv] (sshd-session)
michael 9433 0.0 0.4 1880 3628 ?? S 8:23AM 0:00.04 sshd-session: michael@ttyp0 (sshd-session)
Die Prozesshierarchie zeigt drei Ebenen. Der Listener-Prozess ([listener]) läuft als Root und wartet auf eingehende Verbindungen — die Angabe 0 of 10-100 startups zeigt wie viele Child-Prozesse aktuell aktiv sind. Nach erfolgreicher Authentifizierung startet ein privilegierter Session-Prozess (sshd-session: michael [priv]) der den Benutzer-Kontext aufbaut und als Root läuft. Der eigentliche Session-Prozess (sshd-session: michael@ttyp0) läuft dann als angemeldeter Benutzer — eine Kompromittierung dieses Prozesses betrifft nur diese eine Session.
Der HTTP-Server httpd folgt dem gleichen Muster. Ein Root-Prozess bindet Port 80 und 443. Worker-Prozesse laufen als www und liefern statische Inhalte aus. CGI-Skripte werden in separaten Prozessen mit slowcgi ausgeführt – wiederum als unprivilegierter User.
Diese Architektur verhindert Privilege Escalation. Ein Angreifer der den Worker-Prozess kompromittiert erhält nur www-Rechte – keine Root-Privilegien. Kommunikation zwischen Prozessen erfolgt über IPC-Sockets mit klar definierten Nachrichten-Protokollen.
pledge() – Systemcall-Beschränkung
Der Systemcall pledge() reduziert verfügbare Kernel-Operationen auf eine definierte Menge. Programme deklarieren welche Funktionen sie benötigen. Der Kernel verweigert alle anderen Systemcalls und beendet den Prozess mit SIGABRT bei Verstößen.
Die Syntax definiert erlaubte Operationen als String:
if (pledge("stdio rpath inet dns", NULL) == -1)
err(1, "pledge");
Dieser Aufruf erlaubt Standard-I/O (stdio), lesenden Dateizugriff (rpath), Netzwerk-Operationen (inet) und DNS-Auflösung (dns). Alle anderen Systemcalls sind verboten – das Programm kann keine Dateien schreiben, keine neuen Prozesse starten, keine Geräte öffnen.
Die wichtigsten Promises:
- stdio – Standard-I/O, Speicherallokation, Signale
- rpath – Lesender Dateizugriff
- wpath – Schreibender Dateizugriff
- cpath – Dateien erstellen und entfernen
- inet – IPv4/IPv6-Sockets
- unix – Unix-Domain-Sockets für IPC
- dns – DNS-Auflösung
- proc – Prozess-Operationen (fork, kill)
- exec – Programme ausführen
- prot_exec –
PROT_EXECfürmmap()undmprotect()
Die Promise-Liste entwickelt sich mit jeder OpenBSD-Version weiter. In OpenBSD 7.8 existierte tmppath für temporäre Dateien in /tmp — seit 7.9 ist diese Promise entfernt und gibt EINVAL zurück. Ersatz ist "rpath wpath cpath" kombiniert mit unveil("/tmp", "rwc"). Die vollständige und versionsgenaue Liste liefert:
$ man 2 pledge
Der HTTP-Server httpd nutzt pledge() systematisch. Nach dem Initialisieren und Binden der Ports beschränkt sich der Worker-Prozess:
pledge("stdio rpath inet", NULL);
Worker können Dateien lesen und Netzwerk-Verbindungen bedienen – mehr nicht. Keine Dateien schreiben, keine neuen Prozesse starten, keine Kernel-Parameter ändern. Ein kompromittierter Worker bleibt funktional begrenzt.
pledge() wirkt irreversibel. Nach dem Aufruf kann das Programm die Beschränkungen nicht aufheben. Nur strengere Promises sind möglich durch erneuten pledge()-Aufruf mit weniger Rechten.
Die meisten Base-System-Programme nutzen pledge(). Der Befehl ls benötigt nur stdio und rpath. Der Editor vi braucht zusätzlich wpath und cpath für Dateiänderungen. SSH-Clients verwenden stdio inet dns tty proc exec für Terminal-Interaktion und Remote-Befehle.
unveil() – Dateisystem-Beschränkung
Der Systemcall unveil() beschränkt Dateisystem-Zugriff auf explizit freigegebene Pfade. Der erste Aufruf entfernt die Sichtbarkeit des gesamten Dateisystems — nur der angegebene Pfad mit den definierten Rechten bleibt zugänglich. Weitere Aufrufe können zusätzliche Pfade freigeben.
Die Syntax definiert Pfad und Zugriffsrechte:
if (unveil("/etc/httpd.conf", "r") == -1)
err(1, "unveil");
if (unveil("/var/www", "r") == -1)
err(1, "unveil");
if (unveil(NULL, NULL) == -1)
err(1, "unveil");
Dieser Code erlaubt lesenden Zugriff auf /etc/httpd.conf und /var/www. Der finale unveil(NULL, NULL)-Aufruf deaktiviert weitere unveil()-Aufrufe — die Konfiguration ist eingefroren. Zugriffe auf nicht freigegebene Pfade schlagen fehl: EACCES wenn die Permission nicht passt, ENOENT wenn kein unveil() für den Pfad qualifiziert.
Die verfügbaren Rechte:
- r – Lesen
- w – Schreiben
- x – Ausführen
- c – Erstellen und Entfernen
Der Mail-Transfer-Agent smtpd nutzt unveil() für verschiedene Prozesse. Der Netzwerk-Listener braucht nur Konfigurationsdateien:
unveil("/etc/mail/smtpd.conf", "r");
unveil("/etc/ssl/", "r");
unveil(NULL, NULL);
Der Queue-Manager benötigt Schreibzugriff auf das Spool-Verzeichnis:
unveil("/var/spool/smtpd/", "rwc");
unveil(NULL, NULL);
Diese Trennung isoliert Komponenten. Ein kompromittierter Listener kann keine Mails manipulieren. Ein kompromittierter Queue-Manager erreicht keine Konfigurationsdateien oder TLS-Zertifikate.
unveil(NULL, NULL) sind weitere unveil()-Aufrufe nicht mehr möglich. OpenBSD 7.9 empfiehlt ausdrücklich, unveil() nach der Konfiguration zu sperren — ein Programm das seine Dateisystem-Sicht offen lässt gibt unnötig Angriffsfläche frei.
unveil() kombiniert mit pledge() ergibt mehrschichtige Beschränkung. pledge("stdio rpath") erlaubt Dateizugriff — unveil() definiert welche Dateien. Ein Programm mit beiden Aufrufen läuft in einer präzise definierten Sandbox ohne externe Abhängigkeiten.
Securelevels – Kernel-Sicherheitsstufen
OpenBSD implementiert Kernel-Sicherheitsstufen die selbst Root-Zugriff einschränken. Die Securelevels verhindern Manipulation kritischer Systemkomponenten während des Betriebs. Das System startet in Securelevel -1 und erhöht die Stufe automatisch während des Boots.
Die Stufen definieren zunehmende Beschränkungen:
Securelevel -1: Permanent Insecure – alle Operationen erlaubt. init(8) erhöht den Securelevel nicht automatisch. Diese Stufe nutzen Entwickler für Kernel-Debugging.
Securelevel 0: Insecure – wird beim Bootstrapping und im Single-User-Mode genutzt. Alle Geräte sind les- und schreibbar, System-File-Flags können gelöscht werden.
Securelevel 1: Secure – Standard im Mehrbenutzerbetrieb. Verhindert:
- Öffnen von
/dev/memund/dev/kmem - Schreibzugriff auf Raw-Disk-Devices gemounteter Dateisysteme
- Entfernen von system immutable und append-only File-Flags
- Änderungen an sicherheitsrelevanten
sysctl-Variablen (hw.allowpowerdown,kern.allowkmem,kern.utc_offset) - Erhöhen der
ddb.consoleundddb.panicsysctl-Variablen gpioctlkann nur auf beim Boot konfigurierte GPIO-Pins zugreifen
Securelevel 2: Highly Secure – alle Einschränkungen von Stufe 1, zusätzlich:
- Raw-Disk-Devices sind immer schreibgeschützt — auch wenn nicht gemountet
- Systemzeit kann nicht zurückgestellt oder nahe an einen Überlauf gesetzt werden
pf-Filter- und NAT-Regeln sind vollständig unveränderlich
Das aktuelle Securelevel zeigt sysctl:
$ sysctl kern.securelevel
kern.securelevel=1
Die Erhöhung erfolgt automatisch durch rc(8) beim Boot:
$ doas sysctl kern.securelevel=1
kern.securelevel: 1 -> 1
init(8) — nicht Root, nicht doas. Eine laufende Senkung ist nicht möglich. Für Wartungsaufgaben die niedrigere Securelevels erfordern: im Single-User-Mode arbeiten.
Root kann den Securelevel jederzeit erhöhen — senken ist ab Stufe 1 ausschließlich init(8) vorbehalten. Selbst ein kompromittierter Root-Account kann Kernel-Code nicht ändern oder Firewall-Regeln deaktivieren.
/etc/rc.securelevel enthält Befehle die ausgeführt werden bevor rc(8) den Securelevel erhöht — etwa um File-Flags zu setzen oder Kernel-Parameter anzupassen die danach unveränderlich wären.
Da der Securelevel über den in-kernel Debugger ddb(4) manipuliert werden kann, empfiehlt es sich auf produktiven Systemen den Debugger zu deaktivieren:
$ sysctl ddb.console
ddb.console=0
$ sysctl ddb.panic
ddb.panic=1
$ doas sysctl ddb.panic=0
ddb.panic: 1 -> 0
$ doas sysctl ddb.panic=1
sysctl: ddb.panic: Operation not permitted
Ab Securelevel 1 können diese Werte nicht mehr erhöht werden — die Einstellung ist dann dauerhaft für die laufende Session.
W^X – Write XOR Execute Memory Protection
OpenBSD erzwingt W^X (Write XOR Execute) für alle Speicherbereiche. Jede Speicherseite ist entweder beschreibbar oder ausführbar – niemals beides gleichzeitig. Diese Eigenschaft verhindert Code-Injection-Angriffe die Schadcode in beschreibbare Bereiche schreiben und anschließend ausführen.
Der Memory-Layout trennt Code und Daten strikt. Text-Segmente mit Programm-Code sind markiert als execute-only. Data-Segmente mit Variablen sind write-only. Stack und Heap sind write-only und explizit nicht-ausführbar. Selbst Shared Libraries folgen dieser Trennung.
Das mmap()-Systemcall erzwingt die Regel. Programme können Speicher nicht gleichzeitig als PROT_WRITE und PROT_EXEC markieren.
Beispiel (abgelehnt): PROT_WRITE|PROT_EXEC
void *mem = mmap(NULL, size, PROT_WRITE|PROT_EXEC,
MAP_ANON, -1, 0);
Rückgabe: MAP_FAILED, errno = ENOTSUP
Korrekt: erst schreiben, dann auf ausführbar umschalten
void *mem = mmap(NULL, size, PROT_WRITE,
MAP_ANON, -1, 0);
write_code(mem);
mprotect(mem, size, PROT_EXEC);
Diese Sequenz erlaubt Just-In-Time-Compiler oder Self-Modifying-Code unter kontrollierten Bedingungen. Nach mprotect() mit PROT_EXEC ist der Speicher nicht mehr beschreibbar.
PROT_WRITE|PROT_EXEC nutzen wenn zwei Bedingungen erfüllt sind: Das Dateisystem ist mit wxallowed gemountet und das Binary ist zur Link-Zeit mit wxneeded getaggt. Auf produktiven Servern ohne solche Programme ist diese Ausnahme nicht nötig.
Bei W^X-Problemen hilft kern.wxabort zur Diagnose:
$ sysctl kern.wxabort
kern.wxabort=0
Der Standardwert 0 protokolliert W^X-Verstöße ohne den Prozess abzubrechen. Auf produktiven Systemen empfiehlt sich kern.wxabort=1 um Verstöße sofort zu unterbinden:
$ doas sysctl kern.wxabort=1
kern.wxabort: 0 -> 1
W^X gilt systemweit – Kernel und Userland. Der OpenBSD-Kernel selbst nutzt separate Bereiche für Code und Daten. Kernel-Module existieren in OpenBSD kaum — und würden ebenfalls W^X befolgen müssen.
Moderne CPUs unterstützen W^X über Hardware-Features. Intel NX-Bit (No-Execute) und AMD EVP (Enhanced Virus Protection) markieren Speicherseiten als nicht-ausführbar. OpenBSD nutzt diese Features konsequent und fällt zurück auf Software-Emulation bei älterer Hardware.
Die Implementierung betrifft auch Stack-Schutz. Der Stack ist explizit nicht-ausführbar was Buffer-Overflow-Exploits erschwert. Angreifer können Stack-Daten überschreiben aber keinen Code dort platzieren und ausführen. Kombiniert mit Stack-Protector-Canaries ergibt das mehrschichtige Abwehr.
Kryptographie-Stack
OpenBSD integriert Kryptographie tief ins System. LibreSSL ersetzt OpenSSL als TLS-Bibliothek. Der Zufallsgenerator arc4random() liefert kryptographisch sichere Zufallszahlen ohne Seed-Management. Passwort-Hashing nutzt bcrypt mit automatischer Work-Factor-Anpassung.
LibreSSL – OpenSSL-Fork
Nach der Heartbleed-Schwachstelle 2014 forkte OpenBSD OpenSSL zu LibreSSL. Das Projekt entfernte 90.000 Zeilen veralteten Code – VMS-Support, obsolete Crypto-Algorithmen, redundante Abstraktionsebenen. LibreSSL fokussiert auf aktuelle Systeme und moderne Standards.
Die Integration ins Base-System bedeutet einheitliche Updates. TLS-Bibliothek und Kernel-Komponenten stammen aus der gleichen Quelle. Package-Tools nutzen die gleiche LibreSSL-Installation wie Apache oder Postfix. Keine Versionskonflikte zwischen System-TLS und Anwendungs-TLS.
LibreSSL führt proaktive Sicherheitsverbesserungen ein. TLS 1.3 erschien früher als in OpenSSL-Stable. Unsichere Cipher-Suites werden schnell deaktiviert. Die API bleibt kompatibel zu OpenSSL für Portierungen – mit Ausnahme deprecated Functions.
arc4random() – Kryptographischer PRNG
Die Funktion arc4random() generiert 32-Bit-Zufallszahlen ohne manuelles Seed-Management. Der Name steht für “A Replacement Call for Random” — der Generator nutzt ChaCha20 (nicht RC4 trotz des Namens) und wird vom Kernel regelmäßig neu geseedet. Nach fork(2) erfolgt automatisch ein Re-Seed — zwei Prozesse liefern nie die gleiche Zufallssequenz.
Zufallszahl zwischen 0 und 99:
uint32_t random = arc4random_uniform(100);
Zufälligen Buffer füllen — etwa für Schlüsselmaterial:
unsigned char key[32];
arc4random_buf(key, sizeof(key));
Zufallszahl zwischen min und max:
uint32_t random_range(uint32_t min, uint32_t max) {
return min + arc4random_uniform(max - min + 1);
}
Die arc4random_uniform()-Funktion vermeidet Modulo-Bias. Naive Implementierungen mit arc4random() % n verteilen Zahlen ungleichmäßig bei nicht-Zweierpotenzen. LibC enthält eine optimierte Bias-freie Implementierung.
Der Kernel sammelt Entropie aus Hardware-Quellen — Interrupt-Timing, Disk-I/O, Netzwerk-Pakete. Die Pseudogeräte /dev/random und /dev/urandom liefern identische Qualität (anders als Linux). Programme können aus beiden lesen ohne Blocking — der Generator hat immer genug Entropie.
bcrypt – Password Hashing
OpenBSD nutzt bcrypt für Passwort-Hashing. Der Algorithmus basiert auf Blowfish mit adaptivem Work-Factor — höhere Work-Factors bedeuten mehr CPU-Zeit pro Hash und erschweren Brute-Force-Angriffe auf gestohlene Passwort-Datenbanken.
Die Datei /etc/master.passwd speichert bcrypt-Hashes:
michael:$2b$10$TX6ZNHMmjVF8RiD9q2dfBOU0S5djznlXkxYSzuOj8cLzQ3n4t0RfS:1001:1001:...
Das Format: $2b$ markiert bcrypt. 10 ist der Work-Factor — der Algorithmus führt 2^10 = 1024 Iterationen durch. Der folgende String enthält Salt (128 Bit) und Hash kombiniert.
Der Befehl encrypt(1) generiert bcrypt-Hashes:
$ encrypt "testpassword"
$2b$10$TX6ZNHMmjVF8RiD9q2dfBOU0S5djznlXkxYSzuOj8cLzQ3n4t0RfS
Der Work-Factor wird automatisch anhand der CPU-Geschwindigkeit gewählt — auf realer Hardware typischerweise zwischen 10 und 12. Auf virtuellen Maschinen kann der Wert niedriger ausfallen da QEMU/KVM die CPU-Geschwindigkeitsmessung beeinflusst.
crypt(3) ist deprecated. Neue Programme nutzen crypt_newhash(3) zum Erstellen und crypt_checkpass(3) zum Prüfen von Passwort-Hashes.
Weitere Sicherheitsfeatures
OpenBSD implementiert zusätzliche Schutzmechanismen die Exploit-Entwicklung erschweren. Address Space Layout Randomization (ASLR) verteilt Speicherbereiche zufällig. Stack-Protector-Canaries erkennen Buffer-Overflows. Return-Oriented-Programming (ROP) Gadgets werden durch Compiler-Optimierungen reduziert.
ASLR – Address Space Layout Randomization
ASLR platziert Stack, Heap, Libraries und ausführbare Segmente an zufälligen Adressen. Exploits können nicht mit fest codierten Speicheradressen arbeiten. Jeder Prozess-Start generiert neue Memory-Layouts.
OpenBSD aktiviert ASLR standardmäßig. Die Variable library_aslr in /etc/rc.conf steuert die Library-Randomisierung beim Boot:
$ grep library_aslr /etc/rc.conf
library_aslr=YES # set to NO to disable library randomization
PIE-Support ist Compiler-Standard. Alle Base-System-Programme kompilieren als PIE. Packages im Ports-Tree nutzen ebenfalls PIE wo möglich.
Stack Protector
OpenBSD nutzt Return Address Protection über zufällige Guard-Werte. Der Clang-Compiler fügt automatisch pro Funktion einen individuellen __retguard-Wert ein. Vor dem Function-Return prüft der Code den Guard. Manipulation deutet auf einen Buffer-Overflow hin — das Programm terminiert sofort.
Das ist stärker als klassische Stack-Canaries die einen systemweiten Wert nutzen — jede Funktion bekommt einen eigenen zufälligen Guard:
void test(char *input) {
char buffer[64];
strncpy(buffer, input, sizeof(buffer)-1);
printf("%s\n", buffer);
}
Der Compiler schützt diese Funktion automatisch mit einem eigenen __retguard-Wert. Ein Blick ins kompilierte Binary zeigt die generierten Guards:
$ cc -g -o test test.c
$ nm test | grep retguard
00002bc0 r __retguard_1205
00002bd8 r __retguard_1471
00002be0 r __retguard_1773
00002be8 r __retguard_2333
00002bd0 r __retguard_2473
00002bf0 r __retguard_2997
Jeder Eintrag entspricht einer geschützten Funktion im Binary. Angreifer die eine Return-Adresse überschreiben müssen den korrekten Guard-Wert kennen — der bei jedem Programm-Start neu randomisiert wird.
OpenBSD aktiviert Return Address Protection für alle Userland-Programme. Der Kernel nutzt separate Schutzmechanismen da diese Checks dort Performance-kritisch sind.
ROP-Mitigation
Return-Oriented-Programming (ROP) nutzt existierende Code-Schnipsel (Gadgets) für Exploits. W^X verhindert neue Code-Injection — ROP umgeht das durch Verketten vorhandener Instruktionen.
OpenBSD erschwert ROP durch mehrere Maßnahmen:
Library-Randomization: library_aslr platziert Libraries beim Boot zufällig. ROP-Chains benötigen feste Adressen — Randomisierung macht Chains unreliabel.
Return Address Protection: __retguard schützt jede Funktion mit einem eigenen zufälligen Wert. Manipulation der Return-Adresse wird vor dem Function-Return erkannt.
W^X: Kein Speicherbereich ist gleichzeitig schreibbar und ausführbar. Neu injizierter Code kann nicht ausgeführt werden.
Die Kombination aus W^X, library_aslr und __retguard ergibt Defense-in-Depth. Kein einzelner Mechanismus ist unüberwindbar — zusammen erhöhen sie den Aufwand für erfolgreiche Exploits erheblich.
Sicherheit durch Einfachheit
OpenBSD-Sicherheit entsteht nicht durch reaktive Patches sondern proaktives Design. Kleine Programme mit klarem Zweck enthalten weniger Bugs als Feature-reiche Alternativen. Privilege Separation isoliert Komponenten. pledge() und unveil() beschränken laufende Prozesse. W^X und ASLR erschweren Memory-Corruption-Exploits.
Die kontinuierlichen Code-Audits identifizieren Schwachstellen bevor sie ausgenutzt werden. Das Projekt schreibt Code neu wenn Audits Unsicherheiten zeigen. LibreSSL ersetzte OpenSSL, doas ersetzte sudo, httpd ersetzte Apache für Base-System-Zwecke.
Der geplante Netzwerk und Routing Artikel zeigt Netzwerk-Konfiguration und Routing für produktive Systeme. Der geplante Firewall und pf Artikel erklärt die Packet-Filter-Firewall mit Policy-Design. Die Sicherheitsarchitektur bildet das Fundament für stabile Service-Infrastruktur.
Verwendete Software und Versionen
Diese Anleitung bezieht sich auf:
- OpenBSD: 7.8, 7.9
- Kontext: Server und Desktop
- Stand: Juni 2026