Appearance
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.
Als erstes konfigurieren wir den Server in der Cloud.
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.
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
Layer 2 Link
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.