Blog

Verschlüsselte Postkarte

AppleInsider stellt zwar noch einige verschlüsselte Messenger vor, hat aber eigentlich bereits resigniert:

And remember: if you wouldn't say or do something in public, it's wise not to send it across the internet, either. Even encrypted.

Нулевое доверие

Die größte Sorge des Auswärtigen Amtes in Bezug auf Cyberwar-Aktivitäten gilt nicht den Auswirkungen auf die kritische Infrastruktur in Deutschland oder der fehlenden Zurechenbarkeit von Angriffen, sondern der Bedrohung der vertrauensvollen Zusammenarbeit mit den russischen Strafverfolgungsbehörden durch Angriffe auf russische Systeme:

Wenn die selbst ernannten Online-Kämpfer einen deutschen Pass haben, ziehen die uns mit rein, verdeutlichte Grienberger. Da lasse sich dann nicht mehr unterscheiden, ob es sich um eine private oder staatliche Aktion handle. Wenn Russland auf uns zukommt und verlangt, die virtuellen Eindringlinge zu bestrafen, sind wir in einer schwierigen Situation. Es müsste daher dringend geklärt werden, wie kann man diese Leute wieder demobilisieren?

Man möchte sich gar nicht ausmalen, wie ungehalten die Russische Föderation auf eine verzögerte Auslieferung deutscher Hackerinnen reagieren würde.

Targeted Release Schedule

Dass Vim 9.0 (Vim9 script!) und BBedit 14.5 (Tail Mode! finer-grained control over invisibles display!) am selben Tag veröffentlicht werden, hilft mir über die Vernachlässigung durch einen gewissen Hardware-Lieferanten hinweg.

Zufallskunde

Apple erntet Anerkennung für die Re-Implementierung einer Serienbrieffunktion, zeigt aber in iOS Mail nach wie vor nicht ohne Weiteres die Empfängeradresse einer E-Mail an und entfernt Python und nano aus seinem Betriebssystem. Deutlicher kann das Unternehmen nicht machen, dass ich eigentlich nicht zu seiner Zielgruppe zähle.

Gesteuerte Digitalisierung

Meine Experimentierfreude in technischen Zusammenhängen endet bei der Steuererklärung. Weil das Elster-Projekt der Finanzbehörden bis 2013 eine Windows-Umgebung und/oder Oracle Java voraussetzte, nutze ich seit 2002 einen Online-Dienst, dessen einzige Funktion darin besteht, mich durch den Hauptvordruck und diverse Anlagen zu geleiten und anschließend ein PDF-Dokument zum Ausdruck zu generieren. Obwohl der Postversand einer unterschriebenen Steuererklärung zunehmend archaisch ist, halte ich an diesem Verfahren fest, bis die Finanzbehörden ein klares Zeichen setzen: Ab dem Veranlagungszeitraum 2021 werden papierne Erklärungen nicht mehr akzeptiert.

Zunächst versuche ich, dem vertrauten Online-Dienst ein Mandat für den Belegabruf und die digitale Übermittlung der Steuererklärung erteilen. Dieser Weg scheitert, weil ich in einem (verdrängten) Anflug von Digitalisierungsbereitschaft offenbar vor einigen Jahren bereits ein Mandat erteilt habe, das nicht zum jetzigen Projekt Steuererklärung 2021 passt (ERR-DIO-MISMATCH). Der Support des Online-Dienstes schlägt vor, das vorhandene Mandat über das Elster-Portal zu löschen. Das ist eine gute Gelegenheit, meinen elektronischen Personalausweis zum Einsatz zu bringen. Ich registriere mich mit dem Ausweis, lösche das Mandat und beantrage (innerhalb des Online-Dienstes) ein neues Mandat. Nun erscheint für mich eine neue Fehlermeldung (offener Genehmigungsantrag, ERR-APP-OPEN), während für die mit mir zusammenveranlagte Person weiterhin ERR-DIO-MISMATCH angezeigt wird.

Weil ich nun schon im Elster-Portal registriert bin, erteile ich Elster (und damit mir selbst) die Berechtigung zum Belegabruf. Der Belegabruf für die mit mir zusammenveranlagte Person funktioniert nicht, weil sie keinen elektronischen Personalausweis nutzt und die Freigabe daher über einen Schlüssel bestätigen muss, der ihr per Post zugeschickt wird. Ich kann ihre Daten allerdings auch (wie bisher beim Online-Dienst) manuell eingeben und die fertige Steuererklärung nach etwa 20 Minuten in elektronischer Form einreichen. Sogar eine Zustellung des Steuerbescheids per E-Mail wird mir in Aussicht gestellt. Wer hätte gedacht, dass mich ausgerechnet die deutschen Steuerverwaltungen zur Digitalisierung zwingen?

(Dieser Beitrag wurde auch im Techniktagebuch veröffentlicht.)

Backup 2022

Nach mehreren Ermahnungen auf Twitter ergänze ich meine allzu optimistische Datensicherung um einen weiteren Server für das CoreBackup und verschiebe das verschlüsselte CoreBackup.sparsebundle nach /Users/jan/Library/Mobile Documents/com~apple~CloudDocs, auf dass ein auf Europa begrenzter nuklearer Krieg meine Daten nicht hinwegfegt.

Schlüsseldienst

Ab einer bestimmten Anzahl von PGP-Schlüsseln wird die manuelle Veröffentlichung in einem Web Key Directory unpraktikabel. Das GnuPG-Paket enthält daher mit gpg-wks-client ein Werkzeug, das eine automatische Veröffentlichung ermöglicht. Ich beschränke mich auf die lokale Generierung der erforderlichen Dateien mit der Option --install-key. Durch den Aufruf –

gpg --list-options show-only-fpr-mbox -k '@eden.one'

– kann man den Input für gpg-wks-client erzeugen. Der Befehl liefert eine Liste von Fingerprints und UIDs für alle Schlüssel, in denen mindestens eine UID das Muster @eden.one enthält. Wenn also die Schlüssel weitere Domains erfassen, genügt @eden.one als einigendes Band. Allerdings werden so bekanntlich auch Schlüssel mit dem Status expired oder revoked berücksichtigt. Das Ergebnis muss daher durch einen invertierten grep-Aufruf laufen, der den unerwünschten Schlüssel ausfiltert:

gpg --list-options show-only-fpr-mbox -k '@eden.one' | grep -v xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx | gpg-wks-client -v --directory /var/www/html/site/.well-known/openpgpkey --install-key

Im Prinzip ließen sich beliebig viele grep-Aufrufe verketten, aber in der Praxis ist dieser Workaround nur für einen oder zwei ungültige Schlüssel hilfreich. Das Ergebnis der obigen Befehlskette ist jedenfalls eine Ordnerstruktur für die fortgeschrittene WKD-Methode, und so verzichte ich schweren Herzens auf die direkte WKD-Methode und passe nginx.conf entsprechend an. Dank gpg-wks-client ist die dreijährliche Verlängerung von Schlüssellaufzeiten (mit dem expire-Kommando) nun auch unter WKD-Aspekten handhabbar.

Authentisch gesichert

Zwar gibt es keinen Grund, an der Authentizität meiner Backup-Benachrichtigungen zu zweifeln, aber nach der Wiedereingliederung von OpenPGP in den Mailstack möchte ich natürlich auch skriptbasierte E-Mails signieren. Um wohlgeformte E-Mails des MIME-Typs multipart/signed zu versenden, beschäftige ich mich noch einmal sehr intensiv mit den passenden Python-Modulen, akzeptiere nach einigen erfolglosen Bemühungen, dass das aktuelle API fehlerhaft/ungeeignet ist und verwende das legacy API auf Basis eines 10 Jahre alten Code-Schnipsels. Die Schmach wird etwas gemildert durch das Eingeständnis der Python-Dokumentation, dass mindestens email.mime nicht völlig überholt ist:

This module is part of the legacy (Compat32) email API. Its functionality is partially replaced by the contentmanager in the new API, but in certain applications these classes may still be useful, even in non-legacy code.

Das Versandskript benötigt neben diversen email-Modulen natürlich python-gnupg:

#!/usr/bin/env python3 import os import datetime import __main__ as main import smtplib from email.message import Message from email.mime.multipart import MIMEMultipart from email import charset from email.utils import make_msgid import keyring import gnupg smtp_server = 'mail.eden.one' smtp_user = 'user@eden.one' port = 587 localhost = 'client.eden.one' pgp_entry = 'PGP' pgp_user = pgp_entry def send_message(subject, body): now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') subject = f'{os.path.basename(main.__file__)}: {subject}' body = f'{body}\n\n{now}' basemsg = Message() basemsg.set_payload(body, charset='utf-8')

Ohne den Parameter charset an dieser Stelle besteht basemsg nur aus dem Inhalt der Variablen body, die Angabe eines character sets erzeugt dagegen ein vollwertiges MIME-Objekt:

MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: base64 VGV4dCBtaXQgw5xtbMOkdXRlbi4=

Nun ist es so, dass mutt (u.a. im Zusammenhang mit dem Parameter pgp_strict_enc zur obligatorischen Kodierung von Nachrichten mit trailing whitespace), der RFC 2045 für Text –

The Quoted-Printable encoding is intended to represent data that largely consists of octets that correspond to printable characters in the US-ASCII character set. [...] The Base64 Content-Transfer-Encoding is designed to represent arbitrary sequences of octets in a form that need not be humanly readable.

Rspamd sowie der RFC 2015 (etwas weniger deutlich sein Nachfolger RFC 3156) –

Though not required, it is generally a good idea to use Quoted-Printable encoding in the first step (writing out the data to be signed in MIME canonical format) if any of the lines in the data begin with "From ", and encode the "F". This will avoid an MTA inserting a ">" in front of the line, thus invalidating the signature!

– die platzsparende Kodierung quoted-printable bevorzugen. Um mich nicht des verschwenderischen Umgangs mit Speicherplatz und Bandbreite bezichtigen lassen zu müssen, erweitere ich das Skript um ein entsprechendes Charset()-Objekt und den charset-Parameter

base_charset = charset.Charset('utf-8') base_charset.body_encoding = charset.QP basemsg = Message() basemsg.set_payload(body, charset=base_charset)

– und erhalte:

MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable Text mit =C3=9Cml=C3=A4uten.

Dasselbe Ergebnis erzielt man übrigens auch, wenn man basemsg das Charset()-Objekt nicht im Rahmen von set_payload(), sondern nachträglich zuweist (basemsg.set_charset = base_charset). Eine vorzeitige Zuweisung führt dagegen wieder zu einer base64-Kodierung, weil set_payload() den Effekt von set_charset überschreibt (anders als bei MIMEText()-Objekten, die mit vorzeitigen replace_header()- und set_charset()-Anweisungen offenbar besser umgehen können). Weiter im Skript:

basetext = basemsg.as_string().replace('\n', '\r\n')

Hier sind sich RFC 2015 und RFC 3156 einig:

The data to be signed MUST first be converted to its content-type specific canonical form. For text/plain, this means conversion to an appropriate character set and conversion of line endings to the canonical <CR><LF> sequence.

Nun muss eine Signatur erzeugt und in ein Message()-Objekt umgewandelt werden:

gpg = gnupg.GPG(gpgbinary='/opt/homebrew/bin/gpg') pgp_passphrase = keyring.get_password(pgp_entry, pgp_user) signature = str(gpg.sign(basetext, keyid='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx', passphrase=pgp_passphrase, detach=True)) signmsg = Message() signmsg['Content-Type'] = 'application/pgp-signature; name="signature.asc"' signmsg['Content-Description'] = 'OpenPGP digital signature' signmsg.set_payload(signature)

Der Parameter gpgbinary für gnupg.PGP() ist bei einem Aufruf des Skriptes über eine Shell mit wohlgefülltem $PATH tückischerweise entbehrlich, aber spätestens bei der Einbettung via launchctl zwingend erforderlich. Schließlich werden die beiden Message()-Objekte in einem MIMEMultipart()-Objekt zusammengeführt:

msg = MIMEMultipart(_subtype="signed", micalg="pgp-sha512", protocol="application/pgp-signature") msg.attach(basemsg) msg.attach(signmsg) msg['From'] = 'Sender <sender@eden.one>' msg['To'] = 'Recipient <recipient@eden.one>' msg['Subject'] = subject msg['Message-ID'] = make_msgid(domain=localhost) smtp_password = keyring.get_password(smtp_server, smtp_user) s = smtplib.SMTP(host=smtp_server, local_hostname=localhost, port=port) s.starttls() s.login(smtp_user, smtp_password) s.send_message(msg) del msg

Der Datenverkehr wächst durch die Signatur und den MIME-Overhead je Nachricht (dank quoted-printable nur) um rund 600 Bytes, aber im Gegenzug gewährt Rspamd ein zusätzliches Symbol (SIGNED_PGP (-2)). Und ich kann sicher sein, dass mir niemand ein erfolgreich durchgeführtes Backup vorgaukelt.

Sommerputz

Dank der Entkopplung von Homebrew und uwsgi ist ein Update der Homebrew-Pakete (brew update; brew upgrade) kein Grund zur Nervosität mehr, auch wenn uwsgi wegen der veränderten Umgebung neu gestartet werden muss:

snafu % launchctl unload -w ~/Library/LaunchAgents/localhotel.snafu.uwsgi.plist snafu % launchctl load -w ~/Library/LaunchAgents/localhotel.snafu.uwsgi.plist

Allerdings benötigt das erneuerte Python frische Zugriffsrechte auf meinen Schlüsselbund, andernfalls scheitern alle Skripte, die das keyring-Modul verwenden.

Sicherheitsmail IV: Web Key Directory

Obwohl die PGP-Gemeinde eine gesunde Paranoia auszeichnet, wird PGP weiterhin regelmäßig für unsicher oder sogar obsolet erklärt, nicht zuletzt nach der Entdeckung der EFail-Schwachstelle. Da mein MUA nicht (oder kaum) von EFail betroffen ist, vervollständige ich meinen überkomplexen Mailstack dennoch mit einer jahrelang vernachlässigten Komponente und richte trotz der fehlgeschlagenen Schlüsseldistributionsinitiativen der vergangenen Jahre ein OpenPGP Web Key Directory ein, denn:

Im Unterschied zu den bisher genutzten öffentlichen Key Servern werden beim integrierten Web Key Directory nur authentifizierte E-Mail Adressen inklusive des öffentlichen Schlüssels veröffentlicht. Über das integrierte WKD wird der E-Mail-Server des jeweiligen Anbieters zum maßgeblichen und zuverlässigen Bezugspunkt für den richtigen öffentlichen Schlüssel der jeweiligen E-Mail-Adresse. Denn durch ein Verifizierungsverfahren, idealerweise über die explizite Bestätigung des E-Mail-Nutzers selbst, werden öffentlicher Schlüssel und E-Mail-Adresse fest verknüpft. Eine Verwechselung kann somit weitestgehend ausgeschlossen werden.

Die WKD-Anleitung von Mike Kuketz ist sehr hilfreich, verführt mich aber an einer entscheidenden Stelle dazu, sämtliche Schlüssel zu einer E-Mail-Adresse zu exportieren/veröffentlichen (gpg --no-armor --export user@eden.one > ke39y8fkyw5j8uubuicshffo9hhudk4j), was zu verwirrenden Effekten führt. Andererseits kann ich diese Situation dazu nutzen, sauber signierte Nachrichten an gnugpg-users@gnupg.org zu senden (und sofort Hilfe zu bekommen). Werner Koch selbst nimmt meine Anfrage sogar zum Anlass, das Zusammenspiel von GnuPG und WKD zu überdenken. Ich fühle mich sehr geehrt.

Dank Homebrew verfüge ich bereits über ein aktuelles GnuPG und ein entsprechend kompiliertes mutt, gpgme funktioniert mittlerweile reibungslos, die Generierung modern gekurvter-Schlüssel ist rasch erledigt (gpg --full-gen-key), und da meine Domain zertifikatsbewehrt ist, gestaltet sich auch die WKD-Konfiguration denkbar einfach. Zunächst wird ein Verzeichnis im Webroot-Verzeichnis auf dem Webserver angelegt und innerhalb des neuen Verzeichnisbaums eine (leere) Policy-Datei erzeugt:

mkdir -p /var/www/html/mysite/.well-known/openpgpkey/hu touch /var/www/html/mysite/.well-known/openpgpkey/policy

Anschließend werden der WKD-Hash und der Fingerabdruck des PGP-Schlüssels ausgelesen –

gpg --with-wkd-hash --fingerprint user@eden.one

– um den öffentlichen Schlüssel dann im Binärformat in eine Datei zu schreiben, die den Namen des WKD-Hashes trägt:

gpg --no-armor --export $fingerprint > /var/www/html/mysite/.well-known/openpgpkey/hu/$wkd_hash

Zum Schluss benötigt nginx.conf eine Ergänzung –

location ^~ /.well-known/openpgpkey { default_type application/octet-stream; add_header Access-Control-Allow-Origin * always; }

– für die WKD-konforme Auslieferung des Schlüssels. Und schon:

snafu ~ % gpg -v --locate-external-key user@eden.one gpg: Note: RFC4880bis features are enabled. gpg: using pgp trust model gpg: pub ed25519/7CD6651792B3D1F8 2022-06-06 Snafu <user@eden.one> gpg: key 7CD6651792B3D1F8: "Snafu <user@eden.one>" not changed gpg: Total number processed: 1 gpg: unchanged: 1 gpg: auto-key-locate found fingerprint xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx gpg: automatically retrieved 'user@eden.one' via WKD pub ed25519 2022-06-06 [SC] [expires: 2024-06-05] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx uid [ultimate] Snafu <user@eden.one> sub cv25519 2022-06-06 [E] [expires: 2024-06-05]

Die WKD-Spezifikation unterscheidet zwischen einer direkten und einer fortgeschrittenen Methode –

The benefit of the advanced method is its greater flexibility in setting up the Web Key Directory in environments where more than one mail domain is hosted.

– und obwohl ich nur eine Domain abdecken muss, möchte ich mich nicht auf eine lediglich rückwärtskompatible Variante stützen, sondern den Fortschritt implementieren. Zu diesem Zweck werden ein DNS-Eintrag (A) für die Subdomain openpgpkey.eden.one und ein Zertifikat für die Subdomain benötigt:

certbot certonly -d openpgpkey.eden.one --nginx

Um die Pflege separater Verzeichnisstrukturen für advanced und direct zu vermeiden, ergänze ich nginx.conf so, dass Anfragen an https://openpgpkey.eden.one/.well-known/openpgpkey/eden.one/hu auf das bestehende Verzeichnis abgebildet werden (alle anderen Anfragen an die Subdomain werden auf die reguläre Website umgeleitet):

server { listen 443 ssl http2; server_name openpgpkey.eden.one; ssl_certificate /etc/letsencrypt/live/openpgpkey.eden.one/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/openpgpkey.eden.one/privkey.pem; location / { return 301 https://eden.one; } location ^~ /.well-known/openpgpkey/eden.one { alias /var/www/html/mysite/.well-known/openpgpkey; default_type application/octet-stream; add_header Access-Control-Allow-Origin * always; } }

Ab sofort können die vielen Menschen, die statt eines sicheren Messengers lieber einen WKD-fähigen MUA nutzen, mir mit etwas geringerem Aufwand verschlüsselte E-Mails senden und bei fehlerfreier Konfiguration und Anwendung etwas sicherer sein, den richtigen Schlüssel zu verwenden.

The Right Kind of Secure

Apple kündigt Unterstützung für den WebAuthn-Standard in einer künftigen macOS-Version an, und Mike Peterson ist noch etwas unschlüssig, wie die vielen Vorteile von passkeys widerspruchsfrei zu bewerben sind:

Also, passkeys can be backed up to iCloud and synced across your iPhone, iPad, and Mac devices in an end-to-end encrypted fashion. [...]

Passkeys also can't be phished or stolen in a data breach as easily as passwords can. Because they're stored on your device instead of a web server, they're much more resistant to data breaches.

In scharfem Kontrast zu Mr. Petersons Vertrauen in die iCloud steht die Perspektive der Debian-Gemeinschaft auf den Umgang mit PGP-Primärschlüsseln:

You should keep your private primary key very, very safe. However, keeping all your keys extremely safe is inconvenient: every time you need to sign a new package upload, you need to copy the packages onto suitable portable media, go into your sub-basement, prove to the armed guards that you're you by using several methods of biometric and other identification, go through a deadly maze, feed the guard dogs the right kind of meat, and then finally open the safe, get out the signing laptop, and sign the packages. Then do the reverse to get back up to your Internet connection for uploading the packages.

Behördenoptimismus

Unter den bundesweit verfügbaren Diensten, die mit einem elektronischen Personalausweis genutzt werden können, ist auch das Stasi-Unterlagen-Archiv, und so wird mir am 2022-06-07 (nach sorgfältiger Prüfung meines Antrags vom 2021-11-13, dessen Eingang am 2021-11-16 bestätigt wurde) politische Irrelevanz bis 1989 aus Sicht des MfS bescheinigt:

Die Recherchen in allen nach Ihren Angaben infrage kommenden Karteien haben ergeben, dass zu Ihrer Person keine Hinweis auf Unterlagen vorliegen.

Die Mitarbeiterin des Archivs weist mich allerdings darauf hin, es sei nicht gänzlich auszuschließen, dass bei weiteren Arbeiten noch Unterlagen zu Ihrer Person aufgefunden werden. Ich möge mich in etwa zwei Jahren erneut an sie wenden. Für diese ungebrochene Zuversicht, dass sich immer noch eine Akte finden lassen könnte, sollte es ein Wort geben.

Große Politik

Die Nuancen des diplomatischen Balletts sind mir fremd, aber die Tatsache, dass die klein gewachsenen Staats- und Regierungschefs der Ukraine und Deutschlands im Kontext des russischen Angriffskrieges von ausnehmend großen Herren (Wladimir Klitschko, Friedrich Merz, Ruslan Stefantschuk) aus dem jeweils anderen Land besucht werden, ist sicher kein Zufall.

Vervollständigung 2

Ab einer gewissen Anzahl von Argumenten wünscht man sich für eigene Python-Skripte eine automatische Vervollständigung nach dem Vorbild von Standardwerkzeugen, und wie immer ist man mit diesem Wunsch im Internet nicht allein.

shtab erzeugt automatisch zsh completion scripts, wenn

Ist eine dieser Voraussetzungen nicht erfüllt, scheitert die Generierung, oder das gewünschte completion script wird nicht für rb herangezogen. Andernfalls hat –

shtab --shell=zsh rbackup.get_parser | sudo tee /usr/local/share/zsh/site-functions/_rb

– den gewünschten Effekt. Zwar habe ich mehr als die der Zeitersparnis angemessenen 30 Minuten investiert, aber man kann nicht alles messen.

αὐτόνομος

Der Eintrag von Servernamen in /etc/hosts genügt, um aus einem Redaktionssystem aus Sicht des Browsers einen namentlich ansprechbaren Webserver zu machen, aber auch ambitionierte Amateurinnen sollten nicht auf ein getrenntes System verzichten. Statt DNSMasq oder einem teuren Router kann man unter Ubuntu Bind9 konfigurieren, um eine eigene TLD im lokalen Netz zu etablieren:

# /etc/bind/named.conf.local zone "mytld" { type master; file "/etc/bind/db.mytld"; }; # /etc/bind/named.conf.options options { directory "/var/cache/bind"; dnssec-validation auto; validate-except { "mytld"; }; forwarders { 1.1.1.1; }; listen-on { 127.0.0.1; 192.168.78.0/24; }; listen-on-v6 { any; }; }; # /etc/bind/db.mytld $TTL 604800 @ IN SOA ns.mytld. root.mytld. ( 5 ; Serial 604800 ; Refresh 86400 ; Retry 2419200 ; Expire 604800 ) ; Negative Cache TTL ; @ IN NS ns.mytld. * IN A 192.168.78.42

Als Nameserver für alle anderen Domains (forwarders) kommt Cloudflare (1.1.1.1) zum Einsatz, und die DNSSEC-Validierung wird für die neue TLD deaktiviert. Interessanterweise hält diese Konfiguration Bind9 nicht davon ab, sich bei Cloudflare (erfolglos) nach dem Delegation Signer zu erkundigen:

16:28:34.165569 IP myserver.49728 > one.one.one.one.domain: 45669+% [1au] DS? mytld. (46) 16:28:34.179688 IP one.one.one.one.domain > myserver.49728: 45669 NXDomain* 0/6/1 (1026)

Wenn nun der neue DNS-Server auf einem lokalen Netgear-Router hinterlegt wird, bringt man sich um die Segnungen von DNSSEC, weil Netgear DNSSEC-Unterstützung im privaten Bereich offenbar für unnötig hält. Stattdessen müssen alle lokalen Endgeräte (solange sie sich im heimischen LAN/WLAN aufhalten) direkt mit dem DNS-Server und Cloudflare als Fallback interagieren.

Erstaunlich schwierig ist es, den Server zum dogfooding zu bewegen: Er erhält per DHCP die IP-Adresse des Routers und verwendet sie auch für DNS-Anfragen. Trägt man die Loopback-Adresse 127.0.0.1 in /etc/systemd/resolved.conf ein, meldet resolvectl status zwar:

Global Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported resolv.conf mode: stub Current DNS Server: 127.0.0.1 DNS Servers: 127.0.0.1

aber wegen der fehlenden netplan-Konfiguration auch:

Link 2 (eno1) Current Scopes: DNS Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=no/unsupported Current DNS Server: 192.168.78.1 DNS Servers: 192.168.78.1

Abhilfe schaffen – anders als von Kumar getestet – nur folgende Einträge in /etc/netplan/00-installer-config.yaml:

network: ethernets: eno1: dhcp4: true dhcp4-overrides: use-dns: false nameservers: addresses: [127.0.0.1] version: 2

in Verbindung mit DNSSEC=yes in /etc/systemd/resolved.conf:

Global Protocols: -LLMNR -mDNS -DNSOverTLS DNSSEC=yes/supported resolv.conf mode: stub Link 2 (eno1) Current Scopes: DNS Protocols: +DefaultRoute +LLMNR -mDNS -DNSOverTLS DNSSEC=yes/supported Current DNS Server: 127.0.0.1 DNS Servers: 127.0.0.1

Den autoritativen Nameserver für eine TLD zu betreiben, ist ein erhebendes Gefühl (trotz kleiner Schwierigkeiten mit modernen HTTPS-Einträgen).

αὐτάρκης

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. Auf eine Festplattenverschlüsselung verzichte ich angesichts der notwendigen Prozedur für kopflose Systeme. Die Installation des Webstacks 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.

Deutsche Waffen, deutsches Geld

Trotz der Zeitenwende gelten bestimmte Gewissheiten auch in unruhigen Zeiten.

Implizit

Laut Tagesschau haben drei europäische Regierungschefs die Franzosen implizit dazu aufgerufen, am Sonntag Emmanuel Macron zu wählen:

Die französischen Bürgerinnen und Bürger stehen vor einer kritischen Wahl – für Frankreich und für jede und jeden Einzelnen von uns in Europa, heißt es in einem Gastbeitrag, der in der Zeitung Le Monde veröffentlicht wurde. Es ist die Wahl zwischen einem demokratischen Kandidaten, der weiß, dass Frankreichs Stärke in einer mächtigen und unabhängigen Europäischen Union zunimmt. Und einer Kandidatin der extremen Rechten, die sich offen mit denen solidarisiert, die unsere Freiheit und Demokratie angreifen, heißt es weiter. Die Namen Macrons und Le Pens bleiben ungenannt.

Mit der Nennung der fehlenden Namen nimmt die Redaktion in vorbildlicher Weise ihren Informationsauftrag wahr für die Le Monde-lesende deutsche Öffentlichkeit, die seit gestern darüber rätselt, wer hier wohl gemeint sein könnte.