ES STELLT SICH VOR...

Johannes Truschnigg jun.

Bild von Johannes Truschnigg, Februar 2015

Zur Suche ▼Zum Archiv ▼

Eintrag von 2016-11-11

pglogical: Starthilfe für Logical Replication mit PostgreSQL


Auf der pgconf Europe 2016 habe ich dieses Jahr einen sehr interessanten Vortrag über pglogical besucht - eine Extension für PostgreSQL ab Version 9.4, die "Logical Replication" erlaubt. Logical Replication ist ein naher Verwandter des seit Version 9.0 in Postgres vorhandenen, physischen Replikationsverfahrens: dem Log-Shipping. Während dabei das "Write-Ahead Log" in einer Art endlosem Strom blockweiser Binär-Diffs vom Quell- auf den Zieldatenbankcluster appliziert wird, verwendet pglogical ein Feature namens "Logical Decoding", das aus diesem Datenstrom wieder SQL-Statements errechnen kann.

Dadurch ergibt sich gegenüber dem physischen WAL-Replay eine erhöhte Flexibilität bzw. Granularität für die zu replizierenden Daten: Mit Logical Replication kann man einzelne Relationen bzw. auch nur gewisse Statements replizieren, und ist nicht auf ganze PostgreSQL-Cluster als kleinste Einheit der Replikation festgenagelt. Preis dafür ist eine gesteigerte Komplexität bei der Einrichtung - sowie einige Limitationen bei der Replikation von DDL-Statements, die aber voraussichtlich bis PostgreSQL 10.0 aus der Welt geschafft sein werden.

Logical Replication eignet sich aufgrund dieser Eigenschaften als Ersatzlösung für Trigger-basierte Ansätze zur selektiven Replikation wie etwa Londiste/pgq aus den SkyTools. Vorteil diesen gegenüber ist, dass keine zusätzliche Write-Load am Master anfällt, egal ob eine Mutation zu einer Replikation führt oder nicht - es muss nämlich nicht in einer oder mehreren Extra-Tabellen Buch darüber geführt werden, was an Daten genau wohin zu replizieren ist. Falls die Replikation einmal zwischenzeitlich ausfallen sollte, füllen sich auch nicht am Master durch Trigger befüllte Event-Queue-Tables innerhalb des PostgreSQL-Clusters, sondern es akkumulieren sich WAL-Segmente im Dateisystem des Master-Cluster-Hosts. Den Füllstand dieses Dateisystems sollte man aber ohnehin immer genau im Auge behalten, um Notfalls teildestruktiv (und wahrscheinlich die Verfügbarkeit des Masters sicherstellend) eingreifen zu können.

Beim praktischen Ausprobieren von pglogical bin ich auf einige kleine Stolpersteine gestoßen, die für Kopfkratzen und initiale Ratlosigkeit gesorgt haben. Deswegen will ich hier eine kurze, kommentierte Einführung in Form einer Anleitung bereitstellen, wie man zwei PostgreSQL 9.6-Cluster auf Debian Jessie per Hand passend für pglogical einrichtet, und eine Relation zwischen diesen beiden synchron hält. (Achtung: Sinnvollerweise folgt man dieser Anleitung in genau dieser Form nur auf einem Host, den man nach dem Experiment entsorgen kann! Unter dieser Annahme kümmere ich mich in Folge auch nicht um bspw. das Sicherstellen der Authentizität der GPG-Keys für die Paketmetadaten-Signatur, etc. Weiters sind diese Kommandos - sofern nicht anders angegeben - in einer root-Shell auszuführen.)

Als ersten Schritt müssen wir dazu zwei Repositories als Paketquellen für apt hinzufügen, und dem Schlüsselmaterial für deren Signaturen das Vertrauen des Paketmanagers schenken:

cat <<EOF > /etc/apt/sources.list.d/pgdg.list
deb http://apt.postgresql.org/pub/repos/apt/ jessie-pgdg main
EOF
wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc  | apt-key add -

cat <<EOF > /etc/apt/sources.list.d/2ndquadrant.list
deb [arch=amd64] http://packages.2ndquadrant.com/pglogical/apt/ jessie-2ndquadrant main
EOF
wget --quiet -O - http://packages.2ndquadrant.com/pglogical/apt/AA7A6805.asc | apt-key add -

Nachdem dieser Schritt erfolgreich erledigt wurde, installieren wir die damit verfügbar gewordenen Pakete für PostgreSQL 9.6, und die zu dieser Version passende pglogical-Erweiterung von 2ndquadrant. Dann weisen wir das Debian-Tooling für PostgreSQL dazu an, zwei neue Cluster für unser Demo-Setup zu erstellen, die auf für postgres unüblichen Ports Listener errichten sollen:

apt-get update
apt-get install -y postgresql-9.6 postgresql-9.6-pglogical

pg_createcluster 9.6 demoMASTER -p 15432
pg_createcluster 9.6 demoRX0 -p 15433

Für eine grundsätzliche Funktionsdemonstration werden wir die durch pg_createcluster erstellten Cluster-Konfigurationen nicht weiter optimieren - was aber notwendig ist, ist die Aktivierung der pglogical-Extension, die für PostgreSQL 9.6 als Shared Object implementiert ist. Die folgenden Kommandos kümmern sich auch darum, dass beide neu erstellten Cluster diese Konfigurationsänderung erfahren:

cat <<EOF >/etc/postgresql/9.6/demoMASTER/postgresql_pglogical.conf
wal_level = 'logical'
max_worker_processes = 10
max_replication_slots = 10
max_wal_senders = 10
shared_preload_libraries = 'pglogical'
track_commit_timestamp = on
EOF

cp -a /etc/postgresql/9.6/demoMASTER/postgresql_pglogical.conf /etc/postgresql/9.6/demoRX0/postgresql_pglogical.conf

echo "include = 'postgresql_pglogical.conf'" >> /etc/postgresql/9.6/demoMASTER/postgresql.conf
echo "include = 'postgresql_pglogical.conf'" >> /etc/postgresql/9.6/demoRX0/postgresql.conf

Was nun im designierten Master-Cluster noch fehlt, sind die Replikationsprivilegien für eine Verbindung des Replica-Clusters. Wir gehen davon aus, dass beide postgres-Server über das UNIX Domain Socket des Masters miteinander kommunizieren werden, was auf einem gemeinsamen Host sinnvoll ist. Dieses Szenario benötigt einen tauglichen Eintrag in der pg_hba.conf am Master:

echo 'local replication postgres peer' >> /etc/postgresql/9.6/demoMASTER/pg_hba.conf

Nun starten wir die postgresql-Service-Unit, was dazu führt, dass unsere zwei neu erstellten Cluster hochfahren und (zusätzlich zu etwaigen schon laufenden, anderen PostgreSQL-Clustern) benutzbar werden:

systemctl start postgresql

Alle übrigen Arbeitsschritte benötigen nicht mehr root-Privilegien, sondern stattdessen Superuser-Rechte in PostgreSQL. Um diese zu erlangen, wechseln wir unseren Nutzerkontext zu dem des Systembenutzers postgres:

su - postgres

Bevor wir mittels pglogical Relationen replizieren können, müssen gleichnamige Datenbanken in den beiden Clustern existieren. Dies erledigen wir mit einem Aufruf von createdb gegen beide Cluster:

createdb -p 15432 demodb
createdb -p 15433 demodb

Nun gilt es, die pglogical-Extension in diesen frisch erstellten Datenbanken zu installieren, sodass die pglogical-Funktionen verfügbar werden:

psql -p 15432 demodb -c "CREATE EXTENSION pglogical"
psql -p 15433 demodb -c "CREATE EXTENSION pglogical"

Ab nun unterscheiden sich die Aktionen, die wir gegen den Master bzw. den Slave ausführen, maßgeblich. Der Master-Node will für seine Verwendung mit pglogical folgendermaßen initialisiert werden:

psql -p 15432 demodb -c "SELECT pglogical.create_node(node_name := 'demoMASTER', dsn := 'port=15432 dbname=demodb')"

Der Slave braucht im Kontrast dazu über pglogical (und nicht etwa über die recovery.conf, wie bei Log-Shipping/Streaming Replication) den Auftrag, sich auf den WAL-Stream des eben erstellten Master-Nodes zu subscriben:

psql -p 15433 demodb -c "SELECT pglogical.create_node(node_name := 'demoRX0', dsn := 'port=15433 dbname=demodb')"
psql -p 15433 demodb -c "SELECT pglogical.create_subscription(subscription_name := 'demo_rx0_from_master', provider_dsn := 'port=15432 dbname=demodb', synchronize_data := TRUE)"

Ab jetzt sollte der Master in seinen System-Views Informationen über seinen Replikations-Consumer bereitstellen können. Dies prüfen wir mithilfe der folgenden Abfrage:

psql -p 15432 -c "SELECT * FROM pg_stat_replication" | cat

Ist bis hierher nichts schiefgegangen, können wir die Datenbank am Master mit einer Relation füttern, die wir letztendlich replizieren wollen. Hierzu nutzen wir einen Wrapper aus dem Funktionsumfang von pglogical, um das DDL-Statement auch auf etwaige Subscriber-Nodes anzuwenden - so kommt die Tabelle auch auf unserem Slave-Cluster an:

psql -p 15432 demodb <<EOF
SELECT pglogical.replicate_ddl_command('CREATE TABLE public.films (
    code        char(5) CONSTRAINT firstkey PRIMARY KEY,
    title       varchar(40) NOT NULL,
    did         integer NOT NULL,
    date_prod   date,
    kind        varchar(10),
    len         interval hour to minute
)');
EOF

Momentan haben wir auf Master und Slave also die gleiche, leere Tabelle, deren Schemata ident sind. Man könnte nun annehmen, dass ein INSERT am Master unmittelbar dazu führt, dass der so erzeugte Record gleichlautend am Slave aufscheint, sobald die Replikationslogik diese Transaktion erfolgreich zum Slave übertragen hat. Das ist allerdings etwas voreilig - pglogical kennt das Konzept von Replication Sets, die zum relationenspezifischen Opt-In für die Datensynchronisation dienen. Sozusagen "ab Werk" existiert auch schon ein Replication Set am Master; es heißt "default". Allerdings beinhaltet es noch keine Relationen - das können wir mit dem folgenden Statement nach unserem Wunsch ändern:

psql -p 15432 demodb -c "SELECT pglogical.replication_set_add_all_tables(set_name := 'default', schema_names := ARRAY['public'], synchronize_data := TRUE)"

Da Slaves dem "default"-Replication Set automatisch subscriben, führt dieses Statement dazu, dass ab der nächsten Transaktion Änderungen an der Tabelle am Master zu genau denselben Änderungen an der Tabelle am Slave führen. Über den optionalen Parameter synchronize_data entscheidet man, ob der momentane Ist-Zustand der Relationen im Replication Set auf den Slave übertragen werden soll, oder ob stattdessen nur zukünftige Modifikationen an den Daten den Weg von MASTER nach RX0 finden sollen. Dass das auch tatsächlich funktioniert zeigen wir, indem wir eine Hand voll Records in die Tabelle am Master eintragen, und sie dann am anderen Ende des Replikations-Setups wieder hervorzaubern:

psql -p 15432 demodb -c "INSERT INTO films VALUES ('LOTR1', 'The Fellowship of the Ring', 1, '2001-12-19'::date, 'Fantasy', '2h 58min'::interval)"
psql -p 15432 demodb -c "INSERT INTO films VALUES ('LOTR2', 'The Two Towers', 2, '2002-12-19'::date, 'Adventure', '2h 59min'::interval)"
psql -p 15432 demodb -c "INSERT INTO films VALUES ('LOTR3', 'The Return of the King', 3, '2003-12-17'::date, 'Drama', '3h 21min'::interval)"

psql -p 15433 demodb -c "SELECT * FROM films"

Fertig!

direkter Link ▲

Eintrag von 2015-12-07

OpenWrt 15.05 "Chaos Calmer": A1 (AON / Telekom Austria) ADSL-Setup mit PPTP


In Österreich ist seit jeher alles sehr kompliziert - ist man hierzulande doch als Kunde des größten ADSL-Providers A1 (bzw. AON, vormals auch bekannt als Telekom Austria) an das archaische Point-to-Point Tunneling Protocol zur Herstellung der Internetverbindung gebunden. Andernorts verwendet man zumeist das modernere und für diesen Zweck viel tauglichere Point-to-Point Protocol over Ethernet (PPPoE). Sonst hat man es stellenweise noch in veralteten VPNs mit PPTP zu tun: Die damit verwendbaren Authentifizierungsmechanismen gelten heute aber als fundamental unsicher, und man sollte besser vorgestern als gestern auf eine modernere, sichere Technologie gewechselt haben.

Nicht zuletzt aus diesem Grund hat das OpenWrt-Projekt schon vor geraumer Zeit die Unterstützung für PPTP aus dem Standard-Installationsumfang entfernt - auf der Ziel-Hardware herrscht in vielen Fällen ohnehin ziemlicher Speicherplatzmangel, und den will man nicht durch das Mitschleppen vernünftigerweise nicht mehr zu verwendender Altlasten weiter verschlimmern. Für Kunden von "A1 Festnetz-Internet" (bzw. vormals jet2web, einstmals auch bekannt als AON Speed - wie eben auch immer das ADSL-Produkt der Telekom Austria A1 zur Stunde gerade nach intensivsten Strategiebesprechungen in der Marketingabteilung benamst ist) ist das freilich lästig: Sie müssen, aufgrund der technischen Unbeweglichkeit ihres ISPs, den gesamten PPTP-Support-Stack nachinstallieren, wenn sie hinter ihrem "Single User"-DSL-Modem ein vernünftiges Internetworking Device betreiben wollen.

Vor diese undankbare Aufgabe war ich kürzlich bei meinen Großeltern gestellt: Ein neuer Router bzw. Wireless Access-Point sollte dort eine schwer veraltete Buffalo Airstation G ersetzen. Viele Jahre hatte sie klag- und problemlos mit OpenWrt 0.9 "White Russian" (einer meiner erklärten Lieblingscocktails übrigens!) dort nach IEEE 802.11b/g in die nordsteirische Pampa gefunkt, um so u. a. einem Google Nexus 10, sowie den Wifi-tauglichen Geräten etwaiger Besucher, den Weg ins Internet zu weisen. Freilich ohne irgendwelchen modernen Firlefanz wie WPA2, oder Unterstützung des 5GHz-Bandes. Übernommen hat diese Aufgabe nun ein TP-Link TL-WDR4300 mit OpenWrt 15.05 "Chaos Calmer", der seinem Vorgängergerät freilich in allen technischen Belangen um Lichtjahre voraus ist.

Um nun OpenWrt 15.05 nach der Installation zur Zusammenarbeit mit ADSL von A1 zu überreden, muss man recht tief in der Trickkiste kramen. Da die notwendigen Schritte nicht wirklich dokumentiert zu sein scheinen, will ich sie hier (auch als Gedächtnisstütze für mich selbst, weil ich bestimmt noch öfters in die missliche Lage geraten werde, dieses Setup herstellen zu müssen) festhalten. Zuerst benötigt man mindestens die folgenden Pakete aus den OpenWrt-Repositories, inkl. aller Abhängigkeiten:

Freilich sollte man all diese Pakete besorgen, bevor man nur noch mit der OpenWrt-Basisinstallation auf dem Gerät dasteht - ohne sie bleibt die Internetverbindung über PPTP-ADSL nämlich düster. Im Zweifel, und um nichts vergessen zu können, mirrore ich mir deshalb vor der Installation eines Images stets den gesamten Paketbaum des zu installierenden OpenWrt-Releases (und zwar für die richtige Binärarchitektur! Mein WDR4300 benötigt jene für die Atheros 71xx-SoC-Familie.) auf ein System, das via Ethernet mit dem Router verbunden werden kann. Nach dem Flashen und initialen Setup des Routers kopiere ich dann alle so heruntergeladenen und benötigten Pakete via scp ins Dateisystem des OpenWrt-Geräts, logge mich via ssh dort ein, und installiere die Pakete via opkg nach.

Um danach einen A1-PPTP-WAN-Uplink in OpenWrt zu konfigurieren, ist noch etwas Konfigurations-Akrobatik notwendig: Zuerst braucht das von OpenWrt in der Default-Config mitgebrachte WAN-Interface eine statische IPv4-Konfiguration. Hier ist etwas Vorsicht geboten, denn je nach Router-Hardware (und damit OpenWrt-Installations-Image) kann das leicht unterschiedlich aussehen, vor allem im Hinblick auf den Interface-Namen. Beim TP-Link TL-WDR4300 liest sich die (bereits für den A1-Zugang angepasste) WAN-Konfiguration in /etc/config/network wie folgt:

config interface 'wan'
	option _orig_ifname 'eth1'
	option _orig_bridge 'false'
	option ifname 'eth1'
	option proto 'static'
	option ipaddr '10.0.0.140'
	option netmask '255.255.255.0'
	option gateway '10.0.0.138'

Ist das erledigt, braucht man noch ein zusätzliches, virtuelles Interface für das PPTP, ebenfalls in /etc/config/network. Nach dem Ersetzen der Dummy-Werte ("A1-ADSL-USERNAME", "A1-ADSL-PASSWORT") durch die tatsächlichen Benutzeridentifikationsdaten für die A1-ADSL-Verbindung kann dieser Abschnitt dann 1:1 wie hier übernommen werden:

config interface 'A1'
	option proto 'pptp'
	option server '10.0.0.138'
	option username 'A1-ADSL-USERNAME'
	option password 'A1-ADSL-PASSWORT'
	option peerdns '0'
	option dns '85.214.20.141 213.73.91.35 194.150.168.168'
	option keepalive '10 6'
	option mtu '1460'

Nach der Installation aller Laufzeitkomponenten und dem Einrichten der Interfaces muss man noch die Konfiguration des PPTP-Clients so zurechtbiegen, dass die von A1 benötigten Verbindungsparameter Anwendung finden. Dafür ersetzt man den gesamten Inhalt der Datei /etc/ppp/options.pptp mit dem folgenden:

noipdefault
noauth
nobsdcomp
nodeflate
idle 0
maxfail 0

Danach rebootet man das Gerät einfach. Ist es wieder hochgefahren, verbindet man sich wieder via ssh zu einer shell darauf, und wirft ein Auge auf den Output von logread - wenn sich der PPTP-Client hier nicht lautstark und wiederholt über Authentifizierungsprobleme beschwert, hat man alles richtig gemacht, und der WAN-Uplink wird nach wenigen Sekunden Initialisierungsarbeit den Internetzugang freigeben.

direkter Link ▲

Eintrag von 2015-10-26

2-Faktor-Authentifizierung via libpam_oath am Beispiel OpenVPN


In vielen Unternehmen stellt das lokale Netzwerk nach wie vor die zentrale Trust Boundary zwischen vertrauenswürdigem und grundsätzlich misstrautem Traffic dar. Virtuelle Private Netzwerke (VPN) sind deshalb vielerorts unverzichtbar, möchten heute doch Mitarbeiter auch von unterwegs und zu Hause aus auf schützenswerte Ressourcen und Dienste zugreifen können. OpenVPN ist eine sowohl freie als auch beliebte und erprobte Lösung, um ein solches VPN zu errichten. Die Authentifizierungsmethodik, die über die Berechtigung an der Teilnahme am VPN entscheidet, ist dabei normalerweise X.509/TLS: Ein RSA-Schlüsselpaar aus Private (bzw. Secret) Key und Public Key, sowie diverser anderer Metadaten, die von einer Certificate Authority als vertrauenswürdig eingestuft und deshalb kryptographisch signiert werden.

Das ist durchaus gut so, und soll sich aus heutiger Sicht auf die mathematischen Fundamente, die dieses Verfahren ermöglichen, auch nicht so bald ändern - aber es reicht doch oft in der Realität nicht aus, um dieses Paar aus Zertifikat und Schlüssel als einzigen Schranken zwischen potenziellen Tunichtguten und vertraulichen Datenschätzen zu positionieren: Findet sich das Endgerät mit den Schlüsseldaten (über deren etwaige lokale, symmetrische Verschlüsselung die CA ja nicht entscheiden kann) durch Diebstahl oder eine Unachtsamkeit in die falschen Händen wieder, ist die Kompromittierung der mithilfe des VPN abgeschotteten Systeme zu befürchten.

Solche Überlegungen sind der Grund, warum man für immer mehr kritische Systeme auf Authentifizierungsschemata setzt, die mehr als einen validierenden Faktor in Betracht ziehen. Solche Faktoren können verschiedenster Natur sein: Ein Tupel aus Username und Passwort als einen Faktor hat wohl jeder vernetzte Bürger schon einmal zur Anmeldung an irgendeinem Dienst wie bspw. einem E-Mail-Konto schon verwendet. Vom Online-Banking z. B. kennt man das SMS-TAN-Verfahren, wobei ein einmalig gültiges Passwort, das genau eine bestimmte Transaktion genehmigt, auf ein mit dem Benutzer in Verbindung gebrachtes Mobiltelefon übertragen wird. In wieder anderen Szenarien kommt z. B. die Kombination aus einer Zutrittskarte und einem Fingerabdruckleser zum Zug, um den Zutritt zu kritischer Infrastruktur wie Rechenzentren oder Steuerzentralen zu regeln. Nach oben gibt es hier, ähnlich wie bei der Passwortkomplexität, keine wirkliche Grenzen - es ist durchaus mach- und vorstellbar, fünf Faktoren bzw. Merkmale zu prüfen, und erst beim Überwinden all dieser Hürden den Zugriff bzw. Zutritt zu erlauben. Entscheidend ist es, ein vernünftiges Gleichgewicht aus Sicherheitsvorkehrungen und Benutzungskomfort zu finden, mit dem alle am System Beteiligten zurecht kommen, und das sich durch die organisatorisch Zuständigen ruhigen Gewissens verantworten lässt.

OATH und TOTP - Einwegpasswörter komfortabel und sicher

Auch im OpenVPN-Projekt hat man früh den Bedarf an einem zweiten authentifizierenden Faktor für VPN-Clients erkannt, und sich für eine Implementierung entschieden, die durch ihre Flexibilität und Verbreitung überzeugt: PAM (Pluggable Authentication Modules). PAM ist ein System aus Shared Libraries, die einzelne Rollen zur Authentifizierung und Authorisierung abwickeln können. Unter modernen GNU/Linux-Distributionen ist z. B. das pam_unix-Modul beim Login auf der Konsole dafür verantwortlich, Credentials mit jenen, die in /etc/shadow hinterlegt wurden, beim Anmeldeversuch abzugleichen. Andere Module erlauben es z. B., den Login an das erfolgreiche Entsperren eines zuvor hinterlegten, privaten SSH-Schlüssels zu binden - darüber habe ich vor einigen Jahren schon einmal geschrieben.

Ein anderes Modul, pam_oath, greift die Konzepte aus der Open Authentication-Initiative (OATH) auf und erlaubt das Einbinden von OTP (One-Time Passwords) in den Authenfizierungsstack PAM-kompatibler Services. Um OATH-OTP einsetzen zu können, benötigt man ein Gerät bzw. Software, die die dafür notwendigen Standards implementieren. Wer bereits ein Android- oder iOS-Gerät besitzt, kann auf quelloffene Software für diesen Zweck zurückgreifen. Der YubiKey ist ein beliebter und kostengünstiger Hardware-Token, der stattdessen ebenfalls eingesetzt werden könnte. OATH-OTPs basieren auf einem geteilten Geheimnis zwischen Client und Server - dem Secret. Dies ist eine base32-kodierte, möglichst zufällige Zeichenkette, die als Seed für pseudozufällige Berechnungen benutzt wird. Dadurch ergeben sich zeitlich begrenzt gültige, kurze und rein numerische Passwörter/PINs, die ohne weitere Kommunikation zwischen Client und Server übereinstimmen (müssen). Damit dies funktionieren kann, braucht es außer des initial ausgetauschten Secret auch noch eine weitere Komponente: Entweder den aktuellen Zeitstempel (dann verwendet man TOTP - Time-based OTP), oder die Anzahl der bisher erfolgten Authentifizierungen (dann verwendet man HOTP - HMAC-based OTP). Beide haben gewisse Vor- und Nachteile; TOTP ist auf Geräten, die über eine Echtzeituhr verfügen, sehr einfach umzusetzen. Auf Yubikey-ähnlichen Geräten und Tokens, die selbst ohne Stromversorgung auskommen, eignet sich HOTP aus naheliegenden Gründen besser. Ich werde mich hier auf TOTP beschränken, da die betrachteten Software-Lösungen für Mobiltelefone sowie auch das oathtool problemlos mit diesem Schema arbeiten.

Voraussetzungen am Server

Um nun eine existierende OpenVPN-Installation, die bereits auf die Gültigkeit von X.509-Zertifikaten zur Authorisierung setzt, um TOTP zu erweitern, braucht es auf dem Server drei Schritte:

  1. Installation der OATH-Software und des OATH-PAM-Moduls
  2. OpenVPN muss zur Nutzung von PAM konfiguriert werden
  3. PAM muss für OpenVPN TOTP akzeptieren

Für die ersten beiden Punkt genügt es, (unter Debian) die Pakete oathtool und libpam-oath zu installieren, und das von OpenVPN mitgebrachte auth-pam-Plugin zu aktivieren. Letzteres erledigt man mit einem Eintrag in der OpenVPN-Server-Konfigurationsdatei:

plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so "openvpn-otp"

Das erste Argument zur plugin-Direktive ist dabei der absolute Pfad zum Shared Object, das OpenVPN PAM-fähig macht - auf anderen Distributionen könnte dieser anders lauten; im Zweifel weiß es der Paketmanager. Das zweite Argument ist interessanter: Es ist der Name des PAM-Service, das für diese OpenVPN-Server-Instanz die authoritative Konfiguration ergeben wird. Dieses PAM-Service - bzw. seine Konfiguration - gibt es auf dem System so noch nicht, und sie muss erst erstellt werden. Unter Debian (und wohl auch auf allen anderen Distributionen, die Linux-PAM verwenden) legt man dazu eine neue Datei in /etc/pam.d/ an, die den Namen des zu konstituierenden Service trägt - hier also /etc/pam.d/openvpn-otp. Das folgende Listing konfiguriert einen PAM-Stack der sicherstellt dass der vom OpenVPN-Client an den Server übermittelte Username in einer Datei namens /etc/openvpn/users_whitelist vorkommt, und dass das für diesen User übermittelte One-Time Password gegen die in /etc/openvpn/users_oath hinterlegten OATH-Daten valide ist.

# Check if the given username is in the list of allowed names
auth requisite pam_listfile.so file=/etc/openvpn/users_whitelist item=user sense=allow onerr=fail
# Check for users' time-based One-Time Password from their OATH token device/app
auth requisite pam_oath.so usersfile=/etc/openvpn/users_oath window=10 digits=6

# Permit whitelisted usernames - if this is missing, getpwnam() will fail for non-system users
account sufficient pam_listfile.so file=/etc/openvpn/users_whitelist item=user sense=allow onerr=fail

Sobald und so lange diese beiden Bedingungen erfüllt sind, wird das OpenVPN-PAM-Plugin keine Einwände gegen die Anmeldung eines Clients haben, welcher auch ein valides X.509-Zertifikat präesentieren konnte. Um nun ein Secret zu errechnen, das man im OATH-OTP-Generator seiner Wahl, sowie im im OATH-Usersfile hinterlegen kann, benutzt man `oathtool`. Durch einen Aufruf wie den folgenden erhält man ein kyryptographisch ausreichend starkes Secret in base32-Notation (wie ihn die meisten Software-Token-Generatoren wie FreeOTP erwarten), sowie in hexadezimaler Darstellung (wie es pam_oath benötigt):

$ oathtool -v --totp $(openssl rand -hex 15)
Hex secret: b398741f71c6025eb9476be1d339ca
Base32 secret: WOMHIH3RYYBF5OKHNPQ5GOOK
[...]

OTP-Generator-Setup und -Überprüfung

Das base32-Secret füttert man nun in die OTP-App auf dem Smartphone, während man das Hex-Secret - mit dem gewünschten Usernamen verknüpft - in /etc/openvpn/users_oath hinterlegt:

#PROTO		USER	-	SECRET				COUNTER	LASTOTP	TS
HOTP/T30	vpnuser	-	b398741f71c6025eb9476be1d339ca

Der Inhalt dieser Datei muss als vertraulich eingestuft werden; es empfiehlt sich, die Ownership des Inodes auf root:root, und die Permissions auf 0600 zu setzen. Wichtig ist auch, dass die Datei nicht immutable gemacht wird, da pam_oath.so hierin Accounting betreiben wird. Weiters darf die Datei kein Symlink sein, da die Inode beim Accouting durch PAM unlinked, und wieder neu geschrieben wird. Das in hexadezimaler Notation anzuschreibende Secret darf eine gewisse (undokumentierte) Länge nicht überschreiten, da sonst ein von pam_oath allozierter Buffer nicht ausreicht, um es zur Validierung zu benutzen, was in stillschweigend fehlschlagenden (pam_oath schreibt Debug-Meldungen auf stdout, welche durch das OpenVPN-PAM-Plugin verschluckt werden - zum Debuggen von pam_oath.so bindet man es am besten in den PAM-Stack fuer `su` ein, das dieses lästige Problem nicht teilt) Logins mündet. Man muss hier also eine Menge beachten, um dieses filigrane Zusammenspiel nicht empfindlich zu stören!

Weiters ist es für die Beispielkonfiguration noch notwendig, den Usernamen in die Whitelist-Datei einzufügen - dazu öffnet man /etc/openvpn/users_whitelist mit einem Texteditor, und setzt die Zeichenfolge vpnuser in eine eigene, neue Zeile - fertig.

Hat man nun in Folge das Secret so im OTP-Generator (siehe unten für eine Liste von Apps, die ich persönlich als tauglich erachte) untergebracht, dass dieser sechsstellige Zifferncodes, die sich im 30-Sekunden-Takt erneuern/verändern, ausspuckt, kann man mithilfe von `oathtool` die Gegenprobe machen, ob der am Mobiltelefon errechnete Wert zu dem, den der OATH-Stack am Server errechnet, passt:

$ oathtool --totp b398741f71c6025eb9476be1d339ca
849562
$ oathtool --totp -b WOMHIH3RYYBF5OKHNPQ5GOOK
849562

Stimmt diese Ausgabe mit der Anzeige am Software-Token innerhalb der selben halben Minute überein, hat man alles richtig gemacht, und die App sollte künftig korrekte OTPs errechnen. Dabei ist (für TOTP) selbstverständlich strikt auf sychronisierte Uhrzeiten auf allen teilnehmenden Systemen zu achten - mithilfe des window-Parameters des pam_oath-Moduls kann man bei unzuverlässigen lokalen Zeitnehmern etwas gegensteuern - die Zahl, die man diesem Parameter als Argument übergibt, bestimmt, wie viele Zeitschritte (bei TOTP/30 sind das 30 Sekunden pro Schritt) das angegebene OTP realtiv zum am Server errechneten in der Zukunft bzw. Vergangenheit liegen darf.

Anpassung der OpenVPN-Client-Konfiguration

Jetzt gilt es noch, den OpenVPN-Client dazu zu bringen, eine Eingabemöglichkeit für Usernamen und OTP bereitzustellen. Dazu fügt man die beiden folgenden Zeilen an die existierende Konfigurationsdatei des Clients an:

auth-nocache
auth-user-pass

Die erste Zeile veranlasst den OpenVPN-Client dazu, die erfragte Passphrase nicht zu cachen (was im Falle von One-Time Passwords ohnehin absurd wäre, und immer eine sinnlose Warnung im Client-Log verursacht), während die zweite Zeile vor dem Aufbau der Verbindung zum Server nach Username und Passwort fragt, welche nach dem TLS-Handshake und der erfolgten Zertifikatsvalidierung verschlüsselt an den OpenVPN-Server übertragen, und dort von dessen PAM-Stack gegen die Usernamen-Whitelist und die OATH-TOTP-Daten validiert werden.

pam_oath in Aktion

Startet man nun den OpenVPN-Server neu, und versucht sich mit dem zusätzlich wie eben beschrieben konfigurierten Client anzumelden, kriegt man am Client zunachst die Frage nach Usernamen und Passwort - diese ergeben sich aus den in der user_whitelist und user_oath hinterlegten Daten, und dem aus dem OATH-Secret errechneten OTP, welches von der Smartphone-App angezeigt wird. Ist die Anmeldung mittels OTP einmal erfolgt, kann man in /etc/openvpn/users_oath das pam_oath-Modul beim Arbeiten beobachten. Hier könnte es nach einigen Logins in der Zeile für den Account vpnuser z. B. so aussehen:

#PROTO		USER	-	SECRET				COUNTER	LASTOTP	TS
HOTP/T30	vpnuser	-	b398741f71c6025eb9476be1d339ca	7	865748	2015-10-26T17:33:52L

Anhang: Empfehlenswerte OTP-Generator-Apps:

direkter Link ▲

Eintrag von 2015-07-17

GnuPG-Schlüsselrotation


Beinahe 10 Jahre Gültigkeit sind für einen 1024bit DSA-Key mehr als genug - seit 13. Juli 2015 habe ich deshalb ein neues GnuPG-RSA-Schlüsselpaar mit dem Fingerprint 1AEF 4886 423E EFF6 8A2D 4585 F795 B78C CB18 7CB5. Ich habe den neuen Key mit meinem alten signiert, womit der neue Schlüssel für all jene vertrauenswürdig sein sollte, die schon den alten mit mir persönlich in Verbindung gebracht hatten. Der Schlüssel ist seit gestern auf den übichen verdächtigen Keyservern verfügbar, und kann auch direkt über die Kontakt-Seite meiner Website bezogen werden.

direkter Link ▲

Eintrag von 2014-09-15

Archiv-Helferprogramm für PostgreSQL WAL-Segmente


Wer einen PostgreSQL-Cluster betreibt und Hochverfügbarkeit (oder zumindest kurze Recovery-Zeiten im Falle eines Crashes der Datenbank...) anstrebt, dem ist das gewissenhafte Archivieren der Segmente des WAL (Write Ahead Log) des Clusters ans Herz gelegt. Das WAL ist eine theoretisch endlose Folge aufsteigend nummerierter Datenblöcke, deren fixe Größe beim Kompilieren von PostgreSQL festgelegt wird - in der Standardkonfiguration beträgt diese 16 MByte. Diese Blöcke beinhalten eine Art Liste geplanter Änderungen in einer Form, die lückenlose Anwendbarkeit auf den Ist-Stand der Daten des Clusters garantiert. Sollte der Cluster vor dem erfolgreichen Abarbeiten eines ganzen WAL-Segments abstuerzen oder sonstwie ungeplant unterbrochen worden sein, so ist sichergestellt, dass beim nächsten Start des Clusters vom Beginn des letzten erfolgreich abgearbeiteten WAL-Segments aus fortgesetzt werden kann. In der PostgreSQL-Dokumentation findet man einen detaillierteren Abriss über das WAL.

Das WAL ermöglicht es aber darüber hinaus auch, die Änderungen, die ein Postgres-Cluster auf sich selbst anzuwenden plant, auf einem anderen Cluster (mit demselben Datenbestand bis zu diesem WAL-Segment) ebenfalls durchzuführen. Dazu bedient man sich einer Methode namens "Log Shipping", wobei komplette WAL-Segmente (in der Praxis 16 MByte große Binärdateien) vom Master-Cluster zu (mindestens) einem Slave-Cluster geschickt, d. h. irgendwie in dessen Dateisystem kopiert oder verschoben, werden. Dies ist auch dann noch relevant, wenn man sich der moderneren Replikationsmethode "Streaming Replication", wobei Änderungen am WAL-Strom in feinerer Abstufung als in ganzen WAL-Segmenten über eine persistente TCP-Verbindung zwischen Master und Slave(s) mehr oder weniger in Echtzeit repliziert werden, bedient. Hier benötigt man ein lückenloses WAL-Archiv, aus dem der Slave Segmente lesen kann, falls die replizierende TCP-Verbindung aus irgendwelchen Gründen über eine längere Zeit abreißt - konkret: wenn das erste benötigte WAL-Segment am Slave nicht mehr unter den durch den Master auch ohne explizites WAL-Archiv vorgehaltenen, durch wal_keep_segments festgelegten Segmenten ist.

Als Zielort für ein WAL-Archiv empfiehlt sich bspw. ein (Netzwerk-)Dateisystem, das sowohl von Master als auch Slave gelesen und geschrieben werden kann - der Master muss darauf WAL-Segmente archivieren können, und der Slave sollte applizierte und somit definitiv nicht mehr gebrauchte Segmente aus dem Dateisystem löschen können. Wenn dieses vollläuft, führt das zu sehr unschönen Konsequenzen für den Master-Cluster, sobald das Dateisystem seine Kapazitäten erschöpft, das das pg_xlog-Verzeichnis des Clusters beinhaltet. Ein NFS-Export auf dem Client oder dem Master ist für das Archiv also durchaus tauglich. Wie aber kommen nun die WAL-Segmente vom Master auf den Slave, bzw. in das geteilte Archiv-Verzeichnis? Hierzu dient das archive_command am Master - ein Aufruf via system(3), der das Kopieren des WAL-Segments an den Zielort übernehmen soll.

Die PostgreSQL-Dokumentation beschreibt ausführlich, welche Anforderungen dieses Kommando erfüllen muss:

  1. Wenn das Segment erfolgreich kopiert wurde, muss es mit Status 0 enden.
  2. In allen anderen Fällen darf es nicht mit Status 0 enden.
  3. Es darf keine schon existierenden Dateien (WAL-Segmente) überschreiben.

In der Dokumentation wird hierfür ein Shellscript vorgeschlagen, das um eine Limitation von GNU cp herumarbeitet - dieses ermöglicht es nämlich nicht, den Descriptor der Ziel-Datei mit den Flags O_CREAT und O_EXCL zu öffnen, was die garantierte Erfüllung der dritten Bedingung streng genommen unmöglich macht. Man behilft sich in der offiziellen Dokumentation mit einem zweiten Schritt vor dem tatsächlichen Kopieren: durch einen Aufruf des Programms (bzw. Builtins) `test` soll sichergestellt werden, dass die Zieldatei noch nicht existiert:

archive_command = 'test ! -f /mnt/server/archivedir/%f && cp %p /mnt/server/archivedir/%f'

Hierbei entsteht eine klassische Race Condition, die effektiv nur durch die zuvor erwähnten Flags für den open(2)-Syscall verhindert werden kann. Auch aus diesem Grund habe ich mich dazu entschlossen, ein möglichst minimales C-Programm zu schreiben, das diese Aufgabe (besser) erfüllen kann: pg_archive_wal_segment.

Hat man es kompiliert und an einem beliebigen Pfad im Dateisystem (das tunlichst nicht mit "noexec" gemountet sein sollte :-)) abgelegt - ich nehme /usr/local/bin/ an -, konfiguriert man den PostgreSQL-Master-Cluster einfach fortan so:

archive_command = '/usr/local/bin/pg_archive_wal_segment %p /mnt/server/archivedir/%f'

Das Programm beschränkt sich ganz absichtlich darauf, die Quelldatei (das erste Argument) an den angegebenen Pfadnamen (das zweite Argument) zu kopieren. Das gelingt durch read(2)- und write(2)-syscalls mit einem 512 KByte großen Buffer dazwischen. Im Fehlerfall, oder wenn der Vorgang "verdächtig" lange dauert (Default: 30 Sekunden - dieses Limit kann durch eine Compile-Time-Option festegelegt werden) bzw. wenn das Programm durch ein Signal unterbrochen wird, wird die Zieldatei wieder unlink(2)t und ein Exit-Status ungleich 0 zurückgegeben, worüber pg_archive_wal_segment auf stderr Bescheid gibt. In so einem Fall versucht der Master-Cluster nach kurzer Zeit einen erneuten Aufruf des archive_commands - so lange, bis der Kopiervorgang einmal erfolgreich verläuft. Hat das Kopieren geklappt, vermeldet das Programm den Erfolg der Operation auf stdout und retourniert 0.

direkter Link ▲

© 2007-2016 Johannes Truschnigg | Design by Andreas Viklund (modified) | valid xhmtl & css

Created with free software