Warum FIDO2 USB-Sticks?
Effektive und effiziente Authentifizierung ist immer Fluch und Segen zugleich. Nutzerkomfort und Sicherheit stehen dabei gefühlt in einem ständigen Konflikt. Sind Passwörter oder sonstige zusätzliche Faktoren zu komplex, sind die Nutzer frustriert und der Helpdesk schnell überlastet. Sind Passwörter und Zusatzfaktoren zu simpel, formt sich ein potenzieller Angriffsvektor.
Im Unternehmensumfeld haben sich bereits verschiedene Mechanismen wie Smartcards, Single-Sign-On oder isolierte Intranet-Authentifizierung etabliert. Individuelle Nutzer und Privatanwender haben bisher ausser Passwort-Managern noch keine etablierte Lösung zur Hand für elegantes Authentifizierungs-Management.
—
Das ist unter anderem auch der Grund warum 2012 die FIDO Alliance (Fast IDentity Online) durch PayPal, Lenovo und weiteren zur Etablierung von passwortloser Authentifizierung gegründet wurde.
Mit dem Beitritt von Google, Yubikey und NXP im Jahr 2013 wurden die ersten Implementierungsvorschläge für offene Second-Faktor-Authentifizierungs Implementierungen vorgeschlagen.
Das führte zu dem momentan aktuellsten Standard der Alliance FIDO2/FIDO2 U2F. Dieser wird seit Version 8.2 von OpenSSH zur Authentifizierung unterstützt und führt zu diesem Text. Daneben ist die FIDO-Authentifizierung in erster Linie für browser-basierte Webanwendungen gedacht, welche bisher z.B. von Microsoft und Amazon als zweiter Authentisierungs-Faktor unterstützt wird. Auf https://webauthn.io/ kann man seinen FIDO2/FIDO2 U2F Token ausprobieren.
—
Neben dem populärsten Vertreter solcher USB-Tokens, den Yubikeys von Yubico, gibt es glücklicherweise auch einen deutschen Anbieter solcher FIDO2/FIDO2 UAF-konformer Sticks. Das ist Nitrokey aus Berlin mit dem Nitrokey FIDO2-Stick. Das Besondere an den Produkten von Nitrokey ist, dass sowohl Software als auch Hardware zu 100% Open Source sind.
Warum in Docker Containern?
Es gibt mit der zunehmenden Verbreitung von cloudnativen Netzwerken einen wachsenden Bedarf an containerbasierten Applikationen. Dabei spielen viele Vorteile eine Rolle wie Skalierbarkeit und mächtige Möglichkeiten für Konfigurations-Management. Für dieses Minimalbeispiel nutzen wir folgende Vorteile aus:
- host-unababhängige Applikationen: Nicht jeder hat Lust sich ein OpenSSH >8.4 selbst zu kompilieren und nicht jeder hat bereits ein Ubuntu >20.04 im Betrieb, wo OpenSSH in der notwendigen Version bereits in den Repos ist. So können wir ohne Veränderungen an unserem Betriebssystem bzw. zusätzliche Installationen OpenSSH zur Ausführung bringen.
- versionierbare Infrastruktur: Das Buzzword Infrastructure-As-Code ist in aller Munde. Zu Recht. Dadurch ist es möglich sowohl allein als auch im Team Netzwerk-Aufbau und -Management mit jedem Fortschritt und jeder Veränderung nachvollziehbar archivieren zu können.
Alle diese einzelnen Möglichkeiten und Vorteile könnten jeweils ganze Seiten und Bücher füllen, hier wollen wir uns aber auf die Nutzung von FIDO2 USB-Sticks in einem möglichst einfachen Setup darstellen:
Docker-basierte OpenSSH FIDO2 Authentifizierung mit Nitrokey FIDO2 Sticks
Zur Erprobung dieser Funktionalität ohne Änderungen an unserem Host-System vorzunehmen, bzw. diese Funktionalität in Folge elegant auf einen entfernten Server bringen zu können, nutzen wir docker-compose um den SSH-Client und den SSH-Server zu beschreiben und zu testen. Dabei nutzen sowohl Client als auch Server OpenSSH-Version 8.4 zur Ermöglichung der FIDO-Authentifizierung.
Dazu existiert dieses Repository wo die beschriebenen Schritte nachvollzogen werden können.
- Dieses kann gecloned werden mit
git clone https://github.com/cyberinnovationhub/openssh-docker-nitrokey-fido2.git
.- Auf dem Host installiert sein müssen
docker > 18.06.0
unddocker-compose > 1.22.0
Docker Container für Client und Server
Ich werde beispielhaft für den Client erklären, was die einzelnen Dateien bedeuten, die notwendig sind um einen docker-compose Container erfolgreich zu starten:
SSH Client
docker-compose.yml
Zentrale Konfigurationsdatei für docker-compose ist die docker-compose.yml
: ()[https://github.com/cyberinnovationhub/openssh-docker-nitrokey-fido2/blob/main/sshclient/docker-compose.yml]:
build
: Definition des zu nutzenden Docker Containers bzw. die Umgebung/Ordner mit allen notwendigen Dateien um den Docker Container zu bauen.volumes
:generated_keys
ist sowohl im Container als auch im Host im Dateisystem erreichbar, sodass die erstellten Keys in diesem Ordner persistent auch nach Beendigung des Containers bleiben/dev/bus/usb
und die weiteren Volumes sind keine Ordner sondern Dateideskriptoren, die dem Container ermöglichen auf USB-Devices die am Hostsystem anstecken, zuzugreifen
privileged
: mittrue
stattet den Container mit maximalen Systemrechten aus- aus Sicherheitssicht nicht gut, da die Isolierung der Docker Engine im Host fast aufgehoben wird
- habe aber keine andere einfache Möglichkeit gefunden dem Container Zugriff auf die USB-Schnittstellen des Hosts zu geben
networks
: beschreibt ein durch mich definiertes Netzwerk/Subnetz im Docker Network, wo dieser Container gestartet wird
version: '3.7'
services:
client:
restart: "no"
build:
context: ssh_build
hostname: "client.ssh"
volumes:
- ./generated_keys/:/root/generated_keys/
- /dev/bus/usb:/dev/bus/usb
- /sys/bus/usb/:/sys/bus/usb/
- /sys/devices/:/sys/devices/
- /dev/hidraw0:/dev/hidraw0
privileged: true
networks:
default:
external:
name: fido-ssh-test
Dockerfile
Hier beschreibe ich die Funktion inline als Kommentar in Dockerfile:
# nutze ein Ubuntu 20.04 als Container-Basis
FROM ubuntu:20.04
# alle Pakete updaten und upgraden
RUN apt update && apt -y upgrade
# installiert openssh aus Standard-Repos (ist in Ubuntu 20.04 OpenSSH 8.4)
RUN apt install -y openssh-server
# hinzufuegen eines Skriptes welches zum Startzeitpunkt des Containers ausgefuehrt wird
COPY ./docker_entrypoint.sh /tmp/docker_entrypoint.sh
RUN chmod +x /tmp/docker_entrypoint.sh
# zusaetzlicher non-root user
RUN useradd -ms /bin/bash user
# Starttrigger des Containers mit hinzugefuegtem Skript
ENTRYPOINT ["/tmp/docker_entrypoint.sh"]
docker_entrypoint.sh
Das Skript zur Beschreibung des Starttriggers docker_entrypoint.sh:
#!/bin/bash
# Start des SSH-Servers (ist beim Client nicht zwingend erforderlich, ist eher exemplarisch fuer Nutzung von docker_entrypoint Skript)
/etc/init.d/ssh start
# haelt Docker Container am "Laufen", da sonst nach Ausfuehrung aller Befehle der Container sich beenden wuerde
tail -f /dev/null
SSH Server
Der Server folgt ähnlichem Muster, kopiert aber zusätzlich noch eine sshd_config
in den Container, die den Betrieb des SSH Servers konfiguriert und die Nutzung von zertifikats-basierter Authentifizierung zulässt.
Erstellung Docker-Netzwerk und Container-Start
Wir haben gerade gesehen, dass wir für unsere Container ein eigenes Netzwerk fido-ssh-test
nutzen wollen. Dieses müssen wir jetzt mit Docker erstellen:
sudo docker network create fido-ssh-test
Nun können wir auch die docker-compose Beschreibungen bauen und zu lauffähigen Instanzen machen. Als erstes der SSH-Server:
cd sshserver
sudo docker-compose build
Den erfolgreichen Build können wir jetzt starten:
sudo docker-compose up
Jetzt verfahren wir beim Client genauso aber machen den Build und das Starten in einem Schritt und führen den Container im Hintergrund aus:
cd ..
cd sshclient
sudo docker-compose up -d
Server und Client laufen und lassen sich im Docker-Netzwerk nachweisen:
sudo docker network inspect fido-ssh-test
Erstellung des SSH Keys
Jetzt können wir in dem SSH-Client den privaten und den öffentlichen Key bauen zum Aufbau einer zertifikat-basierten Authentifizierung.
sudo docker exec -it sshclient_client_1 /bin/bash
Nun sind wir im Container und können mit ssh-keygen
die Zertifikate gegen den Nitrokey FIDO2-Stick generieren:
incontainer $ ssh-keygen -t ecdsa-sk -O resident -f generated_keys/first_fido_ssh_key
-
Mit der Option
-t
wird der Typ des kryptografischen Verfahrens für das Schlüsselpaar definiert. Für FIDO2-basierte Authentifizierung mit Nitrokey ist nach meinem Kenntnisstand bisherecdsa-sk
unterstützt. -
Mit der Option
-O
können wir unter anderem FIDO-Stick spezifische Parameter angeben, z.B.:-O resident
: ermöglicht für FIDO2-Sticks das Speichern des Private Key auf dem Stick selbst (wird gleich erklärt)-O no-touch-required
: ermöglicht Authentifizierung mit dem Stick ohne Berührung-O device
: manuelle Spezifikation des FIDO Devices
Der Public-Teil des erstellten Schlüsselpaares first_fido_ssh_key.pub
muss jetzt in den Ordner sshserver/authorized_keys
verschoben werden:
mv generated_keys/first_fido_ssh_key.pub ../sshserver/authorized_keys
Nach dem Hinzufügen des Schlüssels in das Verzeichnis des Servers, muss dieser Container einmal neu gestartet werden:
cd ..
cd sshserver
sudo docker-compose restart
SSH-Verbindung mit dateibasiertem Private Key
Neben dem nachfolgenden Verwahren des Keys auf dem FIDO2-Token selbst, können wir uns in gewohnter Weise mit dem dateibasierten Private Key mit dem Nitrokey zum Server verbinden:
sudo docker exec -it sshclient_client_1 /bin/bash
incontainer $ cd
incontainer $ ssh -i generated_keys/first_fido_ssh_key user@sshserver_server_1
SSH-Verbindung mit token-basiertem Private Key
Mit dem Parameter -O resident
haben wir bereits ein Schlüsselpaar erstellt welches geeignet ist direkt auf dem Nitrokey FIDO2-Stick gespeichert zu werden. Dazu wird das SSH-Tool ssh-agent
genutzt, welches für die einfache Organisation von mehreren SSH-Keys gedacht ist. Weiterhin ist dafür das Setzen eines PINs auf dem Nitrokey FIDO2-Stick erforderlich.
Setzen des PINs mit Chrome Browser
Der Google Chrome Browser bringt “von Haus aus” Funktionalitäten zur Kommunikation mit FIDO2-Tokens mit. Die Funktionalität kann im Chrome Browser gemäß diesem Nitrokey-Beitrag gefunden werden oder kann von dem Screenshot abgeleitet werden:
Speichern des Private Key auf dem Nitrokey FIDO2-Token
Um unseren erstellten first_fido_ssh_key
nun auf dem Token abzulegen müssen wir ssh-agent
starten und mit ssh-add
den Schlüssel hinzufügen.
sudo docker exec -it sshclient_client_1 /bin/bash
incontainer $ eval `ssh-agent -s`
incontainer $ ssh-add -K generated_keys/first_fido_ssh_key
Nachdem der PIN eingegeben wurde, wird der Private Key auf dem Stick hinterlegt. In Chrome können wir dies jetzt überprüfen:
Weiterhin können über ssh-add -K -L
die momentan verfügbaren Keys betrachtet werden.
Verbinden zum Server
Um nachzuweisen, dass der Private Key aus dem Nitrokey kommt und nicht dateibasiert ist, löschen wir die vorhandenen Keys, setzen den Container zurück und verbinden uns mit dem gespeichertem Private Key aus dem Nitrokey:
sudo docker-compose down
sudo docker-compose up -d
sudo docker exec -it sshclient_client_1 /bin/bash
incontainer $ cd
incontainer $ eval `ssh-agent -s`
incontainer $ ssh-add -K
incontainer $ ssh user@sshserver_server_1
Schlussfolgerungen
Wir können sehen, dass ein reiner FIDO2-Token, in diesem Falle von Nitrokey eine gute Möglichkeit darstellen kann, das Sicherheitsniveau zum Zugriff und Nutzung der eigenen Infrastruktur und von bereitgestellten Webservices, bei relativ geringem Aufwand, zu erhöhen. Insbesondere finanziell ergibt sich hier gegenüber Tokens mit verschlüsseltem Storage und verschlüsseltem Passwortspeicher ein klarer Vorteil.
Natürlich skaliert dieses händische Vorgehen nicht auf die Konfiguration und die Pflege einer großen Anzahl solcher Tokens. Dafür werden wir in naher Zukunft vorhandene Lösungen erproben, die vielleicht auch zu einem neuen Blogpost reifen. :)
Weiterhin zeigt sich die Erkenntnis, dass Docker auch sehr gut auf einem kleinen Host-System (Laptop, Desktop) genutzt werden kann um diverse experimentelle Setups (auch mit Abhängikeiten an USB-Schnittstellen) zu erproben und diese dann mit relativ geringem Aufwand auf größere Infrastruktur zu übertragen. Auch zu diesen Themen, Orchestration und skalierbare Serverkonfiguration, werden weitere Blogposts folgen.
Bis dahin, Happy Hacking :)
Achso, und ich/wir haben die Weisheit natürlich nicht mit Löffeln gefressen. Über konstruktive Kritik, dass man beim Vorgehen bestimmte Sachen besser oder anders machen kann, würden wir uns total freuen. #SharingIsCaring