Troubleshooting Linux und IP-Netzwerke

6. Partieller Ausfall

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.

Die ersten Minuten auf dem Server

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.

uptime, w, last - Zeit, Systemlast, Benutzer

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.

vmstat - Performancewerte

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.

free - Hauptspeicher

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.

df - Plattenplatz

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.

Auch wenn dieser Abschnitt vielleicht den Eindruck erweckt, dass ich viele Probleme gleich nach dem Anmelden am Server diagnostizieren kann, passiert es mir immer wieder, dass ich an einem Problem festhänge, weil ich solche Kleinigkeiten, wie die Kontrolle der Inodes mit df -i vergesse.

In einem konkreten Fall hatte CFEngine ein Dateisystem mit kleinen Dateien für Statusmeldungen zugemüllt. Normalerweise generiert CFEngine ungefähr aller 5 Minuten eine Statusmeldung. Bei knapp 300 Statusmeldungen am Tag hatte ich das Problem fast zwei Jahre lang nicht bemerkt, bis alle freien Inodes aufgebraucht waren. Da es kein Mail- oder News-Server war, kam mir nicht in den Sinn, nach den Inodes zu schauen und es war eben nicht zur Routine geworden, sowohl nach dem Plattenplatz als auch nach den Inodes zu sehen. Das hat mich einige unnötige Zeit bei der Fehlersuche gekostet.

Nachdem ich schließlich auf die aufgebrauchten Inodes gekommen war, war das nächste Problem, die Dateien zu finden. Einzeln in alle Verzeichnisse zu schauen dauert zu lange. Mit find kann ich das automatisieren. Aber mit welchen Optionen?

Dazu muss ich wissen, wonach ich suche. Wenn die Inodes aufgebraucht sind, gehe ich davon aus, dass viele Tausend Dateien in relativ wenigen Verzeichnissen existieren. Also muss es Verzeichnisse geben, die sehr viele Dateien oder Unterverzeichnisse enthalten. In dem Fall sind diese Verzeichnisse selbst sehr groß, und genau danach suche ich:

# find $mountpoint -xdev -type d -size +300k

Ich suche unterhalb des Einhängepunkts des betroffenen Dateisystems ($mountpoint) ausschließlich in diesem Dateisystem (-xdev) nach Verzeichnissen (-type d) deren Größe einen bestimmten Wert überschreitet (-size +300k). Bei der Größe muss ich etwas experimentieren, wenn ich zuviel oder zuwenig Ergebnisse bekomme.

Auf genau diese Art bin ich bei diesem Problem auf /var/cfengine/output/ und darüber auf CFEngine als Verursacher gekommen. Der Rechner hatte eine Testkonfiguration und sich selbst als Policy-Server eingestellt. Nach Konfiguration des richtigen Policy-Servers erledigte sich das Problem in kurzer Zeit von selbst.

pstree, ps - laufende Prozesse

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.

PID 1 - Init

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

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.

Upstart

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.

Systemd

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:

.service
Service-Units, die sich um Dienste kümmern, welche SysVInit meist über Init-Skripts startet oder beendet.
.mount
Units zum Ein- und Aushängen von Dateisystemen
.path
Die spezifizierten Dateien und Verzeichnisse werden mit inotify überwacht.
.socket
Ein oder mehrere Sockets, für die Socket-Aktivierung. Für den Start der zugehörigen Dienste ist jedoch eine Service-Unit zuständig.
.target
Gruppen von Units. Diese machen selbst wenig, aktivieren aber andere Units.

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.

Ein Dienst startet nicht

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.

Es gab eine Zeit in der es unter Administratoren als verdienstvoll galt, eine hohe Systemlaufzeit zu erreichen. Laufzeiten von x mal hundert Betriebstagen waren keine Seltenheit. Ich gebe zu, dass ich mich daran beteiligt habe. Für Einzelsysteme, die ständig benötigte Dienste anbieten, halte ich das noch heute für sinnvoll.

Die Gewichte verschieben sich jedoch, sobald ich eine größere Anzahl von Systemen betreuen muss, und genügend Redundanz im Netz vorhanden ist, dass es auf das einzelne System nicht mehr so sehr ankommt. Dann ist wichtiger, dass das System schnell startet und vor allem vollständig, das heißt, mit allen definierten Diensten. Aus diesem Grund habe ich mir angewöhnt, Server häufiger zu starten und anschließend zu kontrollieren, ob alle Dienste verfügbar sind.

Bei dieser Gelegenheit kann ich auch sehen, welchen Einfluss der Ausfall dieses Servers auf das Gesamtsystem hat.

Dienst läßt sich von Hand starten

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.

Bei Debian gibt es in einigen Versionen ein Problem, wenn ntp (der Zeit-Dämon) und ntpdate (das Programm zum Setzen der Systemzeit) zusammen installiert sind. Dann kann es vorkommen, dass ntpd nach dem Systemstart nicht läuft, sich aber problemlos von Hand starten lässt. In den Logs findet sich beim Rechnerstart eine Notiz, dass ntpd nicht startet, weil er keinen UDP-Socket für Port 123 bekommen kann. Diesen Socket verwendet in diesem Moment gerade ntpdate um die Systemzeit zu setzen.

In diesem Fall lasse ich ntpdate mit der Option -u starten, so dass er einen unprivilegierten Port nimmt und Port 123 für ntpd verfügbar bleibt.

Dienst läßt sich nicht von Hand starten

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.

Zugriffsprobleme

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,

Traditionelle Unix-Dateirechte

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.

Grundlegende Rechte

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.

Sonderrechte

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.

Einschränkungen

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.

Ansehen und Bearbeiten der Zugriffsrechte

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.

Erweiterte Dateiattribute

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.

POSIX Capabilities

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.

Wie funktionieren die POSIX Capabilities?

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.

Was muss ich tun?

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.

AppArmor

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.

Nachrichten im Logfile

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
Audit-Einstellungen der Profile

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.

Beschränkungen eines Prozesses untersuchen

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
Probleme mit AppArmor behandeln

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.

Profile in den Beschwerdemodus setzen

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

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.

Wie stelle ich fest, ob SELinux aktiv ist?

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
Betriebsmodi

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.

Konzepte

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.

Informationen zu verweigerten Zugriffen finden

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:

  1. Nicht jede Ablehnung die ich in den Protokollen finde, stellt ein Problem dar. Einige sind nur kosmetischer Natur, sie treten auf, beeinflussen aber das Verhalten der Anwendung nicht. Diese Ablehnungen kann ich in den Regeln durch dontaudit Anweisungen von der Protokollierung ausnehmen.
  2. Ich werde jede Menge Ablehnungen sehen, von denen viele nichts mit dem Problem zu tun haben, das ich gerade untersuche.
  3. Wenn zu viele Ablehnungen hintereinander kommen, kann es vorkommen, dass der Linux-Kernel einige unterdrückt. Wenn das passiert taucht eine Nachricht auf, die angibt wie viele Meldungen unterdrückt wurden. Das heißt dann, dass ich im Log vielleicht nicht alles finde, was SELinux gemeldet hat.
Logeinträge untersuchen

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:

    # mntpnt="$(mount|grep sda1\ on|cut -f3 -d\ )"
    # find $mntpnt -xdev -inum 105872
Versteckte Ablehnungen

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.

Mount-Fehler

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.

mount: / is busy

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.

Offene gelöschte Dateien

Auch Dateilöschungen sind Schreibzugriffe. Wenn Systembibliotheken aktualisiert werden, dann wird der Verweis auf die alte Datei mit dem Systembefehl unlink() gelöscht und die neue Datei mit dem gleichen Namen gespeichert. Neu gestartete Prozesse verwenden die neue Bibliothek. Prozesse, die bereits vor der Aktualisierung liefen, arbeiten weiter mit der alten Bibliothek, weil sie diese vor der Aktualisierung geöffnet hatten.

Abgesehen von möglichen Sicherheitsproblemen, die die alte Bibliothek hat und damit auch die alten Prozesse, kann ich nun das Dateisystem mit der alten Bibliothek nicht read-only umhängen. Diese ist zwar nicht mehr im Dateisystem verlinkt, die betreffenden Blöcke werden aber erst freigegeben, wenn der letzte Prozess, der sie verwendet, beendet ist.

Das gleiche gilt für Dateien, die geöffnet und unmittelbar darauf im Dateisystem gelöscht werden. Diese sind damit nur für den Prozess, der sie geöffnet hat, “sichtbar”.

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.

Sonstige Probleme

Umlaute

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.

Zeitfehler

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.