SELinux

Security Enhanced Linux (SELinux) ist eine Erweiterung des Linux-Kernels, die hauptsächlich von Red Hat und (interessanterweise) der NSA weiterentwickelt wird. Es sorgt auf Basis von Labeln und Regeln (den „Access Vectors“), dass Prozesse nur auf die Ressourcen Zugriff erhalten, die sie unmittelbar benötigen. Man kann sich SELinux also grob als eine Art Applikations-Firewall vorstellen. Es können jederzeit eigene Regeln definiert werden.

SELinux kennt drei verschiedene Betriebs-Modi:

Enforcing

Im Standard aktiv. Die Regeln werden durch den Kernel angewendet. Regel-Anwendungen werden in /var/log/audit/audit.log protokolliert.

Permissive

Die Regeln werden durch den Kernel nicht angewendet, sie werden aber in /var/log/audit/audit.log protokolliert, als würden sie angewendet werden.

Disabled

SELinux ist komplett abgeschaltet.

Da SELinux ein Labeling-System ist, besitzt jeder Prozess, jede Datei, jedes Verzeichnis und jedes System-Objekt (Ports etc.) ein Label. Die SELinux-Policies erlauben und definieren den Zugriff von gelabelten Prozessen auf gelabelte Objekte. Der Kernel sorgt für deren Einhaltung.

Die beiden wichtigsten Konzepte sind

  1. Das Labeling (Dateien, Ports, Prozesse etc.)

  2. Type Enforcement (also die Isolation von Prozessen auf Basis ihrer Labels).

Das korrekte Label-Format ist user:role:type:level.

Um SELinux temporär für das ganze System abzuschalten, setenforce 0 verwenden. Doch Vorsicht: das System ist von da an ungeschützt. Um SELinux zur Laufzeit auf „enforcing“ zu setzen , setenforce 1 nutzen.

So setzt man SELinux dauerhaft auf den Enforcing Mode (was der Standard-Einstellung in CentOS entspricht). Anschliessend ist ein Reboot fällig:

/etc/sysconfig/selinux
SELINUX=enforcing

Hinweis

Es gibt nur sehr wenige Ausnahmen, in denen man SELinux gänzlich abschalten sollte. Sehr wenige. Wirklich. Wer trotzdem darüber nachdenkt, weil man den Aufwand der Policy-Konfiguration scheut, sollte vorher probieren, einzelne Server-Prozesse in den Permissive-Mode zu schicken.

Die umfangreichen Manpages zu SELinux installiert man mit

yum -y install selinux-policy-devel
mandb
Links

Funktionsweise

Das Labeling-System:

  • Prozesse haben Label: Der Apache besitzt z.B. das Label system_u:system_r:httpd_t:s0.

  • Ports haben Label: Der Apache darf im SELinux-Standard auf den Ports 80, 81, 443, 488, 8008, 8009, 8443 und 9000 lauschen.

  • Dateien und Verzeichnisse haben Label: /var/www/html besitzt das Label system_u:object_r:httpd_sys_content_t:s0. Dateien erhalten dazu erweiterte Attribute, die nur mit aktuellen Dateisystemen wie Ext3/Ext4/XFS gespeichert werden können.

  • Der Kernel kontrolliert anhand einer Regel-Datenbank, ob diese Label zusammenspielen und was sie bewirken dürfen. Die SELinux-Datenbank, die diese Policies enthält, wird laufend durch die Installation von Software und Updates per Yum erweitert und gepflegt (in einer Minimal-Installation umfasst sie mindestens 6’000 Regeln, Tendenz steigend).

Ein Label wie system_u:system_r:httpd_t:s0 besteht immer aus vier Komponenten:

  • User Component: system_u. Der SELinux-Benutzer. User Components enden meist auf _u. Andere Beispiele: guest_u, root.

  • Role Component: system_r. Rollenkonzept für Ressourcen. object_r ist die Standard-Rolle. Role Components enden meist auf _r. Andere Beispiele: staff_r.

  • Type Component: httpd_t. das wichtigste Feld, da SELinux im Standard ein Type Enforcement System ist. Der Typ endet meist auf _t.

  • Multi-Level Security (MLS) Component: Identifiziert einen einzelnen Level oder einen Bereich. Steht in einer „targeted“-Policy immer auf s0 (Single Level).

SELinux würde zunächst jeden Zugriff eines Prozesses auf Systemressourcen jeglicher Art (Dateien, Verzeichnisse, Ports etc.) abweisen und nur selektiv anhand von Regeln Zugriffe erlauben - die so genannte „strict“-Policy. In der Praxis ist das nicht handhabbar, weswegen es mehrere Policies gibt:

  • minimum

  • mls (Multi-Level Security)

  • targeted (Single-Level Security - jeweils eine Policy pro Applikation). Die Standard-Policy in RHEL, die das Verhalten ausgewählter System-Prozesse kontrollierbar macht (zu finden unter /etc/selinux/targeted). In der „targeted“-Policy wird die Type-Komponente ausgewertet, nicht aber die Rollen- und Multi-Level-Komponente, weswegen man von einer „Type Enforcement Policy“ spricht. Alles andere läuft in der „unconfined“-Policy, d.h. diese Programme werden nicht durch SELinux kontrolliert.

  • unconfined

Multi-Level Security (MLS)-Enforcement erlaubt Prozessen Zugriff auf Daten anhand ihres Security Levels. Beispielsweise kann dann ein „Secret“-Prozess keine als „Top-Secret“ eingestuften Daten lesen.

Multi-Category Security (MCS)-Enforcement schützt Prozesse voreinander (wie virtuelle Maschinen, OpenShift Gears, SELinux Sandboxes, Container usw.).

Mit sesearch lässt sich ein Blick in die Policy-Datenbank werfen oder herausfinden, wie Policies und Booleans technisch definiert sind:

yum -y install setools-console

sesearch --allow

# CentOS 7
sesearch --bool rsync_export_all_ro --allow --show_cond

# CentOS 8+, Fedora
sesearch --bool rsync_export_all_ro --allow

Der Admin kann mit dem Befehl semanage bequem Datei- und Port-Kontexte einsehen als auch eigene Regeln hinzufügen.

# up to CentOS 7
yum -y install policycoreutils-python

# CentOS 8+
dnf -y install policycoreutils-python-utils

semanage fcontext --list | grep http
semanage port --list | grep http

Welche Regeln werden auf das /var/www-Verzeichnis angewendet?

semanage fcontext --list | grep /var/www | sort

Tipp

Diese man-Pages erklären SELinux: man -k selinux

Dateien und Verzeichnisse

Der Apache darf auf die Verzeichnisse und Dateien /var/www/html und ~/public_html zugreifen, da sie den SELinux-Dateikontext httpd_sys_content_t tragen, nicht aber auf die Passwort-Datei /etc/shadow, da diese den Kontext shadow_t besitzt und keine Regel für Apache existiert, die den Zugriff darauf erlauben würde. Ein Eindringling, der den Apache kompromittiert hat, kann so nie an Systemdateien gelangen.

Gesetzte Datei-Labels ermittelt man immer mit dem Parameter -Z:

  • ls -Z /etc/shadow

Dateien nehmen beim Verschieben ihren SELinux-Kontext mit, beim Kopieren erben sie den Kontext des Zielverzeichnisses. Verschobene Dateien müssen daher „umgelabelt“ werden, was man mit restorecon erledigt.

Die „Datenbank“, in der alle SELinux Datei-Kontexte gepflegt sind, findet man hier:

cat /etc/selinux/targeted/contexts/files/file_contexts
...
/root(/.*)?    system_u:object_r:admin_home_t:s0
...
/var/www(/.*)? system_u:object_r:httpd_sys_content_t:s0
...

Prozesse

Prozess-Labels ermittelt man immer mit dem Parameter -Z:

  • id -Z

  • ps auxZ | grep httpd

Prozess auf „permissive“ setzen

Beispiel logrotate: dieses läuft unter dem Kontext „logrotate_t“. Damit der Prozess auch bei Enforcing auf Dateien zugreifen kann (braucht man beispielsweise, wenn logrotate ein selbstgeschriebenes postscript ausführen soll, welches auf aussergewöhnliche Verzeichnisse zugreifen soll):

semanage permissive --add logrotate_t
Prozess wieder auf „enforcing“ setzen
semanage permissive --delete logrotate_t

SELinux-Booleans

Mit „Policy booleans“ lassen sich einfache SELinux-Sicherheitsvorgaben zur Laufzeit ein- bzw. ausschalten. Booleans sind eigentlich eine Art Shortcut für komplexere Policy-Module. Für fast jeden Prozess gibt es eine Reihe an Schaltern, beispielsweise erlaubt eine aktivierte Einstellung httpd_enable_cgi dem Apache, CGI-Skripte auszuführen. Möchte der Administrator CGI-Skripte unterbinden, setzt er diesen Wert einfach auf 0/false/off. Die meisten Booleans sind standardmässig deaktiviert.

Eine Liste der installierten Booleans (ist immer installationsabhängig) erhält man mit getsebool -a, gefiltert nach denen für Apache mit getsebool -a | grep http. Ausführlicher mit Beschreibung und Default-Werten geht es mit Hilfe von semanage boolean --list --locallist.

Um im obigen Beispiel CGI-Skripte zu deaktivieren, benutzt man setsebool. Der Parameter -P sorgt dafür, dass die Einstellung permanent gesetzt wird und somit einen Neustart überlebt.

setsebool -P httpd_enable_cgi off

Um den Schalter einfach auf seinen entgegengesetzten Wert umzulegen, kann man auch togglesebool httpd_enable_cgi verwenden.

SELinux Audit-Log

Wird einer Software der Zugriff auf eine Ressource untersagt, landet dies schwer lesbar in der /var/log/audit/audit.log, was im Fall eines Apache, der auf einen MariaDB-Server zugreifen wollte, so aussehen kann (und der Übersichtlichkeit halber optisch aufbereitet wurde):

type     = AVC
msg      = audit(1322708342.967:16804): avc: denied { name_connect } for pid = 2724
comm     = "httpd"
dest     = 3306
scontext = unconfined_u:system_r:httpd_t:s0
tcontext = system_u:object_r:mysqld_port_t:s0
tclass   = tcp_socket

Das Format einer Nachricht im SELinux-Log kurz erklärt (Auszug):

  • type: AVC = Access Vector Cache, Kernel Event, SYSCALL = System-Aufruf.

  • msg: Enthält die Seriennummer der Nachricht. So erkennt man zusammenhängende Nachrichten.

  • comm: Name des Executables.

  • dest: Ziel, z.B. ein Port.

  • scontext: Security-Kontext des Source-Objektes.

  • tcontext: Security-Kontext des Target-Objektes.

Es gibt Tools, die einem beim Anschauen helfen oder sogar bis zu einer gewissen Grenze im Klartext erklären können, was seitens SELinux verboten wurde.

ausearch

Statt /var/log/audit/audit.log direkt anzuschauen:

ausearch --start recent --message avc
audit2why

Dazu packt man die ermittelten audit.log-Zeilen in eine separate Datei und füttert diese an audit2why, einem Tool aus den policycoreutils-python:

yum -y install policycoreutils-python
dnf -y install policycoreutils-python-utils

ausearch --start recent --message avc > /tmp/myaudit.log
audit2why < /tmp/myaudit.log
sealert

Wird das nachfolgende Paket installiert und der Server neu gestartet, tauchen lesbare SELinux-Meldungen in /var/log/messages bzw. journalctl auf.

yum -y install setroubleshoot-server

sealert --lookupid 12fd8b04-0119-4077-a710-2d0e0ee5755e
#SELinux is preventing httpd from getattr access on the file /var/www/html/index.html.

Beispiel 1: Apache „Permission Denied“

Sobald SELinux im Enforcing-Mode läuft, wird der Webserver Probleme bekommen, Dateien aus /var/www/html auszuliefern, die aus anderen Verzeichnissen dorthin verschoben wurden. Der Grund liegt im falschen Dateikontext; SELinux erwartet, dass Dateien, die durch httpd aus /var/www ausgeliefert werden dürfen, den Kontext httpd_sys_content_t aufweisen.

Zur Übung erzeugt man eine index.html im /tmp-Ordner. Die Datei besitzt damit den Dateikontext unconfined_u:object_r:user_tmp_t:s0. Verschiebt man sie in das /var/www/html-Verzeichnis, bleibt der Kontext erhalten - SELinux hindert den Apache folgerichtig daran, die Datei bei einem Aufruf per HTTP auszuliefern, der Apache gibt brav eine „Forbidden“ (ich darf nicht)-Meldung aus.

Der Datei-Kontext für /var/www/.* ist wohldefiniert und in der SELinux-Datenbank in Form von regulären Ausdrücken hinterlegt. So ermittelt man den resultierenden SELinux Security Context für Pfade und Dateien:

matchpathcon /var/www/html

Stimmt der Kontext, kann man die verschobenen Dateien einfach auf den Wert aus der Datenbank setzen:

restorecon -r /var/www/html

Beispiel 2: Apache mit neuer DocumentRoot

Angenommen, die DocumentRoot eines virtuellen Apache-Hosts liegt unter /data. SELinux hindert Apache an der Auslieferung der Dokumente, da dort erzeugte Dateien den sehr allgemeinen „unconfined_u:object_r:default_t:s0“-Kontext besitzen, der nicht zum Apache passt.

Hier muss die SELinux File-Context Datenbank ergänzt werden. /data und alle enthaltenen Unterverzeichnisse sollen mit dem File-Kontext des Apache verknüpft werden:

semanage fcontext --add --type httpd_sys_content_t "/data(/.*)?"

Hinweis

restorecon folgt keinen SymLinks, man muss bei der Definition von File-Contexts mit semanage also mit echten Pfadangaben arbeiten.

Falls der Apache Schreibrechte auf ein Verzeichnis benötigt, verwendet man folgenden Kontext:

semanage fcontext --add --type httpd_sys_rw_content_t "/data(/.*)?"

Anschliessend „restauriert“ man den Datei-Kontext unter /data, sprich: man labelt sie anhand der aktualisierten SELinux-Datenbank um.

restorecon -Fvr /data

Die Manpage hilft bei der Syntax der Befehle:

man semanage-fcontext

Löschen eines Kontextes (Beispiel):

semanage fcontext --delete --type httpd_log_t "/data(/.*)?"

Beispiel 3: Apache darf kein Mail senden

Im folgenden Szenario soll ein PHP-Skript, welches ein Mail-Kommando absetzt, durch Apache ausgeführt werden. Das Skript ist durch root im /root-Ordner erzeugt und nach /var/www/html verschoben worden. Apache ist standardmässig über SELinux so konfiguriert, dass keine Mails versendet werden dürfen. Zusätzlich erwartet man hier Probleme wegen des falschen Datei-Kontextes.

Zunächst schafft man die Übungsvoraussetzungen, installiert Apache PHP, erzeugt das Skript und verschiebt es:

setenforce 1
yum -y install httpd php
systemctl restart httpd
firewall-cmd --add-service=http
setsebool -P httpd_can_sendmail off
cat >/root/test.php <<EOF
<?php
if (mail('root', 'subject', 'message')) {
    echo 'OK';
}
else {
    echo 'NOK';
}
?>
EOF
mv /root/test.php /var/www/html
chown apache:apache /var/www/html/test.php

Der Aufruf http://myserver/test.php wird durch den Apache mit einer leeren Seite und „Forbiden“ (403) quittiert. Wenn man mal so tut als wüsste man nicht warum, hilft folgende sequentielle Analyse:

tail /var/log/httpd/access_log
# "403" while accessing test.php
semanage permissive --add httpd_t
# Script works
semanage permissive --delete httpd_t
# Script won't work anymore
tail /var/log/httpd/error_log
# "Permission denied", so an OS related problem

Schnell noch die Datei-Berechtigungen prüfen: ein ls -l /var/www/html/test.php zeigt, dass der Besitzer der Datei und die Gruppe zum Account des Apache passen, und User und Group Leserechte auf die Datei besitzen.

Man weiss längst: SELinux ist das „Problem“. Ein Aufruf des Skripts auf der Kommandozeile mit php /var/www/html/test.php liefert ein „OK“, man muss den Fehler also im Apache-Kontext suchen. Ein Blick in die SELinux-Logs

tail /var/log/audit/audit.log

zeigt schon den falschen Target-Kontext tcontext=unconfined_u:object_r:admin_home_t:s0. Erfahrungsgemäss schaut man sich mit ls -Z /var/www/html/test.php den Datei-Kontext an und korrigiert:

restorecon /var/www/html/test.php

Ruft man die Seite erneut auf, erhält man jetzt immerhin ein „NOK“ - die Seite konnte aufgerufen, das Mail-Kommando jedoch nicht ausgeführt werden. SELinux hindert das im Apache-Kontext laufende PHP an der Mail-Kommunikation, worüber es im Audit-Log auch Auskunft gibt:

type     = AVC
msg      = audit(1410282955.098:649): avc:denied  { search } for  pid = 8840
comm     = "sendmail"
name     = "postfix"
dev      = "dm-1"
ino      = 9078592
scontext = system_u:system_r:httpd_t:s0
tcontext = system_u:object_r:postfix_etc_t:s0
tclass   = dir

type     = SYSCALL
msg      = audit(1410282955.098:649): arch=c000003e syscall=2 success=no exit=-13 a0=7f38ae492ea0 a1=0 a2=0 a3=0 items=0 ppid=8511 pid=8840 auid=4294967295 uid=48 gid=48 euid=48 suid=48 fsuid=48 egid=48 sgid=48 fsgid=48 tty=(none) ses=4294967295
comm     = "sendmail"
exe      = "/usr/sbin/sendmail.postfix"
subj     = system_u:system_r:httpd_t:s0 key=(null)

Die erste Zeile besagt, dass „sendmail“ über „postfix“ nicht ausgeführt werden durfte, Ursache („scontext“) war unser Apache-Prozess. Dafür gibt es einen SELinux-Boolean, wie man schnell herausfindet:

man httpd_selinux | grep mail

führt zu

setsebool -P httpd_can_sendmail on

Beispiel 4: Apache und entfernte MariaDB

Ein Python-Skript unter Apache möchte eine Verbindung zu einer entfernten MariaDB-Datenbank aufbauen, was verhindert wird. Die passende Zeile aus dem audit.log wurde in einer eigenen /tmp/myaudit.log abgelegt. Jetzt untersucht man, was zu tun ist:

ausearch --start recent --message avc > /tmp/myaudit.log
# type     = AVC
# msg      = audit(1322708342.967:16804): avc: denied { name_connect } for pid=2724
# comm     = "httpd"
# dest     = 3306
# scontext = unconfined_u:system_r:httpd_t:s0
# tcontext = system_u:object_r:mysqld_port_t:s0
# tclass   = tcp_socket
yum -y install policycoreutils-python
dnf -y install policycoreutils-python-utils

audit2why < /tmp/myaudit.log

Die Ausgabe sagt, was man zu tun hat:

One of the following booleans was set incorrectly.
Description:
Allow HTTPD scripts and modules to connect to the network using TCP.

Allow access by executing:
# setsebool -P httpd_can_network_connect on
Description:
Allow HTTPD scripts and modules to connect to databases over the network.

Allow access by executing:
# setsebool -P httpd_can_network_connect_db on

Da das Skript sich zu einem „well-known“ Standard-Datenbank-Port verbinden möchte, genügt:

setsebool -P httpd_can_network_connect_db on

Beispiel 5: Apache auf einem anderen Port lauschen lassen

Da der Apache nur auf bestimmten Ports hören darf, wird die Konfiguration eines nicht standardkonformen Ports SELinux auf den Plan rufen. So fügt man eine neue Regel für Port 8080 hinzu:

semanage port --add --type http_port_t --proto tcp 8080

Die Manpage hilft bei der Syntax des Befehls:

man semanage-port | grep semanage

Beispiel 6: Apache „permissive“ laufen lassen

Nur, wenn es unbedingt sein muss, und man SELinux dann doch nicht komplett abschalten möchte:

semanage permissive --add httpd_t

Beispiel 7: Eigenes SELinux Module Package

Angenommen, es gibt wirklich keinen passenden File-Context und auch keinen Boolean, keine der obigen Möglichkeiten hilft - in diesem Fall muss man sich (am besten) anhand der extrahierten audit.log-Ausgaben ein eigenes SELinux Module Package bauen.

cd /tmp
audit2allow --why --input=myaudit.log
audit2allow --input=myaudit.log --module-package=myaudit
semodule --install myaudit.pp

Alternativ (und würde ich persönlich nicht empfehlen) lässt sich das ganze /var/log/audit/audit.log untersuchen: audit2allow --why --all.

audit2allow erzeugt im zweiten Schritt eine /tmp/myaudit.te-Datei, die die „Type Enforcement“-Regelwerke im Klartext enthält. Dort findet man Statements, die man jetzt dem Anfang dieses Kapitels besser zuordnen kann. semodule sorgt für die Compilierung in ein SELinux-Modul.

Das Prozedere aus Beobachten der audit.log, Extraktion der Zeilen, audit2why und, falls keine andere Möglichkeit, audit2allow plus semodule --install wird solange wiederholt, bis SELinux zufrieden ist. Das erinnert an das Vorgehen des Einrichtens einer Firewall, deren DENY- oder REJECT-Meldungen Hinweise auf gesperrte Ports geben.

Bemerkung

.pp-Dateien werden von file als bzip2-komprimiert ausgewiesen. Um an den Inhalt eines Policy Modules zu kommen, braucht es aber noch ein wenig mehr:

yum -y install bzip2
bunzip2 --keep policyfile.pp
yum -y install policycoreutils-devel
# extract mod (binary data) and file context file (text)
semodule_unpackage policyfile.pp.out policyfile.mod policyfile.fc
# get contents of mod file
sedismod policyfile.mod

Beispiel 8: Eigene Module Packages verteilen

SELinux Module Packages verteilt man am besten im .te-Format (Source Code), welches man wie in Beispiel 6 gezeigt erhält, und compiliert sie auf dem Zielsystem. Nachfolgend wird aufgeführt, wie eine .te-Datei aussieht und wie sie auf einem Zielsystem eingespielt wird.

cat >/tmp/myaudit.te <<EOF
module myaudit 1.0;

require {
    type initrc_tmp_t;
    type hi_reserved_port_t;
    type traceroute_t;
    class tcp_socket name_bind;
    class file open;
    class capability2 block_suspend;
}

#============= traceroute_t ==============
allow traceroute_t hi_reserved_port_t:tcp_socket name_bind;
allow traceroute_t initrc_tmp_t:file open;
allow traceroute_t self:capability2 block_suspend;
EOF
cd /tmp
checkmodule --mls -m --output myaudit.mod myaudit.te
semodule_package --outfile myaudit.pp --module myaudit.mod
semodule --install myaudit.pp

Troubleshooting

Fail to load SELinux policy. Freezing.

Wahrscheinlich ist das System falsch gelabelt. Kernel-Parameter selinux=0 beim Booten mitgeben, um SELinux abzuschalten. Anschliessend touch /.autorelabel && reboot ausführen, um das System komplett umlabeln zu lassen. Danach ist Geduld gefordert, wenn das System viele Dateien hostet.

Built on 2022-06-03