NGINX als ReverseProxy

Um einen sicheren Zugriff auf SmartHomeNG und die smartVISU von außen (ohne VPN) zu ermöglichen, empfiehlt es sich einen ReverseProxy mit Basic Authentication oder Clientzertifikaten zu nutzen. Die folgende Dokumentation beschreibt eine Installation von NGINX als ReverseProxy unter Raspberry OS. Dieser ist bspw. auch für das Alexa Plugin oder die Nutzung von SmartHomeNG mit EgiGeoZone / Geofency notwendig.

Annahmen

Diese Anleitung hat folgende Annahmen:

  • NGINX wird auf einem frisch aufgesetzten Raspberry Pi mit Raspberry OS Debian installiert.

  • Der Standarduser heißt weiterhin pi

  • Eine DynDNS (o.ä.) Domain ist vorhanden und leitet auf die aktuelle Internet IP

Basiskonfiguration

  • Deutsches Keyboard festlegen: /etc/default/keyboard editieren und in der Zeile XKBLAYOUT="..." ein de eintragen. Danach sudo reboot now eingeben, um neu zu starten.

  • Aus Sicherheitsgründen das Standard-Passwort für pi ändern: Als User pi mit Standard-Passwort einloggen und mit passwd ein neues Passwort setzen.

NGINX installieren:

sudo apt-get update
sudo apt-get install nginx-full

GeoIP installieren (optional)

Über GeoIP kann mittels der anfragenden IP herausgefunden werden, aus welchem Land eine Anfrage kommt. Darüber lassen sich bspw. Requests aus Risikoländern blockieren.

cd /etc/nginx/
sudo wget https://git.io/GeoLite2-Country.mmdb

Let’s Encrypt Server-Zertifikate

(nach https://goneuland.de/debian-9-stretch-lets-encrypt-zertifikate-mit-certbot-erstellen/)

Über Let’s Encrypt lassen sich kostenlos SSL Zertifikate, bspw. für dyndns-Domains, ausstellen.

Certbot installieren:

sudo apt-get install certbot

Nun die Datei /etc/nginx/snippets/letsencrypt.conf bearbeiten:

sudo nano /etc/nginx/snippets/letsencrypt.conf

Dort folgenden Inhalt einfügen, damit certbot die Identität überprüfen kann.:

location ^~ /.well-known/acme-challenge/ {
 default_type "text/plain";
 root /var/www/letsencrypt;
}
sudo mkdir -p /var/www/letsencrypt/.well-known/acme-challenge
sudo nano /etc/nginx/sites-available/default

Dort unterhalb von listen [::]:80 default_server; die Zeile include /etc/nginx/snippets/letsencrypt.conf; einhängen:

server {
        listen 80 default_server;
        listen [::]:80 default_server;
        include /etc/nginx/snippets/letsencrypt.conf;
[...]
sudo systemctl restart nginx

Am Router müssen Port 80 und 443 auf den Rechner, auf dem NGINX installiert ist, weitergeleitet werden. WARNUNG: Eine Weiterleitung von Port 80 bedeutet, dass von Außen auf lokale Webseiten (evtl. auch die SmartVISU) zugegriffen werden kann. Daher sollte das nur gemacht werden, wenn a) auf dem Rechner sonst nichts läuft, er also explizit für den Reverse Proxy zur Verfügung steht. b) die Portweiterleitung nur temporär für die paar Sekunden aktiviert wird, in denen certbot die Zertifikate erneuert.

sudo certbot certonly --rsa-key-size 4096 --webroot -w /var/www/letsencrypt -d <mydomain>.<myds>.<me>

Nachdem man seine E-Mail eingegeben hat, sollte die Generierung erfolgreich durchlaufen und mit

Generating key (4096 bits): /etc/letsencrypt/keys/0000_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0000_csr-certbot.pem

enden.

NGINX Konfiguration

/etc/nginx/nginx.conf bearbeiten und direkt im http Block die GeoIP Einstellungen hinzufügen, die Länder können natürlich angepasst werden. Unter der Konfiguration der virtual hosts noch einen Block als Schutz gegen Denial of Service Angriffe ergänzen:

http {
    ##
    # GeoIP Settings
    # Nur Länder aus erlaubten IP Bereichen
    geoip2 /etc/nginx/GeoLite2-Country.mmdb {
      auto_reload 5m;
      $geoip2_metadata_country_build metadata build_epoch;
      $geoip2_data_country_code default=DE country iso_code;
      $geoip2_data_country_name country names en;
    }

    fastcgi_param COUNTRY_CODE $geoip2_data_country_code;
    fastcgi_param COUNTRY_NAME $geoip2_data_country_name;
    map $geoip2_data_country_code $allowed_country {
        default yes;
        BY no;
        BR no;
        KP no;
        KR no;
        RS no;
        RO no;
        RU no;
        CN no;
        CD no;
        NE no;
        GH no;
        IQ no;
        IR no;
        SY no;
        UA no;
        HK no;
        JP no;
        SC no;
    }
    ##
    # websocket for shng
    ##
    upstream websocket {
      server <SmartHomeNG LAN IP>:2424;
    }

    ##
    # Basic Settings
    ##
    map $http_upgrade $connection_upgrade {
      default Upgrade;
      '' close;
   }

   sendfile on;
   tcp_nopush on;
   tcp_nodelay on;
   keepalive_timeout 65;
   types_hash_max_size 2048;
   server_tokens off;
[...]
    ##
    # Virtual Host Configs
    ##

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-enabled/*;

    ##
    # Harden nginx against DDOS
    ##

    client_header_timeout 10;
    client_body_timeout   10;
}

NGINX mit sudo systemctl restart nginx neu starten.

/etc/nginx/conf.d/<mydomain>.<myds>.<me>.conf erstellen

server {

    ## Blocken, wenn Zugriff aus einem nicht erlaubten Land erfolgt ##
    if ($allowed_country = no) {
        return 403;
    }

    # https://www.cyberciti.biz/tips/linux-unix-bsd-nginx-webserver-security.html
    ## Block download agents ##
    if ($http_user_agent ~* LWP::Simple|BBBike|wget) {
        return 403;
    }

    ## Block some robots ##
    if ($http_user_agent ~* msnbot|scrapbot) {
        return 403;
    }

    ## Deny certain Referers ##
    if ( $http_referer ~* (babes|forsale|girl|jewelry|love|nudit|organic|poker|porn|sex|teen) )
    {
        return 403;
    }

    listen 443 ssl default_server;
    server_name <mydomain>.<myds>.<me>;

    ##
    # SSL
    ##

    ## Activate SSL, setze SERVER Zertifikat Informationen ##
    # Generiert via Let's Encrypt!
    ssl on;
    ssl_certificate /etc/letsencrypt/live/<mydomain>.<myds>.<me>/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/<mydomain>.<myds>.<me>/privkey.pem;
    ssl_session_cache builtin:1000 shared:SSL:10m;
    ssl_prefer_server_ciphers on;
    # unsichere SSL Ciphers deaktivieren!
    ssl_ciphers    HIGH:!aNULL:!eNULL:!LOW:!3DES:!MD5:!RC4;

    ##
    # HSTS
    ##

    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    ##
    # global
    ##

    root /var/www/<mydomain>.<myds>.<me>;
    index index.php index.htm index.html;

    # Weiterleitung zu SmartHomeNG (Websocket Schnittstelle) mit Basic Auth
    location / {
            if ($http_upgrade != websocket) {
                   return 404;
            }
            try_files /wartung.html @loc_websocket;
    }

    # Zugriff auf die SmartVISU mit Basic Auth
    location /smartVISU {
               auth_basic "Restricted Area: smartVISU";
               auth_basic_user_file /etc/nginx/.smartvisu;
               try_files /wartung.html @loc_smartvisu;
    }

    location /smartvisu {
               auth_basic "Restricted Area: smartVISU";
               auth_basic_user_file /etc/nginx/.smartvisu;
               try_files /wartung.html @loc_smartvisu;
    }

    # Alexa Plugin Weiterleitung
    location /alexa {
        auth_basic "Restricted Area: Alexa";
        auth_basic_user_file /etc/nginx/.alexa;
        try_files /wartung.html @loc_alexa;
    }

    # Network Plugin Weiterleitung
    location /shng {
        auth_basic "Restricted Area: SmartHomeNG";
        auth_basic_user_file /etc/nginx/.shng;
        try_files /wartung.html @loc_shng;
    }

    location @loc_websocket {
            proxy_pass http://websocket;
            include /etc/nginx/headers.conf;
            include /etc/nginx/proxy_params;
            #access_by_lua_file /etc/nginx/scripts/hass_access.lua;
    }

    location @loc_smartvisu {
            proxy_pass http://<SmartHomeNG LAN IP>/$request_uri;
            include /etc/nginx/headers.conf;
            include /etc/nginx/proxy_params;
            #access_by_lua_file /etc/nginx/scripts/hass_access.lua;
    }

    location @loc_alexa {
        proxy_pass http://<SmartHomeNG LAN IP>:<Alexa Plugin Port>/;
        include /etc/nginx/headers.conf;
        include /etc/nginx/proxy_params;
        #access_by_lua_file /etc/nginx/scripts/hass_access.lua;
    }

    location @loc_shng {
        proxy_pass http://<SmartHomeNG LAN IP>:<Network Plugin Port>/;
        include /etc/nginx/headers.conf;
        include /etc/nginx/proxy_params;
        #access_by_lua_file /etc/nginx/scripts/hass_access.lua;
    }
}

Die Datei /etc/nginx/headers.conf muss nun entsprechend angelegt werden:

add_header Strict-Transport-Security "max-age=31536000; includeSubdomains" always;
add_header X-Cache $upstream_cache_status;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Xss-Protection "1; mode=block" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-Proxy-Cache $upstream_cache_status;

Außerdem sollten zumindest folgende Zeilen in die Datei /etc/nginx/proxy_params eingetragen werden:

proxy_http_version      1.1;
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        Upgrade $http_upgrade;
proxy_set_header        Connection $connection_upgrade;
proxy_set_header        X-Forwarded-Proto $scheme;
proxy_set_header        X-SSL-CERT $ssl_client_escaped_cert;

Im Anschluss muss nginx neu gestartet werden:

sudo systemctl restart nginx

Passwort-Files für unterschiedliche User für smartVISU, Alexa, Network Plugin erstellen

Für die Erstellung eines Passworts können verschiedene Tools genutzt werden, am einfachsten ist hier htpasswd zu installieren, das im Paket apache2-utils inkludiert ist. Keine Sorge - der Apache Webserver wird dadurch nicht installiert, nur einige kleine Utilities!

sudo apt-get install apache2-utils

sudo htpasswd -c /etc/nginx/.smartvisu <username>
sudo htpasswd -c /etc/nginx/.alexa <username>
sudo htpasswd -c /etc/nginx/.shng <username>

Dann ein Passwort vergeben.

Der Zugriff auf https://../smartVISU sollte nun klappen.

Nacharbeiten: Port 80 in NGINX deaktivieren

Da NGINX im LAN aktuell noch auf Port 80 konfiguriert ist, sollte man in der /etc/nginx/sites-available/default noch ein return 403 ergänzen:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        return 403;

        include /etc/nginx/snippets/letsencrypt.conf;

Alternativ kann auch eine Weiterleitung von der HTTP (Port 80) auf die HTTPS (Port 443) URL gesetzt werden. Das ist insbesondere beim Erneuern von Zertifikaten von Vorteil, da hier eine Anfrage gegen Port 80 gemacht wird:

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        server_name _;
        return 301 https://$host$request_uri;

        include /etc/nginx/snippets/letsencrypt.conf;

Danach den NGINX neu starten.

Client Zertifikate erstellen (optional)

Clientzertifikate können mittels openssl manuell erstellt werden. Etwas komfortabler läuft das über easy-rsa von https://github.com/OpenVPN/easy-rsa/releases Im Folgenden wird der Weg ohne dieses Tool beschrieben.

openssl.cnf editieren

sudo nano /etc/ssl/openssl.cnf

Folgende Zeilen anpassen:

dir = /etc/ssl/ca                       # Directory where everything is kept
[...]
ew_certs_dir = $dir/certs               # default place for new certs.
[...]
certificate = $dir/ca.crt               # The CA certificate
[...]
crl = $dir/crl.pem                      # The current CRL
private_key = $dir/private/ca.key       # The private key
[...]
default_crl_days= 365
[...]
default_md = sha1 # use public key default MD

Drei neue Verzeichnisse und drei Dateien anlegen:

sudo mkdir -p /etc/ssl/ca/certs/users
sudo mkdir -p /etc/ssl/ca/crl
sudo mkdir -p /etc/ssl/ca/private

sudo touch /etc/ssl/ca/index.txt
sudo touch /etc/ssl/ca/index.txt.attr

In der Datei crlnumber den Wert “01” eintragen und speichern.

sudo nano /etc/ssl/ca/crlnumber

Zertifikat für Certification Authority (CA) erstellen, Passwort für die CA wählen und eigene Daten eingeben:

sudo openssl genrsa -des3 -out /etc/ssl/ca/private/ca.key 4096
sudo openssl req -new -x509 -days 1095 -key /etc/ssl/ca/private/ca.key -out /etc/ssl/ca/certs/ca.crt
sudo openssl ca -name CA_default -gencrl -keyfile /etc/ssl/ca/private/ca.key -cert /etc/ssl/ca/certs/ca.crt -out /etc/ssl/ca/private/ca.crl -crldays 1095

Client Zertifikat für einen User erstellen und ein Passwort für das Client Zertifikat vergeben:

sudo openssl genrsa -des3 -out /etc/ssl/ca/certs/users/<USERNAME>.key 1024
sudo openssl req -new -key /etc/ssl/ca/certs/users/<USERNAME>.key -out /etc/ssl/ca/certs/users/<USERNAME>.csr

Bei folgendem Schritt das Passwort für die CA eingeben:

sudo openssl x509 -req -days 1095 -in /etc/ssl/ca/certs/users/<USERNAME>.csr -CA /etc/ssl/ca/certs/ca.crt -CAkey /etc/ssl/ca/private/ca.key -CAserial /etc/ssl/ca/serial -CAcreateserial -out /etc/ssl/ca/certs/users/<USERNAME>.crt

Bei folgendem Schritt mit dem Passwort für das Client Zertifikat bestätigen und ein Export Passwort wählen:

sudo openssl pkcs12 -export -clcerts -in /etc/ssl/ca/certs/users/<USERNAME>.crt -inkey /etc/ssl/ca/certs/users/<USERNAME>.key -out /etc/ssl/ca/certs/users/<USERNAME>.p12

<USERNAME>.p12 File herunterladen:

sudo cp /etc/ssl/ca/certs/users/<USERNAME>.p12 /home/pi
cd /home/pi/
sudo chown pi <USERNAME>.p12

Bspw. nun via SFTP ziehen und Datei aufs Android Handy übertragen und ausführen oder im Browser unter “Zertifikate” importieren. Dabei muss es mit Export Passwort bestätigt werden.

Client Zertifikate in NGINX nutzen (optional)

Anleitung nach https://arcweb.co/securing-websites-nginx-and-client-side-certificate-authentication-linux/

Der vorige Schritt macht nur Sinn, wenn NGINX auch so konfiguriert wird, dass ein Zugriff nur mit Zertifikat möglich ist. Dieses Vorgehen ist sicherheitstechnisch klar zu präferieren.

/etc/nginx/conf.d/<mydomain>.<myds>.<me>.conf bearbeiten und die Zeilen im SSL Block ergänzen (“ab Client Zertifikat spezifisch”)

##
# SSL
##

## Activate SSL, setze SERVER Zertifikat Informationen ##
# Generiert via Let's Encrypt!
ssl on;
ssl_certificate /etc/letsencrypt/live/<mydomain>.<myds>.<me>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<mydomain>.<myds>.<me>/privkey.pem;
ssl_session_cache builtin:1000 shared:SSL:10m;
ssl_prefer_server_ciphers on;
# unsichere SSL Ciphers deaktivieren!
ssl_ciphers    HIGH:!aNULL:!eNULL:!LOW:!3DES:!MD5:!RC4;

# Client Zertifikat spezifisch
ssl_client_certificate /etc/ssl/ca/certs/ca.crt;
ssl_crl /etc/ssl/ca/private/ca.crl;
ssl_verify_client optional;
ssl_session_timeout 5m;

Die smartVISU relevanten Teile könnten jetzt folgendermaßen über Clientzertifikate geschützt werden:

# Weiterleitung zu SmartHomeNG (Websocket Schnittstelle) mit Clientzertifikat
location / {
    # Clientzertifikat gültig?
    if ($ssl_client_verify != SUCCESS) {
            return 403;
    }

    # Zugreifendes Land erlaubt?
    if ($allowed_country = no) {
            return 403;
    }

    # Nur Websocket Verbindungen gegen "/" durchlassen!
    if ($http_upgrade = websocket) {
            proxy_pass http://<SmartHomeNG LAN IP>:<Websocket Port>;
    }
    if ($http_upgrade != websocket) {
            return 403;
    }
}

# Zugriff auf die SmartVISU mit Clientzertifikat
location /smartVISU {
    # Clientzertifikat gültig?
    if ($ssl_client_verify != SUCCESS) {
            return 403;
    }

    # Zugreifendes Land erlaubt?
    if ($allowed_country = no)  {
            return 403;
    }

    # Hier die weiteren Angaben einfügen wie im vorigen Abschnitt definiert.
}

Wer es doppelt sicher haben möchte, kann die Basic Auth in den jew. Blöcken auch beibehalten.

Testbar ist das Ganze, wenn es im Browser ohne Zertifikat einen 403er Fehler gibt und mit Zertifikat die smartVISU aufbaut.

Erweiterung: LUA Script für Apple Geräte

Apple Geräte wie MacBook oder iPhone kommen mit der oben skizzierten Konfiguration leider nicht klar, sobald Websockets (die für die SmartVISU zwingend nötig sind) im Spiel sind. Daher ist hier auf ein spezielles LUA Script zurückzugreifen.

sudo apt-get install lua5.1 luarocks liblua5.1-dev libnginx-mod-http-lua
luarocks install openssl

Das LUA Script selbst wird in einer neuen Datei namens /etc/nginx/scripts/hass_access.lua erstellt. In der ersten Zeile ist dabei das Passwort anzugeben, mit dem die Zertifikate verschlüsselt wurden.

local HMAC_SECRET = "<SECRETKEY from OPENSSL>"
digest = require('openssl').digest
hmac = require('openssl').hmac

function ComputeHmac(msg, expires)
  return hmac.hmac("sha256", string.format("%s%d", msg, expires), HMAC_SECRET)
end

verify_status = ngx.var.ssl_client_verify

if verify_status == "SUCCESS" then
  client = digest.digest("sha256", ngx.var.ssl_client_cert)
  expires = ngx.time() + 3600

  ngx.header["Set-Cookie"] = {
    string.format("AccessToken=%s; path=/", ComputeHmac(client, expires)),
    string.format("ClientId=%s; path=/", client),
    string.format("AccessExpires=%d; path=/", expires)
  }
  return
elseif verify_status == "NONE" then
  client = ngx.var.cookie_ClientId
  client_hmac = ngx.var.cookie_AccessToken
  access_expires = ngx.var.cookie_AccessExpires

  if client ~= nil and client_hmac ~= nil and access_expires ~= nil then
    hmac = ComputeHmac(client, access_expires)

    if hmac ~= "" and hmac == client_hmac and tonumber(access_expires) > ngx.time() then
      return
    end
  end
end

ngx.exit(ngx.HTTP_FORBIDDEN)

Schließlich muss noch folgende Zeile in der Datei /etc/nginx/conf.d/<mydomain>.<myds>.<me>.conf bei jeder Location eingetragen werden:

access_by_lua_file /etc/nginx/scripts/hass_access.lua;

Erweiterung: Stärkere Diffie-Hellman-Parameter

Damit die Sicherheit “perfekt” wird, sollten stärkere Diffie-Hellman-Schlüssel verwendet werden. Dazu muss ein neues .pem File generiert werden. Es empfiehlt sich, die Erzeugung dieses Files nicht direkt auf den Raspi sondern auf einem PC mit stärker er CPU durchzuführen. Ein Test auf einem Raspi3 dauerte 24 Stunden (!). Ein Intel 4790k brauchte hingegen nur 30 Minuten.

Folgendes ist zu tun:

cd /etc/ssl/certs
sudo openssl dhparam -out dhparam.pem 4096

Alternativ kann das File auch einfach unter /etc/ssl/certs reinkopiert werden.

Danach ist in der SSL Konfiguration von NGINX folgende Zeile zu ergänzen und NGINX neu zu starten:

# Konfiguration editieren
sudo nano /etc/nginx/conf.d/\<mydomain\>.\<myds\>.\<me\>.conf
## Dort folgende Zeile im Block SSL einfügen:

##
# SSL
##
[...]
ssl_dhparam /etc/ssl/certs/dhparam.pem;
[...]
## NGINX neu starten
sudo systemctl restart nginx

Die Sicherheit der eigenen https-Domain kann nun unter https://www.ssllabs.com/ssltest/ getestet werden. Mit den oben genannten Maßnahmen sollte ein A+ erreicht werden.

Der versiertere Nutzer kann sich unter https://mozilla.github.io/server-side-tls/ssl-config-generator/ auch gleich eine eigene Konfiguration generieren lassen.

Wer noch mehr Sicherheit implementieren möchte, installiert sich https://github.com/fail2ban/fail2ban. Damit kann konfiguriert werden, dass IP Adressen automatisch durch die Firewall blockiert werden, sobald sie sich unbefugt Zugang zum Server verschaffen oder z.B. nicht existente Dateien/Ordner aufrufen wollen.

Wartung: Zertifikat nach 3 Monaten erneuern

Nach 3 Monaten muss das Let’s Encrypt Serverzertifikat erneuert werden. Dies sollte prinzipiell automatisiert geschehen, da certbot einen entsprechenden cron Job erstellt.

Damit das Erneuerungs-Skript funktioniert, muss Port 80 im NGINX freigegeben, oder (wie oben dokumentiert) auf HTTPS umgeleitet sein. Außerdem kann/soll das ini File wie folgt adaptiert werden, um nginx nach der Aktualisierung automatisch neu zu starten oder vorher/nachher ein Skript (z.B. zum Weiterleiten von Ports auf der Fritzbox oder An/Ausschalten einer Firewall) auszuführen:

# Manage Firewall
#pre-hook = ufw allow http
#post-hook = ufw deny http

# Restart Postfix & Dovecot
renew-hook = systemctl restart nginx.service

Eine manuelle Erneuerung geht wie folgendermaßen:

sudo certbot certonly --agree-tos --rsa-key-size 4096 --webroot -w /var/www/letsencrypt -d <mydomain>.<myds>.<me>

Danach NGINX neu starten.

sudo systemctl restart nginx

Der Test über https://www.ssllabs.com/ssltest/ gibt nun Aufschluss über die Laufzeit des verlängerten Zertifikats.