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 und docker-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: mit true 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 bisher ecdsa-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

Quellen

Aktualisiert: