In unserem vorherigen Artikel haben wir eine Problemstellung der Entwicklung eines Off-Site-Caches für TLS erläutert. Zum Downloaden von Firmware-Updates für IoT-Devices soll ein Cache bereitgestellt werden, der die TLS-Verbindung zum Content-Delivery-Network unterbricht, um die Inhalte zwischenzuspeichern und so die Last auf die Internetverbindung in der Off-Site-Location zu reduzieren. Dazu benötigt der Cache ein Wildcard-Zertifikat, dem die IoT-Devices vertrauen, das besonders schützenswert ist. Aus diesem Grund soll der Schlüssel für das Zertifikat auf einem Hardware-Security-Modul gespeichert werden. Die genaue Problemstellung und der Lösungsansatz wurden detailliert im letzten Artikel besprochen. In diesem Artikel soll nun die genaue technische Umsetzung erläutert werden.

Einrichten des Grundsystems

Als Grundlage für das gesamte Setup dient ein VM-Ware-Image mit centOS 7. Für das Setup kann man dieses Tutorial verwenden. Als normalen User für das Caching-System wird der User cache eingerichtet, das System heißt hier einfach host. Alle normalen Befehle werden also mit dem Prefix cache@host $ ausgeführt. Befehle, die als Administrator ausgeführt werden müssen, haben das Prefix root@host $. Der User cache ist in der Sudoers-Group, also kann er mit sudo -s root werden. Nach der Installation werden die Paketquellen und das System geupdated und schonmal einige für das Setup benötigte Pakete installiert:

cache@host$ sudo yum install epel-release -y
cache@host$ sudo yum update -y
cache@host$ sudo yum install nano usbutils openssl-pkcs11 gnutls-utils policycoreutils-python -y

Hiernach empfiehlt sich ein Neustart der VM.

Umleitung der Geräte im Netzwerk der Edge-Location

Das erste Problem, das zu lösen ist, ist dass das Caching für die IoT-Devices vollständig transparent erfolgen soll. Hierfür soll die Domain des CDNs über DNS im Netzwerk der Off-Site-Location auf den Cache umgeleitet werden. Um dies zu gewährleisten, soll im Netzwerk der Off-Site-Location der DHCP-Server als Primary-DNS einen DNS-Server liefern, der von uns kontrolliert wird und auf unserer centOS-VM läuft, um die gewünschte Weiterleitung einzurichten. Hierzu verwenden wir dnsmasq. Außerdem installieren wir noch die bind-utils um dig zum Testen des DNS-Setups verwenden zu können.

cache@host$ sudo yum install dnsmasq bind-utils -y

Als nächstes müssen wir dnsmasq konfigurieren. Hierfür müssen wir die Datei /etc/dnsmasq.conf editieren.

cache@host$ sudo vi /etc/dnsmasq.conf

Hier müssen wir die Zeile

#conf-dir=/etc/dnsmasq.d/,*.conf

einkommentieren (die Raute am Anfang der Zeile entfernen), um eine eigene Konfigurationsdatei in /etc/dnsmasq.d/ hinterlegen zu können, die von dnsmasq auch herangezogen wird. Als nächstes fügen wir in /etc/dnsmasq.d eine Basiskonfiguration ein.

domain-needed
bogus-priv
no-hosts
keep-in-foreground
no-resolv
expand-hosts
#quad9 dns servers
server=9.9.9.9 
server=149.112.112.112

Neben Standardeinstellungen werden mit server die DNS-Server angegeben, die dnsmasq verwenden soll, wenn die angefragte Domain nicht umgeleitet werden soll. Bei den beiden IP-Adressen handelt es sich um die Adressen von quad9. Als nächstes muss die eigentliche Weiterleitung konfiguriert werden. Hierfür schreiben wir in die Datei /etc/dnsmasq.d/1.overwriting.conf. Der Eintrag für die Umleitung sieht wie folgt aus: address=/<Hier die Domain, die umgeleitet werden soll>/<hier die IP-Adresse des Caches> In unserem Fall soll der Cache auf der selben centOS-VM laufen, wie dnsmasq, daher ist die IP-Adresse die Adresse der centOS-VM. Mit folgendem Befehl kann die Datei und der Eintrag erstellt werden:

cache@host$ sudo sh -c 'echo "address=/ <Hier die Domain, die umgeleitet werden soll> /$(hostname -I)" > /etc/dnsmasq.d/1.overwriting.conf'

Mit dig können wir testen, ob die Weiterleitung funktioniert:

cache@host$ dig @localhost <Hier die Domain, die umgeleitet werden soll>

@localhost sorgt hier dafür, dass dig den lokalen DNS-Server, also unser dnsmasq verwendet. Wollen wir aber, dass in der centOS-VM grundsätzlich dnsmasq als DNS-Server verwendet wird, können wir das in der /etc/resolv.conf eintragen.

cache@host$ sudo cat /etc/resolv.conf
 [sudo] password for cache:
 # Generated by NetworkManager
 # nameserver 192.168.2.1
 nameserver 127.0.0.1

Der Eintrag nameserver 127.0.0.1 sorgt dafür, dass in der gesamten centOS-VM nun grundsätzlich erstmal unser mit der Weiterleitung konfiguriertes dnsmasq als DNS-Server verwendet wird. Als nächstes muss jedoch auch dafür gesorgt werden, dass dnsmasq als DNS-Server auch für die IoT-Devices von außen erreicht werden kann. Dafür müssen wir die Firewall-Einstellungen der centOS-VM anpassen. Hierfür editieren wir die Datei /etc/firewalld/zones/public.xml.

cache@host$ sudo vim /etc/firewalld/zones/public.xml

Sie sollte folgenden Inhalt haben:

<?xml version="1.0" encoding="utf-8"?>
 <zone>
 	<short>Public</short>
 	<description>For use in public areas. You do not trust the other computers on networks to not harm your computer. Only selected incoming connections are accepted.</description>
 	<service name="dns"/>
 	<service name="ssh"/>
 	<service name="https"/>
 	<service name="dhcpv6-client"/>
 </zone>

Für dnsmasq brauchen wir den Eintrag <service name="dns">. Die anderen Einträge sind für ssh, um auf die centOS-VM zu kommen, den nginx, der als Cache dient und DHCP. Danach müssen wir die Firewall neustarten:

cache@host$ sudo service firewalld restart

Nun setzen wir dnsmasq noch auf Autostart:

cache@host$ sudo chkconfig dnsmasq on

Jetzt können wir unseren Redirect nochmal mit dig testen, diesmal ohne @localhost, da ja der DNS-Server über die /etc/resolv.conf bereits auf localhost zeigt:

cache@host$ dig <Hier die Domain, die umgeleitet werden soll>

 ; <<>> DiG 9.9.4-RedHat-9.9.4-73.el7_6 <<>> @localhost 
 ; (2 servers found)
 ;; global options: +cmd
 ;; Got answer:
 ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 63252
 ;; flags: qr aa rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0

 ;; QUESTION SECTION:
 ;<Hier die Domain, die umgeleitet werden soll>. IN A

 ;; ANSWER SECTION:
 <Hier die Domain, die umgeleitet werden soll>. 0	IN A <Hier steht jetzt hoffentlich die IP der centOS-VM>

 ;; Query time: 0 msec
 ;; SERVER: ::1#53(::1)
 ;; WHEN: Mo Mai 13 12:05:45 CEST 2019
 ;; MSG SIZE  rcvd: 94

Wenn alles geklappt hat, steht an der Stelle <Hier steht jetzt hoffentlich die IP der centOS-VM> die IP der centOS-VM. Damit ist der DNS-Redirect fertig.

Anbinden des HSMs

Da die Grundinstallation nun abgeschlossen ist, kümmern wir uns nun um die Anbindung des Hardware Security Moduls. Als erstes müssen wir das USB Modul der VM verfügbar machen. Das genaue Vorgehen lässt sich aus der entsprechenden ESXI Dokumentation entnehmen. Ein einfacher Test erfolgt mit dem lsusb tool:

cache@host$ lsusb
 Bus 001 Device 001: ID 1d6b:0002 Linux Foundation 2.0 root hub
 Bus 002 Device 004: ID 1050:0030 Yubico.com
 Bus 002 Device 003: ID 0e0f:0002 VMware, Inc. Virtual USB Hub
 Bus 002 Device 002: ID 0e0f:0003 VMware, Inc. Virtual Mouse
 Bus 002 Device 001: ID 1d6b:0001 Linux Foundation 1.1 root hub

Es sollte hier ein Device von Yubico.com aufgelistet werden. Damit eine Software mit dem HSM kommunizieren kann, benötigen wir ebenfalls das hsm2-sdk:

cache@host$ curl wget https://developers.yubico.com/YubiHSM2/Releases/yubihsm2-sdk-2019-12-centos7-amd64.tar.gz --output /tmp/yubihsm2-sdk-2019-12-centos7-amd64.tar.gz
cache@host$ cd /tmp/
cache@host$ tar xfz yubihsm2-sdk-2019-03-centos7-amd64.tar.gz
cache@host$ chmod +x bin/*

Wir installieren die entsprechenden Utilities und referenzieren die benötigten openssl engines.

cache@host$ sudo cp -R bin/* /usr/bin/
cache@host$ sudo cp -Rf lib/* /usr/lib64/
cache@host$ sudo ln -s /usr/lib64/engines-1.1/pkcs11.so /usr/lib64/openssl/engines/libpkcs11.so

Als weiteres nützliches Tool hat sich pkcs11-tool erwiesen, welches wir nun ebenfalls installieren:

cache@host$ sudo yum install opensc -y

Die Kommunikation mit dem USB Modul selbst erfolgt über den yubihsm-connector, dieser lässt sich als daemon betreiben, welchen wir nun als root user starten:

cache@host$ sudo -s
root@host$ yubihsm-connector -d

Da der Connector ein REST-basiertes Interface verwendet, können wir die ordnungsgemäße Funktion sehr einfach mit curl überprüfen:

cache@host$ curl localhost:12345/connector/status
 status=OK
 serial=*
 version=2.2.0
 pid=23705
 address=localhost
 port=12345

Damit ein Client weiß, wie der Connector konfiguriert ist, wird eine Konfigurationsdatei benötigt. Diese liegt immer im Homedirectory des aktiven Benutzers. Für den User cache können wir diese wie folgt erstellen:

cache@host$ echo 'connector = http://127.0.0.1:12345' > /home/cache/yubihsm_pkcs11.conf

Nun können wir ebenfalls die Anbindung per openssl-engine testen, indem wir das Tool pkcs11-tool verwenden:

cache@host$ pkcs11-tool --module /usr/lib64/pkcs11/yubihsm_pkcs11.so  -L -O
 Available slots:
 Slot 0 (0x0): YubiHSM Connector localhost
   (empty)
 No slot with a token was found.

Um mehr Informationen über das Modul zu erhalten, benötigen wir einen PIN, welcher als Default auf 0001password gesetzt ist:

cache@host$ pkcs11-tool --module /usr/lib64/pkcs11/yubihsm_pkcs11.so --pin 0001password  -L -O
 Available slots:
 Slot 0 (0x0): YubiHSM Connector localhost
   token label        : YubiHSM
   token manufacturer : Yubico (www.yubico.com)
   token model        : YubiHSM
   token flags        : login required, rng, token initialized, PIN initialized
   hardware version   : 2.101
   firmware version   : 2.101
   serial num         : 07550837
 No slot with a token was found.

Die obige Ausgabe zeigt, dass bislang keine Keys auf dem Modul abgelegt worden sind. Sollten schon Keys auf dem Modul vorhanden sein, so ändert sich die Ausgabe z.B. zu:

cache@host$ pkcs11-tool --module /usr/lib64/pkcs11/yubihsm_pkcs11.so --pin 0001password  -L -O
 Available slots:
 Slot 0 (0x0): YubiHSM Connector localhost
   token label        : YubiHSM
   token manufacturer : Yubico (www.yubico.com)
   token model        : YubiHSM
   token flags        : login required, rng, token initialized, PIN initialized
   hardware version   : 2.101
   firmware version   : 2.101
   serial num         : 07550837
 Using slot 0 with a present token (0x0)
 Private Key Object; RSA
   label:      nginxkey
   ID:         5787
   Usage:      decrypt, sign
 Public Key Object; RSA 2048 bits
   label:      nginxkey
   ID:         5787
   Usage:      encrypt, verify
 Private Key Object; RSA
   label:      nginxkey2
   ID:         b670
   Usage:      sign
 Public Key Object; RSA 2048 bits
   label:      nginxkey2
   ID:         b670
   Usage:      verify

Eine Administration des HSMs erfolgt über das Utility yubihsm-shell, welches eine interaktive Verbindung zu dem Modul aufbaut. Über dieses Tool lassen sich z.B. vorhandene Keys wieder löschen:

cache@host$ yubihsm-shell
 Using default connector URL: http://127.0.0.1:12345
yubihsm> connect
 Session keepalive set up to run every 15 seconds
yubihsm> session open 1 password
 Created session 1
yubihsm> delete 1 0x5787 asymmetric-key

Die 1 ist hier die Session ID, 0x5787 ist die ObjectID vom Key (siehe vorherige Ausgabe) und asymmetric-key der object-type.
Weitere Beispiele finden sich in der Dokumentation von yubico, ebenfalls dort findet sich auch eine Übersicht über alle Objects.

Bevor wir nun mit der Einrichtung fortfahren, sollten wir den PIN ändern:

yubihsm> change authkey 0 1 <your-password>
 Changed Authentication key 0x0001

Einrichten der OpenSSL Engine

Damit openssl ebenfalls das HSM verwenden kann, muss eine openssl engine konfiguriert werden. Bevor wir fortfahren, erstellen wir ein Backup der Originalkonfiguration:

cache@host$ sudo mv /etc/pki/tls/openssl.cnf /etc/pki/tls/openssl.cnf.bak

Die neue openssl.cnf enthält nun die Konfiguration für die pkcs11 engine:

openssl_conf = openssl_init

[openssl_init]
engines = engine_section

[engine_section]
pkcs11 = pkcs11_section

[pkcs11_section]
engine_id = pkcs11
dynamic_path = /usr/lib64/engines-1.1/pkcs11.so
MODULE_PATH = /usr/lib64/pkcs11/yubihsm_pkcs11.so
PIN = "0001<b style="color:red">&lt;your-passwordl&gt;</b>"
init = 0

[req]
distinguished_name = req_dn
string_mask = utf8only
utf8 = yes

[req_dn]
commonName = ssc

Ob die Konfiguration korrekt ist, lässt sich mit openssl testen:

cache@host$ openssl engine -t -c pkcs11
 (pkcs11) pkcs11 engine
 [RSA, rsaEncryption]
     [ available ]

Wir sollten nun ebenfalls in der Lage sein, einen Schlüssel auf dem Modul zu erstellen:

cache@host$ pkcs11-tool --module /usr/lib64/pkcs11/yubihsm_pkcs11.so \
    --login \
    --pin 0001<your password> \
    --keypairgen \
    --label "nginx-private-key" \
    --key-type rsa:2048 \
    --usage-sign
 Using slot 0 with a present token (0x0)
 Key pair generated:
 Private Key Object; RSA
 label:      nginx-private-key
 ID:         c168
 Usage:      sign
 Public Key Object; RSA 2048 bits
 label:      nginx-private-key
 ID:         c168
 Usage:      verify

Mit openssl lässt sich nun auch ein certificate signing request (CSR) für den neuen Schlüssel erstellen:

cache@host$ openssl req -new \
    -subj '/CN=nginx-private-key/' \
    -sha256 \
    -engine pkcs11 -keyform engine -key 0:c168 -out /tmp/csr.pem
  engine "pkcs11" set.

cache@host$ cat /tmp/selfsigned.pem
 -----BEGIN CERTIFICATE-----
 MIICtDCCAZwCCQDBL+/A9ykCbjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFu
 …
 Q1UTUBPIdzGeO39ycfopeWIdr4xXQ6a1Llq4zctAbnvlVgI2bdkrkoL6NSXksnTZ
 5kQx2XBcZFL1m2bwmNhNe6jqEVqqWeGj
 -----END CERTIFICATE-----

Den yubihsm-connector als Service einrichten

Im produktiven Einsatz ist es natürlich nicht zu empfehlen, dass der connector zum einen mit root Rechten läuft, zum anderen manuell gestartet werden muss.

Deshalb bleibt als letzter Schritt das Erstellen einer Service Konfiguration, damit der connector automatisch nach dem Hochfahren der VM gestartet wird und zudem nur mit den Rechten eines normalen Benutzers läuft.

Als erstes legen wir hierfür einen neuen Benutzer an:

cache@host$ sudo useradd yubihsm-connector -m

Damit dieser Benutzer auf das USB Device zugreifen kann, muss per udev Regel der Zugriff umgestellt werden. Eine udev Regel ist eine Art Script, welches auf bestimmte Events eines Devices (hier add und change) reagiert. Die Rule findet sich ebenfalls im Repository vom yubihsm-connector.

cache@host$ sudo cat /etc/udev/rules.d/70-yubihsm-connector.rules

 ACTION!="add|change", GOTO="yubihsm_connector_end"

 #Yubico YubiHSM2
 SUBSYSTEM=="usb", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0030", OWNER="yubihsm-connector"

 LABEL="yubihsm_connector_end"

Damit die neue udev Regel aktiviert wird, muss das System einmal neu gestartet werden.

Danach erstellen wir eine Konfigurationsdatei /etc/yubihsm-connector.yaml:

# Certificate (X.509)
#cert: ""
#
# Certificate key
#key: ""
#
# Listening address. Defaults to "127.0.0.1:12345".
#listen: "127.0.0.1:12345"
#
# Device serial in case of multiple devices
#serial: ""
#
# Log to syslog/eventlog. Defaults to "false".
#syslog: "false"

Diese hat aktuell keine aktiven Einträge, muss aber vorhanden sein. Weiterhin muss diese Konfigurationsdatei auch vom yubihsm-connector Benutzer gelesen werden können:

cache@host$ sudo chown -R yubihsm-connector:yubihsm-connector /etc/yubihsm-connector.yaml

Der letzte Schritt ist das Anlegen eines systemd Services. Die Konfigurationsdatei /etc/systemd/system/yubihsm-connector.service hat den Inhalt:

[Unit]
Description=YubiHSM connector
Documentation=https://developers.yubico.com/YubiHSM2/Component_Reference/yubihsm-connector/
After=network-online.target
Wants=network-online.target systemd-networkd-wait-online.service

[Service]
Restart=on-abnormal

; User and group the process will run as.
User=yubihsm-connector
Group=yubihsm-connector

ExecStart=/bin/yubihsm-connector -c /etc/yubihsm-connector.yaml

; Use private /tmp and /var/tmp, which are discarded after caddy stops.
PrivateTmp=true
; Hide /home, /root, and /run/user. Nobody will steal your SSH-keys.
ProtectHome=true
; Make /usr, /boot, /etc and possibly some more folders read-only.
ProtectSystem=full

[Install]
WantedBy=multi-user.target

Nun aktualisieren wir die Daemons Konfiguration und starten des Service:

cache@host$ sudo systemctl daemon-reload
cache@host$ sudo service yubihsm-connector restart

Damit der Service jedes Mal beim Systemstart ausgeführt wird, aktivieren wir den Autostart:

cache@host$ sudo chkconfig yubihsm-connector on

 Note: Forwarding request to 'systemctl enable yubihsm-connector.service'.
 Created symlink from /etc/systemd/system/multi-user.target.wants/yubihsm-connector.service to /etc/systemd/system/yubihsm-connector.service.

Damit ist das Setup vom HSM abgeschlossen.

Aufsetzen des Caches

Nachdem nun das HSM angebunden ist, können wir den Cache aufsetzen. Dafür müssen wir als erstes nginx installieren, den wir als Cache verwenden wollen.

cache@host$ sudo yum install nginx

Nachdem wir nginx installiert haben, müssen wir die Konfiguration so anpassen, dass sie unseren Anforderungen entspricht:

cache@host$ sudo vim /etc/nginx/nginx.conf

Die Datei sollte wie folgt aussehen:

ssl_engine pkcs11;
user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    proxy_cache_path /var/lib/nginx/cache levels=1:2  keys_zone=STATIC:10m  inactive=24h  max_size=100g;

    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    &#x0023; tcp_nopush     on;

    keepalive_timeout  65;

    &#x0023; gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

Anschließend müssen zwei Verzeichnisse angelegt werden und dem nginx die Berechtigungen für diese Verzeichnisse erteilt werden:

cache@host$ sudo mkdir -p  /etc/nginx/certs /var/lib/nginx/cache
cache@host$ sudo chown -R nginx:nginx /var/lib/nginx/cache

Als nächstes erzeugen wir einen CSR mit dem HSM:

cache@host$ openssl req -new -subj '/CN=<hier der Common Name für das eigene Zertifikat>/' -sha256 -engine pkcs11 -keyform engine -key <hier die eigene Key-ID eintragen, z.B. 0:c168> -out /tmp/csr.pem

Dann signieren wir den CSR einfach selbst:

cache@host$ openssl x509 -engine pkcs11 -signkey <hier die eigene Key-ID eintragen, z.B. 0:c168> -keyform engine -in /tmp/csr.pem -out /tmp/cert.pem

Das selbstsignierte Zertifikat kopieren wir nun in das dafür angelegte Verzeichnis:

cache@host$ sudo cp cert.pem /etc/nginx/certs/cert.pem

Hätten wir kein selbstsigniertes Zertifikat, sondern ein von anderer Stelle signiertes, so bräuchten wir jetzt die Signaturkette. Diese muss in /etc/nginx/certs/serverchain.pem abgelegt werden. Da unser Zertifikat aber selbstsigniert ist, kopieren wir einfach unser Zertifikat stattdessen nach /etc/nginx/certs/serverchain.pem:

cache@host$ sudo cp cert.pem /etc/nginx/certs/serverchain.pem

Nun gilt es, die URL für den engine-key vom HSM zu ermitteln:

cache@host$ p11tool --provider /usr/lib64/pkcs11/yubihsm_pkcs11.so --list-privkeys --login
 Token 'YubiHSM' with URL 'pkcs11:model=YubiHSM;manufacturer=Yubico%20%28www.yubico.com%29;serial=07550837;token=YubiHSM' requires user PIN
 Enter PIN:
 Object 0:
     URL: pkcs11:model=YubiHSM;manufacturer=Yubico%20%28www.yubico.com%29;serial=07550837;token=YubiHSM;id=%c1%68;object=nginx-private-key;type=private
     Type: Private key
     Label: nginx-private-key
     Flags: CKA_PRIVATE; CKA_SENSITIVE;
     ID: c1:68

Jetzt kennen wir die URL "pkcs11:model=YubiHSM;manufacturer=Yubico%20%28www.yubico.com%29;serial=07550837;token=YubiHSM;id=%c1%68;object=nginx-private-key;type=private". Diese können wir nun nehmen und damit unsere Konfiguration unseres Caches anpassen:

cache@host$ vim /etc/nginx/conf.d/000-default.conf

Diese sollte wie folgt aussehen:

server {

    listen 443;
    server_name <Hier die Domain, die umgeleitet werden soll>;

    ssl_certificate             /etc/nginx/certs/cert.pem;
    #das hier ist die Engine-URL:
    ssl_certificate_key         "engine:pkcs11:pkcs11:model=YubiHSM;manufacturer=Yubico%20%28www.yubico.com%29;serial=07550837;token=YubiHSM;id=%c1%68;object=nginx-private-key;type=private";
    ssl_stapling                on;
    ssl_stapling_verify         on;
    ssl_trusted_certificate     /etc/nginx/certs/serverchain.pem;

    ssl on;
    ssl_session_cache           builtin:1000  shared:SSL:10m;
    ssl_protocols               SSLv3 TLSv1.1 TLSv1.2;
    ssl_ciphers                 ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
    ssl_prefer_server_ciphers   on;
    ssl_session_cache           shared:SSL:10m;
    ssl_verify_client           off; 

    access_log                  /var/log/nginx/cache_access.log;
    error_log                   /var/log/nginx/cache_error.log debug;

    location / {

        proxy_set_header        Host $host;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;

        proxy_ssl_server_name   on;
        proxy_ssl_name          <Hier die Domain, die umgeleitet werden soll>;
        proxy_pass              <Hier eine Domain, die zusätzlich auf das CDN zeigt>;
        proxy_read_timeout      90;       

        proxy_buffering         on;
        proxy_cache             STATIC;
        proxy_cache_valid       200  10d;

    }
}

Bei der Konfiguration ist wichtig, dass bei proxy-pass eine andere Domain angegeben wird, unter der die gleichen Inhalte verfügbar sind, wie unter der umgeleiteten Domain. Dies ist notwendig, damit der Cache die eigentlichen Inhalte herunterladen kann, ohne durch die Umleitung der Domain immer im Kreis zu laufen. In unserem Fall hatte unser Content-Delivery-Network ohnehin eine interne Domain <eigener teil>.cloudfront.net vergeben und wir hatten zusätzlich eine eigene Domain eingerichtet, die wir für die Umleitung verwenden. Somit konnten wir hier einfach die interne Domain des CDNs verwenden.

Als nächstes braucht nginx noch eine Konfiguration, um mit dem yubihsm-connector zusammenzuarbeiten:

cache@host$ sudo echo 'connector = http://127.0.0.1:12345' > /var/lib/nginx/yubihsm_pkcs11.conf
cache@host$ sudo chown -R nginx:nginx /var/lib/nginx/yubihsm_pkcs11.conf

Danach haben wir den nginx noch als Service konfiguriert:

cache@host$ sudo nano /usr/lib/systemd/system/nginx.service

Die Datei sollte so aussehen:

[Unit]
Description=The nginx HTTP and reverse proxy server
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=forking
PIDFile=/run/nginx.pid
# nginx will fail to start if /run/nginx.pid already exists but has the wrong
# SELinux context. This might happen when running `nginx -t` from the cmdline.
# https://bugzilla.redhat.com/show_bug.cgi?id=1268621

Environment="YUBIHSM_PKCS11_CONF=/var/lib/nginx/yubihsm_pkcs11.conf"
ExecStartPre=/usr/bin/rm -f /run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/bin/kill -s HUP $MAINPID
KillSignal=SIGQUIT
TimeoutStopSec=5
KillMode=process
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Anschließend laden wir die Konfiguration aller Dienste einmal neu:

cache@host$ sudo systemctl daemon-reload

Und sorgen dafür, dass nginx automatisch mit dem System startet:

cache@host$ sudo chkconfig nginx on
 Note: Forwarding request to 'systemctl enable nginx.service'.
 Created symlink from /etc/systemd/system/multi-user.target.wants/nginx.service to /usr/lib/systemd/system/nginx.service.

Nun starten wir nginx nochmal neu:

cache@host$ sudo service nginx restart
 Redirecting to /bin/systemctl restart nginx.service

Beim ersten Start haben wir dabei noch ein Problem mit SE-Linux. Um das zu ändern, schauen wir als erstes den Fehler im Log an:

cache@host$ sudo grep nginx /var/log/audit/audit.log | grep denied
 type=AVC msg=audit(1556899011.196:782): avc:  denied  { name_connect } for  pid=10095 comm="nginx" dest=12345 scontext=system_u:system_r:httpd_t:s0 tcontext=system_u:object_r:unreserved_port_t:s0 tclass=tcp_socket permissive=0
 type=AVC msg=audit(1556899552.351:818): avc:  denied  { read } for  pid=10592 comm="nginx" name="cache" dev="dm-0" ino=25749899 scontext=system_u:system_r:httpd_t:s0 tcontext=unconfined_u:object_r:default_t:s0 tclass=dir permissive=0

Es gibt ein Tool audit2allow, dass aus diesen Fehlern automatisch eine Regel erzeugt:

cache@host$ sudo yum install audit2allow

Damit erzeugen wir nun eine solche Regel:

cache@host$ sudo grep nginx /var/log/audit/audit.log | grep denied | audit2allow -m nginx > nginx

Das Ergebnis sollte in etwa so aussehen:

cache@host$ sudo cat nginx

  module nginx 1.0;
  
  require {
      type httpd_t;
      type default_t;
      type unreserved_port_t;
      class tcp_socket name_connect;
      class dir read;
  }
  
  #============= httpd_t ==============
  
  #!!!! WARNING: 'default_t' is a base type.
  allow httpd_t default_t:dir read;
  
  #!!!! This avc is allowed in the current policy
  allow httpd_t unreserved_port_t:tcp_socket name_connect;
Dieses Modul muss nun wie folgt kompiliert werden:
cache@host$ sudo grep nginx /var/log/audit/audit.log | audit2allow -M nginx
Und zum Schluss muss das Modul noch aktiviert werden (was etwas dauern kann):
cache@host$ sudo semodule -i nginx.pp
Noch einmal `nginx` neustarten, diesmal sollte alles klappen.
cache@host$ sudo service nginx restart

Um alles zu testen, brauchen wir eine Datei mit dem Zertifikat und seiner Signaturkette. Wenn wir weiter mit dem selbstsignierten Zertifikat arbeiten, reicht es, wenn wir nur das Zertifikat in diese Datei kopieren, ansonsten braucht es auch die Signaturkette. Dann können wir mit curl den gesamten Aufbau testen:

cache@host$ curl -vvv --cacert <Hier eine Datei mit dem Zertifikat und, wenn vorhanden der Zertifikatskette angeben>.pem <Hier die Domain, die umgeleitet werden soll><und hier ein Pfad auf eine Datei, die es zu cachen gilt> > testimage.bin

Der HTTP-Statuscode sollte 200 sein und die gecachte Datei sollte in testimage.bin liegen. Wenn testimage.bin löscht und den Befehl nochmals ausführt, sollte der Download diesmal aus dem Cache erfolgen.

Problembehandlung beim Anbinden des HSMs

Während der Testphase sind uns ein paar Probleme im Zusammenspiel mit dem YubiKey HSM und der VM begegnet. Die Probleme und die verwendeten Lösungen wollen wir hier noch einmal abschließend schildern.

Problem: Die Verbindung zum USB Device geht verloren

Manchmal kann es passieren, dass der yubihsm-connector die Verbindung zum HSM Device verliert. Eine Statusabfrage liefert dann die folgende Ausgabe:

cache@host$ curl localhost:12345/connector/status
 status=NO_DEVICE
 serial=*
 version=2.2.0
 pid=6677
 address=localhost
 port=12345

Im Debug Log lassen sich dann die folgenden Einträge lesen:

DEBU[0000] preflight complete                            cert= config= key= pid=6658 seccomp=false serial= syslog=false version=2.2.0
DEBU[0000] takeoff                                       TLS=false listen="localhost:12345" pid=6658
DEBU[0003] reopening usb context                         Correlation-ID=d3a27afe-67b7-69ad-4b43-852b71978459 why="status request"
DEBU[0003] usb context not yet open                      Correlation-ID=d3a27afe-67b7-69ad-4b43-852b71978459
WARN[0005] status failed to open usb device              X-Request-ID=d3a27afe-67b7-69ad-4b43-852b71978459 error="device not found"
INFO[0005] handled request                               Content-Length=0 Content-Type= Method=GET RemoteAddr="127.0.0.1:45628" StatusCode=200 URI=/connector/status User-Agent=curl/7.29.0 X-Real-IP=127.0.0.1 X-Request-ID=d3a27afe-67b7-69ad-4b43-852b71978459 latency=1.629251502s
DEBU[0007] reopening usb context                         Correlation-ID=acaa699f-bd22-25ae-4cfe-6d1030559047 why="status request"

Per lsusb Befehl ist das Device aber immer noch auffindbar.

Ein erster Versuch hier war, dass wir die USB Autosuspend Funktion des Linux Kernels deaktiviert haben.

Hierzu haben wir die GRUB-Config in /etc/default/grub entsprechend angepasst:

GRUB_TIMEOUT=5
GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)"
GRUB_DEFAULT=saved
GRUB_DISABLE_SUBMENU=true
GRUB_TERMINAL_OUTPUT="console"
GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet usbcore.autosuspend=-1"
GRUB_DISABLE_RECOVERY="true"

Nachdem diese mit sudo grub2-mkconfig -o /boot/grub2/grub.cfg aktiviert worden ist, haben wir die VM neu gestartet.

Danach war das Verhalten etwas besser, aber wirklich verlässlich gelöst konnte das Problem dadurch nicht werden.

Wir haben dann ein kleines watchdog script erstellt, welches periodisch den Status überprüft und bei einem status != OK den USB Driver einmal neu lädt. Das Script sieht so aus:

#!/bin/bash

STATUS=$(curl -s localhost:12345/connector/status | grep status=)

if [ $STATUS != "status=OK" ]; then
    for i in /sys/bus/pci/drivers/[uoex]hci_hcd/*:*; do
      [ -e "$i" ] || continue
      echo "${i##*/}" > "${i%/*}/unbind"
      echo "${i##*/}" > "${i%/*}/bind"
    done
fi

Wir lassen nun das Script alle 5 Minuten per Cron ausführen:

root@host$ crontab -e

*/5 * * * * /root/watchdog.sh >> /var/log/watchdog.log 2>&1

Wie oft das Script nun auf ein Problem gestoßen ist, lässt sich durch einen Blick in das Log /var/log/watchdog.log überprüfen. Durch diesen Ansatz konnte der Fehler verlässlich behoben werden.

Das Problem selbst haben wir auch bereits bei Yubico gemeldet. Es steht noch aus, ob es ein Bug an der verwendeten libusb Bibliothek ist oder tatsächlich am VMWare USB Stack liegt. Da unser Workaround bislang ohne Probleme funktioniert, arbeiten wir erst einmal damit.

Problem: Die OpenSSL Engine funktioniert nicht

Manchmal scheint die OpenSSL Engine nicht wie erwartet zu funktionieren. Dies führt dann sowohl beim Aufruf von openssl als auch vom pkcs11-tool zu den folgenden Fehlern:

cache@host$ openssl x509 -engine pkcs11 -signkey 0:c168 -keyform engine -in /tmp/csr.pem -out /tmp/cert2.pem
 Unable to load module /usr/lib64/pkcs11/yubihsm_pkcs11.so
 can't use that engine
 140625983563664:error:82065006:PKCS#11 module:pkcs11_check_token:Function failed:p11_load.c:92:
 140625983563664:error:260B806D:engine routines:ENGINE_TABLE_REGISTER:init failed:eng_table.c:175:
 Getting Private key
 no engine specified
 unable to load Private key

cache@host$ pkcs11-tool --module /usr/lib64/pkcs11/yubihsm_pkcs11.so --pin 0001password  -L -O
 error: PKCS11 function C_Initialize failed: rv = CKR_FUNCTION_FAILED (0x6)
 Aborting.

Das Problem ist, dass die Engine die benötigte yubihsm Konfiguration nicht finden kann, wenn der Aufruf nicht im gleichen Verzeichnis ausgeführt wird, in dem sich auch die Konfigurationsdatei befindet.

Die Lösung ist das Setzen einer entsprechenden Umgebungsvariable:

cache@host$ export YUBIHSM_PKCS11_CONF=/home/cache/yubihsm_pkcs11.conf

Fazit

Für den hier dokumentierten Use-Case haben die Autoren keine gute Dokumentation finden können.
Deshalb erfolgte hier eine Zusammenfassung.

Sind die Anfangsprobleme erst einmal umschifft, ist ein stabiler Betrieb aber möglich. Insbesondere das HSM von Yubikey bietet eine kostengünstige und variable Möglichkeit, die sich auch für dezentrale Anwendungsfälle verwenden lässt.

Foto von Adi Goldstein auf Unsplash.