Skip to content

3 · URG-Cloud

Danger

Die Zusage, an der sich jede Zeile dieses Teils messen lassen muss: Kein Fahrbefehl aus der Cloud. Nie. Aus dieser Richtung nimmt das Gerät genau zwei Dinge an — eine fassungsfreigabe und eine sperre. Alles andere wird verworfen und protokolliert, auch aus einer gültigen Verbindung mit gültigem Zertifikat.

Die Sperre ist die einzige erlaubte Richtung nach unten, weil sie nur wegnimmt: sie hält an, sie fährt nicht. Diese Eigenschaft hat nur, wer sie nirgends aufweicht — nicht „nur für die Fernwartung", nicht „nur im Prüfstand". Wer im Empfang einen dritten Fall hinzufügt, hebt die Zusage auf, mit der dieses Gerät verkauft wird.

Zweiter Grundsatz: die Cloud ist ein Zusatz, nie eine Voraussetzung. Ein Roboter ohne Internet fährt in vollem Funktionsumfang weiter, puffert seine Meldungen und reicht sie nach.

3.1 Verbindung

Punkt Wert
Träger AWS IoT Core über MQTT, TLS beidseitig, X.509 je Gerät
clientId = Thing-Name = Seriennummer des Fahrgestells
Anschluss 8883; bei einer Firewall, die das sperrt, 443 mit der von AWS verlangten ALPN-Kennung x-amzn-mqtt-ca
QoS 1 in beide Richtungen
Wurzelzertifikat ohne eigene Datei die Wurzeln von Node (Amazon Root CA 1 ist darin). rejectUnauthorized bleibt in jedem Fall an
Wiederverbinden selbst gesteuert: 1 s, 5 s, 15 s, 60 s, 300 s, jeweils mit ±20 % Streuung — mehrere Roboter am selben WLAN dürfen nach einer Störung nicht in derselben Sekunde anklopfen
Puffer Meldungen, die nicht hinausgehen, landen in daten/wolke-puffer.jsonl und werden beim nächsten Verbinden in der ursprünglichen Reihenfolge nachgereicht. Bei Überlauf (5000) fliegt die älteste Kennzahl, nie die älteste Störung

3.2 Themenbaum je Gerät

Alles, was ein Roboter sagen oder hören darf, liegt unterhalb eines einzigen Pfades:

urg/<umgebung>/geraet/<seriennummer>/<art>

<umgebung> ist pruef oder betrieb. <seriennummer> ist der Thing-Name, und den setzt AWS über die Richtlinienvariable ${iot:Connection.Thing.ThingName} durch. Was im Pfad steht, hat AWS erzwungen; was im Rumpf steht, hat der Absender selbst geschrieben und ist eine Behauptung. Alle Regeln in der Cloud holen die Seriennummer deshalb mit topic(4) aus dem Pfad und niemals aus dem Rumpf.

Richtung Thema Inhalt Was in der Cloud damit geschieht
Gerät → Cloud …/zustand Zustand, Fassungen Lambda schreibt die Zeile des Gerätes fort (nur UpdateItem, kein Anlegen)
Gerät → Cloud …/kennzahlen viel, gleichförmig Firehose, spätere Auswertung
Gerät → Cloud …/stoerung Störung SNS — ein Mensch soll es merken
Gerät → Cloud …/karte {vorgang:"gesichert", archivId, name, bytes, pruefsumme} Vermerk
Gerät → Cloud …/karte/anfrage Steckbrief einer Kartensicherung, siehe 3.7 Lambda antwortet mit vorsignierten Ablageadressen
Gerät → Cloud …/update/rueckroll Rückrollmeldung, siehe 3.6 Lambda zählt Geräte und sperrt notfalls die Fassung
Cloud → Gerät …/befehl nur fassungsfreigabe und sperre alles andere verwirft das Gerät
Cloud → Gerät …/+/antwort Antwort auf eine eigene Frage, zugeordnet über anfrageId Antwort ohne offene Frage wird verworfen
beidseitig $aws/things/<name>/jobs/… IoT Jobs; bestellt wird notify-next siehe 3.5
beidseitig $aws/things/<name>/shadow/… Geräteschatten trägt u. a. die laufende Fassung — sie passt nicht in die drei durchsuchbaren Merkmale des Thing-Typs

Umschlag jeder Meldung nach oben: {art, zeit, inhalt}. zeit ist die Uhr des Roboters; die Cloud übernimmt sie bewusst nicht als Empfangszeit — ein Gerät puffert bei Netzausfall und reicht nach, und seine Uhr kann danebenliegen. Wann etwas gehört wurde, weiss nur die Cloud; wann es gemessen wurde, steht im Inhalt.

3.3 Was das Gerät darf — aus der IoT-Richtlinie

Recht Worauf Warum genau so
iot:Connect client/${iot:Connection.Thing.ThingName} Die wichtigste Zeile der ganzen Richtlinie. Ohne sie dürfte jedes gültige Gerätezertifikat unter jeder clientId verbinden — alle Roboter teilen sich dieselbe Richtlinie. Ein einziges übernommenes Gerät (und ein Roboter steht in einem Krankenhausflur, nicht in einem Rechenzentrum) könnte sich dann als jeder andere ausgeben: fremde Zustandsmeldungen fälschen, fremde Update-Aufträge abholen, eine fremde Verbindung abwerfen (MQTT trennt bei gleicher clientId den älteren Teilnehmer). Die gesamte Rechtetrennung fällt auf diese eine Bindung zurück, weil dieselbe Variable auch in allen Themenpfaden steckt. Nebenwirkung, die gewollt ist: die Variable ist nur belegt, wenn das Zertifikat an einem Ding hängt — ein Zertifikat ohne Registrierung kann sich gar nicht verbinden
iot:Publish urg/<umgebung>/geraet/<name>/*, $aws/things/<name>/jobs/*, $aws/things/<name>/shadow/* Kein Platzhalter weiter oben im Baum, also kein Weg in den Zweig eines anderen Roboters oder in einen Verwaltungspfad. Die feste Umgebung schliesst zusätzlich aus, dass ein Prüfgerät in die Betriebsumgebung hineinredet
iot:Subscribe dieselben Pfade als topicfilter/… Das Gerät bestellt örtlich mit #; in der Richtlinie steht *, weil ein ARN keine MQTT-Platzhalter kennt. Wichtig ist, was davor steht: der Pfad ist bis zum Gerätenamen fest
iot:Receive dieselben Pfade AWS prüft dieses Recht beim Zustellen jeder einzelnen Nachricht, nicht nur beim Bestellen — die Erlaubnis zum Bestellen reicht nicht
iot:DescribeJobExecution, GetPendingJobExecutions, StartNextPendingJobExecution, UpdateJobExecution thing/<name> und job/* Für den HTTP-Weg der Job-Schnittstelle, wenn das Gerät Fortschritt meldet. job/* ist nötig, weil die Auftragskennung erst zur Laufzeit entsteht; die Bindung hält über thing/

Nicht in der Richtlinie und mit Absicht: Zertifikate. Ein Zertifikat gehört genau einem Gerät und entsteht bei dessen Erstanmeldung, nicht bei terraform apply. Ebenso hängt der Anmeldedienst das Zertifikat an Richtlinie und Ding — sonst müsste jeder neue Roboter durch ein terraform apply, und ein Techniker vor Ort könnte kein Gerät in Betrieb nehmen.

3.4 Das Job-Dokument, Feld für Feld

Der Vertrag zwischen Cloud und Gateway. Vorlage: terraform/update-vorlagen/job-dokument.json.tftpl; gelesen wird er von pi/ugo-aktualisierer.sh. Feste Werte setzt Terraform, ${aws:iot:parameter:…} setzt derjenige, der aus der Vorlage einen Job erzeugt.

Feld Beispiel Bedeutung / Regel
dokument_fassung 1 ändert sich nur, wenn Felder wegfallen
vorhaben "ugo-update" Erkennungsmarke; alles andere ignoriert das Gateway
stufe "pruefgeraete" pruefgeraete | vorhut | zehntel | flotte, kommt aus der Vorlage
schicht "server" Nur server oder kiosk. Alles andere lehnt das Gateway ab — das ist die Stelle, an der ein Tippfehler in einem Ausrollbefehl aufhört, gefährlich zu sein. Das Fahrgestell wird von diesem Weg nie angefasst.
fassung "2026.08.19-3" was gespielt wird; Schlüssel der Tabelle fassungen
fassung_vorher_erwartet "2026.08.12-1" leer = beliebig. Sonst: passt der Stand nicht, wird nicht gespielt
paket.adresse vorsigniert, kurzlebig aus $${aws:iot:s3-presigned-url:…}, Gültigkeit aus update_presigned_gueltig_sekunden
paket.sha256 Prüfsumme des tar.zst wird vor dem Entpacken geprüft
paket.signatur_b64 ECDSA-P256-Signatur über die 32 Byte des SHA-256, nicht über das Paket. KMS liefert sie DER-kodiert — genau die Form, die openssl erwartet, also kein Umformschritt dazwischen
paket.signatur_verfahren "ECDSA_SHA_256"
paket.signatur_schluessel alias/urg-<umgebung>-update-signatur Der öffentliche Teil liegt auf dem Gerät unter /etc/ugo/paket-signatur.pub (rund 100 Byte) und braucht danach nichts mehr — keine Kette, keine Uhr, kein Netz. Der Preis dieser Wahl: kein Widerruf, bewusst genommen
paket.groesse_bytes Zahl als Zeichenkette Vorabprüfung gegen freien Plattenplatz
zeiger.dokument zeiger.paket vorsigniert, feste Schlüssel pakete/zeiger/<stufe>.json bzw. .tar.zst Zweiter Weg, siehe unten
sperren.nicht_waehrend_fahrt true Ein Roboter, der gerade einen Auftrag fährt, wird nicht neu gestartet. Auch nicht „kurz"
sperren.mindest_akku_prozent 40 Das Gateway rechnet mit max(Dokument, Gerätevorgabe) — das Dokument darf strenger sein, nie lockerer
sperren.nur_im_wartungsfenster true Fenster je Kunde in /etc/ugo/wartungsfenster.conf. Steht dort keins, wird gar nicht aktualisiert — kein Vorgabewert, keine Kulanz. Ein Fenster ist eine Zusage, die ein Mensch dem Kunden gegeben hat
gesundheit.frist_sekunden 60 danach rollt das Gerät von allein zurück
gesundheit.pfad / .port "/" / 5173 Gesundheitsprüfung nach dem Umschalten
rueckrollen.erlaubt true
rueckrollen.melde_thema urg/<umgebung>/geraet/{seriennummer}/update/rueckroll siehe 3.6

Die drei Sperren stehen im Dokument, sind aber keine Bitte. Das Gerät prüft sie selbst und meldet REJECTED mit Grund, wenn eine greift.

Warning

Warum dieselben Angaben zweimal im Dokument stehen. Fassung, Prüfsumme und Signatur ändern sich mit jeder Freigabe; eine Job-Vorlage ist dagegen fest. AWS füllt ${aws:iot:parameter:…} beim Anlegen eines Jobs über --document-parameters. Ungeprüft ist, ob AWS das auch für eigene (nicht AWS-verwaltete) Vorlagen zulässt — die Beschreibung von CreateJob schränkt documentParameters auf verwaltete Vorlagen ein, und ohne Konto lässt sich das nicht ausprobieren. Deshalb trägt das Dokument beides, und das Gerät nimmt, was da ist:

  1. Parameter — der kurze Weg, wenn AWS ihn zulässt.
  2. Zeiger — sind die Felder unersetzt geblieben, lädt das Gerät zeiger.dokument und nimmt die Angaben von dort.

Der Zeiger ist kein Vertrauensproblem: das Gerät glaubt ihm nur, was die Signatur trägt. Wer den Zeiger austauscht, ohne den Signaturschlüssel zu haben, erreicht genau eine Zeile im Protokoll (signatur-ungueltig).

3.5 Ausrollen in vier Stufen

Stufe Vorlage Ziel Wartezeit davor
1 Prüffeld update_stufe1_pruefgeraete Dinggruppe urg-<umgebung>-pruefgeraete — hier steht 8982504805371oD
2 Vorhut update_stufe2_vorhut ein einzelnes abgesprochenes Kundengerät 48 h
3 Zehntel update_stufe3_zehntel von Hand zusammengestellte Liste über mehrere Kunden — bewusst keine feste Gruppe 72 h
4 Flotte update_stufe4_flotte Dinggruppe urg-<umgebung> eine Woche

terraform apply legt nur die Vorlagen an. Der Job selbst — und damit das Ziel — entsteht von Hand über aws iot create-job. Das ist Absicht: „auf welche Geräte" soll nie ein Nebeneffekt sein. Die Wartezeiten kann Terraform nicht erzwingen; sie entstehen dadurch, dass ein Mensch den nächsten Job anlegt.

Die Job-Kennung muss mit urg-<umgebung>-update- beginnen: die Notbrems-Lambda bricht nur Jobs mit diesem Präfix ab, und ihre Rolle darf auch nur diese anfassen.

Warum IoT Jobs und nicht ein eigener Weg: der Roboter holt, niemand drückt. Kein offener Port beim Kunden. Ein Gerät, das eine Woche aus war, bekommt seinen Job beim nächsten Verbinden — ohne dass irgendwo von Hand eine Liste „noch offen" geführt wird.

3.6 Rückrollen

Kommt der Roboter nach dem Umschalten nicht binnen der Frist gesund hoch, hängt er den Verweis zurück, startet neu und meldet auf urg/<umgebung>/geraet/<seriennummer>/update/rueckroll:

{ "fassung": "2026.08.19-3", "schicht": "server", "grund": "gesundheit", "job_id": "urg-pruef-update-stufe1-…", "zeitpunkt": "…" }

Die Lambda trägt die Seriennummer in eine Menge ein. Ab drei verschiedenen Geräten wird gesperrt = true auf dem Posten (fassung + schicht) gesetzt und die laufenden Jobs dieser Fassung werden abgebrochen. Verschiedene Geräte, nicht drei Meldungen — ein Gerät in einer Schleife ist ein Gerätefehler, kein Fassungsfehler.

3.7 Kartensicherung — der Umweg über eine vorsignierte Adresse

Warum ein Umweg: MQTT trägt höchstens 128 KB je Nachricht; eine Carto-Karte eines Krankenhausflurs liegt darüber. Stückeln hiesse, eine eigene Wiederzusammensetzung samt Lücken- und Reihenfolgeproblem zu bauen — für eine Verbindung, die in einem Krankenhaus regelmässig abreisst.

  1. Gerät → …/karte/anfrage: nur der Steckbrief — {vorgang:"ablageadressen", archivId, name, pruefsumme, dateien:[{name, bytes}], anfrageId}. Das ist klein und passt immer.
  2. Lambda legt eine vorläufige Zeile an („erwartet") und antwortet auf …/karte/antwort mit je einer vorsignierten PUT-Adresse, 15 Minuten gültig. Erlaubt sind genau drei Dateinamen: karte.pbstream, raster.png, beschreibung.json. Eine vorsignierte Adresse ist ein Schreibrecht — wer den Dateinamen bestimmen dürfte, bestimmte den Ort im Eimer.
  3. Gerät lädt per HTTPS unmittelbar nach S3, beschreibung.json zuletzt. Reisst die Verbindung ab, wird wiederholt; die Schlüssel bleiben dieselben.
  4. S3 meldet die eingetroffene beschreibung.json zurück; erst dieser Schritt setzt die Zeile auf „gesichert".

Warum die Beschreibung der Auslöser ist und nicht die Carto-Karte: sie ist die Marke für einen vollständigen Satz. Bricht es vorher ab, liegt oben Bruchstückhaftes ohne Beschreibung — sichtbar unvollständig statt scheinbar heil.

Unverändert Gesichertes wird übersprungen (Vergleich über die Prüfsumme). Das ist kein Geiz: manche Häuser hängen den Roboter an einen Mobilfunkstick.

3.8 Anmeldestrecke

Der Kernpunkt, um den sich das ganze Verfahren dreht: der private Schlüssel verlässt das Gerät nie. Der Roboter erzeugt sein Schlüsselpaar selbst und schickt nur eine Zertifikatsanfrage (CSR). Die Cloud ruft CreateCertificateFromCsr auf; AWS IoT unterschreibt mit seiner eigenen CA. Ein Einbruch in unsere Cloud gibt einem Angreifer damit keine Möglichkeit, sich als ein Roboter auszugeben — der Schlüssel lag dort nie. Ein Plattentausch am Roboter kostet dafür ein neues Zertifikat; das ist der bewusst in Kauf genommene, günstigere Preis.

Es wird ausdrücklich keine eigene Zertifizierungsstelle betrieben. Eine eigene CA hiesse einen CA-Schlüssel besitzen, sichern, drehen und im Ernstfall sperren — eine Aufgabe für eine Organisation mit Schlüsseltresor und Verfahren, nicht für ein Vorhaben, dessen Kern ist, dass ein Roboter auch ohne Cloud fährt.

Schritt Was passiert
1 Techniker gibt auf der Technikerseite den einmaligen Beitrittscode ein → POST /api/wolke {aktion:"anmelden", beitrittscode}
2 Das Gerät liest seine Seriennummer vom Fahrgestell, erzeugt Schlüssel und CSR
3 POST <WOLKE_ANMELDUNG_URL> mit {beitrittscode, seriennummer, typ, csr}
4 Der Dienst prüft Code und Rumpf, legt Ding, Zertifikat und Verknüpfungen an und antwortet
5 Das Gerät legt Zertifikat und Vermerk ab und verbindet sich neu

Endpunkt: POST /anmeldung (HTTP-API, gedrosselt auf 10 Anfragen je Sekunde, Spitze 20). Es gibt genau diesen einen Pfad — keinen Zweig, keinen Parameter und keinen Fehlerfall, aus dem ein Fahrbefehl entstehen könnte.

Feld der Anfrage Prüfung
beitrittscode Zeichenkette, höchstens 64 Zeichen
seriennummer höchstens 64 Zeichen, ^[A-Za-z0-9_-]+$ — sie wird Dingname und Teil eines Themenpfades, es darf nichts Fremdes hinein
typ ^[A-Za-z0-9_-]{1,32}$. Das ist die Baureihe (uGo, uServe, …), nicht die Einsatzart. Bewusst eine Zeichenprüfung statt einer Liste: der Katalog der Baureihen ist offen, und eine geschlossene Liste hiesse, dass ein Roboter einer neuen Baureihe im Krankenhaus stehenbleibt, bis jemand eine Lambda neu ausrollt
csr höchstens 8192 Zeichen, muss -----BEGIN (NEW )?CERTIFICATE REQUEST----- enthalten. Ein fertiges Zertifikat wird abgewiesen — wer eines einreicht, versucht etwas anderes als eine Anmeldung
Feld der Antwort Bedeutung
zertifikatPem das ausgestellte Zertifikat
thingName = Seriennummer, zugleich die clientId
kunde Kurzkennzeichen des Betreibers, kein Klarname
iotEndpunkt MQTT-Endpunkt

Fehlerverhalten, das bewusst so ist: unbekannter, abgelaufener und verbrauchter Code ergeben dieselbe Meldung mit 403. Wer Codes rät, darf aus der Antwort nicht lernen, ob er nah dran war; der Techniker vor Ort braucht die Unterscheidung nicht, sein nächster Schritt ist in allen drei Fällen derselbe. Der Ablauf wird zusätzlich geprüft, nicht nur dem TTL der Tabelle überlassen — DynamoDB löscht abgelaufene Einträge träge (bis 48 Stunden).

Wiederholung nach Abbruch: ein bereits verbrauchter Code wird noch einmal angenommen, wenn dieselbe Seriennummer mit demselben CSR-Fingerabdruck wiederkommt; dann wird dasselbe Zertifikat erneut herausgegeben, statt ein zweites auszustellen.

Auf dem Gerät gilt: ganz oder gar nicht. Schlägt ein Schritt fehl, bleibt kein halbes Zertifikat und kein halber Vermerk zurück. War das Gerät vorher schon angemeldet, wird an der vorhandenen Identität nichts angefasst — sonst nähme ein Tippfehler im Code einem laufenden Roboter seine Anmeldung. Der Beitrittscode wird nirgends protokolliert.

3.9 Ein AWS-Fallstrick, der teuer war

Danger

AWS IoT fasst beim Anlegen einer Job-Vorlage jede vollständig ausgeschriebene vorsignierte S3-Adresse tatsächlich an. Existiert das Objekt nicht, meldet AWS „Permission denied". BELEGT 20.08.2026.

Adressen, in denen ein ${aws:iot:parameter:…} steckt, werden übersprungen — sie sind zur Anlegezeit nicht auflösbar. Deshalb scheiterte nie die Paketadresse, sondern immer nur die beiden Zeiger, deren Schlüssel fest ausgeschrieben sind.

Der Beweis kam aus dem Fehler selbst: sobald die vier zeiger/<stufe>.json angelegt waren, nannte AWS plötzlich zeiger/<stufe>.tar.zst — gleiche Meldung, anderer Schlüssel. Die Lehre, teurer als nötig bezahlt: eine Fehlermeldung, die sich ändert, ist ein Messwert. Gesucht wurde stattdessen viermal an den Rechten, weil die Meldung von Rechten sprach.

Zweiter, unabhängiger Punkt an derselben Stelle: AWS prüft beim Anlegen einer Vorlage auch, ob die angegebene Rolle die vorsignierte Adresse überhaupt erzeugen darf — die Rechte müssen dann schon wirksam sein, nicht nur beschrieben. Terraform kannte nur die Kante zur Rolle, nicht zu deren eingebetteter Richtlinie, und legte drei von vier Vorlagen zu früh an. Behoben mit einem depends_on je Vorlage. Bleibt in einem frischen Konto derselbe Fehler, ist die Antwort schlicht ein zweites terraform apply: IAM ist „eventually consistent".