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"><your-passwordl></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;
# tcp_nopush on;
keepalive_timeout 65;
# 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;
cache@host$ sudo grep nginx /var/log/audit/audit.log | audit2allow -M nginx
cache@host$ sudo semodule -i nginx.pp
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.