Blog

Beantwortbar

Mit meiner Abneigung gegen HTML-Mails bin ich nicht allein, denn auch jenseits der offensichtlich kriminellen Sphäre und schamloser Überwachung durch halbstaatliche Akteure werden die Möglichkeiten von HTML nach Kräften missbraucht. John Gruber befasst sich in einem Blog-Post mit dem Einsatz von tracking pixels und resümiert:

Don’t get me started on how predictable this entire privacy disaster was, once we lost the war over whether email messages should be plain text only or could contain embedded HTML. Effectively all email clients are web browsers now, yet don’t have any of the privacy protection features actual browsers do.

Leider kann ich seit kurzem HTML-Mails ohne Textfassung nicht mehr nur ignorieren oder im äußersten Fall an einen echten Browser übergeben, sondern muss sie auch beantworten. Wenn aber meine in mutt erstellte Antwort auf eine E-Mail wie folgt eingeleitet wird, verlieren die Empfängerinnen eventuell die Lust, weiterzulesen:

> <html xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:w="urn:schemas-microsoft-com:office:word" xmlns:m="http://schemas.microsoft.com/office/2004/12/omml" xmlns="http://www.w3.org/TR/REC-html40"> > <head> > <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> > <meta name="Generator" content="Microsoft Word 15 (filtered medium)"> > <!--[if !mso]><style>v\:* {behavior:url(#default#VML);} > o\:* {behavior:url(#default#VML);} > w\:* {behavior:url(#default#VML);} > .shape {behavior:url(#default#VML);} > </style><![endif]--> > <style> <!-- > /* Font Definitions */ > @font-face > {font-family:"Cambria Math"; > panose-1:2 4 5 3 5 4 6 3 2 4;} > @font-face > {font-family:Calibri; > panose-1:2 15 5 2 2 2 4 3 2 4;} > /* Style Definitions */ > p.MsoNormal, li.MsoNormal, div.MsoNormal > {margin:0cm; > margin-bottom:.0001pt; > font-size:11.0pt; > font-family:"Calibri",sans-serif; > mso-fareast-language:EN-US;} > a:link, span.MsoHyperlink > {mso-style-priority:99; > color:#0563C1; > text-decoration:underline;} > a:visited, span.MsoHyperlinkFollowed > {mso-style-priority:99; > color:#954F72; > text-decoration:underline;} > p.msonormal0, li.msonormal0, div.msonormal0 > {mso-style-name:msonormal; > mso-margin-top-alt:auto; > margin-right:0cm; > mso-margin-bottom-alt:auto; > margin-left:0cm; > font-size:11.0pt; > font-family:"Calibri",sans-serif;} > span.E-MailFormatvorlage18 > {mso-style-type:personal; > font-family:"Arial",sans-serif; > color:windowtext;} > span.E-MailFormatvorlage19 > {mso-style-type:personal; > font-family:"Arial",sans-serif; > color:windowtext;} > span.E-MailFormatvorlage20 > {mso-style-type:personal; > font-family:"Calibri",sans-serif; > color:#1F497D;} > span.E-MailFormatvorlage21 > {mso-style-type:personal; > font-family:"Arial",sans-serif; > color:windowtext;} > span.E-MailFormatvorlage22 > {mso-style-type:personal; > font-family:"Arial",sans-serif;} > span.E-MailFormatvorlage24 > {mso-style-type:personal; > font-family:"Arial",sans-serif; > color:windowtext;} > .MsoChpDefault > {mso-style-type:export-only; > font-size:10.0pt;} > @page WordSection1 > {size:612.0pt 792.0pt; > margin:70.85pt 70.85pt 2.0cm 70.85pt;} > div.WordSection1 > {page:WordSection1;} > --> > </style> > > <!--[if gte mso 9]><xml> > <o:shapedefaults v:ext="edit" spidmax="1026" /> > </xml><![endif]--> > <!--[if gte mso 9]><xml> > <o:shapelayout v:ext="edit"> > <o:idmap v:ext="edit" data="1" /> > </o:shapelayout></xml><![endif]--> > </head> > <body lang="DE" link="#0563C1" vlink="#954F72"> > <div class="WordSection1">

Erfreulicherweise sind selbst Menschen, die HTML-Mails noch viel kritischer sehen –

HTML email is, without doubt, evidence of the imminent end of civilized life as we know it; much like the Golgafrincham diaspora, it is attributable to a depraved cabal of marketing consultants and provides the same level of social good as syphilis and fistulas. Suffice to say, it is a blight.

– lösungsorientiert und setzen auf eine Kombination aus dem textbasierten HTML-Browser w3m für ein mutt-internes Rendering und den regulären Browser als Rückfallposition:

# .muttrc auto_view text/html # view html automatically alternative_order text/plain text/enriched text/html # save html for last # .mailcap text/html; ~/scripts/view_attachment %s html text/html; w3m -I %{charset} -T text/html; copiousoutput;

Der Parameter copiousoutput sorgt dafür, dass w3m Vorrang vor dem Übergabeskript bekommt.

Backup 2021

Ohne konkreten Anlass überarbeite ich nach vier Jahren meine Backup-Strategie und füge ein zusätzliches NAS (mit 250GB SSD-Cache) als Primärspeicher und Medienserver hinzu. Peinlicherweise beginne ich bereits, mühsam gewonnene Erkenntnisse zu vergessen und versäume daher, den schlüsselbasierten SSH-Zugriff auf Anhieb richtig zu konfigurieren. Konkret ist das Home-Verzeichnis der SSH-Nutzerin allzu zugänglich, und nach einer halben Stunde erfolglosem Debugging erkundige ich mich etwas voreilig nach einer Lösung, die ich wenige Minuten später selbst finde, erst in einem fremden, dann in meinem eigenen Blog (chmod 700 ~). Auch die offensichtliche Tatsache, dass die Plex-Nutzerin Leserechte für die Medienordner benötigt, und dass für mein SFTP-Skript SFTP auch aktiviert sein sollte (File Services → FTP → Enable SFTP service), realisiere ich jeweils erst nach einigen Minuten. Vielleicht sollte ich meine Freizeit doch lieber damit verbringen, Sonette zu verfassen.

Immerhin stoße ich bei der Abkehr vom proprietären Hyper Backup-Format für den Abgleich von Datenständen zwischen den NAS-Systemen auf ein reales Problem, das viele Synology-Nutzerinnen nervt. Die Funktion Shared Folder Sync basiert auf rsync, ist aber sehr eigenwillig implementiert. Existierende Ordner auf dem Ziel-NAS, die zur Synchronisation förmlich einladen, werden zum Anlass genommen, neue Ordner anzulegen:

If on the destination exists a shared folder with the same name (e.g., SharedFolder) as the shared folder on the source, a new folder with a numbered name (e.g., SharedFolder_1) will be created at the destination when syncing.

Das ist nicht sehr günstig, wenn umfangreiche Ordner schon auf beiden beteiligten NAS existieren. Glücklicherweise gibt es einen Workaround:

  1. Alle zu synchronisierenden Ordner auf dem Ziel-NAS umbenennen (XXX_tmp)
  2. Einen Synchronisations-Task auf dem Quell-NAS anlegen, und für jeweils einen Quell-Ordner kurzzeitig (!) starten. Der neue Ziel-Ordner wird angelegt (mit ausgesprochen restriktiven Zugriffsrechten).
  3. Die Zugriffsrechte (Advanced Permissions) für die Ziel-Ordner anpassen (Entfernen der read only-Regel für die Admin-Gruppe, Einrichtung von Schreib-/Leserechten für die synchronisierende Benutzerin)
  4. Bewegen (Move (overwrite), nicht Copy) der Inhalte aus den umbenannten ursprünglichen Ordnern auf dem Ziel-NAS (XXX_tmp → XXX).
  5. Den Synchronisations-Task für alle Quell-Ordner konfigurieren und manuell starten.

Auf diesem Weg dauert die abschließende Synchronisation von mehr als 5 TB rund 5 Minuten.

Wenige Tage später tritt erstmals ein Problem auf, das ich schon vor Jahren erwartet hatte (und das bei anderen Nutzerinnen auch auftrat): Wie vom Security Audit anempfohlen, hatte ich im Zuge der NAS-Erstkonfiguration den allgemeinen SSH-Port (Control Panel → Terminal) geändert, nicht aber den SSH-Port für rsync (Control Panel → File Services → rsync). Nachdem meine rsync-Skripte lange Zeit trotz dieser offensichtlichen Diskrepanz funktioniert haben, wird das Thema Ports nach dem sonntäglichen DSM-Update plötzlich ernst genommen (Permission denied). Das Debugging dauert wieder etwas, die Fehlerbehebung selbst nicht. Warum aber jahrelang wohlwollend ein falscher Port akzeptiert wurde, bleibt mysteriös.

Nach der erfolgreichen Inbetriebnahme fällt mir auf, dass es bisher kein Backup der NAS-Konfiguration gab, was sich aber rasch korrigieren lässt (Update & Restore → Configuration Backup).

Erstaunlich schwierig gestaltet sich ein zusätzlicher Automatisierungsschritt für das bislang manuell ausgeführten Backup-Skript (ein Python-Wrapper, der Shortcuts für unterschiedliche rsync-Konfigurationen bereitstellt). Die Einstellung einer monatlichen Frequenz ist schon sehr viel umständlicher als ein entsprechender crontab-Eintrag, aber die entscheidende Hürde sind die Zugriffsberechtigungen für Anwendungen, die per launchd gestartet werden. Trotz vieler hilfreicher Foreneinträge, die mir raten, rsync, launchd und/oder der Shell (zsh/bash) Full Disk Access zu erteilen, verweigert das System rsync standhaft den Zugriff auf meine Ordner:

rsync: opendir "/Users/username/Backup" failed: Operation not permitted (1)

Andererseits funktioniert rsync mit derselben Konfiguration einwandfrei, wenn es direkt als LaunchAgent gestartet wird. Channing Walton bestätigt meinen Verdacht: Statt das Skript aufzurufen und auf die Shebang-Zeile zu vertrauen –

<key>ProgramArguments</key> <array> <string>/Users/jan/bin/rbackup.py</string> <string>nas1_core</string> </array>

– sollte der jeweilige Interpreter explizit in der property list für launchd genannt –

<key>ProgramArguments</key> <array> <string>/opt/homebrew/bin/python3</string> <string>/Users/jan/bin/rbackup.py</string> <string>nas1_core</string> </array>

und mindestens mit Zugriffsrechten auf ~ ausgestattet werden.

Dank launchd und Shared Folder Sync hält sich der regelmäßige Aufwand für die folgende Sicherungsmatrix in akzeptablen Grenzen:

DatenQuelleZielFrequenzMethodeScheduled
Dokumente / Konfiguration/Users/jan/Backupnas1:/Volume1/CoreBackupwöchentlich (So, 10 Uhr)rbackup.py
nas2:/Volume1/CoreBackupmonatlich (1. Sonntag, 11 Uhr)rbackup.py
nas3:/Volume1/CoreBackupmonatlich (3. Sonntag, 11 Uhr)rbackup.py
usbkey1:/CoreBackupwöchentlich (Sonntag)rbackup.py
usbkey2:/CoreBackupwöchentlich (Dienstag)rbackup.py
usbkey3:/CoreBackupwöchentlich (Donnerstag)rbackup.py
/Users/jan/CoreBackup.sparsebundlewöchentlich (Sonntag, 9 Uhr)rbackup.py
/Users/jan/CoreBackup.sparsebundleserver1:/home/backupwöchentlich (Montag, 10 Uhr)rbackup.py
server2:/home/backupwöchentlich (Mittwoch, 10 Uhr)rbackup.py
Bilder/Users/jan/Picturesnas1:/Volume1/Pictureswöchentlich (Sonntag, 12 Uhr)rbackup.py
nas2:/Volume1/Picturesmonatlich (1. Sonntag, 12 Uhr)rbackup.py
nas3:/Volume1/Picturesmonatlich (3. Sonntag, 12 Uhr)rbackup.py
Musik/Users/jan/Musicnas1:/Volume1/Musicmonatlich (1. Sonntag, 12 Uhr)rbackup.py
nas2:/Volume1/Musicmonatlich (3. Sonntag, 12 Uhr)rbackup.py
Büchernas1:/Volume1/Booksnas2:/Volume1/Booksmonatlich (1. Sonntag, 14 Uhr)Shared Folder Sync
nas3:/Volume1/Booksmonatlich (3. Sonntag, 15 Uhr)Shared Folder Sync
Zeitungennas1:/Volume1/Newsnas2:/Volume1/Newsmonatlich (1. Sonntag, 14 Uhr)Shared Folder Sync
nas3:/Volume1/Newsmonatlich (3. Sonntag, 15 Uhr)Shared Folder Sync
Filmenas1:/Volume1/Moviesnas2:/Volume1/Moviesmonatlich (1. Sonntag, 14 Uhr)Shared Folder Sync
TV-Seriennas1:/Volume1/TV Showsnas2:/Volume1/TV Showsmonatlich (1. Sonntag, 14 Uhr)Shared Folder Sync
Softwarenas1:/Volume1/Softwarenas2:/Volume1/Softwaremonatlich (1. Sonntag, 14 Uhr)Shared Folder Sync
[Data]/System/Volumes/Datahdd1:/monatlichSuperDuper!
hdd2:/monatlichSuperDuper!
hdd3:/vierteljährlichSuperDuper!
nas1:/Volume1/FullBackupwöchentlich (Sonntag)SuperDuper!

Die Fotobibliothek in /Users/jan/Pictures wird darüber hinaus in der iCloud gesichert. /Users/jan/Backup enthält hauptsächlich Symlinks auf verschiedene Verzeichnisse (~/...) und wird wöchentlich mit aktuellen Konfigurationsdateien des Webservers ergänzt.

Anders als noch vor einem Jahrzehnt nehme ich das Thema Ransomware sehr ernst und setze auf verschieden lange Backup-Zyklen, außerdem sind mittlerweile alle Backup-Medien verschlüsselt. Allerdings befinden sich die Speichermedien ausnahmslos auf dem Staatsgebiet der Bundesrepublik Deutschland – meine Daten werden also bestimmte apokalyptische Szenarien in Zentraleuropa nicht überstehen (abgesehen von den in Nordamerika gesicherten Fotos und Passwörtern).

Für eine vorwiegend nordamerikanische Apokalypse ist dagegen vorgesorgt: Der freundliche 1Password-Support hat mir für den Fall einer world catastrophe where all our servers have been wiped out den nicht ganz offensichtlichen Speicherort der lokalen 1Password-Datenbank (~/Library/Group Containers/2BUA8C4S2C.com.agilebits/Library/Application Support/1Password/Data) verraten – den ich als Symlink in mein Backup-Verzeichnis aufnehme – und zusätzlich die Ablage meiner exportierten Vaults in einem verschlüsselten Disk Image empfohlen:

Everything breaks at some point, so while it's extremely unlikely anything catastrophic would happen that you couldn't recover from, I understand where you're coming from and I can see why you'd like to manage your own backups in addition to what we do.

Das ist die richtige Einstellung.

Ernstfall

Auch wenn der Ausdruck Blog ursprünglich unschöne Assoziationen (roughly onomatopoeic of vomiting) wecken sollte, hat sich das Format durchgesetzt und wird von sturen Menschen wie Jason Kottke weiterhin nach dem POSIOP-Prinzip hergestellt:

I will continue to forgo publishing on platforms with ever-shifting strategies, morals, and agreements in favor of my patchwork "system" of publishing tools held together with chewing gum. At least it's *my* chewing gum.

Hear, hear. Nachdem verschiedene Anwendungen in meiner lokalen Arbeitsumgebung aktualisiert wurden, habe ich die Option, neues Kaugummi auf dem Webserver zu applizieren oder den Kaugummibedarf (die Komplexität des Systems) zu reduzieren. Konkret besteht eine Inkompatibilität zwischen PostgreSQL 9 und PostgreSQL 13, die das Import-Skript für den lokalen Datenbankdump auf dem Server scheitern lässt. Die Aussicht, einen kompletten Webstack bei laufendem Betrieb neu aufzusetzen, ist nicht sehr attraktiv, und ich greife auf ein Konzept zurück, das für Notzeiten gedacht war.

Die vorbereitete nginx-Konfiguration auf dem Server ergänze ich um Caching-Anweisungen –

http { map $sent_http_content_type $expires { default off; text/html 1d; text/css 30d; application/pdf 30d; ~image/ 30d; } server { expires $expires; } }

– und im Backrezept make_static korrigiere ich die Anweisungen für Blog-Seiten, um (wie in der dynamisch generierten Version) jeweils die jüngsten 20 Blogeinträge anzuzeigen. Weil das Modul django.utils.feedgenerator mittlerweile selbständig Bytestrings UTF-8-kodiert, muss ich die Funktion feed_creator() geringfügig anpassen (page.content.encode('utf-8')page.content). Die Backautomatik funktioniert auch nach Jahren noch ohne jeden Anpassungsbedarf, so dass ich lediglich meinem rsync-Wrapper rbackup.py eine Konfiguration zur Synchronisation mit dem Webserver hinzufüge. Es wäre natürlich möglich, auch diesen Schritt zu automatisieren, aber aus Solidarität mit den gebeutelten Zeitungsverlagen leiste ich mir eine manuelle Endredaktion.

Google PageSpeed blickt weiterhin sehr wohlwollend auf diese Website (100/100), und laut ab wird die Startseite in durchschnittlich 171 Millisekunden ausgeliefert (ab -n100 -c10 https://eden.one/).

Setup 2021

Nach vier Jahren mit einer ausgesprochen empfindsamen Tastatur wechsele ich zu einer solideren Konstruktion und einer leistungsfähigeren Prozessorarchitektur. Letztere lässt mich etwas besorgt auf das Setup-Ritual blicken, denn die letzten Durchgänge waren ziemlich aufwendig.

Homebrew

Der Schlüssel zu einem Setup in Rekordzeit ist der Verzicht auf die relativ plattformunabhängige Low Level-Reproduzierbarkeit einzelner Installationsvorgänge, das heißt: Die Verwendung eines Paketmanagers. Dank Homebrew sind Mailstack (samt GnuPG!) und Webstack innerhalb von Minuten installiert:

brew install mutt brew install offlineimap brew install msmtp brew install w3m brew install nginx brew install postgresql@13

Weil die von Apple gelieferten vim- und rsync-Versionen einige Funktionen vermissen lassen (u.a. den für meine Backup-Strategie zentralen rsync-Parameter --ignore-missing-args), folgt:

brew install vim brew install rsync

Python 3.9 installiere ich zunächst über das Installationspaket für macOS in /Library/Frameworks, bevor mir (gerade noch rechtzeitig) auffällt, dass Homebrew Python 3.9 bereits (als Voraussetzung für vim) in /opt/homebrew/bin platziert hat. Ich entferne das Mac-Python (und die entsprechenden Symlinks in /usr/local/bin) und fahre mit dem Brew-Python fort:

pip3 install Django==3.1.7 brew install uwsgi pip3 install uwsgi

Die doppelte uwsgi-Installation ist eine – nicht vollständig rationale – Verzweiflungstat. Beim manuellem Aufruf (uwsgi --socket /private/tmp/eden.sock --module djangoapp.wsgi --chmod-socket=664) lässt sich uwsgi nämlich durchaus bewegen, meine Django-Applikation auszuliefern, beim Start als launchctl-Job (homebrew.mxcl.uwsgi) wird das Python-Plugin aber erst gefunden, wenn uwsgi.ini ausdrücklich auf Python hinweist (plugins = python3). Und auch nur dann, wenn man der Brew-Installation mit der pip3-Installation nachhilft. Ich muss nicht alles verstehen, obgleich das Deployment-Tutorial die Lieferkette (the web client ←→ the web server ←→ the socket ←→ uwsgi ←→ Django) und mögliche Fehlerquellen eigentlich sehr verständlich darstellt. Für verschiedene Skripte werden einige Python-Module nachinstalliert (pip3 install xxx), und das letzte verbleibende Python 2-Skript wird endlich migriert. In der Django-Applikation muss trotz des Versionssprungs (Django 1.x → Django 3.1.7) nur sehr wenig geändert werden: Das Modul django.core.urlresolvers heißt nun django.urls, und die Klasse ForeignKeys sieht einen zusätzlichen Parameter (on_deletion) vor. Selbst das komplexe Kommando make_static funktioniert auf Anhieb.

Konfiguration

Aus dem Backup übernehme ich die wichtigsten Konfigurationsdateien (mutt, GnuPG, MSMTP, Offlineimap, SSH), so dass der Mailstack völlig ohne Anpassungen in Betrieb genommen werden kann. Wenn man davon absieht, dass ich einen dritten Mail-Account konfiguriere (→ .muttrc, .msmtprc und .offlineimaprc) und etwas länger brauche, um den Wert des Offlineimap-Parameters maxsyncaccounts entsprechend zu erhöhen. Die Migration von bash auf zsh ist erstaunlich einfach (.bash_profile.zshrc), lediglich die Pfadergänzungen aus .bash_profile erhalten eine neue Heimat in .zshenv.

Ebenfalls aus dem Backup werden ~/Library/Scripts, ~/Library/Keychains, ~/Library/LaunchAgents, ~/Library/texmf und einige Ordner in ~/Library/Application Support übernommen. Dank Homebrew lassen sich die LaunchAgents für PostgreSQL, Offlineimap und nginx ohne selbstgeschriebene property files verwalten (brew services start|stop postgresql|offlineimap|nginx)), meine eigenen LaunchAgents müssen manuell in ~/Library/LaunchAgents abgelegt und geladen (launchctl load net.janeden.xxx.plist) werden. Damit die LaunchAgents ihre Arbeit verrichten können, erhalten zsh und python3.9 (/opt/homebrew/bin/python3) Full Disk Access (System Preferences → Security & Privacy Preference Pane → Privacy). In diesem Zusammenhang tritt später der einzige Nachteil der aktiven Paketpflege durch das Homebrew-Projekt zu Tage: Nach jeder Aktualisierung des Python-Paketes muss ich dem Interpreter erneut Zugriffsrechte erteilen. Ein Problem mit brew cleanup entsteht dagegen nur dann, wenn man ungeschickterweise ein Homebrew-Paket per sudo installiert und entsprechend keine Zugriffsrechte auf bestimmte Dateien hat. Die Anpassung der macOS-Standardkonfiguration übernimmt eine modifizierte Fassung des .macOS-Skripts.

Apps

Im dritten Schritt begebe ich mich in das macOS-GUI und installiere die folgenden Apps:

Anders als in den Vorjahren lautet der korrekte Befehl, um meine Schriften in LaTeX nutzen zu können, sudo texhash; updmap -user --enable MixedMap pad.map. TeXLive unterscheidet mittlerweile zwischen systemweiten und nutzerspezifischen Mappings.

Daten

Schließlich muss ~ aus dem Backup rekonstruiert werden. Bei dieser Gelegenheit repariere ich endlich die Fotobibliothek, die seit Jahren nicht vollständig mit der iCloud synchronisiert wird: Ein vollständiger Export und Re-Import aller Bilder bedeutet weniger Arbeit als befürchtet, weil Photos Duplikate beim Import einigermaßen zuverlässig erkennt. Das Verzeichnis ~/Backup wird zum Schluss mit Symlinks zu den wichtigsten Ordnern innerhalb von ~ versehen, ~/Documents/configuration/webstack ergänze ich um Symlinks zu /opt/homebrew/etc/nginx/nginx.conf und /opt/homebrew/etc/uwsgi/apps_enabled/uwsgi.ini.

TTL < 4h.