Automatische Updates für alles!

6 Minuten Lesezeit

Wie der Titel schon sagt, möchte ich Sicherheitsupdates gern installiert haben, sobald sie erscheinen. Dies gilt gleichsam für das Ubuntu meines Cloud-Servers wie auch für alle Applikationen, die ich in Docker betreibe. Da ich faul bin, möchte ich diese Sicherheitsupdates nicht manuell vornehmen, sondern vollautomatisch ablaufen lassen.

Für Funktionsupdates und -Erweiterungen hingegen ist es auch weiterhin OK für mich, von Zeit zu Zeit manuell einzugreifen. Beginnen wir also mit den Systemupgrades.

Sicherheitsupdates per unattended-upgrades

Ubuntu stellt - wie eine kurze Suchmaschinenabfrage ergibt - bereits ein konfigurierbares Werkzeug für solche automatisierten Updates zur Verfügung: unattended-upgrades. Ich prüfe zuerst, ob es bereits auf meinem System vorhanden ist:

Installation

schallbert:~# which unattended-upgrades
/usr/bin/unattended-upgrades

Prima, es ist bereits da. Ansonsten hätte ich die Installation schnell mit apt install unattended-upgrades nachgeholt.

unattended-upgrades konfigurieren

Hier hilft das Debian Wiki weiter: Die Konfigurationsdateien liegen in /etc/apt/apt.conf.d.

Zuerst gehe ich sicher, dass automatische Updates aktiviert sind. Hierfür prüfe ich die Datei 20auto-upgrades und stelle sicher, dass Paketindex-Updates und Upgradeinstallation aktiviert sind:

APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";

Sehr gut. Jetzt sollte ich noch festlegen, zu welchem Zeitpunkt das Tool loslaufen und welcher Typ Updates installiert werden soll. Hierfür öffne ich die Datei 50unattended-upgrades und entferne die Kommentare bei den entsprechenden Zeilen.

Updatequellen festlegen

Der erste Abschnitt der Konfigurationsdatei widmet sich der erlaubten Quellen für die Updates. Hier sollten selbstverständlich nur vertrauenswürdige Provider stehen. Meine Konfig sieht hier wie folgt aus:

Unattended-Upgrade::Allowed-Origins {
        "${distro_id}:${distro_codename}";
        "${distro_id}:${distro_codename}-security";
        "${distro_id}ESMApps:${distro_codename}-apps-security";
        "${distro_id}ESM:${distro_codename}-infra-security";
};

Erlaubt werden hier Quellen für die von mir verwendete Linux-Distribution im Allgemeinen und Sicherheitsupdates inklusive ESM, was für “Extended Security Maintenance” steht.

Updates auch für Dev Releases?

Die folgende Zeile legt fest, ob auch Vorabversionen von Ubuntu Updates erhalten sollen. Bei mir steht der Wert auf auto.

Unattended-Upgrade::DevRelease "auto";

Automatischer Neustart

Einige Sicherheitsupdates (z.B. den Kernel betreffend) erfordern einen Neustart der Maschine. Sie werden daher nur in Kraft treten, wenn man in der Datei entsprechende Werte gesetzt hat. Dabei ist WithUsers natürlich Geschmackssache, denn man wird ausgeloggt, falls zufälligerweise gerade ein Update installiert wurde, was einen Neustart erfordert. Um das praktisch auszuschließen, habe ich unter Reboot-Time eine Zeit eingetragen, bei der ich sicher nicht an der Maschine hocke.

Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-WithUsers "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:45";

Unangekündigte Neustarts haben natürlich Rückwirkung auf alle Programme, die ich verwende. Daher stelle ich in meinen docker-compose.yml Dateien sicher, dass dort bei restart: auch wirklich always oder unless-stopped notiert ist.

Benachrichtigungen

Automatische Mails oder anderweitige Push-Benachrichtigungen habe ich erst einmal nicht eingeschaltet, da ich mich noch im Testbetrieb befinde und keine große Lust auf einen Haufen weiterer Nachrichten habe. Sobald ich dies ändere, gibt es hier ganz bestimmt ein Update.

Funktionsprüfung

Um zu sehen, ob unattended-upgrades tut was es soll, schaue ich ein paar Tage später mal in die Logs unter /var/log/unattended-upgrades. Hier steht in der Datei unattended-upgrades.log zum Beispiel folgendes:

# [...]
2024-01-12 06:17:59,781 INFO Starting unattended upgrades script
2024-01-12 06:17:59,781 INFO Allowed origins are: o=Ubuntu,a=jammy, o=Ubuntu,a=jammy-security, o=UbuntuESMApps,a=jammy-apps-security, o>
2024-01-12 06:17:59,781 INFO Initial blacklist:
2024-01-12 06:17:59,782 INFO Initial whitelist (not strict):
2024-01-12 06:14:33,956 INFO Packages that will be upgraded: libc-bin libc-dev-bin libc-devtools libc6 libc6-dev locales python3-twisted
2024-01-12 06:14:33,956 INFO Writing dpkg log to /var/log/unattended-upgrades/unattended-upgrades-dpkg.log
2024-01-12 06:14:54,578 INFO All upgrades installed
# [...]

Die Datei unattended-upgrades-shutdown.log ist allerdings noch leer. Ich werde zu einem späteren Zeitpunkt prüfen, ob hier auch alles funktioniert.

Container-Updates mit Watchtower automatisieren

Nun zu den Updates meiner Applikationen. Ein kurzes Gespräch mit ein paar Admins ergab:

  • Es ist mühsam, alle Programme von Hand auf Stand zu halten
  • Für Anwendungen im Container gibt es Dienste, die diese Aufgabe zentral lösen
  • Es gab bereits schlechte Erfahrungen, wenn in der docker-compose.yml auf den aktuellsten Release referenziert wird
  • Bei image: <application>:latest seien hier nicht alle Anwendungen stabil bzw. erzeugten Probleme mit Abhängigkeiten

Einer Lösung für dieses Problem bietet Watchtower, welches in meinem Falle selbst wieder in einem Container läuft.

Installation

Wie üblich schreibe ich mir in einem neuen Ordner unter opt/watchtower eine docker-compose.yml:

# copied from https://containrrr.dev/watchtower/notifications/

# watchtower/docker-compose.yml
version: "3"
services:
  watchtower:
    image: containrrr/watchtower
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

Watchtower benötigt docker.sock, um die Updates zu ziehen und auf den vorhandenen Containern anzuwenden. Ansonsten habe ich hier erst einmal nichts weiter unternommen. Keine Benachrichtigungen (aus obengenannten Gründen), keine Logs, nichts. Ich hoffe, dieses Setup bleibt geräuschlos und ich muss mich hier lange nicht mit einem Update zu Wort melden.

Weitergehende Schritte für die Validierung der Funktion von Watchtower finden sich z.B. in einem Tutorial von DigitalOcean.

Ablauf

Watchtower prüft die Installierten Docker images lt. Doku täglich auf Updates. Sollte des ein neues Image-release geben, schickt Watchtower ein SIGTERM-Signal an upzudatende Container, worauf sich selbige herunterfahren (Quelle). Anschließend werden die Container wieder hochgefahren.

Unfreiwillige Funktionsprüfung

Ein paar Tage nach Start des Containers wählte ich mich nochmal in meinen Server ein und sah auf gut Glück ein paar Logs durch. Ich wollte nur kurz nach dem Rechten schauen.

In den fail2ban Logdateien sah ich zu einem bestimmten Zeitpunkt, Uhrzeit 14:42:56 plötzlich seitenweise Einträge. Mehr davon, als ich die Tage vorher insgesamt gesehen hatte. Als ich zum Beginn dieser Kette von Einträgen hochscrollte, sah ich, dass fail2ban mir wohl kurz ausgestiegen war.

 2024-01-26 14:42:50,817 <container_id> INFO  Shutdown in progress...
 2024-01-26 14:42:50,817 <container_id> INFO  Observer stop ... try to end queue 5 seconds
 2024-01-26 14:42:50,837 <daemon_id> INFO  Observer stopped, 0 events remaining.
 2024-01-26 14:42:50,878 <container_id> INFO  Stopping all jails
 2024-01-26 14:42:50,879 <container_id> INFO  Removed logfile: '/var/log/auth.log'
 2024-01-26 14:42:50,879 <container_id> INFO  Removed logfile: '/var/log/caddy2/gitea_access.log'
 2024-01-26 14:42:50,879 <container_id> INFO  Removed logfile: '/var/log/caddy2/server_access.log'
 2024-01-26 14:42:51,052 <daemon_id> NOTIC [sshd] Flush ticket(s) with iptables
 2024-01-26 14:42:51,063 <container_id> INFO  Jail 'sshd' stopped
 2024-01-26 14:42:51,137 <daemon_id> NOTIC [caddy-status] Flush ticket(s) with iptables-multiport
 2024-01-26 14:42:51,137 <container_id> INFO  Jail 'caddy-status' stopped
 2024-01-26 14:42:51,138 <container_id> INFO  Connection to database closed.
 2024-01-26 14:42:51,139 <container_id> INFO  Exiting Fail2ban
 2024-01-26 14:42:56,173 <new_container_id> INFO  --------------------------------------------------
 2024-01-26 14:42:56,173 <new_container_id> INFO  Starting Fail2ban v1.0.2
 2024-01-26 14:42:56,173 <new_container_id> INFO  Observer start...
[...]

Die ganzen Log-Einträge zur selben Uhrzeit beginnen am Ende des oben gezeigten Abschnitts. Ein ganzer Haufen IP-Adressen wird dort zur Vorbereitung des Banns in Fail2ban geladen. Leicht beunruhigt schaute ich ins Syslog von Ubuntu:

Jan 26 14:42:55 schallbert systemd[1]: docker-<ID>.scope: Deactivated successful
Jan 26 14:42:55 schallbert systemd[1]: docker-<ID>.scope: Consumed 21:05 CPU time
Jan 26 14:42:55 schallbert dockerd[690]: time="2024-01-26T14:42:55.228607617Z" level=info msg="ignoring event" 
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.229864632Z" level=info msg="shim disconnected" 
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.230125409Z" level=warning msg="cleaning up after shim disconnected" i>
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.230145478Z" level=info msg="cleaning up dead shim"
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.240586557Z" level=warning msg="cleanup warnings time=\"2024-01-26T14:>
Jan 26 14:42:55 schallbert dockerd[690]: time="2024-01-26T14:42:55.241839655Z" level=warning msg="ShouldRestart failed
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.870912387Z" level=info msg="loading plugin \"io.containerd.event.v1.p>
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.870997689Z" level=info msg="loading plugin \"io.containerd.internal.v>
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.871007368Z" level=info msg="loading plugin \"io.containerd.ttrpc.v1.t>
Jan 26 14:42:55 schallbert containerd[639]: time="2024-01-26T14:42:55.871150831Z" level=info msg="starting signal loop" namespace=moby path>

Hm, cleaning up dead shim. Das klingt ja bedrohlich. Eine kurze Recherche zeigt, dass die Shim eine Zwischenschicht zwischen Containermanager und dem Container selbst bildet und Ein-und Ausgaben des Containers weiterleitet. Ihre Laufzeit ist an die des Containers gekoppelt.

Also war dies ein normales Herunterfahren mit anschließendem Neustart. Zum Glück kein Einbruch! Bleibt nur noch der Grund für den Neustart von Fail2ban herauszufinden. Da fällt mir ein, dass ich noch gar nicht weiß, wann Watchtower genau die Updates fährt…

Also mal schnell in die Logs von Watchtower geschaut mit docker container logs <container_id> und siehe da:

time="2024-01-26T14:42:47Z" level=info msg="Found new lscr.io/linuxserver/fail2ban:latest image (9bda077d765e)"
time="2024-01-26T14:42:50Z" level=info msg="Stopping /fail2ban (e0317a105d53) with SIGTERM"
time="2024-01-26T14:42:55Z" level=info msg="Creating /fail2ban"
time="2024-01-26T14:42:55Z" level=info msg="Session done" Failed=0 Scanned=6 Updated=1 notify=no

Damit habe ich den Beweis: Watchtower tut was es soll. Es zieht Container auf eine neue Version hoch, sobald Updates verfügbar werden. Prima!

PS: Da ich Watchtower nicht groß konfiguriert habe, nehme ich anhand der in den Logs befindlichen Zeitstempel an, dass die Updates täglich zur Zeit des Starts von Watchtower gefahren werden. Bald werde ich von latest für meine Container wohl auf ein älteres Tag release umschwenken.