Bei einem teilweisen Ausfall auf einem Server sind nur einzelne Dienste des Systems betroffen. Dann steht mir, im Gegensatz zum Totalausfall, meist die gesamte Vielfalt an Werkzeugen zur Diagnose zur Verfügung.
Bevor ich mich wegen eines Problems an einem Server anmelde, habe ich bereits einige Informationen gesammelt.
Damit habe ich schon eine gewisse Vorstellung, wonach ich schauen werde, wenn ich mich anmelde.
Manche Informationen frage ich fast immer ab, wenn ich mich an einem Server anmelde. Damit bekomme ich einen intuitiven Einblick in den Allgemeinzustand des Servers und eventuell weitere Hinweise.
Das Programm uptime
zeigt mir neben der Systemzeit, wie lange der
Systemstart zurückliegt, wieviel Benutzer angemeldet sind und die
durchschnittliche Systemlast der letzten, der letzten fünf und
der letzten fünfzehn Minuten.
Eine Abweichung der Systemzeit erschwert die Korrelation von Lognachrichten. Außerdem kann sie bei einigen kryptographischen Protokollen, wie zum Beispiel kerberos, die Verbindung stören.
Die Zeit seit dem letzten Neustart weist auf vorangegangene Probleme hin, wenn sie unerwartet kurz ist, weil der Rechner außerplanmäßig neu gestartet wurde.
Die Anzahl der Benutzer zeigt mir an, ob ich mit jemand Kontakt aufnehmen muss, bevor ich schwerwiegende Eingriffe in das System vornehme.
Die Last schließlich zeigt an, wieviel Prozesse in dem betreffenden Zeitraum durchschnittlich auf Rechenzeit gewartet haben. Ein Durchschnittswert, der kleiner ist als die Anzahl der Prozessoren bedeutet, dass die Prozessoren Freilauf hatten, der Server nicht ausgelastet war. Ist der Durchschnittswert größer als die Anzahl der Prozessoren bedeutet das, dass während des betreffenden Zeitabschnitts mehr Rechenleistung benötigt wurde als zur Verfügung stand. In diesem Fall ist der Trend interessant, den ich durch Vergleich der Last für die drei Zeiträume erkennen kann. Bei steigender Last versuche ich für Entlastung zu sorgen. Ist der Trend fallend, brauche ich nichts unmittelbar zu unternehmen, außer die Last weiter im Auge zu behalten. Das System kommt nach einer Lastspitze gerade von selbst wieder in den normalen Betriebsbereich. Natürlich werde ich versuchen die Ursache der Lastspitze zu ermitteln, um zu entscheiden, welche präventiven Maßnahmen ich für die Zukunft ergreifen will.
Eine Alternative zu uptime ist das Programm w
,
das in der ersten Zeile die gleichen Daten wie uptime anzeigt.
In den nachfolgenden Zeilen zeigt es, wer gerade angemeldet ist, wann er das
letzte Mal etwas eingegeben hat und welches Programm er zuletzt aufgerufen hat.
Damit weiß ich, mit wem ich Kontakt aufnehmen muss, falls jemand
anderes angemeldet ist.
Das Programm w
entnimmt, ebenso wie das Programm who
die Informationen
über angemeldete Benutzer der Datei /var/log/utmp.
Falls ich den Beginn des Problems mit letzten Änderungen am System korrelieren
will, rufe ich das Programm last
auf.
Dieses Programm zeigt an, wer wann und wie lange am System angemeldet war und
außerdem die letzten Systemstarts.
Meist begrenze ich die Anzahl der angezeigten Zeilen, da mich nur die
letzten Anmeldungen interessieren:
$ last -5
mathias pts/2 ..Thu Nov 7 07:38 still...
mathias pts/1 ..Thu Nov 7 07:33 still...
mathias pts/0 ..Thu Nov 7 07:21 still...
reboot system boot..Thu Nov 7 06:48 - 07:57...
mathias pts/1 ..Wed Nov 6 09:10 - 11:49...
Diese Daten entnimmt last
der Datei /var/log/wtmp.
Die Datei wird üblicherweise am Monatsanfang umbenannt in /var/log/wtmp.1,
so dass immer nur die Daten des laufenden Monats zur Verfügung stehen.
Will ich die Anmeldungen des Vormonats sehen, gebe ich dieses explizit an:
$ last -f /var/log/wtmp.1
Wenn nur ein Konto für alle Anmeldungen verwendet wird, kann ich mit history
nachschauen, was zuletzt gemacht wurde.
Bei mehreren Konten schaue ich in der Datei .bash_history im
Benutzerverzeichnis des in der fraglichen Zeit angemeldeten Benutzers nach.
Falls der Benutzer die csh verwendet, schaue ich stattdessen in .history.
Mit dem Programm vmstat
bekomme ich ein Gefühl dafür, wie es einer
Maschine im Moment gerade geht.
Beim Anmelden an der Maschine rufe ich dieses oft wie folgt auf:
$ vmstat -SM 1 5
Das kostet mich etwa 5 Sekunden, bis die vollständige Ausgabe erscheint:
procs --memory--- -swap- ---io-- -system- ----cpu----
r b swpd..cache si so bi bo in cs us sy id wa
1 0 0.. 651 0 0 113 14 105 192 3 2 94 1
0 0 0.. 651 0 0 0 0 78 192 2 0 98 0
0 0 0.. 651 0 0 0 0 88 231 1 1 98 0
0 0 0.. 651 0 0 0 0 73 189 1 1 98 0
0 0 0.. 651 0 0 0 0 91 201 1 1 98 0
Die erste Zeile zeigt die Durchschnittswerte des Systems an. Ich konzentriere mich auf die folgenden vier Zeilen, die die Werte der jeweiligen Sekunde anzeigen.
Um mich mit dem Rechner vertraut zu machen, das heißt vor einem Problem, schaue ich mir die Spalten unter system und cpu an. Insbesondere die Anzahl der Interrupts (in) und Kontextwechsel (cs) sowie der Prozentwert der CPU-Idle-Zeit (id) geben mir eine Idee dafür, was auf dem betreffenden Rechner “normal” ist. Diese Normalwerte kann ich auch in der Rechnerdokumentation festhalten.
Weichen diese Werte signifikant ab, hat der Rechner ein Lastproblem und ich
schaue mir die anderen Werte an.
Außerdem lasse ich dann das Programm oft mit den Optionen vmstat -SM 1
dauerhaft in einer Extra-Konsole laufen um meine Bemühungen, die Rechnerlast
zu reduzieren, verifizieren zu können.
Sind unter swap Werte ungleich 0 zu sehen, heißt das dass das Betriebssystem in diesem Moment Hauptspeicher aus- (so) oder einlagert (si). Das deutet auf Speicherprobleme hin.
Der nächste Befehl, der mir hilft ein Bild über den Zustand der
Maschine zu bekommen, ist free
.
$ free -m
total used free shared buffers c...
Mem: 7734 3466 4267 0 676 ...
-/+ buffers/cache: 1432 6301
Swap: 5153 0 5153
Dieser Befehl zeigt mir den insgesamt vorhandenen, den benutzten und den freien Speicher an und außerdem, wieviel Speicher der Kernel gerade für Dateicaches verwendet. Mich interessiert vor allem die letzte Zeile, die angibt, wieviel Auslagerungsspeicher zur Verfügung steht und wieviel der Kernel bereits ausgelagert hat. Gerade bei hoher Systemlast sorgt auf Festplatten ausgelagerter RAM für eine weitere spürbare Verlangsamung des Systems, so dass ich hier schnell für Abhilfe sorgen möchte.
Der vierte Befehl schließlich ist df
.
Dieser zeigt mir den verfügbaren Platz auf den Dateisystemen der eingehängten
Partitionen an.
Ich rufe diesen Befehl zweimal auf, einmal mit Option -i
, für die Statistik
der Inodes und einmal ohne für die Belegung des Plattenplatzes.
Die Option -h
ist für die Ausgabe in lesbarer Form,
zum Beispiel 586G statt der Anzahl in Bytes.
$ df -h
Filesystem Size Used Avail Use% Mounted on
/dev/sda1 586G 207G 350G 38% /
...
$ df -hi
Filesystem Inodes IUsed IFree IUse% Mounted on
/dev/sda1 37M 2.7M 35M 8% /
...
Mich interessiert die Spalte mit der prozentualen Nutzung. Hat diese 100% erreicht, muss ich mit Ausfällen von Diensten rechnen, die, für sich betrachtet, nicht gleich auf ein Problem mit dem Plattenplatz gedeutet hätten. Ab einer Nutzung von 80-90% mache ich mir Gedanken, wie ich für zusätzlichen verfügbaren Plattenplatz sorgen kann.
Die verfügbaren Inodes in einem Dateisystem werden seltener aufgebraucht. Dazu müssten auf dem Dateisystem viele kleine Dateien angelegt sein. Bei Mailservern mit Postfächern im Maildir-Format oder bei Newsservern kann das vorkommen.
Platzprobleme auf den Laufwerken können sich auf verschiedene Art bemerkbar machen, noch bevor ich sie beim Besuch des Rechners entdecke. Das Problem ist, dass sie von außen meist nicht als solche erkennbar sind, weil nur ein oder wenige Dienste den Betrieb einstellen. Insbesondere, wenn die Platzprobleme durch große Protokolldateien verursacht werden, kann es passieren, dass genügend Platz ist, wenn ich mich anmelde und nachsehe. In der Nacht werden die Protokolldateien rotiert und dadurch wieder Platz geschaffen, bis der Plattenplatz erneut aufgebraucht ist. Aus diesem Grund ist es sinnvoll, den Plattenplatz automatisch, zum Beispiel von Nagios, überwachen zu lassen.
Der letzte Befehl, pstree
, zeigt mir, ob die notwendigen Prozesse laufen und
ob gegebenenfalls Prozesse zuviel sind.
Die Ausgabe des Befehls sieht auf jeder Maschine anders aus, entsprechend den
Diensten, die diese anbietet.
Mit etwas Erfahrung sehe ich damit schon intuitiv, ob ein Rechner “krank” ist.
$ pstree -A
init-+-courierlogger---authdaemond---5*[auth...
|-2*[courierlogger---couriertcpd]
|-cron
|-getty
|-master-+-pickup
| |-qmgr
| `-tlsmgr
|-ntpd
|-portmap
|-rsyslogd---3*[{rsyslogd}]
|-sshd---sshd---sshd---bash---pstree
|-6*[stunnel4]
`-udevd
Der Befehl ps xjf
, auf Systemen, die pstree
nicht installiert haben, ist
weniger übersichtlich und nur ein schwacher Ersatz.
Mit diesen ersten Befehlen, die ich gewohnheitsmäßig nach der Anmeldung am Server aufrufe, bekomme ich schnell eine intuitive Einschätzung - ein Gefühl - für den Zustand des Systems. Alle weiteren Schritte hängen vom konkreten Problem und der Situation auf dem Rechner ab.
Bei dem ersten vom Kernel gestarteten Prozess, habe ich es je nach Alter und Distribution des Linux-Systems mit einem der folgenden Programme zu tun:
Bei sehr alten Systemen besteht noch die Möglichkeit, dass ein BSD-Init im Einsatz ist. Ehrlich gesagt, habe ich das schon sehr lange nicht mehr gesehen.
SysVInit ist einer der ältesten Init-Dämonen unter Linux. Er wird über die Datei /etc/inittab gesteuert. Bei diesem Dämon läuft der Rechner in einem von mehreren möglichen Runleveln, die bestimmten Systemzuständen entsprechen. Je nachdem, in welchem Zustand sich das System gerade befindet, startet oder beendet SysVInit die zugehörigen Dienste.
Die Runlevel selbst unterscheiden sich zwischen den Distributionen. Die folgende Zuordnung trifft jedoch für die meisten zu:
0 | aus, hinunterfahren, beenden |
1/S | Systemstart, Single-User |
2-5 | Multi-User, mit Netzwerk, mit graphischer Oberfläche |
6 | Neustart |
Den aktuellen Runlevel kann ich mit dem Befehl runlevel
erfragen.
Für jeden Runlevel $rl
gibt es ein Verzeichnis /etc/rc$rl.d/, in dem sich
Skripts zum Beenden von Diensten (K…) und zum Starten von Diensten
(S…) befinden.
Bei jedem Wechsel ruft init
ein Skript auf, das im Wesentlichen
nichts anderes macht, als die Skripts deren Namen mit K beginnt, mit der
Option stop
aufzurufen und die Skripts, deren Name mit S beginnt, mit der
Option start
aufzurufen.
Vergleichbar diesem Shellcode:
for k in /etc/rc$rl.d/K*; do
$k stop
done
for s in /etc/rc$rl.d/S*; do
$s start
done
Die Reihenfolge, in der die Skripts aufgerufen werden, hängt vom Namen der
Skripts ab.
Dieser setzt sich zusammen aus dem Buchstabe K
oder S
, einer
zweistelligen Zahl und dem eigentlichen Namen des Skripts.
Die Zahl bestimmt die Reihenfolge und bei gleicher Zahl der Name.
Wenn SysVInit Dienste parallel startet, dann diejenigen mit der selben Zahl.
Üblicherweise liegt das Skript im Verzeichnis /etc/init.d/ oder /etc/rc/init.d/ während sich in den Verzeichnissen für die Runlevel nur ein symbolischer Link darauf befindet, dessen Name der obigen Konvention entspricht.
Etwas komplizierter, als eben beschrieben, verläuft das Starten und Anhalten
von Diensten, wenn SysVInit mit Parallelverarbeitung läuft.
In diesem Fall müssen die Abhängigkeiten für die Startreihenfolge in allen
Skripts in Kommentarkopfzeilen beschrieben sein.
Diese Kopfzeilen beschreibt die Handbuchseite zu insserv
.
Für das Aktivieren und Deaktivieren von Boot-Skripts empfiehlt sich auf
Debian-basierenden Systemen das Programm update-rc.d
, welches die
erforderlichen Links von /etc/rc$rl.d/ zu den Skripts in /etc/init.d/
anlegt.
Dieses Skript kann auf zweierlei Arten aufgerufen werden.
Im älteren (Legacy-)Modus gebe ich Reihenfolge und Runlevel auf der
Kommandozeile vor.
Im Default-Modus bestimmt es mittels insserv
die Abhängigkeiten aus den
LSB-konformen Kommentarkopfzeilen der Skripts und berechnet damit die
Reihenfolge.
Sollte ich eine andere Reihenfolge benötigen, brauche ich nicht die Zeilen im Skript selbst ändern, sondern kann diese mit Dateien im Verzeichnis /etc/insserv.override/ überschreiben.
Insserv funktioniert auch mit dem vorwiegend für Ubuntu entwickelten und zu SysVInit kompatiblen Programm Upstart zusammen. Upstart arbeitet ereignisorientiert mit Jobs, die über Dateien in /etc/event.d/ gesteuert werden. Dabei gibt es keine feste Reihenfolge. Tritt ein Ereignis auf, startet Upstart parallel alle Jobs, die darauf gewartet haben.
Eine einfache Job-Datei könnte so aussehen:
start on event1
stop on event2
exec /path/to/script
Mit start on
und stop on
gebe ich an, bei welchen Ereignissen der Job
gestartet oder angehalten werden soll.
Nach exec
folgt das Programm, welches Upstart aufruft.
Dabei erwartet Upstart im Gegensatz zu SysVInit, dass das Programm im
Vordergrund läuft, so dass Upstart mitbekommt, wenn der Prozess endet und
dann entsprechende Ereignisse, wie stopped $job
erzeugen kann.
Von Hand kann ich die Jobs mit
# initctl start $job
starten und mit
# initctl stop $job
beenden.
$ initctl list
avahi-daemon start/running, process 657
...
ureadahead stop/waiting
zeigt mir den Zustand aller bekannten Jobs und
$ initctl status dbus
dbus start/running, process 599
zeigt den Status eines einzelnen Jobs, in diesem Fall von dbus.
Beim Beenden eines Jobs kümmert sich Upstart nur um den per exec
angegebenen
Prozess.
Diesem sendet es zunächst ein SIGTERM und, falls er sich nicht schnell genug
beendet, ein SIGKILL.
Mit den Befehlen pre-stop
und post-stop
in der Beschreibung des Jobs kann
ich Befehle angeben, die Upstart vor und nach Beenden des Dienstes ausführen
soll.
Damit kann ich das System aufräumen und übrig gebliebene PID-Dateien entfernen.
Analog gibt es mit pre-start
und post-start
Anweisungen für den Start
eines Jobs.
Allerdings wird post-start
gemeinsam mit dem Job ausgeführt, da Upstart
nicht einschätzen kann, wann genau der Job gestartet ist und läuft.
Anstelle von exec
oder eben genannter Befehle kann ich auch einen
Befehlsblock setzen, den ich mit script
und end script
einfasse.
pre-start script
if [ ! -e /var/run/job ]; then
mkdir -p /var/run/job
fi
end script
Eine weitere Möglichkeit in die Steuerung von Upstart einzugreifen, ist das Erzeugen eines Ereignisses mit
# initctl emit $event
Damit kann ich zum Beispiel über Udev Reaktionen auslösen, wenn ein Stück Hardware angesteckt oder abgezogen wird.
Für die Fehlersuche wichtiger ist, dass ich damit gezielt einzelne Details von Upstart untersuchen kann, indem ich ein Ereignis vorgebe und die Reaktion der Programme beobachte.
Mit Systemd ist nach Upstart ein zweites modernes Programm für die Systeminitialisierung und Ressourcenverwaltung am Start. Neben dem parallelen Start von Hintergrunddiensten, den bereits Upstart und ansatzweise auch SysVInit beherrschen, ist ein wesentliches Merkmal von Systemd, dass ich Abhängigkeiten zwischen Diensten nicht explizit festlegen muss.
Damit Systemd Hintergrunddienste ohne explizite Konfiguration der Abhängigkeiten parallel starten kann, nutzt es die sogenannte Socket-Aktivierung. Das bedeutet, dass Systemd die Sockets anlegt, über die die verschiedenen Dienste miteinander kommunizieren, wie /dev/log für den Protokolldienst. Anschließend übergibt es die Sockets den Diensten beim Start. Der Kernel blockiert einen Prozess, der in einen Socket schreibt, bis ein anderer Prozess aus diesem Socket liest. Umgekehrt wird ein lesender Prozess blockiert, bis ein anderer Prozess in den Socket geschrieben hat. Auf diese Weise aktiviert der Kernel die einzelnen Dienste automatisch, wenn die Gegenstelle am Socket verfügbar ist.
Das hat den zusätzlichen Vorteil, dass ich einen abgestürzten Dienst einfach neu starten kann, ohne dass andere mit dem Socket verbundene Programme die Verbindung verlieren. Da der Kernel eingehende Anfragen puffert, kann der neue Dienst dort fortfahren, wo der alte aufgehört hat.
Die Aufgaben von Systemd sind in Units organisiert. Für jede Aufgabe benötige ich eine Konfigurationsdatei für die entsprechende Unit. Diese Dateien liegen im Verzeichnis /lib/systemd/system/. Gibt es eine gleichnamige Datei unter /etc/systemd/system/, ignoriert Systemd die Datei unter /lib/. So ist es möglich, die Konfiguration dauerhaft an das eigene System anzupassen, ohne Gefahr zu laufen, dass die Änderungen bei der nächsten Aktualisierung des Systems überschrieben werden.
Mit dem Befehl systemctl
kann ich mir eine Liste der Units ausgeben lassen.
Es gibt verschiedene Typen von Units, die Systemd an der Endung des Dateinamens erkennt:
Beim Hochfahren des Systems ruft Systemd die Unit namens default.target
auf.
Das ist meist nur ein Link auf eine andere Unit wie zum Beispiel
multi-user.target
.
Systemd steckt jeden Dienst beim Start in eine nach dem Dienst benannte Control Group, eine Prozessgruppe. Bei modernen Kerneln kann ich darüber Prozesse isolieren und Ressourcen steuern. Da Kindprozesse die Gruppenzugehörigkeit erben, können diese Prozessgruppen als Einheit verwaltet werden und alle zugehörigen Prozesse beim Beenden eines Dienstes zuverlässig beendet werden.
Sehr häufig setze ich das Programm systemctl
bei der Fehlersuche ein.
Ohne Argumente zeigt dieses mir eine Liste der Units an.
Da das sehr viele sein können, kann ich mit den entsprechenden Optionen die
Auswahl einschränken.
# systemctl --type=service
Dieser Befehl zeigt mir alle Service-Units an.
systemctl status ntpd.service
Damit bekomme ich weitere Informationen zum NTP-Dienst.
Unter anderem den Zeitpunkt des Abbruchs und den Fehlercode, bei Units,
die in der Liste als failed
markiert sind.
Wie bereits erwähnt fassen Target-Units mehrere Units zusammen. Diese bieten damit ein Konzept, das den Runlevels bei SysVInit ähnelt. Ohne weitere Angaben aktiviert Systemd die Unit default.target. Will ich beim Systemstart eine andere Unit, kann ich diese im Bootloader auf der Kernel-Kommandozeile angeben:
systemd.unit=rescue.target
Will ich im Betrieb umschalten, geht das zum Beispiel mit
# systemctl isolate multi-user.target
Um eine Unit permanent zum Standard zu machen, passe ich den symbolischen Link
von default.target
an, so dass er auf die gewünschte Unit verweist.
Mit dem Befehl show
liefert systemctl weitere Informationen zu den laufenden
Units.
Um eine Service-Unit permanent zu deaktivieren, verwende ich
# systemctl disable $unit.service
Das wirkt sich jedoch erst auf den nächsten Systemstart aus. Um den Dienst sofort zu beenden oder zu starten, verwende ich
# systemctl stop $unit.service
# systemctl start $unit.service
Um herauszufinden, welche Prozesse zu einem Dienst gehören, kann ich
systemd-cgls
verwenden, oder
# ps xaw -eo pid,args,cgroup
Treten Probleme beim Systemstart auf, kann ich die folgenden Kernelparameter beim Bootloader angeben:
systemd.log_target=kmsg systemd.log_level=debug
Damit landen die Fehlermeldungen im Kernelpuffer und können später mit dmesg
betrachtet werden.
Über die systemctl-Befehle poweroff
, halt
und reboot
kann systemd
das System ausschalten, anhalten und neu starten.
Mit systemctl kexec
kann ich darüber hinaus direkt einen vorkonfigurierten
Kernel vom laufenden Kernel starten lassen und so das System unter Umgehung
von BIOS und Bootloader neu starten.
Seit einiger Zeit enthält Systemd ein Journal, welches die Meldungen
aufzeichnet, die die Dienste an den Syslog-Dämon senden oder auf Kommandozeile
ausgeben.
Diese Meldungen kann ich mit dem Befehl journalctl
ansehen und filtern.
# journalctl -u sshd
zeigt zum Beispiel alle Meldungen des SSH-Dienstes an. Journalctl kann die Ausgabe auch nach einzelnen Programmen oder Geräten filtern:
# journalctl /sbin/dhclient
# journalctl /dev/sd?
Bei einigen Distributionen, Fedora wäre zu nennen, kann Systemd bereits aus dem Initramfs starten.
Seit einiger Zeit ist es möglich Timer-Units anzulegen, die andere Units zu
bestimmten oder wiederkehrenden Zeitpunkten starten können.
Damit kann Systemd Aufgaben von cron
und at
übernehmen.
Der Rechner fährt hoch, ich kann mich anmelden, auf den ersten Blick scheint alles in Ordnung. Bis mich Nagios oder ein Kunde darauf aufmerksam macht, dass mindestens ein Dienst nicht funktioniert.
Also noch einmal anmelden und den Dienst von Hand starten.
Wie das genau geht, hängt vom init
Dämon ab,
darauf bin ich im vorigen Abschnitt eingegangen.
Funktioniert der Dienst jetzt, ist zunächst alles in Ordnung, die Kunden können arbeiten. Es bleibt jedoch die Ungewissheit, warum der Dienst nicht automatisch startete.
Wenn ich keinen offensichtlichen Grund finde, warum ein Dienst nicht lief, also keine Hinweise in den Logs finde und auch niemand den Dienst abgeschaltet hatte, muss ich den Startvorgang des Dienstes beim Systemstart untersuchen.
Lässt sich der Dienst problemlos von Hand starten, kommen folgende Gründe in Frage:
Bei einem Problem mit einem fehlenden anderen Dienst experimentiere ich mit der Reihenfolge.
Als erstes schalte ich die Protokollierung des Bootvorgangs ein.
Bei Systemen, die auf Debian basieren und SysVInit verwenden kann ich
dafür in der Datei /etc/default/rcS die Variable VERBOSE=yes
setzen.
Oft kann ich dann an Hand der Reihenfolge, in der die Dienste starten,
schon sehen, was nicht in Ordnung ist.
Wie ich die Reihenfolge anpasse, steht bei der Beschreibung der init
Programme.
Rechteprobleme bei Startskripten sind eher selten, da init
diese mit
UID=0
startet, genauso, wie ich sie von Hand aufrufe.
Denkbar sind Probleme mit Mandatory Access Control Systemen, wie SELinux,
wenn die Skripts in unterschiedlichem Kontext laufen, je nachdem, ob ich
sie von Hand starte oder init
sie startet.
Manchmal startet ein Dienst nicht, weil eine benötigte Ressource belegt ist.
Das kann ein TCP- oder UDP-Socket an einem bestimmten Port sein, der von
einem anderen Prozess belegt ist oder eine Geräteschnittstelle, die von
einem anderen Programm benutzt wird und über eine Datei in /var/lock/
gesperrt wird.
Wenn ich Glück habe, finde ich einen Hinweis darauf in den
Systemprotokollen.
Andernfalls kann ich das mit strace
entdecken.
Startet ein Dienst auch nicht, wenn ich ihn von Hand starte, dann schaue ich als erstes in die Systemprotokolle. Gegenüber dem Systemstart muss ich viel weniger Zeilen auswerten, um die gesuchten zu finden. Ich kann den Startzeitpunkt selbst festlegen und muss nur einen begrenzten Zeitabschnitt untersuchen.
Finde ich trotzdem nichts, so greife ich auf die Shell und auf strace
als Werkzeuge zurück.
Starte ich ein Shellscript nicht einfach über seinen Namen, sondern mit
$ sh -x /$path/$to/$script
zeigt die Shell jeden aufgerufenen Befehl, und jede gesetzte Variable an. In den meisten Fällen sehe ich dann schon, was den Fehler verursacht.
Bei Skripts für die Bourn Again Shell, die deren Erweiterungen nutzen,
muss ich bash
als Interpreter angeben.
$ bash -x /$path/$to/$script
Reicht auch das nicht und ich weiß nur, welches Binärprogramm nicht
richtig arbeitet, aber nicht warum, dann setze ich strace
ein.
Damit finde ich auch in schwierigen Fällen oft die Ursache eines Problems.
In vielen Fällen ist es eine Datei, die sich nicht am erwarteten
Platz befindet oder ein Problem mit Zugriffsrechten.
Statt strace
kann ich ltrace
verwenden, das außer Systemaufrufen
auch Bibliotheksaufrufe protokolliert und somit den Prozess noch
detaillierter beobachtet.
Dafür muss ich dann mehr Protokollzeilen auswerten, was
das Finden der relevanten Zeilen nicht einfacher macht.
Ein Indiz für ein Zugriffsproblem ist zum Beispiel ein fehlgeschlagener Systemaufruf, den ich in der Ausgabe von strace finden kann, wie diesen hier:
open("abc", O_RDONLY) = -1 EACCES (Permission denied)
Um Zugriffsprobleme analysieren zu können, muss ich die Grundlagen der Zugriffskontrolle verstehen. Außerdem ist ein minimales Verständnis der Rolle von Dateien und Verzeichnissen nötig, wie im Grundlagenkapitel zu Linux gegeben.
Ich beginne meine Betrachtungen mit der traditionellen benutzerbestimmten Zugriffskontrolle (discretionary access control, DAC) und komme danach zu erweiterten Dateiattributen, Capabilities, AppArmor und SELinux.
Allen Mechanismen für Zugriffskontrolle gemeinsam ist, dass in dem Moment die Zugriffsrechte geprüft, und gewährt oder verweigert werden, in dem ein Subjekt - ein Prozess - eine Aktion, wie zum Beispiel Lesen oder Schreiben, auf ein Objekt - eine Datei - anwenden will,
Im traditionellen Zugriffsmodell führt das Dateisystem die Informationen über die Zugriffsrechte zusammen mit den Informationen über den Eigentümer und die Gruppe der Datei im Inode, und zwar als Bitmap mit je drei Bits, die die grundlegenden Rechte für den Dateieigentümer, die Gruppe sowie alle anderen bestimmen. In dieser Reihenfolge prüft der Kernel auch die Rechte gegen die UID und GID des Prozesses.
Mit dem Leserecht (r, read) darf ein Prozess Daten aus regulären und Gerätedateien lesen und Verzeichnisse auflisten.
Hat ein Prozess das Schreibrecht (w, write), darf er Daten in reguläre Dateien und Gerätedateien schreiben. Bei Verzeichnissen bedeutet es, dass er Einträge neu anlegen beziehungsweise entfernen darf, unabhängig von den Rechten der Datei, auf die der betreffende Eintrag verweist. Somit ist es möglich, eine Datei zu löschen oder umzubenennen, auf die ein Prozess keine Schreib- oder Leserechte besitzt. Das wird klar, wenn ich mir vor Augen halte, dass ein Verzeichniseintrag nichts weiter ist, als ein Name für und ein Verweis auf eine Datei und nur insofern die Eigenschaften der Datei beeinflusst als im Inode gezählt wird, wie viele Verweise auf die Datei es gibt. Erst, nachdem der letzte Verweis entfernt wurde, gibt der Kernel den von der Datei belegten Speicherplatz wirklich frei.
Das Ausführrecht (x, execute) bei einer regulären Datei bedeutet, dass ein Prozess sie ausführen kann. Dabei ist bei einer Binärdatei nicht einmal das Leserecht auf diese Datei nötig. Bei einem ausführbaren Skript benötigt der Prozess das Leserecht, da der Skript-Interpreter die Datei lesen muss, um ihr Programm abzuarbeiten. Bei einem Verzeichnis bedeutet das x Bit, dass der Prozess in das Verzeichnis wechseln und darin verzeichnete Dateien benutzen darf. Dafür braucht er kein Leserecht, wenn er den Namen der Datei kennt.
Zusätzlich zu den Standardrechten gibt es drei Bits für Sonderrechte.
Ist bei einer ausführbaren Datei das setuid Bit gesetzt, ändert sich die UID
des ausführenden Prozesses zu der der Datei.
Ich erkenne das setuid Bit in der Ausgabe von ls
daran, dass
das x
bei den Benutzerrechten durch ein s
ersetzt ist:
$ ls -l /bin/su
-rwsr-xr-x 1 root root 36832 Sep 13 2012 /bin/su
Das setgid Bit bei einem Verzeichnis bewirkt, dass in diesem Verzeichnis
neu angelegte Dateien automatisch der Gruppe des Verzeichnisses anstelle der
aktiven Gruppe des erzeugenden Prozesses zugeordnet werden.
Dieses erkenne ich in der Ausgabe von ls
daran, dass das x
bei den
Gruppenrechten durch ein s
ersetzt ist:
$ ls -ld /var/mail/
drwxrwsr-x 2 root mail 4096 Jul 2 2008 /var/mail/
In Verzeichnissen mit gesetztem sticky Bit dürfen nur root oder der
Eigentümer einer Datei diese löschen.
Dieses Bit ist üblicherweise beim /tmp/ Verzeichnis gesetzt.
In der Ausgabe von ls
erkenne ich dieses Bit daran, dass das x
bei den
Rechten für alle durch t
ersetzt ist:
$ ls -ld /tmp/
drwxrwxrwt 16 root root 12288 Mai 21 08:17 /tmp/
Bei einer ausführbaren Datei bewirkte das sticky Bit früher, dass der Programmcode nach der Ausführung im Hauptspeicher verblieb. Das brachte Vorteile bei Programmen, die sehr häufig benutzt wurden und dann nicht mehr jedesmal von der Platte geladen werden mussten. Bei modernen Systemen ist das obsolet.
Ich kann die Zugriffsrechte durch Optionen beim Einhängen des Dateisystems
einschränken.
Diese Einschränkungen kann ich mit dem Befehl mount
ohne Parameter anzeigen
lassen.
Die Option noexec bewirkt, dass das x Bit keinen Effekt hat. Damit gekennzeichnete Dateien kann ich nun nicht einfach durch Angabe ihres Pfades starten.
Die Option nosuid bewirkt, dass das setuid Bit keinen Effekt hat. Auch diese Dateien werden dann immer mit der UID des aufrufenden Prozesses ausgeführt.
Um die traditionellen Zugriffsrechte einer Datei anzusehen kann ich das
Programm ls
mit der Option -l
verwenden:
$ ls -a -l
insgesamt 116
drwxr-xr-x 2 mathias mathias 4096... 5 10:19 .
drwxr-xr-x 5 mathias mathias 4096...30 22:26 ..
-rw-r--r-- 1 mathias mathias 34...30 22:26 .project
...
Das Programm zeigt mir in der ersten Spalte den Typ und die Rechte der Datei an, in der dritten Spalte den Eigentümer und in der vierten die Gruppe.
Mit dem Programm id
kann ich die UID und GIDs meiner Shell ermitteln.
Das Programm ps
zeigt mir diese für beliebige Prozesse an.
Um den Eigentümer einer Datei zu ändern, verwende ich das Programm chown. Diese Operation ist nur root erlaubt, da alle anderen Benutzer damit ihre Rechte an der Datei verlieren würden.
Die Gruppe einer Datei kann ich mit chgrp ändern. Das darf sowohl der Eigentümer der Datei als auch root.
Die Dateirechte ändere ich mit dem Programm chmod. Auch diese Operation darf nur der Eigentümer oder root.
Details zu den Programmen finden sich in den entsprechenden Handbuchseiten.
Wenn die Standardzugriffsrechte einer Datei in Ordnung sind, ich aber trotzdem keinen Zugriff habe, prüfe ich als nächstes die erweiterten Attribute des Dateisystems für diese Datei.
Zum Anzeigen verwende ich das Programm lsattr
:
$ ls -l tmp/abc
-rw-r--r-- 1 mathias mathias 4 Okt 9 11:11 tmp/abc
$ echo def > tmp/abc
bash: tmp/abc: Keine Berechtigung
$ lsattr tmp/abc
----i---------- tmp/abc
Ändern kann ich die Attribute mit dem Programm chattr
.
$ sudo chattr -i tmp/abc
$ echo def > tmp/abc
$ cat tmp/abc
def
Die Handbuchseite listet sämtliche Aufrufoptionen des Programmes und alle möglichen Dateiattribute auf. Ich gehe hier nur auf die wichtigsten für die Fehlersuche ein.
Bei Dateien mit dem Attribut a
kann ich den vorhandenen Text nicht
verändern, sondern lediglich neuen Text hinzufügen.
Dieses Attribut ist eine gute Wahl für Logdateien.
Dateien mit dem Attribut c
werden komprimiert auf der Platte abgelegt.
Das ist im Normalbetrieb nicht weiter von Belang.
Es beeinträchtigt aber die Möglichkeit, Dateien nach einem Plattencrash zu
retten, weil Programme, welche Dateien anhand ihrer Signatur erkennen, damit
nicht funktionieren.
Dateien mit dem Attribut i
kann ich nicht ändern, löschen oder umbenennen.
Ich kann keinen zusätzlichen Link anlegen und den Dateiinhalt nicht
ändern.
Bei Dateien mit dem Attribut s
überschreibt der Kernel beim Löschen alle
Blöcke mit 0x0
und schreibt diese vor dem Löschen der Datei auf die Platte
zurück.
Das heißt, ich kann diese Dateien nicht wiederherstellen.
Bei sensiblen Daten würde ich mich jedoch nicht darauf verlassen,
da mitunter die Elektronik des
Speichermediums Sektoren vor dem Betriebssystem verbirgt und meine
Anstrengungen, eine Datei sicher zu löschen, zunichte macht.
Demgegenüber werden bei Dateien mit dem Attribut u
beim Löschen Inode
und Dateiblöcke explizit vor weiterer Verwendung geschützt, so dass die
Datei auch bei intensiver Plattennutzung wieder gerettet werden kann.
Bei Dateien mit dem Attribut t
gibt es kein tail merge.
Beim Tail-Merging werden die Daten des letzten Dateiblocks für mehrere
Dateien in einen gemeinsamen Block geschrieben.
Damit kann zum Beispiel der Bootlader LILO nicht umgehen.
Mit dem Attribut t
für die Datei des Kernel-Images wird Tail-Merging
unterdrückt und es gibt keine Probleme mit dem Bootlader.
Bleibt zum Schluss nur noch anzumerken, dass ext2 und ext3 die Attribute
c
, s
und u
nicht honorieren.
Diese beiden Dateisystem verwenden auch kein Tail-Merging.
Das Ziel bei der Entwicklung der POSIX Capabilities war, die alles umfassenden Privilegien des root Benutzers aufzuteilen in einzelne Privilegien, die je nach Bedarf einzelnen Prozessen und/oder Programmen zugewiesen werden. Traditionell darf ein Prozess, der unter UID 0 läuft, in einem UNIX-System fast alles: auf alle Speicherbereiche, alle Geräte, alle Dateien zugreifen, Netzwerkschnittstellen direkt nutzen, …
Wenn ein Programm, wie ping, welches direkt auf die Netzwerkschnittstelle zugreift, nur eines dieser Privilegien benötigt, dann bekommt der Prozess, der es ausführt mit dem SUID-Bit alle anderen Privilegien ebenfalls. Gelingt es einem Angreifer, einen Programmfehler auszunutzen, kann er damit seine Privilegien erhöhen, also die Rechte von root erlangen.
Genau an dieser Stelle setzen die POSIX Capabilities an.
Auf einem System, das diese kennt, kann ich dem Programm ping das SUID-Bit
entfernen und stattdessen die Capability CAP_NET_RAW
vergeben.
Damit funktioniert das Programm wie vorher, bei einem ausgenutzten
Programmfehler gewinnt der Angreifer maximal genau dieses Privileg.
Wer sich tiefer in das Thema einarbeiten möchte, dem empfehle ich die Seite von Chris Friedhoff zum Thema POSIX Capabilities & File POSIX Capabilities. Hier zeige ich nur, wie man einen Einstieg in das Thema findet, um sich im Rahmen einer Fehlersuche ein Bild machen zu können. Trotzdem komme ich nicht um einige Grundlagen herum.
Welche Capabilities es im Einzelnen gibt, erfahre ich aus der Handbuchseite
capabilities in Sektion 7 (man 7 capabilities
), oder direkt aus der Datei
/usr/include/linux/capabilities.h.
Capabilities werden zum einen für ausführbare Dateien festgelegt und zum anderen für Prozesse. Sie können das Kennzeichen permitted (erlaubt), effective (aktiv) oder inheritable (vererbbar) haben. Alle Capabilities mit einem bestimmten Kennzeichen bilden das entsprechende Set.
Das Permitted Set einer Datei verleiht die entsprechenden Capabilities dem Prozess, der sie ausführt. Das Permitted Set eines Prozesses enthält alle Capabilities, die dieser Prozess verwenden darf.
Das Inheritable Set einer Datei geht nur dann in das Permitted Set eines Prozesses ein, wenn dieser die entsprechenden Kennzeichen ebenfalls in seinem Inherited Set hat. Damit ist es möglich, die an eine Datei vergebenen Capabilities nur ausgewählten Prozessen verfügbar zu machen, die über die entsprechenden inheritable Kennzeichen verfügen.
Das Effective Set einer Datei benötigt zusätzlich noch das effective oder inheritable Kennzeichen der einzelnen Capabilities. Dann setzt es das Effective Set des Prozesses für die Capabilities, die im Permitted Set des Prozesses enthalten sind.
Kommen wir zum praktischen Teil, am Beispiel von ping. Nachdem ich das SUID-Bit entfernt habe, funktioniert das Programm nicht mehr:
$ ls -l /bin/ping
-rwxr-xr-x 1 root root 35712 Nov 8 2011 /bin/ping
$ ping -c1 localhost
ping: icmp open socket: Operation not permitted
Die Fehlermeldung zeigt mir schon die Ursache, strace macht es noch einmal deutlich:
$ strace ping -c1 localhost 2>&1|grep EPERM
socket(PF_INET, SOCK_RAW, IPPROTO_ICMP) = -1 EPERM
Hier habe ich die Ausgabe von strace
nach EPERM
gefiltert, welches mir
Probleme mit den Zugriffsrechten anzeigt.
Die ausgegebene Zeile besagt, dass der socket()
Systemaufruf mit den
Parametern PF_INET
, SOCK_RAW
und IPPROT_ICMP
einen Fehler zurückgab
(Rückgabewert -1) und die Variable errno
auf EPERM gesetzt war, was
“Operation not permitted” bedeutet.
Details zum Programm strace
finden sich in
Kapitel 8, Informationen zum socket()
Systemaufruf und zur globalen Variable errno
finden sich in den
entsprechenden Handbuchseiten.
$ man 2 socket
$ man 3 errno
Mit dem Programm setcap kann ich ping die benötigte Capability verleihen:
$ sudo setcap cap_net_raw=ep /bin/ping
$ getcap /bin/ping
/bin/ping = cap_net_raw+ep
$ ping -c1 localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_req=1 ...
--- localhost ping statistics ---
1 packets transmitted, 1 received, 0% packet loss...
rtt min/avg/max/mdev = 0.107/0.107/0.107/0.000 ms
Und schon funktioniert es wieder. Damit kann jeder auf dem System ping verwenden, ohne dass dieses Programm mit den Rechten von root laufen muss. Will ich die Anzahl der Prozesse und/oder Benutzer einschränken, die das Programm nutzen können, verwende ich statt dem Kennzeichen permitted das Kennzeichen inheritable:
$ sudo setcap cap_net_raw=ei /bin/ping
$ getcap /bin/ping
/bin/ping = cap_net_raw+ei
$ /sbin/getpcaps $$
Capabilities for `4848': =
$ ping -c1 localhost
ping: icmp open socket: Operation not permitted
Da die File-Capability mit dem Kennzeichen inheritable nur wirkt, wenn auch der Prozess das Kennzeichen inheritable für diese Capability besitzt, fehlen mir die nötigen Rechte.
Diese kann ich beim Login am System, oder mit su
bekommen, wenn ich libpam-cap installiert habe.
Für su
füge ich die folgende Zeile in /etc/pam.d/su ein
auth required pam_cap.so
und “vererbe” mir via /etc/security/capability.conf die nötigen Capabilities:
$ egrep -v '^(|#.*)$' /etc/security/capability.conf
cap_net_raw mathias
none *
Nun muss ich mir noch die nötigen Rechte holen:
$ /sbin/getpcaps $$
Capabilities for `4848': =
$ su - mathias
Password:
$ /sbin/getpcaps $$
Capabilities for `22807': = cap_net_raw+i
$ ping -c1 localhost
PING localhost (127.0.0.1) 56(84) bytes of data.
64 bytes from localhost (127.0.0.1): icmp_req=1 ...
--- localhost ping statistics ---
1 packets transmitted, 1 received, 0% packet loss,...
rtt min/avg/max/mdev = 0.103/0.103/0.103/0.000 ms
Und schon funktioniert es wieder.
Halten wir fest, dass ich für die Analyse von Problemen vier Programme verwenden kann:
Und natürlich die Datei /usr/include/linux/capabilities.h beziehungsweise die Handbuchseite capabilities, die mir zeigen, welche Capabilities ich verwenden kann.
Auch bei AppArmor reiße ich das Thema nur kurz aus dem Blickwinkel Fehlersuche an. Für weiterführende Informationen zu AppArmor verweise ich auf die Projekt-Homepage.
Wie bei jedem Sicherheitssystem, dass unerlaubte Aktivitäten verhindern soll, kann es auch bei AppArmor vorkommen, das erlaubte Aktivitäten gestört oder verhindert werden. Habe ich bei einem Problem AppArmor im Verdacht, überprüfe ich als erstes, ob es aktiv ist:
# aa-status
apparmor module is loaded.
22 profiles are loaded.
20 profiles are in enforce mode.
/sbin/dhclient
...
/usr/share/gdm/guest-session/Xsession
2 profiles are in complain mode.
/usr/sbin/libvirtd
/usr/sbin/ntpd
3 processes have profiles defined.
1 processes are in enforce mode.
/usr/sbin/cupsd (727)
2 processes are in complain mode.
/usr/sbin/libvirtd (1667)
/usr/sbin/ntpd (1436)
0 processes are unconfined but have a profile defined.
AppArmor beschränkt nur Anwendungen, für die Profile definiert sind. Falls AppArmor nicht aktiviert ist oder keine Profile geladen sind, kann ich davon ausgehen, dass das Problem nicht von AppArmor verursacht wird.
Wenn AppArmor aktiviert ist, schaue ich als nächstes nach Nachrichten im Systemlog. Diese finde ich unter Ubuntu mit:
# grep type=1400 /var/log/syslog
Ist AppArmor aktiviert, aber ich sehe keine Logmeldungen, überprüfe ich den Audit-Modus:
# cat /sys/module/apparmor/parameters/audit
normal
Der Audit-Modus kann die folgenden Werte haben:
Um den Audit-Modus zu ändern, schreibe ich den gewünschten Modus in die Datei:
# echo -n all > /sys/module/apparmor/parameters/audit
AppArmor prüft nur Tasks (Prozesse), für die es ein Profil gibt. Dabei kennt es vier Modi:
Profile können Kennzeichen (Flags) enthalten, die das Audit beeinflussen.
Wenn ich ein Problem mit AppArmor untersuche, schaue ich zunächst, ob AppArmor
den betroffenen Prozess überhaupt beschränkt.
Das kann ich entweder mit ps
machen oder indem ich mir die entsprechenden
Attribute des Prozesses im proc Dateisystem anschaue:
# ps -Z 727
LABEL PID TTY STAT TIME COMMAND
/usr/sbin/cupsd 727 ? Ss 0:01 /usr/sbin/cupsd -F
# cat /proc/727/attr/current
/usr/sbin/cupsd (enforce)
# ps -Z 1667
LABEL PID TTY STAT TIME COMMAND
/usr/sbin/libvirtd 1667 ? Sl 0:00 /usr/sbin/li...
# cat /proc/1667/attr/current
/usr/sbin/libvirtd (complain)
# ps -Z 1
LABEL PID TTY STAT TIME COMMAND
unconfined 1 ? Ss 0:00 /sbin/init
# cat /proc/1/attr/current
unconfined
Ich kann das Programm aa-logprof
verwenden, um ein Profil anzupassen.
Dieses interaktive Programm durchsucht die Protokolldatei und schlägt bei
unbekannten AppArmor-Ereignissen Änderungen am jeweiligen Profil vor.
Am Ende schreibt es die Änderungen in die Profildatei und lädt die
geänderten Profile neu.
Falls nötig, aktualisiert es die Profile laufender Prozesse.
Alternativ kann ich die Profildatei unter /etc/apparmor.d/ mit einem Editor direkt bearbeiten und mit
# apparmor_parser -r /etc/apparmor.d/$profile
neu laden.
Wichtig! Ich kann zwar einem laufenden Prozess zusätzliche Rechte gewähren. Will ich aber einer laufenden Anwendung Rechte entziehen, habe ich nur die Möglichkeit, das Profil ganz zu entfernen, es anschließend mit weniger Rechten wieder hinzuzufügen und dann den Prozess neu zu starten.
Wenn ich ein Profil im Moment nicht anpassen kann, kann ich es in den Beschwerdemodus setzen. Entweder zeitweilig, bis zum Reboot:
# apparmor_parser -Cr /etc/apparmor.d/$profile
Oder permanent:
# aa-complain $profile
Dann sollte die entsprechende Anwendung sich verhalten, wie eine unbeschränkte, während AppArmor weiterhin Protokollnachrichten schreibt.
SELinux ist, wie AppArmor ein Mandatory Access Control System. Das heißt, die Vergabe von Rechten unter dem Einflussbereich von SELinux liegt nicht im Ermessensspielraum des Benutzers, sondern wird vom System durch Richtlinien und Regeln vorgegeben.
SELinux besteht aus einem Kernelmodul, unterstützenden Werkzeugen und Konfigurationsdateien. Damit kann ich die Zugriffskontrolle sehr feinkörnig einstellen. SELinux funktioniert dabei völlig unabhängig von den traditionellen Benutzernamen und Gruppen.
Um herauszufinden, ob auf dem untersuchten System SELinux läuft, schaue ich
nach, ob es ein Verzeichnis /selinux/ gibt und ob dieses Dateien enthält.
Der Befehl mount
sollte anzeigen, dass an diesem Punkt im Dateisystem ein
selinuxfs eingehängt ist:
$ mount | grep selinux
none on /selinux type selinuxfs (rw,relatime)
Mit sestatus
bekomme ich erste Informationen über den Zustand von SELinux
auf dem betrachteten System:
$ sudo sestatus
SELinux status: enabled
SELinuxfs mount: /selinux
Current mode: enforcing
Mode from config file: permissive
Policy version: 24
Policy from config file: default
SELinux kennt zwei Modi: Im Zwangsmodus (enforcing mode) verweigert der Kernel jede Aktion, für die SELinux die Erlaubnis verweigert. Im zulassenden Modus (permissive mode) gelten die Beschränkungen des traditionellen Rechtesystems, SELinux protokolliert nur verweigerte Aktionen.
Als root kann ich zwischen beiden Modi mit dem Programm setenforce
umschalten.
# setenforce 0
schaltet in den permissive Mode.
# setenforce 1
schaltet in den enforcing Mode.
Mit dem Kernelparameter enforcing=0
kann ich den permissive Mode bereits
beim Rechnerstart erzwingen.
Damit kann ich einem System temporär wieder auf die Beine helfen, dessen
Richtlinien und Regeln überhaupt kein Arbeiten mehr erlauben.
Eine Richtlinie (Policy) ist eine Sammlung von Vereinbarungen und Regeln, die dem SELinux-Kern sagen, was erlaubt ist, was nicht und wie er sich in verschiedenen Situationen verhalten soll.
Dabei unterscheidet man zwischen gezielten Richtlinien (targeted policy), die nur wenige Anwendungen einschränken und strengen Richtlinien (strict policy), die versuchen, alle Aktivitäten des Rechners mit SELinux zu kontrollieren.
Richtlinien werden kompiliert und können als Binärmodule jederzeit ge- und entladen werden. Beim Start des Rechners lädt init eine Anfangsrichtlinie (initial policy).
Das zweite wichtige Konzept bei SELinux ist der Kontext.
Jeder Prozess und Socket, jede Datei und Pipe ist mit einem Kontext markiert,
den ich zum Beispiel mit ps -Z
, oder ls -Z
erfragen kann.
$ ls -Z /etc/fstab
system_u:object_r:etc_t:s0 /etc/fstab
$ ps -Z
LABEL PID TTY TIME CMD
unconfined_u:unconfined_r:unconfined_t:s0-s0:\
c0.c1023 1215 pts/0 00:00:00 bash
unconfined_u:unconfined_r:unconfined_t:s0-s0:\
c0.c1023 1359 pts/0 00:00:00 ps
$ id
uid=1000(mathias) gid=1000(mathias) \
Gruppen=1000(mathias),4(adm),24(cdrom),25(floppy),\
27(sudo),29(audio),30(dip),44(video),46(plugdev) \
Kontext=unconfined_u:unconfined_r:unconfined_t:\
s0-s0:c0.c1023
Der Kontext ist unabhängig von der traditionellen UNIX-UID oder -GID.
Programme mit gesetztem SUID-Bit, su
oder sudo
ändern den Kontext
nicht:
$ id
uid=1000(mathias) gid=1000(mathias)
...
Kontext=unconfined_u:unconfined_r:unconfined_t:s0-s0\
:c0.c1023
$ sudo id
uid=0(root) gid=0(root) Gruppen=0(root)
Kontext=unconfined_u:unconfined_r:unconfined_t:s0-s0\
:c0.c1023
Der Kontext besteht aus den drei Teilen Benutzer, Rolle und Typ. Den Typ nennt man bei Prozessen Domain. Alle drei Teile sind nur Namen, die erst durch die Regeln einer Richtlinie eine Bedeutung für SELinux bekommen.
Der Kontext von Dateien wird in den erweiterten Attributen gespeichert.
Mit chcon
kann ich den Kontext einer Datei temporär ändern.
Für dauerhafte Änderungen verwende ich setfiles
.
Auf Dateisystemen ohne erweiterte Attribute, wie VFAT, ISO, NFS oder Samba,
bekommen alle Dateien einen einheitlichen Kontext, entsprechend den Optionen
beim Einhängen mit mount
.
Die wichtigste Eigenschaft von SELinux ist, dass es alle Aktionen
protokolliert, sowohl genehmigte als auch abgewiesene Aktionen.
Im laufenden Betrieb ist es in den meisten Fällen nicht notwendig, jede
einzelne Aktion zu protokollieren, die genehmigten sind meist uninteressant.
Mit dontaudit
kann ich diese von der Protokollierung ausnehmen.
Wo ich die Protokolle von SELinux finde, hängt von der benutzten Distribution ab, meist finde ich sie unterhalb von /var/log/. Läuft auf dem System der Linux Audit Dämon, finde ich die Protokolle in /var/log/audit/audit.log oder /var/log/audit.log. Andernfalls suche ich nach einer Datei avc.log.
Beim Betrachten der Audit-Protokolle gilt es ein paar Dinge im Kopf zu behalten:
dontaudit
Anweisungen
von der Protokollierung ausnehmen.In den SELinux-Tutorials im Gentoo Wiki findet sich eine ausführliche Anleitung zur Auswertung der SELinux Protokolle.
Betrachten wir eine Ablehnung im audit.log des Audit-Dämons,
die ich am Text type=AVC
am Zeilenanfang erkenne:
type=AVC \
msg=audit(1384529907.797:27): \
avc: \
denied \
{ execute } \
for pid=2347 \
comm="hello" \
path="/lib/i686/cmov/libc-2.11.3.so" \
dev=sda1 ino=105872 \
scontext=unconfined_u:unconfined_r:haifux_t:s0-s0\
:c0.c1023 \
tcontext=system_u:object_r:lib_t:s0 tclass=file
AVC steht für Access Vector Cache.
Die einzelnen Teile bedeuten:
type=AVC
Der Protokolltyp. Diesen finde ich nur in der Datei audit.log.
msg=audit(1384529907.797:27)
Der Zeitstempel in Sekunden seit epoch, also seit dem ersten Januar 1970.
Diesen kann ich mit date -d @1384529907
in ein besser lesbares Format
umwandeln.
Nochmal der Protokolltyp, also ein AVC-Eintrag.
Wie SELinux entschieden hat, entweder denied oder granted. Im permissive Mode steht hier ein denied, auch wenn die Operation ausgeführt wurde.
{ execute }
Die Operation, für die um Erlaubnis gefragt wurde. Das können auch mehrere Operationen sein.
Die ID des Prozesses, der die Aktion ausführen wollte.
Der Befehl (ohne Optionen und auf 15 Zeichen beschränkt), den der Prozess ausführt, dessen Operation abgewiesen wurde.
Die Zieldatei der Operation.
Dazu muss ich wissen, dass
/lib/i686/cmov/libc-2.11.3.so die Standard-C-Bibliothek ist, die das
Programm hello als eine der ersten öffnet und mit mmap() und dem
Argument PROT_EXEC
in seinen Speicherbereich einblenden will.
Das Gerät (Dateisystem), auf dem sich die Zieldatei der Operation befindet.
Die Inodenummer auf dem Gerät.
Um die entsprechende Datei zu finden, ermittle ich zunächst den Mountpoint
des Gerätes und verwende dann find
:
# mntpnt="$(mount|grep sda1\ on|cut -f3 -d\ )"
# find $mntpnt -xdev -inum 105872
Der Quellcontext (source context) des Prozesses, die Domain.
Der Zielkontext (target context) der Ressource, auf die zugegriffen werden soll, in diesem Fall die Datei.
Ich hatte schon angedeutet, dass ich Ablehnungen, welche
das Verhalten einer Anwendung nicht beeinflussen, mit dontaudit
Anweisungen von der Protokollierung ausnehmen kann.
Sollte ich bei meiner Fehlersuche den Verdacht haben, dass eine dieser versteckten Ablehnungen mein Problem verursacht, schaue ich als erstes nach, wie viele es überhaupt gab:
# seinfo --stats|grep audit
Auditallow: 19 Dontaudit: 4601
Möchte ich die versteckten Ablehnungen im Protokoll sehen, dann kann ich mit
semodule die dontaudit
Anweisungen deaktivieren:
# semodule --disable_dontaudit --build
Habe ich genug davon, aktiviere ich sie wieder:
# semodule --build
Damit habe ich einen Einstieg in die Fehlersuche bei Problemen mit SELinux. Natürlich kann ich damit noch nicht alle Probleme lösen, doch hilft es zumindest bei den ersten Schritten.
Wenn ganze Dateisysteme nicht verfügbar sind oder nicht ausgehängt werden können, habe ich es mit einem Mountproblem zu tun. Kann ich ein Dateisystem nicht einhängen, kann das vielfältige Ursachen haben. Kann ich es nicht aushängen, hängt das sehr oft an geöffneten Dateien.
Dieser Fehler tritt bei umount
auf, aber auch, wenn ich ein
Dateisystem von read-write auf read-only umhängen will.
Die Meldung is busy
deutet es schon an, der Kernel, genauer gesagt,
das Dateisystem ist beschäftigt.
Wenn ich das Dateisystem aushängen will, reicht ein Prozess, der auf
irgendeine Art auf das Dateisystem zugreift. Will ich es read-only umhängen,
muss ich nach Prozessen suchen, die Dateien zum Schreiben geöffnet haben.
In [Weidner2012] habe ich einen solchen Fall detailliert
beschrieben, hier gehe ich nur kurz auf die Schritte ein um die
betreffenden Prozesse und die geöffneten Dateien zu finden.
Prozesse, die in irgendeiner Weise auf ein Dateisystem zugreifen, finde ich am schnellsten mit:
# fuser -vm $mntpnt
Diesen Befehl rufe ich mit den Privilegien von root auf, um alle Prozesse
angezeigt zu bekommen.
Dabei interessieren mich neben der PID und dem Prozessnamen (COMMAND) vor
allem die Angaben unter ACCESS. Insbesondere Prozesse mit F
und
m
, da diese es sind, die das read-only Umhängen des Dateisystems
verhindern.
Ich könnte diese Prozesse mit Option -k
gleich von fuser beenden
lassen, aber besser ist es, erst mit
# lsof -p $pid
nachzuschauen, welcher Prozess das genau ist und welche Dateien er offen hält. Auch kann ich dann mit
# pstree -p
abschätzen, ob ich vitale Systemfunktionen beende, wenn ich den Prozess einfach abschieße. Handelt es sich um einen Systemdienst, wie SSH, dann ist oft der Listening-Daemon bereits neu gestartet und nur die Instanz, über die ich angemeldet bin, verwendet noch die alte Bibliothek. In diesem Fall reicht es meist, wenn ich mich ein zweites Mal anmelde und danach die alte Verbindung trenne.
Es erscheint vielleicht anachronistisch, dass ich heutzutage, wo es möglich ist, mehrsprachige Texte am Computer zu verarbeiten, von Umlauten als Problem spreche.
Darum muss ich hier differenzieren. Umlaute und sprachspezifische Sonderzeichen in Texten stellen in den allermeisten Fällen kein Problem dar, wenn die Kodierung bekannt und konsistent ist.
Problematisch werden Umlaute genau dann, wenn das nicht der Fall ist. Das heißt, wenn die Kodierung nicht bekannt oder nicht konsistent ist.
Das ist oft der Fall, wenn es um IDs geht, die über mehrere heterogene Systeme verwendet werden oder deren Kodierung nicht bekannt ist.
Was meine ich hier mit IDs?
Das sind zum Beispiel Loginnamen. Zwar gibt es in der Passwortdatenbank eine numerische UID und einen alphanumerischen Benutzernamen. Da letzterer zur Anmeldung am System verwendet wird, stellt er eine ID dar. Aus diesem Grund haben Umlaute hier nichts zu suchen.
O.K. Welcher UNIX-Administrator, der etwas auf sich hält, würde hier Umlaute verwenden?
Die alphanumerischen Benutzer-IDs kommen heutzutage nicht mehr nur aus der Passwortdatenbank /etc/passwd. Schon wenn die Anmeldeinformationen aus einem Directory, zum Beispiel via LDAP kommen, habe ich mitunter keinen Einfluss mehr darauf. Allerdings ist die Anzahl der Personen, die neue Benutzer-IDs vergeben dürfen, meist überschaubar und hier greifen erzieherische Maßnahmen.
Es gibt noch andere IDs, bei denen Umlaute zu Problemen führen können.
Dateinamen sind IDs. Auch wenn man mir zeigt, wie sich eine Datei mit Umlauten im Namen anlegen und problemlos weiter verwenden läßt, ist das kein Beweis des Gegenteils.
Ich sagte eingangs, dass Umlaute problemlos sind, wenn die Kodierung bekannt und konsistent ist. Das ist auf einem einzelnen System immer gegeben, innerhalb eines Netzes homogener Systeme ebenfalls.
Erst bei heterogenen Systemen wird es problematisch.
Heterogen ist zum Beispiel ein Webserver, der seine Seiten in einer Kodierung
ausgibt, dessen Dateisystem aber eine andere Kodierung verwendet.
Dann kann ich vielleicht eine verlinkte Datei mit Umlaut im Namen im
Directorylisting des Webservers sehen und mit ls
den gleichen Namen im
Dateisystem.
Trotzdem bekomme ich einen Fehler, wenn ich die Datei im Browser laden will.
Und, wo wir gerade bei Webservern sind: auch die Namen auf einfachen Formularbuttons sind IDs und Umlaute können den Button unbenutzbar machen. Zum Beispiel, wenn ein Proxy zwischen Webserver und Browser liegt, der die ausgelieferten Webseiten umkodiert, ein Problem, das mich bereits einmal längere Zeit beschäftigt hatte..
Einmal hatte ich den Fall, dass ein Benutzer Datenbanktabellen mit Umlauten im Tabellen- oder Spaltennamen angelegt hatte und sich wunderte, dass er auf einem System problemlos damit arbeiten konnte und auf einem anderen gar nicht. Auch das sind IDs und das Problem lag an der unterschiedlichen Kodierung der Umlaute.
Ich hatte bereits erwähnt, dass einige Programme empfindlich auf eine falsche Systemzeit reagieren.
Das betrifft vor allem Systeme, die über mehrere Rechner synchron gehalten werden müssen, wie zum Beispiel Failoversysteme, oder kryptographische Protokolle, die die Systemzeit verwenden, um Replay-Attacken zu vermeiden.
Neben diesen gibt es Programme, die auf eine monoton ansteigende Zeit angewiesen sind. Zum Beispiel, weil sie die Systemzeit zur Sortierung von Ereignissen verwenden oder zur Bestimmung von Dateinamen.
Einige dieser Systeme überwachen die Systemzeit und beenden sich, sobald sie bemerken, dass diese rückwärts läuft. Das muss ich beachten, wenn ich die Systemzeit von Hand auf einen älteren Zeitpunkt zurücksetzen will.
In einem Fall beendete sich ein Mailserver-Prozess, weil die Systemzeit einen größeren Sprung rückwärts machte. Ich hatte beim Setzen der Zeit nicht daran gedacht und bekam das erst durch das Monitoring mit.
Bei virtuellen Systemen muss ich im Hinterkopf behalten, dass die Systemzeit auch vom Hostsystem zu den Gastsystemen übernommen werden kann. Setze ich unbedacht die Systemzeit des Hostsystems zurück, kann ich unter Umständen sehr schnell einige Gastsysteme unbrauchbar machen.
Ein Grund mehr, die Systemzeit aller Rechner im Netz synchron zu halten.