Schlagwort-Archive: Planet

Tag für den Ubuntuusers.de Planeten.

Protokoll zum Ausfall eines Webdienstes nach der fehlgeschlagenen Erneuerung eines Let’s Encrypt Zertifikats

In diesem Beitrag werden die Ursache und der Verlauf der Störung protokolliert, welche die vorübergehende Nichterreichbarkeit einer Seafile1-Installation zur Folge hatte, welche ich für den Freifunk Lippe2 betreibe.

Beschreibung der Umgebung

Die Seafile-Installation wird ausführlich in „Installation von Seafile auf einem Ubuntu/Debian Server“ beschrieben. Die aktuelle Installation wurde wie dort erwähnt zusätzlich mit HSTS (TLS-Kochbuch, Abschnitt 2.7) und HPKP (TLS-Kochbuch, Abschnitt 2.8) gesichert. Das benötigte TLS-Zertifikat stammt von Let’s Encrypt (Abschnitt 3.4.1, TLS-Kochbuch).

Zur automatisierten Verlängerung wird das Skript smartrenew.sh verwendet, welches in Abschnitt 5.4 im TLS-Kochbuch beschrieben wird.

Eingang der Störungsmeldung

Die Störungsmeldung ging am 04.02.2017 per E-Mail. Die Meldung wies auf einen Fehler bei der Erneuerung des Zertifikats durch Let’s Encrypt hin:


arsing account key...
Parsing CSR...
Registering account...
Already registered!
Verifying seafile.example.com...
Traceback (most recent call last):
  File "acme-tiny-by-frezbo/acme_tiny.py", line 200, in 
    main(sys.argv[1:])
  File "acme-tiny-by-frezbo/acme_tiny.py", line 196, in main
    signed_crt = get_crt(args.account_key, args.csr, args.acme_dir, verifychallenge=args.verifychallenge, log=LOGGER, CA=args.ca)
  File "acme-tiny-by-frezbo/acme_tiny.py", line 150, in get_crt
    domain, challenge_status))
ValueError: seafile.example.com challenge did not pass: {u'status': u'invalid', u'validationRecord': [{u'url': u'http://seafile.example.com/.well-known/acme-challenge/ag0bYKs4XzfkYlzRPYLC1cFtv-ypEIQCEVfYulPlMGk', u'hostname': u'seafile.example.com', u'addressUsed': u'IPv4-Adresse', u'port': u'80', u'addressesResolved': [u'IPv4-Adresse', u'IPv6-Adresse']}, {u'url': u'https://seafile.example.com/.well-known/acme-challenge/ag0bYKs4XzfkYlzRPYLC1cFtv-ypEIQCEVfYulPlMGk', u'hostname': u'seafile.example.com', u'addressUsed': u'IPv4-Adresse', u'port': u'443', u'addressesResolved': [u'IPv4-Adresse', u'IPv6-Adresse']}], u'keyAuthorization': u'ag0bYKs4XzfkYlzRPYLC1cFtv-ypEIQCEVfYulPlMGk.pW0Gip0L_WLx7XEnmH_3ZArt9Vi4TbokaUuSXsc1dm4', u'uri': u'https://acme-v01.api.letsencrypt.org/acme/challenge/u5hporNk0I5YunYlQQeJPKdRyd-9BKGMWjBhm_7Za9E/578797992', u'token': u'ag0bYKs4XzfkYlzRPYLC1cFtv-ypEIQCEVfYulPlMGk', u'error': {u'status': 403, u'type': u'urn:acme:error:
 unauthorized', u'detail': u'Invalid response from http://seafile.example.com/.well-known/acme-challenge/ag0bYKs4XzfkYlzRPYLC1cFtv-ypEIQCEVfYulPlMGk: "\r\n404 Not Found\r\n\r\n404 Not Found\r\n"'}, u'type': u'http-01'}
 * Reloading nginx configuration nginx
   ...fail!

Der Fehlermeldung ist zu entnehmen, dass die ACME-Challenge nicht verifiziert werden konnte, da der Server auf die Anfrage mit der Meldung „404 Not Found“ antwortete. Dieser Fehler konnte durch Aufruf der URL mit dem Programm wget bestätigt werden.

Ergebnis der Systemanalyse

Die Analyse der NGINX-Konfiguration ergab, dass der von Let’s Encrypt signierte öffentliche Schlüssel des Zertifikats nicht zum dazugehörenden privaten Schlüssel passte. Dies hatte zur Folge, dass sich die Webseite im Browser, der HSTS und HPKP unterstützt, nicht mehr aufgerufen werden konnte. Denn durch HSTS wird auch ein Aufruf der Seite über HTTP automatisch auf HTTPS umgeleitet. Hier wird nun durch HPKP ebenfalls festgestellt, dass der gepinnte Key nicht mit dem vom Webserver ausgelieferten übereinstimmte und der Browser verweigerte den Aufruf der Webseite.

Die Ursache hierfür lag in der fehlenden Fehlerbehandlung im folgenden Skript:


python acme-tiny-by-frezbo/acme_tiny.py --no-verify --account-key /var/www/seafile.example.com/ssl/account.key --csr /var/www/seafile.example.com/ssl/seafile.example.com.csr --acme-dir /var/www/seafile.example.com/public/.well-known/acme-challenge/ > /var/www/seafile.example.com/ssl/seafile.example.com.crt

cat /var/www/seafile.example.com/ssl/seafile.example.com.crt /var/www/seafile.example.com/ssl/lets-encrypt-x3-cross-signed.pem > /var/www/seafile.example.com/ssl/seafile.example.com_chained.crt

sudo service nginx reload

Zuerst versucht das Skript, das Zertifikat zu erneuern und in die Datei seafile.example.com.crt zu schreiben. Schlägt dieser Vorgang fehl, wird durch den zweiten Teil des Skripts trotzdem aus der korrupten Datei und dem Intermediate-Zertifikat die Zertifikatskette erstellt und in der Datei seafile.example.com_chained.crt gespeichert.

In diesem Fall trat bei der Erneuerung des Zertifikats ein Fehler auf. Es wurde jedoch trotzdem eine Datei namens seafile.example.com.crt mit einer Größe von 0 Byte erstellt. Daher fehlte das Zertifikat auch in der daraufhin erstellten Zertifikatskette. Dies hatte zur Folge, dass durch HPKP der Webbrowser den Zugriff auf die Domain verweigerte und die Anwendung nicht mehr erreichbar war.

Eine Überprüfung der Datensicherung ergab leider, dass das Verzeichnis mit den Zertifikatsdateien nicht Bestandteil des Backups war. Somit war die schnelle Wiederherstellung der Zertifikatskette nicht möglich.

Störungsbeseitigung

Um die Störung zu beseitigen, wurde zuerst die bestehende vHost-Konfiguration für die Seafile-Instanz deaktiviert. Anschließend wurde ein neuer vHost eingerichtet, der ausschließlich via HTTP das Verzeichnis ausliefert, welches die ACME-Challenge beinhaltet:


server {
        listen       80;
        server_name  seafile.example.com;

        root /var/www/seafile.example.com/public/;
        location / {
        index index.html index.htm index.php;
        }
}

Nun wurde das Skript zur Erneuerung des Let’s Encrypt Zertifikats erneut gestartet. Das Skript lief diesmal fehlerfrei durch und es wurde wieder eine vollständige Zertifikatskette erstellt und in der vorgesehenen Datei gespeichert.

Daher konnte die temporäre vHost-Konfiguration nun wieder deaktiviert und die ursprüngliche Konfiguration wieder aktiviert werden.

Maßnahmen zur Risikominimierung

Nachdem mich die Störung natürlich zur ungünstigsten Zeit (welche Zeit ist für eine Störung schon günstig?) erwischt hat, habe ich mir Gedanken gemacht, wie sich das Risiko minimieren lässt, dass mich der gleiche Ärger in Zukunft nochmal ereilt.

Datensicherung anpassen

Zuerst habe ich meine Datensicherung angepasst und das Verzeichnis, welches die Zertifikatsdateien enthält, mit ins Backup aufgenommen. Selbstverständlich ist das Backup nur die halbe Miete. Ob sich dieses erfolgreich wiederherstellen lässt, muss natürlich auch getestet werden.

Fehler bei der Skript-Ausführung abfangen

Wie weiter oben bereits beschrieben, begann die ganze Problematik damit, dass das Skript zur Zertifikatserneuerung weiterarbeitete, obwohl bereits bei der Erneuerung des Zertifikats ein Fehler aufgetreten war.

Zukünftig soll das Skript abbrechen, wenn bei der Verarbeitung ein Fehler auftritt. Dazu wurde es wie folgt erweitert:


function check() {
  if [ $1 -gt 0 ]; then
    echo "Uuups, hier ist was schiefgegangen"
    echo "exit $1"
    exit 1
  fi
}

python acme-tiny-by-frezbo/acme_tiny.py --no-verify --account-key /var/www/seafile.example.com/ssl/account.key --csr /var/www/seafile.example.com/ssl/seafile.example.com.csr --acme-dir /var/www/seafile.example.com/public/.well-known/acme-challenge/ > /var/www/seafile.example.com/ssl/seafile.example.com.crt

check $?

cat /var/www/seafile.example.com/ssl/seafile.example.com.crt /var/www/seafile.example.com/ssl/lets-encrypt-x3-cross-signed.pem > /var/www/seafile.example.com/ssl/seafile.example.com_chained.crt

check $?

sudo service nginx reload

Schlägt die Erneuerung des Zertifikats fehl, gibt das Kommando einen Exit-Status größer Null zurück. Dies wird von der check-Funktion erkannt und das Skript abgebrochen. Dadurch bleiben die alte Datei mit der Zertifikatskette erhalten und der Dienst bleibt verfügbar, bis das Problem mit der Zertifikatserneuerung behoben werden kann.

NGINX verweigert Neustart – [emerg]: bind() to 0.0.0.0:80 failed (98: Address already in use)

In diesem Artikel möchte ich einige Informationen zur NGINX-Fehlermeldung „[emerg]: bind() to 0.0.0.0:80 failed (98: Address already in use)“ wiedergeben.

Als ich heute Morgen die E-Mail-Reports meiner Server durchgesehen habe, fiel mir die Meldung ins Auge, dass auf einem meiner Server die NGINX-Konfiguration nicht erneut eingelesen werden konnte. Auch der Versuch eines manuellen Neustarts wurde mit folgender Meldung quittiert:

# sudo service nginx restart
* Restarting nginx nginx [fail]

Konfiguration überprüfen

Die obige Meldung gibt noch keinerlei Hinweise auf die Ursache des Fehlers. Mit dem folgenden Kommando lässt sich die Konfiguration des NGINX überprüfen und der Fehler etwas eingrenzen:

# sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: [emerg] listen() to 0.0.0.0:80, backlog 511 failed (98: Address already in use)
nginx: configuration file /etc/nginx/nginx.conf test failed

Die Fehlermeldung sagt aus, dass der NGINX-Prozess nicht erneut an den Port 80 gebunden werden kann, da dieser bereits verwendet wird. Für mich noch immer etwas verwirrend, da NGINX ja noch läuft und selbstverständlich auf Port 80 lauscht. Weshalb dadurch plötzlich kein Neustart mehr möglich ist, erschließt sich mir noch nicht.

Eine Lösung

Bei der Internetrecherche nach einer Lösung, bin ich auf einen englischsprachigen Troubleshootingguide gestoßen, welcher empfiehlt, alle Prozesse, die auf Port 80 lauschen, mit dem folgenden Kommando zu beenden:

sudo fuser -k 80/tcp

Anschließend konnte ich den NGINX wie gewohnt neu starten (restart) oder die Konfiguration neu einlesen (reload). Das Problem scheint damit erstmal behoben zu sein.

WordPress fordert zum Update Verbindungsinformationen (FTP-Zugangsdaten) an

Wie im letzten Beitrag geschildert, ist dieser Blog auf einen anderen Server umgezogen.

Aktuell teste ich noch alle Funktionen und versuche, die gefundenen Fehler zu bereinigen. An dieser Stelle möchte ich mich auch schon einmal bei all denen bedanken, die mich auf Fehler aufmerksam gemacht haben.

Heute wurde ich im Backend auf das WordPress-Update 4.7.1 aufmerksam, welches ich umgehend installieren wollte. Dabei forderte mich WordPress unerwartet auf, FTP-Zugangsdaten einzugeben, um das Update durchführen zu können.

Diese Meldung irritierte mich etwas, da ich diese Informationen auf dem alten Server nicht benötigte. Auf dem aktuellen Server läuft noch ein weiterer WordPress-Blog, welcher ebenfalls ohne Angabe dieser Verbindungsinformationen aktualisiert werden konnte.

Nach kurzer Internetrecherche fand ich die Lösung für mein Problem in dem Artikel WordPress Berechtigungen korrekt setzen (Problem mit FTP-Serverdaten Aufforderungen beheben) von Ansas Meyer. Ich habe wie dort beschrieben die Berechtigungen überprüft. Diese waren jedoch bereits korrekt gesetzt. Anschließend habe ich der Datei wp-config.php folgende Zeile hinzugefügt:

define('FS_METHOD', 'direct');

Anschließend konnte das Update wie gewohnt über das Backend eingespielt werden.

Die Ursache, die für die plötzliche Anforderung der Verbindungsinformationen verantwortlich ist, kenne ich leider immer noch nicht. Jedoch bin ich froh, dass ich dank des Artikels von Ansas die Folgen behandeln konnte.

Kurztipp: nginx_modsite

In diesem Post möchte ich ganz kurz das Skript nginx_modsite vorstellen.

Dieses ermöglicht die einfache De-/Aktivierung von Nginx-Konfigurationsdateien in den Standard-Verzeichnissen /etc/nginx/sites-available und /etc/nginx/sites-enabled.

Das Skript wurde ursprünglich 2010 von Michael Lustfield erstellt. Es ist u.a. in meinem GitHub-Repository1 zu finden.

Zur einfachen Verwendung kopiert man das Skript in das Verzeichnis /usr/local/sbin. Ruft man es mit den Argumenten -h bzw. --help auf, erhält man eine Übersicht der verfügbaren Optionen:

sudo nginx_modsite -h
Usage: nginx_modsite [options]
Options:
<-e|--enable> Enable site
<-d|--disable> Disable site
<-l|--list> List sites
<-h|--help> Display help

If is left out a selection of options will be presented.
It is assumed you are using the default sites-enabled and
sites-disabled located at /etc/nginx.

Somit müssen die Links in /etc/nginx/sites-enabled nicht mehr per Hand erstellt werden. Alles in allem ein sehr schönes Skript, welches ich nicht mehr missen möchte.

Ansible – Was ich am Ad-hoc-Modus schätze

Schon seit einiger Zeit hilft mir Ansible1 fast täglich dabei, meine Arbeit leichter zu gestalten. Heute möchte ich euch ganz kurz erzählen, was ich am Ad-hoc-Modus schätze.

Der Ad-hoc-Modus bietet die Möglichkeit, einfache Kommandos parallel auf einer Gruppe von Nodes ausführen zu lassen, ohne zuvor ein Playbook erstellen zu müssen. Ein Ad-hoc-Befehl besitzt z.B. den folgenden Aufbau:

ansible [-m module_name] [-a args] [options]

Ein einfaches Beispiel aus der Ansible-Dokumentation2 soll die Anwendung verdeutlichen:

# ansible all -m ping -i staging --limit=e-stage
host01.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
host02.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}
host03.example.com | SUCCESS => {
"changed": false,
"ping": "pong"
}

Das Schlüsselwort all gibt an, dass das Kommando auf allen Nodes ausgeführt werden soll, welche in der Inventar-Datei enthalten sind. Mit -m ping wird das zu verwendende Ansible-Modul spezifiziert. Da das verwendete Modul keine weiteren Argumente besitzt, findet -a in diesem Beispiel keine Anwendung. Mit der Option -i kann die zu verwendende Inventar-Datei angegeben werden. Lässt man diese Option weg, wird die Standard-Inventar-Datei /etc/ansible/hosts verwendet. Mit der Option --limit=e-stage wird die Ausführung noch weiter eingeschränkt. So wird in diesem Fall das Modul ping nur auf den Nodes der Gruppe e-stage ausgeführt. Das in diesem Beispiel verwendete Inventar besitzt den folgenden Aufbau:

[e-stage]
host01.example.com
host02.example.com
host03.example.com
host06.example.com
host07.example.com

[i-stage]
host04.example.com

[p-stage]
host05.example.com

Verknüpfung mit weiteren Kommandos

Selbstverständlich lassen sich Ansible-Ad-hoc-Kommandos auf der Kommandozeile auch weiter verknüpfen. Dies soll an zwei kleinen Beispielen verdeutlicht werden.

Status eines Dienstes prüfen

In diesem ersten Beispiel soll der Status des Dienstes chronyd überprüft werden, ohne den aktuellen Status zu ändern. Dabei soll das Kommando systemctl status chronyd.service via Ansible parallel auf den Nodes ausgeführt werden.

Zuvor habe ich mir auf einem Node angesehen, wie die Ansible-Ausgabe in Abhängigkeit vom Dienststatus aussieht (Ausgabe gekürzt):

# Der Dienst auf dem Node ist gestartet
root@ansible-control-machine>ansible all -m command -a'/usr/bin/systemctl status chronyd.service' -i staging -l host01.example.com
host01.example.com | SUCCESS | rc=0 >>
* chronyd.service - NTP client/server
Loaded: loaded (/usr/lib/systemd/system/chronyd.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2016-12-15 14:52:02 CET; 19h ago

# Der Dienst auf dem Node ist gestoppt
root@ansible-control-machine>ansible all -m command -a’/usr/bin/systemctl status chronyd.service‘ -i staging -l host01.example.com
host01.example.com | FAILED | rc=3 >>
* chronyd.service – NTP client/server
Loaded: loaded (/usr/lib/systemd/system/chronyd.service; enabled; vendor preset: enabled)
Active: inactive (dead) since Fri 2016-12-16 10:04:34 CET; 4s ago

# Das Paket, welches den Dienst enthaelt ist nicht installiert
root@ansible-control-machine>ansible all -m command -a’/usr/bin/systemctl status chronyd.service‘ -i staging -l host01.example.com
host01.example.com | FAILED | rc=4 >>
Unit chronyd.service could not be found.

Anhand der Ausgaben ist zu erkennen, dass Ansible den Task als „| SUCCESS |“ markiert, wenn der Dienst läuft und als „| FAILED |“, wenn der Dienst gestoppt bzw. gar nicht installiert ist. Durch Verknüpfung des Kommandos mit grep kann man sich nun schnell einen Überblick über den Dienststatus auf seinen Rechnern verschaffen:

root@ansible-control-machine> ansible all -m command -a'/usr/bin/systemctl status chronyd.service' -i staging --limit=e-stage | grep '| SUCCESS |\|| FAILED |'
host01.example.com | SUCCESS | rc=0 >>
host02.example.com | SUCCESS | rc=0 >>
host03.example.com | FAILED | rc=3 >>
host06.example.com | SUCCESS | rc=0 >>
host07.example.com | SUCCESS | rc=0 >>

Anhand der Ausgabe ist leicht zu erkennen, dass der Dienst chronyd auf host03 nicht läuft. Anhand des Return-Codes rc=3 lässt sich weiterhin erkennen, dass das notwendige Paket offensichtlich installiert ist, der Dienst jedoch nicht gestartet wurde. Dies kann nun jedoch schnell durch folgenden Befehl korrigiert werden (Ausgabe gekürzt):

root@ansible-control-machine>ansible host03.example.com -m systemd -a'name=chronyd state=started' -i staging
host03.example.com | SUCCESS => {
"changed": true,
"name": "chronyd",
"state": "started",
"status": {...}
}

Eine erneute Ausführung des ersten Kommandos bestätigt, dass der Dienst nun auch auf dem Node host03 ausgeführt wird.

root@ansible-control-machine> ansible all -m command -a'/usr/bin/systemctl status chronyd.service' -i staging --limit=e-stage | grep '| SUCCESS |\|| FAILED |'
host01.example.com | SUCCESS | rc=0 >>
host02.example.com | SUCCESS | rc=0 >>
host03.example.com | SUCCESS | rc=0 >>
host06.example.com | SUCCESS | rc=0 >>
host07.example.com | SUCCESS | rc=0 >>

Paketversion überprüfen

In diesem Beispiel möchte ich die installierte Version des Pakets tzdata abfragen. Dies geschieht auf einem einzelnen Host mit dem Kommando rpm -qi :

# rpm -qi tzdata
Name : tzdata
Version : 2016i
Release : 1.el7
Architecture: noarch
Install Date: Wed Nov 9 08:47:03 2016
Group : System Environment/Base
Size : 1783642
License : Public Domain
Signature : RSA/SHA256, Fri Nov 4 17:21:59 2016, Key ID 199e2f91fd431d51
Source RPM : tzdata-2016i-1.el7.src.rpm
Build Date : Thu Nov 3 12:46:39 2016
Build Host : ppc-045.build.eng.bos.redhat.com
Relocations : (not relocatable)
Packager : Red Hat, Inc. <http://bugzilla.redhat.com/bugzilla>
Vendor : Red Hat, Inc.
URL : https://www.iana.org/time-zones
Summary : Timezone data
Description :
This package contains data files with rules for various timezones around
the world.

Mich interessiert lediglich die zweite Zeile, welche die Version des Pakets enthält. Die frage ich nun wie folgt ab:

root@ansible-control-machine> ansible all -m command -a'/usr/bin/rpm -qi tzdata' -i staging --limit=e-stage | grep 'SUCCESS\|Version'
host01.example.com | SUCCESS | rc=0 >>
Version : 2016f
host02.example.com | SUCCESS | rc=0 >>
Version : 2016g
host03.example.com | SUCCESS | rc=0 >>
Version : 2016i
host06.example.com | SUCCESS | rc=0 >>
Version : 2016i
host07.example.com | SUCCESS | rc=0 >>
Version : 2016i

Ohne Ansible hätte ich diese Aufgaben entweder mit Iteration in einem kurzen Shell-Skript lösen müssen, oder zuerst ein Kochbuch, Manifest, etc. schreiben, welches dann anschließend ausgeführt werden kann. So wurde mir hingegen einiges an Zeit gespart, die ich für andere Dinge verwenden konnte.

Quellen und weiterführende Links

RHEL/CentOS 7 – Root Passwort zurücksetzen

In diesem Tutorial wird beschrieben, wie das root-Passwort bei RHEL/CentOS 7 mit Bordmitteln zurückgesetzt werden kann. Mit Bordmitteln bedeutet, dass hierbei keine externen Bootmedien verwendet werden.

Aufgepasst! Macht man bei der Ausführung der folgenden Prozedur Fehler, können diese dazu führen, dass man jeglichen Zugang zu einem System verliert. Möchte man die Vorgehensweise üben, empfehle ich, dies ausschließlich auf Testsystemen zu tun.

Die Ausgangssituation

Das root-Passwort eines RHEL/CentOS 7 Servers ist unbekannt und muss zurückgesetzt werden. Es ist keine geöffnete root-Shell vorhanden und es ist kein Benutzer am System angemeldet, welcher über vollständige sudo-Berechtigungen verfügt.

Das Passwort soll ohne Einsatz externer Bootmedien zurückgesetzt werden.

Vorgehensweise

Schritt 1: Es wird ein Neustart des Systems durchgeführt. Sobald das Grub2-Boot-Menü erscheint, wird der Bootloader-Countdown durch Drücken einer beliebigen Taste unterbrochen.

Nun wählt man den gewünschten Eintrag (dies ist meist der erste in der Liste) aus und wechselt mit Drücken der Taste ‚e‘ in den Bearbeitungsmodus (siehe Abbildung 1).

abb1-grub2-menu

Abbildung 1: Grub2-Bootmenü

Schritt 2: Der Cursor wird zur Kernel-Kommandozeile bewegt. Diese beginnt üblicherweise mit dem Wort linux16. Hier wird, wie in Abbildung 2, ein „rd.break“ an das Ende der Zeile angefügt.

edit-kernel-command-line

Abbildung 2: Bearbeitete Startparameter für den Kernel

Die Bearbeitung wird durch Drücken der Tastenkombination Strg+x beendet und der Bootvorgang fortgesetzt. Der Bootvorgang wird an der Stelle angehalten, an der man sich in der Initial-Ram-Disk befindet, direkt bevor das eigentliche System gestartet wird. An dieser Stelle findet man den Inhalt des eigentlichen /-Dateisystems unterhalb von /sysroot.

Schritt 3: Nach Schritt 2 befindet man sich nun in einer root-Shell. Da das eigentliche Dateisystem unterhalb von /sysroot schreibgeschützt eingehängt wurde, muss dieses zunächst remountet werden. Dies geschieht mit dem folgenden Kommando (vgl. Abbildung 3):

switch_root:/# mount -oremount,rw /sysroot
remount-sysroot

Abbildung 3: Remount /sysroot

Schritt 4: In diesem Schritt wechselt man mittels chroot1 in das /sysroot-Verzeichnis und setzt ein neues Passwort für den Benutzer root.

switch_root:/# chroot /sysroot
sh-4.2# passwd root
Changing password for user root.
New password:
Retype new password:
passwd: all authentication tokens updated successfully.

Wichtig: SELinux2 ist zu diesem Zeitpunkt noch nicht aktiv. Dies bedeutet, dass alle neuen Dateien ohne einen entsprechenden SELinux-Kontext erstellt werden. Das Programm passwd arbeitet so, dass es erst eine neue Datei erstellt und mit dieser anschließend die alte Datei überschreibt. Die neue Datei /etc/shadow besitzt damit keinen SELinux-Kontext.

Um sicherzustellen, dass alle Dateien (inkl. der /etc/shadow) während des Bootvorgangs mit einem SELinux-Label versehen werden, muss die Datei autorelabel im aktuellen Verzeichnis erstellt werden:

sh-4.2# touch /.autorelabel

Wichtig: Sämtliche Dateien und Verzeichnisse werden erneut mit einem SELinux-Label versehen. Dies kann bei großen Dateisystemen einige Zeit dauern. Um Zeit zu sparen, können Dateisysteme (außer dem Dateisystem auf dem sich die /etc/shadow befindet) in der /etc/fstab auskommentiert werden. Nachdem das SELinux-Relabeling durchgeführt und das System gestartet wurde, können diese wieder eingehängt werden.

Abschließend verlässt man durch zweimalige Eingabe von exit zuerst die chroot-Umgebung und anschließend die Debug-Shell der Ram-Disk (vgl. Abbildung 4). Das System setzt den Bootvorgang an der Stelle fort, an der dieser unterbrochen wurde.

autrelabel

Abbildung 4

Es werden zunächst sämtliche Dateien von SELinux relabelt und anschließend ein Neustart ausgeführt.

Hinweis: Vergisst man die Datei .autorelabel zu erstellen und wird SELinux im Modus „Enforcing“ ausgeführt, kann man sich nach einem Neustart nicht am System anmelden. Man muss dann erneut booten und obige Schritte ausführen, um die Datei erstellen zu können.

Wurden die oben aufgeführten Schritte erfolgreich angewendet, kann man sich nun mit dem vergebenen Passwort am System anmelden und hat damit die Kontrolle zurückgewonnen.

Auch wenn ich diese Anleitung mehrere Male erfolgreich getestet habe, wünsche ich uns allen, dass wir sie möglichst niemals brauchen werden.

SELinux Booleans

Bei SELinux Booleans handelt es sich um kleine Schalter, mit denen sich das Verhalten der SELinux-Richtlinien beeinflussen lässt. Dieser Artikel knüpft an die „Einführung in das grundlegende Konzept von SELinux“ an und erläutert die Verwendung von SELinux Booleans anhand eines einfachen Beispiels.

Hinweis: Das Beispiel aus diesem Artikel wurde auf einem RHEL/CentOS 7.3 getestet. Unter CentOS 7.2 funktioniert die hier gezeigte Konfiguration nicht. Für Details wird auf das Topic1 im CentOS-Support-Forum verwiesen.

Im Einführungsartikel2 wurde SELinux dazu genutzt, um den Zugriff des Apache auf das DocumentRoot-Verzeichnis /var/www/html zu beschränken. Nun möchte der Webmaster den Benutzern gestatten, Webseiten über ihre HOME-Verzeichnisse zu veröffentlichen und aktiviert dazu die Konfiguration für das Modul Userdir.3 4 5

[root@centos ~]$ cat /etc/httpd/conf.d/userdir.conf

    UserDir enabled
    UserDir public_html



    AllowOverride FileInfo AuthConfig Limit Indexes
    Options MultiViews Indexes SymLinksIfOwnerMatch IncludesNoExec
    Require method GET POST OPTIONS


[root@centos ~]#

Nun kann ein Benutzer in seinem HOME-Verzeichnis den Ordner public_html erstellen und eine test.txt-Datei erstellen:

[jkastning@centos ~]$ mkdir public_html
[jkastning@centos ~]$ sudo chmod 711 /home/jkastning/
[jkastning@centos ~]$ sudo chmod 755 public_html/
[jkastning@centos ~]$ vim public_html/test.txt

Hello User

Nach einem Neustart des Dienstes httpd sollte sich nun die Datei index.html aus dem Benutzerverzeichnis abrufen lassen. Statt dessen wird der Zugriff verweigert.

apache_userdir_forbidden

httpd_enable_homdirs –> off

In den Logdateien finden sich Hinweise, die auf SELinux als Ursache hindeuten.

[root@centos ~]# tail /var/log/audit/audit.log|grep AVC
type=AVC msg=audit(1480446615.354:844): avc:  denied  { getattr } for  pid=23150 comm="httpd" path="/home/jkastning/public_html/index.html" dev="sda1" ino=1052157 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:httpd_user_content_t:s0 tclass=file

[root@centos ~]# tail /var/log/messages | grep SELinux
Nov 29 20:10:44 centos setroubleshoot: SELinux is preventing httpd from getattr access on the file /home/jkastning/public_html/index.html. For complete SELinux messages. run sealert -l 13419731-cedd-4433-abb7-b2e7715d5636

Um weitere Informationen zu erhalten, führen wir das Kommando aus /var/log/messages aus (Ausgabe gekürzt):

[root@centos ~]# sealert -l 13419731-cedd-4433-abb7-b2e7715d5636
SELinux is preventing httpd from getattr access on the file /home/jkastning/public_html/index.html.

*****  Plugin catchall_boolean (24.7 confidence) suggests   ******************

If you want to allow httpd to enable homedirs
Then you must tell SELinux about this by enabling the 'httpd_enable_homedirs' boolean.
You can read 'None' man page for more details.
Do
setsebool -P httpd_enable_homedirs 1

In der obigen Ausgabe wird neben der Ursache auch gleich die Lösung mitgeliefert. Nach der Aktivierung des SELinux Boolean httpd_enable_homedirs kann der Inhalt der Datei index.html im Webbrowser abgerufen werden.

apache_userdir_allowed

httpd_enable_homedirs –> on

Damit wurde eine weitere Funktionalität von SELinux kurz vorgestellt. Für weiterführende Informationen sei auf die Manpages booleans(8)6, getsebool(8)7 und setsebool(8)8 verwiesen.

Einführung in das grundlegende Konzept von SELinux

SELinux1 (Security-Enhanced Linux; engl. „sicherheitsverbessertes Linux“) ist eine Erweiterung des Linux-Kernels, mit der eine zusätzliche Sicherheitsschicht in das Betriebssystem eingezogen wird.

In dieser Einführung wird das zu Grunde liegende Konzept kurz vorgestellt und erläutert, welche Modi SELinux besitzt, wie diese angezeigt und umgeschaltet werden können. An einem einfachen Beispiel mit dem Apache Webserver wird dargestellt, wie SELinux in der Praxis wirkt. Des Weiteren wird mit Hilfe des Beispiels das Vorgehen bei einer Fehleranalyse erläutert.

SELinux ist dazu gedacht, das bestehende Berechtigungskonzept unter Linux zu erweitern, um zu verhindern, dass kompromittierte Dienste auf Daten zugreifen, auf die kein Zugriff erforderlich ist.

Als einfaches Beispiel mag hier der Webserver Apache2 dienen.

Dieser liest die Dateien, welche er ausliefern soll, in der Standardkonfiguration aus dem dem Verzeichnis /var/www/html. Daneben darf der Dienst z.B. auch alle Verzeichnisse und Dateien unterhalb der Verzeichnisse /tmp und /var/tmp lesen.

user@host:~$ ls -ld /tmp
drwxrwxrwt. 5 root root 4096 Nov 27 19:05 /tmp
user@host:~$ ls -ld /var/tmp
drwxrwxrwt. 2 root root 4096 Nov 10 06:34 /var/tmp
user@host:~$

Grundsätzlich hat der Dienst Zugriff auf alle Verzeichnisse und Dateien, welche den Lese- bzw. Schreibzugriff für alle Benutzer erlauben.

SELinux kann nun dazu genutzt werden, um genau diesen Zugriff zu unterbinden. Die folgende Abbildung soll dies verdeutlichen:

selinux-example-apache

Mögliche Zugriffe mit und ohne SELinux am Beispiel des Apache

SELinux stellt einfach gesprochen ein Regelwerk dar, nach welchem bestimmt wird, auf welche Verzeichnisse, Dateien, Prozesse und Ports ein Dienst zugreifen darf. Dazu erhalten diese Objekte ein Label mit einem Security Context. Nur wenn das SELinux-Regelwerk den Zugriff von einer Ressource auf eine weitere Ressource explizit erlaubt, ist der Zugriff gestattet. Andernfalls wird die Interaktion unterbunden. Dabei unterscheidet SELinux die verschiedenen Label nach Kontexten. Diese sind:

  • user
  • role
  • type
  • sensitivity

Im Folgenden wird ausschließlich der Kontext „type“ weiter betrachtet. So besitzt z.B. der Dienst Apache das Label httpd_t, während Dateien unterhalb von /var/www/html das Label httpd_sys_content_t tragen. Da SELinux eine Regel besitzt, welche den Zugriff des Kontext httpd_t auf Dateien und Verzeichnisse mit dem Kontext httpd_sys_content_t gestattet, kann der Apache die Datei index.html aus dem Verzeichnis /var/www/html anzeigen:

output-index-html

Ausgabe der Datei /var/www/html/index.html

Obiger Screenshot enthält neben dem obligatorischen „Hallo Welt“ auch noch die Ausgabe des Verzeichnis-Listings, welches neben den üblichen Angaben wie Berechtigungen, Benutzer und Gruppe auch den SELinux-Kontext mit ausgibt. Wie man SELinux steuert und sich den Kontext von Verzeichnissen, Dateien und Prozessen anzeigen lässt, wird im Folgenden Abschnitt erläutert.

Steuerung von SELinux

SELinux kennt drei Modi:

  • Enforcing
  • Permissive
  • Disabled

In welchem Status sich SELinux befindet, kann mit dem Kommando getenforce abgefragt werden:

[root@centos ~]# getenforce
Enforcing
[root@centos ~]#

Obige Ausgabe bedeutet, dass SELinux im Modus „Enforcing“ ausgeführt wird. In diesem Modus verhindert SELinux Zugriffe, welche nicht explizit durch das Regelwerk erlaubt werden. Im Unterschied dazu werden diese Zugriffe im Modus „Permissive“ nicht verhindert, sie werden jedoch protokolliert. Dieser Modus eignet sich daher hervorragend, um das aktuelle Verhalten von Diensten zu analysieren und die Konfiguration ggf. anzupassen. Die Umschaltung zwischen den Modi „Enforcing“ und „Permissive“ kann zur Laufzeit erfolgen:

[root@centos ~]# getenforce
Enforcing
[root@centos ~]# setenforce 0
[root@centos ~]# getenforce
Permissive
[root@centos ~]# setenforce 1
[root@centos ~]# getenforce
Enforcing
[root@centos ~]#

Der Standard-Modus wird in der Datei /etc/selinux/config festgelegt.

Anzeige der SELinux-Kontexte

Der kleine Punkt (im Screenshot rot markiert) hinter den Berechtigungen zeigt an, dass ein SELinux-Kontext für eine Datei bzw. ein Verzeichnis existiert:

Der Punkt zeigt einen vorhandenen SELinux-Kontext an

Der Punkt zeigt einen vorhandenen SELinux-Kontext an

Welchen Kontext eine Datei bzw. ein Prozess besitzt, kann angezeigt werden, indem bekannte Kommandos mit der Option „-Z“ benutzt werden. Das folgende Listing zeigt einige Beispiele:

[root@centos ~]# ps -eZ | grep httpd
system_u:system_r:httpd_t:s0     2876 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     2920 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     2921 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     2922 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     2923 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     2924 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     2925 ?        00:00:00 httpd
system_u:system_r:httpd_t:s0     3152 ?        00:00:00 httpd
[root@centos ~]# ls -lisaZ /var/www/html/
total 12
drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 .
drwxr-xr-x. root root system_u:object_r:httpd_sys_content_t:s0 ..
-rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 index.html
[root@centos ~]# ls -lisaZ /var/tmp
total 28
drwxrwxrwt. root root system_u:object_r:tmp_t:s0       .
drwxr-xr-x. root root system_u:object_r:var_t:s0       ..
drwxr-xr-x. abrt abrt system_u:object_r:abrt_var_cache_t:s0 abrt
drwx------. root root system_u:object_r:tmp_t:s0       systemd-private-f844adcb52b14995b675d9aa065e925c-colord.service-MczkBW
drwx------. root root system_u:object_r:tmp_t:s0       systemd-private-f844adcb52b14995b675d9aa065e925c-cups.service-c3Aouq
drwx------. root root system_u:object_r:tmp_t:s0       systemd-private-f844adcb52b14995b675d9aa065e925c-httpd.service-oAqhlb
drwx------. root root system_u:object_r:tmp_t:s0       systemd-private-f844adcb52b14995b675d9aa065e925c-rtkit-daemon.service-xpSsO2
[root@centos ~]# ls -lisaZ /tmp |head -n4
total 96
drwxrwxrwt. root      root      system_u:object_r:tmp_t:s0       .
dr-xr-xr-x. root      root      system_u:object_r:root_t:s0      ..
-rw-r--r--. root      root      system_u:object_r:tmp_t:s0       anaconda.log
[root@centos ~]#

Die Datei index.html hat bei ihrer Erstellung im Verzeichnis /var/www/html automatisch den korrekten Kontext httpd_sys_content_t erhalten und kann daher im Webbrowser angezeigt werden. Wird eine Datei im Verzeichnis /tmp erstellt, erhält diese automatisch den Kontext user_tmp_t:

[root@centos ~]# echo 'TEST' > /tmp/test.txt
[root@centos ~]# ls -lisaZ /tmp/test.txt
-rw-r--r--. root root unconfined_u:object_r:user_tmp_t:s0 /tmp/test.txt
[root@centos ~]#

Was tun, wenn’s klemmt?

Um zu erläutern, wie man einem Fehler auf die Spur kommt, baue ich zuerst einen ein. Dazu verschiebe ich die im vorangegangenen Abschnitt erzeugte Datei in das DocumentRoot des Apache und versuche, diese im Browser aufzurufen.

forbidden

Forbidden

SELinux verhindert den Zugriff des Dienstes „httpd“ auf die Datei „text.txt“, da keine Regel existiert, welche den Zugriff vom Kontext httpd_t auf user_tmp_t explizit erlaubt. Doch wie kann man dies feststellen, wenn man nicht bereits zu Beginn um den falschen Kontext weiß?

Zuerst wirft man einen Blick auf die Dateiberechtigungen, welche aber keinen Fehler erkennen lassen:

[root@centos ~]# ls -lZ /var/www/html/
-rw-r--r--. root root unconfined_u:object_r:httpd_sys_content_t:s0 index.html
-rw-r--r--. root root unconfined_u:object_r:user_tmp_t:s0 test.txt
[root@centos ~]#

Da die Dateiberechtigungen als Fehlerquelle ausscheiden und in diesem Beispiel keine POSIX-ACL3 verwendet werden, bleibt nur noch SELinux als Fehlerquelle übrig. Daher suchen wir einmal in /var/log/audit/audit.log nach Ereignissen vom Typ „AVC“:

[root@centos ~]# tail /var/log/audit/audit.log | grep AVC
type=AVC msg=audit(1480277043.225:537): avc:  denied  { open } for  pid=2922 comm="httpd" path="/var/www/html/test.txt" dev="sda1" ino=924457 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:user_tmp_t:s0 tclass=file

Treffer! Um die Sache weiter zu analysieren, untersuchen wir als nächstes /var/log/messages (Ausgabe gekürzt):

Nov 27 21:29:13 centos setroubleshoot: SELinux is preventing /usr/sbin/httpd from open access on the file /var/www/html/test.txt. For complete SELinux messages. run sealert -l 7a6417b8-06e2-4499-8f0f-ed0bb16f2b2a

Hier findet sich eine Bestätigung, dass SELinux den Zugriff blockiert hat und ein Vorschlag, wie dieser Vorfall genauer analysiert werden kann. Damit lassen sich umfassende Informationen abrufen, welche sogar die Lösung mit ausgeben (Ausgabe gekürzt):

[root@centos ~]$ sealert -l 7a6417b8-06e2-4499-8f0f-ed0bb16f2b2a
SELinux is preventing /usr/sbin/httpd from open access on the file /var/www/html/test.txt.

*****  Plugin restorecon (92.2 confidence) suggests   ************************

If you want to fix the label. 
/var/www/html/test.txt default label should be httpd_sys_content_t.
Then you can run restorecon.
Do
# /sbin/restorecon -v /var/www/html/test.txt

*****

Der Bericht gibt an, wie das Standard-Label für die Datei text.txt gesetzt sein sollte. Darüber hinaus ist dem Bericht der Befehl zu entnehmen, mit dem die Label wieder auf den Standardwert zurückgesetzt werden können. Nachdem dieser Befehl ausgeführt wurde, kann die Datei text.txt im Webbrowser geöffnet werden.

[root@centos ~]# /sbin/restorecon -v /var/www/html/test.txt
/sbin/restorecon reset /var/www/html/test.txt context unconfined_u:object_r:user_tmp_t:s0->unconfined_u:object_r:httpd_sys_content_t:s0
[root@centos ~]#
correct-selinux-context

Aufruf von test.txt mit korrektem SELinux-Kontext

Schlusswort

Es wurde in das grundlegende Konzept von SELinux und die möglichen Betriebsmodi eingeführt. Darüber hinaus wurde erläutert, wie man sich die zu einem Objekt gehörenden SELinux-Kontexte anzeigen lassen kann und wie diese auf einen Standard-Kontext zurückgesetzt werden können. Abschließend wurde ein kleiner Einblick in das Vorgehen zur Fehleranalyse gegeben.

Damit bin ich am Ende dieser kleinen Einführung angekommen, wobei das Thema SELinux damit noch lange nicht abschließend behandelt ist. Für weiterführende Informationen sei an dieser Stelle auf die Dokumentation der einzelnen Distributionen verwiesen.4 5 6

Quellen und weiterführende Links

Benutzer die Ausführung eines Skripts mit sudo gestatten

In diesem kurzen Tutorial wird beschrieben, wie man einem normalen Benutzer das Recht einräumt, ein einzelnes Skript mit sudo auszuführen. Das Tutorial ist auf alle Linux-Distributionen anwendbar, welche sudo1 unterstützen.

Schritt 1: Skript und Benutzerkonto erstellen

Zuerst wird natürlich das Skript benötigt, welches der neue Benutzer später ausführen soll. Als Beispiel mag hier folgendes einfaches Beispiel dienen:

#!/bin/bash
echo "Hallo Welt."

Wichtig! Der Benutzer, welcher das Skript später ausführen soll, darf selbst keine Schreibrechte auf darauf besitzen. Andernfalls könnte er das Skript bearbeiten und durch eintragen von bash eine root-shell öffnen. Danke an Gerald für diesen wichtigen Hinweis.

Der Benutzer kann, sofern er nicht schon existiert, mit folgendem Kommando angelegt werden:

sudo adduser USERNAME

Schritt 2: /etc/sudoers konfigurieren

Um einem Benutzer das Recht zu verleihen, gibt es grundsätzlich mehrere Möglichkeiten.

Benutzer einer Gruppe hinzufügen

Auf vielen Linux-Distributionen existiert bereits eine Gruppe, deren Mitglieder die Berechtigung zur Verwendung von sudo besitzen. Unter Ubuntu ist dies z.B. die Gruppe ’sudo‘. Unter RHEL, CentOS und Fedora ist dies bspw. die Gruppe ‚wheel‘. Um welche Gruppe es sich konkret handelt, kann in der Datei /etc/sudoers nachgeschlagen werden. Dort findet sich auf einem Ubuntu 16.04 LTS z.B. folgender Eintrag:

# Allow members of group sudo to execute any command
%sudo   ALL=(ALL:ALL) ALL

Da dem Benutzer im hier beschriebenen Fall jedoch nur erlaubt werden soll, ein einziges Skript mittels sudo auszuführen, ist diese Methode ungeeignet.

Benutzer in /etc/sudoers

Um einem Benutzer das Recht zu gewähren, ein bestimmtes Skript oder Programm mit sudo auszuführen, kann der Benutzer wie folgt in die Datei /etc/sudoers eingetragen werden.

Wichtig! Die Datei /etc/sudoers sollte nur als root mit dem Kommando visudo editiert werden, da hiermit eine Syntaxprüfung erfolgt. Eine Beschädigung der Datei /etc/sudoers, z.B. durch Syntaxfehler, kann dazu führen, dass das gesamte System unbrauchbar wird.

# User privilege specification
USERNAME    ALL=/path/to/script.sh

Mit obiger Zeile wird dem Benutzer ‚USERNAME‘ erlaubt, das Skript unter /path/to/script.sh mit sudo auszuführen.

Diese Methode ist bereits geeignet, um die gestellte Aufgabe zu lösen.

Datei unter /etc/sudoers.d/ erstellen

Unter aktuellen Versionen von Debian, Ubuntu, RHEL, CentOS und Fedora besteht die Möglichkeit, eine Datei im Verzeichnis /etc/sudoers.d/ zu erstellen, welche den Eintrag aus dem vorangegangenen Abschnitt enthält. Voraussetzung dafür ist, dass die Datei /etc/sudoers folgende Direktive enthält:

# See sudoers(5) for more information on "#include" directives:

#includedir /etc/sudoers.d

Beachte: Das Zeichen ‚#‘ vor ‚includedir‘ stellt in diesem Fall kein Kommentarzeichen dar.

Diese Methode hat den Vorteil, dass die Datei /etc/sudoers unverändert bleibt und es bei Updates nicht zu einem Versionskonflikt kommen kann.

Fazit

Mittels /etc/sudoers ist es möglich, sudo-Berechtigungen granular an Benutzer zu delegieren. Neben dem in diesem Tutorial beschriebenen Beispiel existieren noch weitere Möglichkeiten. Beispiele dazu finden sich in der Manpage von sudoers.