Blog

αὐτάρκης

Gut ein Jahr nach dem Rückbau des Webstacks auf meinem hessischen Server siegt mein Basteltrieb über den Homo oeconomicus, und ich repliziere meine Django-Betriebsumgebung auf einem lokalen Ubuntu-Server. Die Installation gestaltet sich zunächst sehr einfach –

apt install nginx apt install postgresql

– bis der Datenbankkonnektor über eine fehlende Header-Bibliothek stolpert. Daher:

apt install install libpq-dev apt install psycopg2

In Bezug auf das Sorgenkind uwsgi bietet apt (anders als Homebrew) ein Plugin-Paket für Python 3, so dass ich nicht auf pip und die Definition einer eigenen Service Unit zurückgreifen muss:

apt install uwsgi apt install uwsgi-plugin-python3

Die initiale Anmeldung am PostgreSQL-Server erfolgt neuerdings über das Peer-Verfahren, so dass ich zunächst das Systempasswort für postgres ändern muss, um mich anschließend in der Rolle dieser Nutzerin passwortlos anmelden zu können:

sudo passwd postgres su - postgres psql postgres=# CREATE USER myuser WITH PASSWORD 'snafu'; postgres=# GRANT ALL ON DATABASE db1 TO myuser; postgres=# \q exit psql db1 < ~/var/dbdump/db1.sql

Die Änderung der Authentifikationsmethode von peer auf scram-sha-256 in /etc/postgresql/14/main/pg_hba.conf für lokale Nutzerinnen ermöglicht die Verwendung des Passworts snafu auch ohne eine Systemnutzerin myuser.

Nun folgt die Installation des Webframeworks –

pip3 install Django

und der Transfer der Projektdaten auf den Server. Abschließend müssen lediglich geeignete Konfigurationsdateien in /etc/uwsgi/apps-enabled bzw. /etc/nginx/sites/enabled hinterlegt und alle relevanten Services gestartet, ~/.pgpass angelegt und die Umgebungsvariablen PGHOST, PGUSER und PGDATABASE in ~/.bashrc eingetragen werden.

Natürlich sollte die Synchronisation des Datenbestandes zwischen Redaktionssystem und Server weitgehend automatisiert ablaufen:

#!/usr/local/bin/python3 import os import subprocess import argparse import paramiko local_dir = '/Users/snafu/website/dbdump/' remote_dir = '/var/www/djangosite/dbdump/' db = 'mydb' dbuser = 'myuser' extension = '.sql' dumpfile = local_dir + db + extension targetfile = remote_dir + db + extension configparser = paramiko.SSHConfig() configparser.parse(open('/Users/snafu/.ssh/config')) ssh_config = configparser.lookup('myserver') ssh_key = paramiko.ECDSAKey.from_private_key(open(ssh_config['identityfile'][0])) def print_status(return_code): if return_code == 0: print('done.') else: print('failed!') def dump_db(): print('Dumping local database...') with open(dumpfile, 'w') as writefile: completed_process = subprocess.run(['pg_dump', '-E', 'UTF8', '-c', db], stdout=writefile) print_status(completed_process.returncode) def transfer_db(): print('Sending database dump to server ...') transport = paramiko.Transport((ssh_config['hostname'],int(ssh_config['port']))) transport.connect(username=ssh_config['user'], pkey=ssh_key) sftp = paramiko.SFTPClient.from_transport(transport) try: sftp.put(dumpfile,targetfile) print_status(0) except: print_status(1) if sftp: sftp.close() if transport: transport.close() def update_db(): print('Updating remote database ...') client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect(ssh_config['hostname'], username=ssh_config['user'], port=ssh_config['port'], key_filename=ssh_config['identityfile'][0]) stdin, stdout, stderr = client.exec_command(f'psql -U {dbuser} {db} < {targetfile}') return_code = stdout.channel.recv_exit_status() print_status(return_code) client.close() def call_func(function_name): dispatcher = { 'dump_db' : dump_db, 'transfer_db' : transfer_db, 'update_db' : update_db } return dispatcher[function_name]() def main(): p = argparse.ArgumentParser(description='Synchronize website database') p.add_argument('-d', '--dump', dest='actions', action='append_const', const='dump_db') p.add_argument('-t', '--transfer', dest='actions', action='append_const', const='transfer_db') p.add_argument('-u', '--update', dest='actions', action='append_const', const='update_db') arguments = p.parse_args() if arguments.actions: arguments.actions.sort() for action in arguments.actions: call_func(action) if __name__=='__main__': main()

Die Abfrage des return codes (return_code = stdout.channel.recv_exit_status()) in update_db() dient nicht nur dazu, das Skript kommunikativer zu gestalten, sondern wandelt exec_command() auch in einen blocking call (alternativ könnte man den psql-Aufruf auch mit nohup dekorieren, damit die Datenbankaktualisierung auch nach dem Schließen der SSH-Verbindung ausgeführt wird). Warum Transport() einen bereits ausgelesenen SSH-Schlüssel als Parameter benötigt, SSHClient() aber nur den Dateinamen, bleibt dagegen das Geheimnis der Paramiko-Entwicklerinnen.

Mobilmuschel

Eigentlich ist die Verheißung einer stabilen Terminal-Verbindung bei wechselnden Internet-Anbindungen für meine relativ statischen SSH-Gepflogenheiten nicht relevant, aber mit einigen Jahren Verspätung möchte ich natürlich auch den hot shit von 2012 verwenden. Die Installation von mosh ist mit Homebrew oder apt auf Client und Server denkbar einfach, und in der Firewall des kontaktierten Servers müssen lediglich einige UDP-Ports (60000-61000) geöffnet werden, um eine mosh-Session zu erlauben.

Mit einem MacBook als Server ist es etwas komplizierter. Der Eintrag von mosh-server in die Firewall-Konfiguration auf dem üblichen Weg (System Preferences → Security & Privacy → Firewall → Firewall Options) hat unter macOS Monterey keinen Effekt, während eine Konfiguration per CLI funktioniert. Ich verzichte auf die Erstellung eines LaunchDaemons und kombiniere stattdessen die Aktivierung des SSH-Daemons, die Firewall-Konfiguration und die Konfiguration servergemäßer Schlafgewohnheiten in zwei Shell-Skripten:

# moshon.sh #!/bin/zsh sudo systemsetup -f -setremotelogin on local fw='/usr/libexec/ApplicationFirewall/socketfilterfw' local mosh_sym="$(which mosh-server)" local mosh_abs="$(readlink -f $mosh_sym)" sudo "$fw" --setglobalstate off sudo "$fw" --add "$mosh_sym" sudo "$fw" --unblockapp "$mosh_sym" sudo "$fw" --add "$mosh_abs" sudo "$fw" --unblockapp "$mosh_abs" sudo "$fw" --setglobalstate on sudo pmset -a sleep 0 sudo pmset -a hibernatemode 0 sudo pmset -a disablesleep 1; # moshoff.sh #!/bin/zsh sudo systemsetup -f -setremotelogin off local fw='/usr/libexec/ApplicationFirewall/socketfilterfw' local mosh_sym="$(which mosh-server)" local mosh_abs="$(readlink -f $mosh_sym)" sudo "$fw" --setglobalstate off sudo "$fw" --remove "$mosh_sym" sudo "$fw" --remove "$mosh_abs" sudo "$fw" --setglobalstate on sudo pmset -a sleep 1 sudo pmset -a hibernatemode 3 sudo pmset -a disablesleep 0;

Endlich kann ich von meinem privaten Golfplatz aus eine mosh-Session zwischen iPad und MacBook aufbauen, während bislang mein Caddy das MacBook mitführen musste. In der Realität ist mosh für Terminal-Verbindungen im lokalen Netzwerk (oder auch zu einem gut angebundenen Rechenzentrum in Baden-Baden) recht überflüssig, aber für künftige Services mit sehr hohen Latenzen oder Reisen durch die deutsche Provinz ist mosh die perfekte Lösung. Mit Blink gibt es auch eine iOS-App mit mosh-Unterstützung, die das schöne, aber hakelige Prompt ablösen kann, das aktuell an SSH-Verbindungen zu Ubuntu 22.04 mit ECDSA-Schlüsseln scheitert.

Inkompatible Symbolik

Wenige Tage nach dem Umstieg von der Frettkatze auf die Qualle ereilt mich ein Problem, das durch die unterschiedliche Konfiguration von rsync bei zwei aufeinander folgenden Aufrufen verursacht wird: Der erste Durchlauf (rsync -av) überträgt den Symlink /etc/postfix/makedefs.out -> /usr/share/postfix/makedefs.out als Symlink, während der zweite Durchlauf (rsync -ivrLtzS) auf einem anderen System den Symlink auflösen möchte und am fehlenden Referenten scheitert.

Glückliche Qualle

Entgegen meiner generellen Risikoaversion aktualisiere ich am Veröffentlichungstag von Ubuntu 22.04 LTS nicht nur meine Homeserver, sondern auch die Mail- und Webserver (do-release-upgrade -d). Zum Glück gibt es keine besonderen Vorkommnisse. Ich muss lediglich meine sorgfältig kuratierten Konfigurationsdateien vor dem Überschreiben durch Standard-Konfigurationsdateien schützen (keep the local version currently installed) und eine obsolete Datei (/etc/dovecot/conf.d/auth-vpopmail.conf.ext) entfernen. Zeitgleich mit dem Upgrade liefert mir der Certbot erstmals neue Schlüssel und damit die Gelegenheit zum Test der Fingerabdruckautomatik unter realen Bedingungen. Die neue nginx-Konfigurationsdatei warnt vor einer Sicherheitslücke durch die Verbindung von Kompression und sensiblen Eingaben, aber dank des Verzichts auf jegliche Interaktivität ist meine Website nicht betroffen.

Apple nimmt die Veröffentlichung der glücklichen Qualle übrigens zum Anlass, die Reste seines eigenen Server-Betriebssystems zu den Fischen zu schicken.

Stagnation

Wenn ich gewusst hätte, dass mit einer kleinen Änderung in /etc/logrotate.d/nginx (rotate 14rotate 31) und dem monatlich ausgeführten Befehl

gunzip -c /var/log/nginx/access.log.*.gz | goaccess --log-format=COMBINED -o analytics_2022-02.html

aufschlussreiche Auswertungen möglich sind, hätte ich nicht 16 Jahre ohne jegliche Traffic-Analyse verstreichen lassen, zumal sich die monatliche Auswertung natürlich automatisieren lässt:

#!/usr/local/bin/python3 import os import shutil import glob import subprocess import datetime import gzip logfiles_pattern = '/var/log/nginx/access.log.*.gz' log_archive = '/var/www/analytics' last_month = datetime.date.today().replace(day=1) - datetime.timedelta(days=1) last_month_string = last_month.strftime('%Y-%m') target_dir = f'{log_archive}/{last_month_string}' combined_log = f'{target_dir}/combined.log' report = f'{log_archive}/{last_month_string}_report.html' try: os.mkdir(target_dir) except: print(f'{target_dir} exists, exiting.') exit() file_content = b'' for file in glob.glob(logfiles_pattern): with gzip.open(file, 'rb') as f: file_content += f.read() shutil.move(file, target_dir) with open(combined_log, 'wb') as f: f.write(file_content) command_list = ['goaccess', combined_log, '--log-format=COMBINED', '-o', report] subprocess.run(command_list) os.remove(combined_log)

Leider fordert GoAccess eine ausgesprochen riskante Content Security Policy ein (Content-Security-Policy "script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'"), um seine dynamischen HTML-Reports per Webserver ausliefern und darstellen zu lassen, aber der lokale Aufruf funktioniert auch ohne Sicherheitsrisiken.

Jedenfalls zeigt der Report in unangenehmer Deutlichkeit, wie konstant die Popularität meiner Website seit der letzten Auswertung mit Webalizer im Jahr 2006 ist: Von knapp 1400 Besucherinnen/Tag habe ich mich auf rund 1500 Besucherinnen/Tag gesteigert. Zwar haben sich die täglichen Aufrufe etwa verdoppelt (von 10.000 auf 20.000), aber das ist eher als Rückschritt zu betrachten. Außerdem weist GoAccess den Anteil der Crawler (54,12% der Besucherinnen, 82,83% der Aufrufe, 76,39% des Traffics) aus, so dass ich mich wohl von meinem Berufsziel Influencer verabschieden kann.

Unam Profanam

Wann immer man glaubt, nun könne das Erzbistum Köln seine Kirchenaustrittskampagne nicht mehr intensivieren, überrascht Rainer Maria Woelki die Öffentlichkeit mit neuen Ideen. Im aktuellen Fall wurde die Fürsorgepflicht für einen gestrauchelten Mitbruder sehr weit ausgelegt, um zunächst dessen Spielschulden und anschließend die aus dieser Zahlung erwachsene Steuerschuld zu begleichen, natürlich an den mittlerweile auch in Kirchenkreisen existierenden Kontrollgremien vorbei, und teilweise (500.000 Euro) aus dem Sondervermögen für Ausgleichszahlungen an Opfer sexuellen Missbrauchs.

Dank der überzeugenden Argumente gegen die Zahlung von Kirchensteuern und der großzügigen Alimentation des Kirchenpersonals dürfte der schweigsame Herr Woelki bald zu Verhandlungen mit RTL gezwungen sein (Arbeitstitel: Sodom und Gomorrha op Kölsch). Wenn der Kirchendude das Ruder nicht noch herumreißt.

Neustart

Obwohl respektable deutsche Hosting-Provider in kurzen Abständen schlecht kommunizierte (wenn auch vergleichsweise kurze) Strom- und Hardware-Ausfälle verzeichnen, sind sie nach wie vor vertrauenswürdiger als cloudgestützte Datenhändler. Ich glaube zwar nicht, dass sich Milliarden Nutzerinnen sozialer Netzwerke demnächst Wordpress-Instanzen einrichten, aber vielleicht ist Anil Dashs Optimismus angesichts der wachsenden Unzufriedenheit mit aufmerksamkeitsorientierten Algorithmen und den auf ihnen basierenden Geschäftsmodellen nicht völlig unbegründet:

On your own site, though, under your own control, you can do things differently. Build the community you want. I'm not a pollyanna about this; people are still going to spend lots of times on the giant tech platforms, and not everybody who embraces the open web is instantly going to become some huge hit. Get your own site going, though, and you’ll have a sustainable way of being in control of your own destiny online.

Freud/Leid

Todd Ditchendorfs praktischer HTTP Client für macOS wurde seit vielen Jahren nicht aktualisiert und agiert mittlerweile so wacklig, dass Todd selbst andere (und teilweise erstaunlich kostspielige) Anwendungen empfiehlt. In dieser Situation kommt ein Missgeschick von Jakub Roztočil wie gerufen, indem es mich auf das Python-basierte HTTPie for Terminal aufmerksam macht.

Konditioniert

John Regehr hat schon vor vielen Jahren beobachtet, dass erfahrene und erfolgreiche Software-Nutzerinnen vor allem gut dressiert sind. Mit den folgenden Verhaltensweisen –

– unterscheiden sie sich von störrischen Menschen, die auf instabile Zustände von Computersystemen nicht mit Geduld und Vorsicht reagieren, und die sich weigern, Problemvermeidungsstrategien zu erlernen. Unglücklicherweise arbeiten diese Menschen selten als Software-Testerinnen, sondern leben ihre besondere Begabung aus, indem sie Help Desk-Mitarbeiterinnen mit vagen Beschreibungen schlecht reproduzierbarer Fehler beschäftigen.

Abhängigkeiten

uwsgi bleibt das Sorgenkind in meinem Webstack. Mit dem heutigen Update der Homebrew-Pakete wird neben Python 3.10.2 auch ein von Python 3.10.2 abhängiges uwsgi 2.0.20 installiert, was im Zusammenspiel mit Python 3.9.12 und Django 4.0.2 zu Problemen führt. Zum Glück hilft die bereits erprobte Deinstallation (brew uninstall uwsgi; pip3 uninstall uwsgi), diesmal in Verbindung mit einem rein pip-basierten Neuanfang (pip3 install uwsgi --no-cache), denn pip3 fühlt sich Python 3.9.12 weiterhin verbunden.

Für den automatischen Start von uwsgi übertrage ich die Inhalte von homebrew.mxcl.uwsgi.plist nach snafu.localhotel.uwsgi.plist und kann fürderhin auf Homebrew verzichten (jedenfalls in Bezug auf uwsgi).

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>KeepAlive</key> <true/> <key>Label</key> <string>snafu.localhotel.uwsgi</string> <key>ProgramArguments</key> <array> <string>/opt/homebrew/bin/uwsgi</string> <string>--uid</string> <string>_www</string> <string>--gid</string> <string>_www</string> <string>--master</string> <string>--die-on-term</string> <string>--autoload</string> <string>--logto</string> <string>/opt/homebrew/var/log/uwsgi.log</string> <string>--emperor</string> <string>/opt/homebrew/etc/uwsgi/apps-enabled</string> </array> <key>RunAtLoad</key> <true/> <key>WorkingDirectory</key> <string>/opt/homebrew</string> </dict> </plist>

Textnichtsauger

BBEdit wird heute 30 Jahre alt, und es gab eine Zeit, in der allein dieser Editor ein wichtiges Argument gegen einen Wechsel auf andere Plattformen war (heute binden mich vor allem mobile Endgeräte und Cloud-Dienste). Seit mehr als 10 Jahren sind nun vim und mutt meine wichtigsten Werkzeuge, zumal der Slogan This one just sucks less besser zu meinem empirischen Realismus passt als das apodiktische It doesn't suck.

Trotzdem erwerbe ich eine Lizenz für jede neue BBEdit-Version, weil Barebones Software ausgesprochen freundlich und großzügig ist. Apple dagegen zeigt mit jeder lizenzbedingten Entscheidung, dass gute Texteditoren keine relevante Kategorie für absurd profitable IT-Unternehmen sind. Positiv betrachtet: Veraltete Unix-Tools im macOS-Lieferumfang kollidieren immer seltener mit aktuellen Homebrew-Versionen.

Offenes Wissen, geschlossene Gesellschaft

Unter tausenden Wikipedia-Änderungen aus deutschen Behördennetzen finden sich neben einigen Rechtsverstößen auch verzweifelte Versuche, den ehrbaren Beamtinnenstand vor Karrieristinnen zu bewahren:

Mit Ausnahme der Besoldungsgruppe B 1, die der A 15 in der Endstufe entspricht, sind in der Besoldungsordnung B die Ämter des Spitzenpersonals des öffentlichen Dienstes zu finden. Die Besoldung aller nach B 2 aufwärts eingestuften Ämter ist höher als die der Ämter nach der Besoldungsordnung A. Nur ein Bruchteil der Beamten des höheren Dienstes erreicht im Laufe ihrer Karriere Ämter, die nach B 2 aufwärts eingestuft sind. Das Erreichen solcher Ämter bedeutet daher eine Karriere, die eher außergewöhnlich gut verläuft. Man sollte beim Eintritt in den öffentlichen Dienst nicht davon ausgehen, solche Ämter erreichen zu können.

Die Sichtung dieser wichtigen Ergänzung vom 5. März 2022, 21:33 Uhr, seitens der IP-Adresse 2003:c7:729:4639:3124:c70c:df83:5dc9 steht noch aus.

Staatsbankrott

Leider kann ein Staat wie Deutschland bei sinkendem Wohlstand kaum mehr tun als Armenspeisungen nicht zu behindern und Fruchtbarkeit in den unteren Klassen zu belohnen, aber vielleicht ist die gehobene Mittelschicht ja bereit, sich mildtätig zu zeigen.

Inkrementell

Die Absicherung von Backups gegen heimtückische Modifikationen von Informationswerten hilft natürlich nicht gegen versehentliche Löschungen und andere Ungeschicklichkeiten, wenn jeweils das bestehende Backup durch ein neues vollständig überschrieben wird. Eine inkrementelle Backup-Methode ermöglicht dagegen die regelmäßige Sicherung eines veränderten Datenbestands, ohne die vorangehenden Sicherungen zu beeinträchtigen.

Bis vor kurzem bin ich davon ausgegangen, dass ich entweder rsync nutzen oder inkrementelle Backups erstellen kann. Tatsächlich bietet rsync (natürlich!) mit dem unscheinbaren Parameter --link-dest eine Möglichkeit, die zu sichernden Daten mit einem bestehenden Ziel-Verzeichnis abzugleichen und für alle unveränderten Dateien lediglich einen zusätzlichen hard link zu setzen. Mein Wrapper rbackup muss wie folgt ergänzt werden:

class BackupProfile: # ... @property def target_dir(self): if self.incremental: return f'{self.target_path}/{datetime.date.today().strftime("%Y-%m-%d")}' else: return self.target_path @property def link_dest(self): if self.incremental: return [f'--link-dest=../latest'] else: return [] # ... def backup(self): print(f'Executing backup profile "{self.label}" ...') self.ransom() command_list = ["/opt/homebrew/bin/rsync", "-e", "ssh"] command_list.extend(self.options) command_list.extend(self.link_dest) command_list.extend(self.excludepath) command_list.extend([self.source, self.target_dir]) return_code = subprocess.call(command_list) self.rotate()

../latest ist ein Symlink auf das jeweils aktuellste Zielverzeichnis, er muss also initial von Hand erstellt (ln -s /volume1/backup/YYYY-MM-DD latest) und nach jeder Sicherung aktualisiert werden (mit der Funktion self.rotate()). Für lokale Speichermedien ist das sehr simpel (os.unlink() und os.symlink()), aber für serverbasierte Backups muss eine SFTP-Verbindung her:

def ssh_key(self, id_file): if hasattr(self, 'keytype'): if self.keytype == 'ecdsa': return paramiko.ECDSAKey.from_private_key(open(id_file)) else: return paramiko.RSAKey.from_private_key(open(id_file)) else: return False def rotate(self): if self.incremental: if hasattr(self, 'target_host'): my_config = paramiko.SSHConfig() my_config.parse(open('/Users/jan/.ssh/config')) conf = my_config.lookup(self.target_host) transport = paramiko.Transport((conf['hostname'],int(conf['port']))) transport.connect(username=conf['user'], pkey=self.ssh_key(conf['identityfile'][0])) sftp = paramiko.SFTPClient.from_transport(transport) sftp.unlink(self.sftp_latest_dir) sftp.symlink(self.target_dir, self.sftp_latest_dir) sftp.close() transport.close() else: os.unlink(self.latest_dir) os.symlink(self.target_dir, self.latest_dir)

Wenn man ein Synology NAS zur Sicherung nutzt, gibt es eine weitere Besonderheit: SSH-Verbindungen erfordern eine vollständige Pfadangabe samt Volume (z.B. /volume1/backup), für SFTP-Verbindungen dagegen wird der jeweilige shared folder als Root-Verzeichnis betrachtet (/backup). Der Symlink in unserer Konstellation muss daher als /backup/latest aktualisiert werden, aber auf /volume1/backup/YYYY-MM-DD verweisen (andernfalls findet rsync den Vergleichsordner beim nächsten Durchlauf nicht). Deshalb bedarf es eines ausgesprochen schmutzigen Hacks, um self.sftp_latest_dir abhängig vom Zielsystem zu generieren (während self.target_dir für jedes Zielsystem unverändert genutzt werden kann):

@property def sftp_latest_dir(self): if hasattr(self, 'target_host'): if self.target_host.startswith('synology'): return self.latest_dir[8:] else: return self.latest_dir

Eventuell werde ich demnächst vom interstellaren Servicedesk hinter Synology abgeholt.

Um den Platzbedarf der einzelnen Backup-Verzeichnisse zu eruieren, lässt sich das Verhalten eines altgedienten Unix-Tools bei Verwendung mehrerer Argumente ausnutzen (Files having multiple hard links are counted (and displayed) a single time per du execution.) –

snafu@localhost % du -sh . 2022-04-05 2022-04-07 2022-04-08 27G . 27G 2022-04-05 60M 2022-04-07 177M 2022-04-08

– und neue/geänderte Dateien im aktuellsten Backup-Verzeichnis lassen sich als diejenigen identifizieren, für die nur ein Link existiert (find . -type f -links 1 -print).

Integer

Eine Backup-Strategie, die mit maximalem Hardware-Einsatz regelmäßig einen korrumpierten Datenbestand sichert, wiegt die Nutzerin in trügerischer Sicherheit. Um unautorisierte Manipulationen von Dateien vor einem Backup zu erkennen, bietet sich daher ein Integritätsmonitor auf Basis kryptographischer Hashes an. Zunächst werden die Hashes aller relevanten Dateien gebildet und gespeichert (in der Hoffnung, dass zu diesem Zeitpunkt noch keine Ransomware auf dem System gewütet hat):

#!/usr/local/bin/python3 import hashlib import json import os hashpath = '/hashes.json' docpath = '/home/snafu/documents' def get_hash(file_path): myhash = hashlib.sha256() with open(file_path,'rb') as file: content = file.read() myhash.update(content) return myhash.hexdigest() def create_baseline(): with open(hashpath, 'r') as f: hash_dict = json.load(f) for current_dir, subdirs, files in os.walk(docpath): for filename in files: relative_path = os.path.join(current_dir, filename) absolute_path = os.path.abspath(relative_path) barename, extension = os.path.splitext(filename) if not filename.startswith('.') and os.path.isfile(absolute_path): if absolute_path in hash_dict: if hash_dict[absolute_path] == get_hash(absolute_path): continue else: selection = input(f'Hash for file {absolute_path} does not match baseline. Update hash and proceed (y) or cancel (n)? ') if selection == 'n': print(f'Not updating hash for file {absolute_path}') continue print(f'Updating hash/creating new hash for file {absolute_path}...') hash_dict[absolute_path] = get_hash(absolute_path) for absolute_path in list(hash_dict): if not os.path.isfile(absolute_path): print(f'Removing hash for deleted file {absolute_path}...') del hash_dict[absolute_path] with open(hashpath, 'w') as f: json.dump(hash_dict, f) if __name__ == '__main__': create_baseline()

Ab dem zweiten Durchlauf enthält die Datei hashes.json den Maßstab für nicht manipulierte Dateien; jede Änderung muss bei der Aktualisierung von hashes.json manuell akzeptiert werden. Diese Herangehensweise eignet sich offensichtlich nur für relativ statische Verzeichnisse, in denen vor allem neue Dateien abgelegt werden, und sie ist inkompatibel mit der automatischen Aktualisierung von Dateien (launchctl unload my.localhotel.serversync.plist).

Mit Hilfe von hashes.json kann jedenfalls zum Backup-Zeitpunkt geprüft werden, ob alle Dateien integer sind (unter Verwendung der Funktion get_hash() aus dem obigen Skript):

#!/usr/local/bin/python3 import os import json import pathlib from fhashlib import get_hash import scriptmail hashpath = '/hashes.json' docpath = '/home/snafu/documents' def compare_to_baseline(): print('Checking for hash mismatches...') warnings = '' with open(hashpath, 'r') as f: hash_baseline = json.load(f) for current_dir, subdirs, files in os.walk(docpath): for filename in files: relative_path = os.path.join(current_dir, filename) absolute_path = os.path.abspath(relative_path) barename, extension = os.path.splitext(filename) if not filename.startswith('.') and os.path.isfile(absolute_path): if absolute_path in hash_baseline and hash_baseline[absolute_path] != get_hash(absolute_path): warnings += f'File {absolute_path} has been tampered with.\n' if warnings: subject = 'Alert – file hash mismatch!' message = warnings return_code = 1 else: subject = 'No hash mismatch.' message = subject return_code = 0 scriptmail.send_message(subject, message) return return_code

Ein gesundes Misstrauen prüft aber nicht nur bekannte Dateien, sondern sucht auch nach zwielichtigen Mustern (*want your files back.*):

patternpath = '/patterns.json' with open(patternpath, 'r') as f: patterns = json.load(f) def check_files(): print('Checking for ransomware file patterns...') warnings = '' p = pathlib.Path(docpath) for pattern in patterns: result_list = sorted(p.rglob(pattern)) if result_list: warnings += f'Ransomware filename pattern {pattern} is present in {docpath}:\n{result_list}\n' if warnings: subject = 'Alert – ransomware file pattern detected!' message = warnings return_code = 1 else: subject = 'No ransomware file pattern detected.' message = subject return_code = 0 scriptmail.send_message(subject, message) return return_code

Die beiden Funktionen check_files() und compare_to_baseline() können in rbackup integriert werden und verhindern bei Bedarf die Datensicherung:

import ransomguard def ransom(self): if hasattr(self, 'ransomware_check'): if ransomguard.check_files(): print('Ransomware pattern found, not backing up!') exit(1) if ransomguard.compare_to_baseline(): print('Hash mismatch, not backing up!') exit(1) print('Proceeding to backup...')

Der zeitliche Aufwand für check_files() lässt sich wieder einsparen, indem man ohne Integritätseinbuße bei der Dateiübertragung auf den rechenintensiven rsync-Parameter --checksum

Generating the checksums means that both sides will expend a lot of disk I/O reading all the data in the files in the transfer, so this can slow things down significantly (and this is prior to any reading that will be done to transfer changed files)

– verzichtet, und die Verwendung von SHA256 statt MD5 in get_hash() spart ebenfalls wertvolle Sekunden (create_baseline() – MD5: 4.50s user 0.76s system 72% cpu 7.263 total | SHA256: 1.11s user 0.84s system 48% cpu 4.035 total).

Ein gezielter Angriff würde sich den beiden Verteidigungslinien widmen und die Skripte selbst bzw. die JSON-Dateien manipulieren, aber ich vertraue darauf, dass ich kein lohnendes Ziel für PLA Unit 61398 oder Fancy Bear bin.

The Nethercutt Conundrum

Wenn man Kriegsverbrechen vor einem internationalen Gericht anklagen lassen möchte, sollte man die Legitimität und Autorität dieses Gerichtes anerkennen.

Porös

Das Erzbistum Köln wird nicht müde, negative Nachrichten zu produzieren. Neben Meldungen aus den Registern bestürzend/abstoßend und erbärmlich wird die über Jahrtausende perfektionierte üble Nachrede auch gegenüber dem Papst

Mir liegt es vollkommen fern, den Papst als einen alten und realitätsfremden Mann darzustellen

– zelebriert, und der großzügige Umgang mit staatlichen Mitteln korrespondiert mit einer ausgesprochen klandestinen Haushaltsführung. Immerhin: Durch einen Verkauf der Fernsehrechte könnte Herr Woelki sich langfristig aller finanziellen Sorgen entledigen.

Wortverantwortung

Offenbar musste ich durch meinen Studienabschluss in den Wirren der Nachkriegszeit nicht nur auf Segnungen wie ein Transcript of Records oder ein Diploma Supplement verzichten, sondern auch auf eine individuelle Wortverantwortung.

Standardhafen

Die Änderung des SSH-Ports ist auf normalen Systemen eine Banalität (sudo vi /etc/ssh/sshd_config), erfordert aber unter macOS Monterey gewisse Verrenkungen. Änderungen in /private/etc/ssh/sshd_config helfen allein nicht, stattdessen muss dem System sehr grundsätzlich in /etc/services mitgeteilt werden, über welchen Port SSH-Verbindungen ablaufen sollen.

Als Alternative hätte eine Modifikation des zuständigen LaunchDaemons (/System/LaunchDaemon/ssh.plist) in früheren Zeiten funktioniert, aber dank der System Integrity Protection sind die Möglichkeiten im Bereich /System mittlerweile sehr begrenzt.

Die Änderung in /etc/services hat den Seiteneffekt, dass auch ausgehende SSH-Verbindungen standardmäßig den neuen Port nutzen. Für die in ~/.ssh/config eingetragenen Verbindungen ist das unerheblich, aber beim Kontakt zu fremden Systemen ist eine vorgegebene Abweichung vom Standardport ein support incident waiting to happen. Es bleibt daher bei Port 22 mit einem schlüsselbasierten Zugang:

# /private/etc/ssh/sshd_config PasswordAuthentication no ChallengeResponseAuthentication no PermitRootLogin no PubkeyAuthentication yes