Skip to content
On this page

14. Cloud and Web Applications

Ziel des Labs

In diesem Lab werden wir eine Embedded Applikation mit Cloud Infrastruktur erstellen. Hierbei wird das Raspberry Pi als Gateway für MQTT verwendet und um eine Webapplikation zur Verfügung zu stellen. Ein ESP32 wird als Messtation für Sensorwerte verwendet (via MQTT/WiFi ans Raspberry Pi) und kann Daten aus der Cloud für den user (OLED) anzeigen. Via Wireguard VPN sind Server, Gastsystem und Raspberry Pi im gleichen virtuellen LAN. Sensorwerte werden schlussendlich an InfluxDB gesendet, welche in der Cloud durch den Server gehostet wird.

Für den Server in der Cloud wird Linode verwendet. Der Server verfügt über eine öffentliche IP und ist übers Internet zugänglich.

Buildroot Packages neu kompilieren

In diesem Lab werden wir häufig neue Packages Buildroot hinzufügen. Hierfür werden wir nur die neu kompilierten Files auf die SD-Karte kopieren.

Um diesen Vorgan zu vereinfachen, finden Sie an dieser Stelle Target als MQTT Broker eine Hilfestellung.

Cloud Setup

Für dieses Lab werden wir ein Cloud-Setup für Raspberry Pi, ESP32 und Hostmaschine erstellen. Hierzu werden wir Wireguard als VPN verwenden um unseren Traffic zu verschlüsseln und NAT (Network Address Translation) zu durchdringen. Dank Wireguard wird es somit auch möglich sein via SSH auf das Target zu verbinden ohne zusätzliche Portforwards auf NAT-Seite des Targets.

Auf Serverseite werden wir Ubuntu 22.04 in der Cloud verwenden. Hierzu können wir uns mit dem Server in der Cloud für das Setup via SSH verbinden.

  1. Als erstes konfigurieren wir den Server in der Cloud.

  2. Wir erstellen ein VPN via Wireguard für

    • Raspberry Pi Targets
    • Ubuntu Gastsysteme (Hosts)
    • Server in der Cloud

    Der Server wird hierzu auf eingehende Wireguardverbindungen hören.

  3. Auf dem Server werden wir InfluxDB via Docker-Compose installieren um später vom Raspberry Pi Werte in die Cloud zu senden.

Netzwerksetups mit NAT

Häufig wird in lokalen Netzen NAT eingesetzt. Eine öffentliche IP Adresse wird so für viele Hosts im LAN "geteilt". Dies hat sicherheitstechnisch Vorteile, da so bereits eine Firewall besteht. Es ist allerdings unmöglich aus dem Internet heraus so direkt mit einem Host eine Verbindung herzustellen.

Gerade eine Verbindung zu einem Target über SSH wird so meistens verunmöglicht. Neben der Möglichkeit explizit für gewisse Hosts selektiv Portforwards einzurichten, wird häufig VPN (mit Keepalive) verwendet um diese Barriere zu umgehen und so NAT-Passtrhough zu ermöglichen. Wir werden Wireguard (im Kernel implementiertes, schlankes VPN) verwenden und erhalten hiermit auch gleich Verschlüsselten Traffic.

Wireguard basiert auf UDP und hat somit keinen grossen Overhead (TCP in TCP wäre z.B. sehr unpraktisch).

In Buildroot werden wir hierzu zusätzlich Wireguard Tools konfigurieren. Gast- und Serverseitig verwenden wir Ubuntu 22.04, welches wireguard-tools via apt Package-Management zur Verfügung stellt.

Server in der Cloud

Der Server in der Cloud dient dank der öffentlichen IP (wir verwenden keinen DNS) als Server für Wireguard. Zusätzlich verwenden wir den Server in der Cloud um via Docker eine InfluxDB Instanz zu hosten. Es wäre natürlich auch möglich InfluxDB auf dem Target zu hosten. Möchte man allerdings die Daten auch ohne VPN zur Verfügung haben macht es mehr Sinn InfluxDB direkt in der Cloud zu hosten. Zusätzlich werden mit dieser Lösung auch Resourcen auf dem Target entlastet.

User und SSH einrichten

Die Instanz des Servers in der Cloud verfügt nur über einen root User. Als erstes wollen wir einen neuen User ubuntu erstellen, welcher via sudo Commands als Root ausführen kann. Auch hinterlegen wir von unserem Gastsystem den SSH Public Key, um ohne Passwort via SSH einloggen zu können.

Verbinden Sie Sie mit dem Server via SSH.

bash
ssh root@<ip-des-servers-der-gruppe>

Erstellen Sie zuerst einen neuen user ubuntu.

bash
adduser ubuntu

Fügen Sie den user der Gruppe sudo hinzu:

bash
adduser ubuntu sudo

Wechseln Sie zu ubuntu:

bash
su ubuntu

System updaten

Führen Sie ein upgrade des Systems durch:

bash
sudo apt update
sudo apt upgrade

Bei Dialogen können Sie jeweils die Defaults wählen.

SSH Public Key kopieren

Kopieren Sie nun den Public Key Ihrer Gastsysteme in das File ~/.ssh/authorized_keys.

bash
# .ssh erstellen

mkdir ~/.ssh

# Keys hier einfügen
nano ~/.ssh/authorized_keys

Eigene Public Keys

Die Public Keys finden Sie im Gastsystem unter ~/.ssh/id_rsa.pub. Falls Sie keine Keys erstellt haben, holen Sie dies (auf dem Gastsystem) nach via

bash
ssh-keygen -t rsa

Loggen Sie sich aus SSH aus und verbinden Sie sich wieder. Verifizieren Sie, dass Sie mit allen Gastsystemen ohne Passwort einloggen können.

Logins als Root / mit Passwort disablen

Aus Sicherheitsgründen sollte der Login via root immer verhindert werden. Studieren Sie das File /etc/ssh/sshd_config und setzen die entsprechenden Optionen auf no.

PermitRootLogin no
PasswordAuthentication no

Starten Sie den SSH Server neu:

bash
sudo systemctl restart sshd

Testen Sie von einem anderen System ob Sie immer noch via Root einloggen können und ob Passwörter noch erlaubt sind.

Wireguard aufsetzen

Für das VPN (Wireguard) werden wir das Netz 10.10.10.0/24 verwenden. Hiefür stellt der Server auf Port 8080 (frei gewählt) den Wireguard Dienst zur Verfügung.

Installieren Sie das Packet wireguard-tools auf dem Server- und Gastsystem.

Weitere Infos zu Wireguard

Infos zu Wireguard finden Sie unter diesem Link https://www.wireguard.com/quickstart/

Für Host- und Gastsystem werden wir wg-quick verwenden: ein Tool, welches uns erlaubt eine Konfiguration (inkl. IP) vorzunehmen, die bei Systemboot auch direkt mit systemd gestartet wird.

Wireguard funktioniert ähnlich wie SSH über ein Public-Privatekeyverfahren. Hierzu wird ein Private Key erstellt und Public Key davon abgeleitet.

In einem einzigen Command können Public und Private Key erstellt werden.

Die folgenden Commands können Sie auf dem Gastsystem und auch auf dem Server ausführen.

bash
cd ~
mkdir wireguard
cd wireguard

# Keys erstellen
wg genkey | tee privatekey | wg pubkey > publickey

In dem Ordner ~/wireguard sind nun Public und Private Key generiert worden.

Sicherheit

In einem Production Setup sollten Sie immer sicherstellen, dass der Private Key nie in die Öffentlichkeit gelangt. Zusätlich können Sie auch noch einen Pre-Shared Key für die Verbindung generieren (vgl. https://www.wireguard.com/protocol/).

Serverseite

Als nächstes erstellen wir das Serverseitige Setup. Hier ist es wichtig, dass wir auf Port 8080 hören.

Öffnen Sie das File /etc/wireguard/wg0.conf (z.B. mit sudo nano) und setzen Sie den Inhalt wiefolgt:

[Interface]
Address = 10.10.10.1/24
ListenPort = <Listen Port>
PrivateKey = <Private Key der generiert wurde>
MTU = 1420
PostUp = iptables -A FORWARD -i %i -o %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -o %i -j ACCEPT

[Peer]
PublicKey = <Public Key des Gastsystems 1>
AllowedIPs = 10.200.0.2/32
PersistentKeepalive = 25

[Peer]
PublicKey = <Public Key des Gastsystems 2>
AllowedIPs = 10.200.0.3/32
PersistentKeepalive = 25

Das File wird für die Konfiguration des Interfaces wg0 verwendet. Mit iptables erlauben wir das Kommunizieren zwischen den Hosts unter sich im gleichen VPN.

PersistentKeepalive sorgt dafür, dass die Ports beim NAT offen gehalten werden. Alles 25 Sekunden ist ein Setting, welches breit von Routern unterstützt wird.

Starten Sie nun das Wireguard Interface mit:

bash
sudo wg-quick up wg0

Schauen Sie sich den status an mit

bash
sudo wg

Falls Sie neue Hosts hinzufügen möchten, können Sie diese weiterhin im File /etc/wireguard/wg0.conf anhängen. Achten Sie jeweils gut auf Public / Private Keys. Hier lohnt es sich mehrmals zu prüfen, ob diese richtig konfiguriert sind, da sonst Fehlermeldungen bei der Kommunikation via IP auftreten werden.

Starten Sie jeweils Wireguard nach Änderungen mit wg-quick down wg0 und wg-quick up wg0 wieder neu.

Gastsystem

Nachdem Sie den Server konfiguriert haben, wollen wir die Gastsysteme ins gleiche Wireguardnetz verbinden. Erstellen Sie auch ein File /etc/wireguard/wg0.conf auf Ihren Gastsystemen.

[Interface]
PrivateKey = <Private Key des Gastsystems>
Address = <IP des Gastsystems wie vom Server erwartet>
# Beispiel 10.10.10.2/24


[Peer]
PublicKey = <Public Key vom Server>
Endpoint = <Public IP des Servers>:<Port>
AllowedIPs = 10.10.10.0/24

AllowedIPs = 10.10.10.0/24 führt dazu, dass gleich ein Eintrag in die Routing Table beim starten des interfaces hinzugefügt wird.

Starten Sie auf dem Gastsystem nun das Wiregaurd Interface.

sudo wg-quick up wg0

Hat alles geklappt, dann können Sie nun den Server pingen via ping 10.10.10.1.

Wireguard Interface mit systemd automatisch hochfahren

Nach einem Reboot wird das Wiregaurdinterface nicht hochgefahren. Es existiert allerdings ein einfacher "hook" für systemd um wg0 beim booten hochzufahren:

bash
 sudo systemctl enable wg-quick@wg0

Führen Sie diesen Command auf allen Gastsystemen sowie auf dem Server aus.

Wireguard auf dem Target

Weiter möchten wir Wireguard auf dem Target verwenden. Bevor Sie starten, stellen Sie sicher, dass folgende Voraussetzungen erfüllt sind:

  • WiFi funktioniert auf dem Target
  • Kernel unterstützt Wireguard (in menuconfig verifizieren)
  • Buildroot hat wireguard-tools kompiliert

wireguard-tools nachkompilieren

Sie können in Buildroot das Wiregaurd Package selektieren und das RFS neu kompilieren. Für das Deployment reicht es möglicherweise, wenn Sie wg in das /usr/bin Verzeichnis kopieren.

Falls Sie wg nicht ausführen können, sichern Sie Ihre targetseitigen config Files auf das Gastsystem und redeployen Sie buildroot vollständig auf die SD-Karte.

Leider existiert das praktische Programm wg-quick nicht im Buildroot repository. Allerdings können wir mit dem tool wg den Link Layer erstellen und später mit ifconfig resp. ip addr auch eine IP Adresskonfiguration vornehmen.

Public und Private Key auf dem Target erstellen

Generieren Sie wie bereits auf dem Gastsystem und Server den public und private key für wiregaurd:

bash
mkdir wireguard
cd wireguard
wg genkey | tee privatekey | wg pubkey > publickey

Serverseitig IP und Public Key eintragen

Auf der Serverseite können Sie nun den Public Key eintragen und eine IP Adresse für das Target auswählen (verwenden Sie diese später für das wg0 interface des Targets).

Vergessen Sie beim Server nicht das Wireguard Interface neuzustarten.

wg0.conf Config File

Erstellen Sie auf dem Target ein File wg0.conf mit folgendem Inhalt.

[Interface]
PrivateKey = <Eigener Private Key>

[Peer]
PublicKey = <Public Key des Servers>
Endpoint = <Public IP vom Server>:<Port>
AllowedIPs = 10.10.10.0/24

Erstellen Sie nun ein neues wireguard interface mit ip link:

bash
ip link add dev wg0 type wireguard

Laden Sie die wg0.conf mit:

bash
wg setconf wg0 wg0.conf

Das Interface fahren Sie nun hoch mit

ip link set up dev wg0

IP konfigurieren

Fügen Sie nun dem Wireguardinterface noch die IP hinzu:

# IP muss serverseitig passen
ip addr add 10.10.10.4/24 dev wg0

Falls alles geklappt hat sollten Sie nun 10.10.10.1 pingen können.

Sogar sollten Sie jetzt in der Lage sein von Ihrem Gastsystem via SSH auf 10.10.10.4 connecten zu können.

Das funktioniert nun sogar von Ausserhalb des WiFi Netzes! Starten Sie einen Accesspoint mit Ihrem Smartphone, connecten Sie Ihr Hostsystem und verbinden Sie sich via SSH über das Gastsystem mit dem Target um die Verbindung zu validieren.

mit /etc/network/interfaces automatisch wg0 starten

Da uns wg-quick auf dem Target fehlt, starten wir wg0 automatisch mit /etc/network/interfaces.

Editieren Sie das File und fügen Sie die wg0 section hinzu. Achten Sie auf absoulute Pfade und verifizieren Sie, dass die File tatsächlich existieren.

auto wg0
iface wg0 inet static
    requires wlan0
    pre-up ip link add $IFACE type wireguard
    pre-up wg setconf $IFACE /root/$IFACE.conf
    pre-up ip link set up dev $IFACE
    address 10.10.10.4
    netmask 255.255.255.0
    post-down ip link del $IFACE

Nach einem Reboot sollte nun wg0 von selber starten.

Serverseitiges Setup für InfluxDB

InfluxDB https://docs.influxdata.com/influxdb/v2.2/get-started/ ist eine Webapplikation und Datenbank optimier für Zeitserien. Wir werden InfluxDB verwenden um Messdaten zu speichern und visualisieren.

Um die Applikation möglichst modular zu halten und unabhängig von unserer Cloud Instanz zu sein, werden wir Docker und Docker Compose verwenden um InfluxDB zu verwalten.

Docker und Docker Compose Installieren

Auf dem Server installieren Sie Docker und Docker Compose

bash
sudo apt install docker docker-compose

Adden Sie Ihren user nun der Gruppe docker.

bash
sudo adduser ubuntu docker

Rebooten Sie nun den Server.

Erstellen Sie nun ein Directory und Volume für influxdb:

bash
cd ~
mkdir influxdb

# das volume wird im docker container gemountet
mkdir influxdb-volume

Docker Compose File

Erstellen Sie das Docker-Compose File docker-compose.yml.

version: "3.2"
services:
  influxdb:
    image: influxdb
    restart: unless-stopped
    env_file:
      - 'env.influxdb'
    volumes:
      - ./influxdb-volume:/var/lib/influxdb2
    network_mode: "host"

Erstellen Sie auch ein File env.influxdb mit Environment Variablen für das Setup:

DOCKER_INFLUXDB_INIT_MODE=setup
DOCKER_INFLUXDB_INIT_USERNAME=root
DOCKER_INFLUXDB_INIT_PASSWORD=embedded
DOCKER_INFLUXDB_INIT_ORG=embedded
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=token
DOCKER_INFLUXDB_INIT_BUCKET=embedded

Docker Compose starten

Starten Sie nun Docker Compose mit docker-compose up -d. Das Docker Image hostet nun influxdb. Über Port 8086 kann die Applikation im Webbrowser angezeigt und konfiguriert werden:

Auf dem Gastsystem können Sie die Applikation im Webbrowser via http://10.10.10.1:8086 besuchen und sicht mit root und pw embedded einloggen.

Sicherheit

In einem Produktionsumfeld macht es Sinn gute Passwörter auszwählen. Auch wenn wir unsere Applikation nicht gegen "aussen" verfügbar machen, empfiehlt es sich trotzdem übliche Sicherheitsmassnahmen vorzunehmen.

Um in eine Shell des Docker Containers via ssh zu gelangen (z.B. um mit influx als Command zu arbeiten) kann folgender Command verwendet werden:

docker-compose exec influxdb sh

Messwerten an InfluxDB senden

InfluxDB stellt eine API via http zur Vefügung um Messwerte zu senden. Ein Beispiel hierzu finden Sie unter https://docs.influxdata.com/influxdb/v2.2/write-data/developer-tools/api/. Mit einem einfachen curl Command (HTTP Request von der Commandline her) können Daten an InxluxDB gesent werden.

curl --request POST \
"http://10.10.10.1:8086/api/v2/write?org=embedded&bucket=embedded&precision=ms" \
  --header "Authorization: Token token" \
  --header "Content-Type: text/plain; charset=utf-8" \
  --header "Accept: application/json" \
  --data-binary '
  airSensors humidity=35.23103248356096,co=0.48445310567793615 1630424257000
  '

Testen Sie den Command vom Gastsystem und vom Target her.

Der Wert 1630424257000 ist der Zeitwert, bei welchem die Messung stattgefunden hat als EPOCH Time (ms die vergangen sind seit dem 1. Januar 1970).

Ermitteln Sie die aktuelle Zeit in ms nach EPOCH und senden Sie einige imaginäre Messwerte via curl an InxluxDB. Validieren Sie, dass diese unter dem Bucket embedded unter Explore im Userinterface erscheinen.

curl fehlt auf dem Target

curl wird standardmässig von buildroot nicht selektiert. Sie finden das Paket unter Libraries / libcurl. Selektieren Sie auch das binary.

Nach make können Sie der Anleitung unter Target als MQTT Broker folgen um die Files auf die SD-Karte zu kopieren.

Target als MQTT Broker

Das Target soll für den ESP32 als MQTT Broker dienen und gleichzeitig empfangene Werte an InfluxDB senden.

Kompilieren Sie das RFS neu und kopieren Sie binaries auf die SD-Karte. Sie können mit folgendem Command die neu erstellten Files als tar archiv verpacken:

bash
# zuerst ins buildroot source dir wechseln

cd output/target
# idealerweise alle files löschen, die konfiguriert wurden
rm etc/network/interfaces
rm etc/wpa_supplicant.conf
tar -cvf ../rootfs.tar -N "1 hour ago" *

Sie können nun die nur eben erstellten Files auf die SD-Karte kopieren:

bash
cd ..
# erstelltes archiv
ls -al rootfs.tar

# sicherstellen, dass die SD gemountet ist

lsblk

# über dem rfs der SD entpacken

sudo tar -xvf rootfs.tar -C /media/${USER}/rootfs

Mosquitto wird bereits mit einem Eintrag in /etc/init.d gestartet. Validieren Sie mit

netstat -lt

und

ps

.

MQTT lokal testen

Studieren Sie die Commands mosquitto_sub und mosquitto_pub. Installieren Sie auf dem Gastsystem mosquitto und testen Sie Publish / Subscribe.

MQTT - CURL Brücke

Eine sehr einfache Lösung Messwerte von MQTT an InfluxDB weiterzugeben ist via Shell Script.

Als erstes warten wir genau einmal auf eine MQTT Message

bash
mosquitto_sub -h localhost -C 1 -t "sensor/temp"

Die Zeit in Sekunden nach 1970 erhalten wir mit dem Command date +%s.

Als nächstes können wir einen curl absetzen mit dem Output vom Sensorwert (z.B. in einer Shell Variablen zwischenspeichern).

curl --request POST \
"http://10.10.10.1:8086/api/v2/write?org=embedded&bucket=embedded&precision=s" \
  --header "Authorization: Token token" \
  --header "Content-Type: text/plain; charset=utf-8" \
  --header "Accept: application/json" \
  --data-binary "
  airSensors temperature=<sensorwer> <sekunden nach 1. Jan 1970>
  "

Zusammen würde das mit diesem Skript bereits einmal funktionieren:

bash
#!/bin/sh

MSG=`mosquitto_sub -h localhost -t "sensor/temp" -C 1`
T=`date +%s`
echo "$MSG $T"

curl --request POST \
"http://10.10.10.1:8086/api/v2/write?org=embedded&bucket=embedded&precision=s" \
  --header "Authorization: Token token" \
  --header "Content-Type: text/plain; charset=utf-8" \
  --header "Accept: application/json" \
  --data-binary "
  airSensors temperature=${MSG} ${T}
  "

Was noch fehlt, ist das ganze beliebig lang zu wiederholen. Ändern Sie das Script so, dass dieses "for ever" ausgeführt wird.

ESP32 MQTT Publish

Implementieren Sie nun eine Firmware auf dem ESP32, welche den Temperatursensor ausliest und periodisch Werte an den MQTT Broker sendet.

Erweitern Sie dann die Funktionalität mit dem Humiditysensor.

Avahi Config

Fügen Sie Buildroot avahi hinzu. Der Daemon erlaubt es mDNS (Multicast DNS) zu verwenden. Zur Zeit muss die IP-Adresse des Raspberry Pi in der Firmware des ESP hardgecoded werden. Mit mDNS, kann das Raspberry Pi einen Hostnamen publizieren, was es dem ESP erlaubt einen Namen anstelle einer IP zu verwenden.

Sie können das Target mit der folgenden avahi config unter <hostname>.local erreichen. Schauen Sie sich zu diesem Thema die Library für den ESP32 an: https://www.arduino.cc/reference/en/libraries/mdns_generic/.

Hier der Inhalt von /etc/avahi/avahi-daemon.conf:

[server]
host-name=<hostname hier eingeben>                                                        
domain-name=local                                                   
#browse-domains=0pointer.de, zeroconf.org                        
use-ipv4=yes               
use-ipv6=yes
#allow-interfaces=eth0                                            
#deny-interfaces=eth1                                         
#check-response-ttl=no                                               
#use-iff-running=no
#enable-dbus=yes
#disallow-other-stacks=no                                            
#allow-point-to-point=no
#cache-entries-max=4096
#clients-max=4096
#objects-per-client-max=1024
#entries-per-entry-group-max=32
ratelimit-interval-usec=1000000          
ratelimit-burst=1000
            
[wide-area]           
enable-wide-area=yes 
                      
[publish]          
#disable-publishing=no
#disable-user-service-publishing=no
#add-service-cookie=no  
#publish-addresses=yes 
publish-hinfo=no 
publish-workstation=no      
#publish-domain=yes            
#publish-dns-servers=192.168.50.1, 192.168.50.2
#publish-resolv-conf-dns-servers=yes
#publish-aaaa-on-ipv4=yes
#publish-a-on-ipv6=no
                    
[reflector]
#enable-reflector=no
#reflect-ipv=no       
#reflect-filters=_airplay._tcp.local,_raop._tcp.local
                      
[rlimits]             
#rlimit-as=     
#rlimit-core=0        
#rlimit-data=8388608
#rlimit-fsize=0                                
#rlimit-nofile=768                  
#rlimit-stack=8388608    
#rlimit-nproc=3 

Starten Sie nach dem ändern avahi neu. Beachten Sie, dass mDNS nicht über Wireguard funktioniert, da Wireguard die Broadcast Domain trennt.