Access control — sessions, service key, origin¶
How the uGo server decides who gets through, since access protection ULS-238 (uGo service 1.0.3/1.0.4, build id 1788703020348). Read from tuersteher.ts, anmeldung.ts, herkunft.ts, dienstschluessel.ts, bremse.ts and the role matrix in api/chassis/rollen.test.ts. BELEGT = taken from code and tests.
Status of this page: 06.09.2026.
The rule in one sentence
Whoever reaches the robot from the network — browser or data — needs credentials. Driving over the API works only behind a session; configuring only as technician. Requests from the device itself (kiosk shell, loopback) are unchanged. The rule is enforced on the device, by the manufacturer — not at the customer's router.
1 Origin: device or network¶
The origin is read from the socket (remoteAddress of the accepted connection) in server.js, never from a header — X-Forwarded-For, Host, Origin are set by the caller. BELEGT
| Origin | Addresses | Consequence |
|---|---|---|
| Gerät (device) | 127.0.0.0/8, ::1, ::ffff:127.x.x.x — the kiosk shell on the tablet, the service's self-test, the localproxy of the maintenance tunnel |
the pre-ULS-238 rules apply (section 3) |
| Netz (network) | everything else, including the device's own LAN address | everything needs a session (section 3) |
| no context at all | started via node build/index.js instead of server.js, or vite dev |
counts as network. ZUGRIFF_ENTWICKLUNG=frei turns it into "device" only when no context exists — under server.js the variable is ignored. Never set it in a build. |
VORSICHT A site computer whose systemd unit still starts node build/index.js treats every request as network — including its own kiosk. Start via node server.js (npm start does).
2 Sessions¶
| Point | Value | Why |
|---|---|---|
| carrier | cookie ugo_sitzung, httpOnly, sameSite: strict, secure: false, maxAge 8 h |
the robot speaks HTTP in its own network; secure: true would make the tablet drop the cookie silently |
| validity | 10 minutes after the last access, extended on every access | a technician page left open in a corridor would otherwise stay open all day |
| persistence | daten/sitzungen.json, mode 0600, session marks with role and expiry only — no password; rewritten at most once a minute |
the server is rebuilt several times a morning; a restart must not log the tablet out |
| password change | ends all other sessions | changing the technician password is done to throw the previous holder out |
| roles | techniker, bediener — derived from which password matched |
— |
| throttle | area anmeldung: 5 failures per address → 60 s, doubling per further failure, cap 1 h; plus a global counter (20 → 30 s, cap 5 min). Persisted in daten/bremse.json |
a six-digit password is guessed in seconds if guessing is free |
POST /api/anmeldung {kennwort} → 200 {rolle} · 401 Kennwort stimmt nicht. · 409 Am Server ist kein Kennwort eingerichtet. · 429 Zu viele Fehlversuche. Bitte N Sekunden warten. GET /api/anmeldung → {kennwortEingerichtet, angemeldet, rolle}. DELETE ends the session. BELEGT
3 What a path requires (zugangsbedarf)¶
| Origin | Condition | /service/** |
/fahrtenbuch |
everything else |
|---|---|---|---|---|
| device | no password configured at all | open | open | open |
| device | password configured | technician | session — or open when the operator is set to "frei" | open |
| network | no technician password (factory state) | einrichtung |
einrichtung |
einrichtung — except the allow-list below |
| network | technician password configured | technician | session (also when the operator is "frei") | session |
Always open from the network (allow-list, not deny-list): /anmeldung, /api/anmeldung, /einrichtung, /api/kennwort, /api/leben, /_app/**, /favicon*, /schrift/**, /gesicht/**, /urg-logo.svg. The path is decodeURIComponent-ed before comparison (the router decodes too); a path that does not decode, or contains ./.. segments, matches nothing and is therefore closed. BELEGT
/api/flotte/** is deliberately not on the list: the fleet talks UDP with its own key; the HTTP endpoints are information for humans and name serial numbers.
Answers when the requirement is not met¶
| Requirement | Page (303) |
API (/api/**) |
|---|---|---|
einrichtung |
→ /einrichtung |
401 {"fehler":"Zuerst das Servicekennwort einrichten."} |
| session, none present | device: → /anmeldung · network: → /anmeldung?zurueck=<path+query> |
401 {"fehler":"Nicht angemeldet."} |
| technician, but operator session | → /fahrtenbuch (no login loop) |
403 {"fehler":"Ändern darf nur der Techniker."} |
The login page accepts zurueck only as an absolute path of this server (starts with /, not //).
Factory state¶
Factory state = no technician password (neither in daten/kennwoerter.json nor SERVICE_KENNWORT_TECHNIKER in the environment). An operator password alone does not end it. A damaged password file counts as "technician set" and is therefore not factory state — a broken device must not open the first-password path to whoever broke it. POST /api/kennwort sets the first technician password without a session only while no password is effective; the reset with a code from the cloud ({ruecksetzcode, kennwort}) also works without a session and is throttled in its own area. BELEGT
4 The service key (x-urg-dienstschluessel)¶
For automatons — site computer, building service, test scripts — that cannot type a password.
| Point | Value |
|---|---|
| header | x-urg-dienstschluessel: urg-<64 hex> (whitespace at the ends is tolerated) |
| creation | technician page → Device → Service key; POST /api/dienstschluessel returns the clear text once; stored as SHA-256 in daten/dienstschluessel.json (0600) with erzeugt timestamp |
| count | exactly one per robot; "renew" replaces, the old key is void everywhere immediately; DELETE /api/dienstschluessel revokes |
| rights | the request runs as an operator session (AsyncLocalStorage, valid for that one request, never stored, never extended) |
| precedence | a valid cookie wins; an invalid cookie does not fall back to the key (a browser with a stale cookie must see that, not silently continue with the key's rights); no cookie + valid key = operator |
| throttle | own area dienstschluessel, per address, no global counter: a valid key is compared first (constant time) and never touches the throttle; a wrong key: 429 when the address is blocked, else 401 {"fehler":"Der Dienstschlüssel stimmt nicht."} and one failure counted |
GET /api/dienstschluessel → {vorhanden, erzeugt?} — never the key. POST → {vorhanden, erzeugt, schluessel}. DELETE → {vorhanden:false}. All three: technician only — 403 also for the key itself (a key that creates keys could never be revoked). BELEGT
curl -s -H "x-urg-dienstschluessel: urg-<64 hex>" \
-H "content-type: application/json" \
-d '{"aktion":"taster","kennung":"ruf-labor-2"}' \
http://192.168.0.10:5301/api/regeln/aktion
5 Roles per route — driving yes, configuring no¶
The matrix from api/chassis/rollen.test.ts (20 paths × 4 roles) and the per-route guards. "Session" means operator, technician or service key. From the device with no password configured, everything is open (B7).
Chassis pass-through /api/chassis/<path>¶
| Method + path | No session (network) | Operator / service key | Technician |
|---|---|---|---|
GET * |
401 | passes | passes |
POST chassis/moves, POST chassis/moves/ |
401 from the network — no route guard (the stop button on the device needs no password) | passes — a move is created | passes |
PATCH chassis/moves/<n>, PATCH chassis/moves/<n>/ |
as above | passes — a move is cancelled | passes |
PATCH/POST chassis/pose |
401 | 403 | passes |
PATCH/POST chassis/current-map |
401 | 403 | passes |
POST mappings/, PATCH mappings/<n> |
401 | 403 | passes |
POST maps/, PATCH maps/<n> |
401 | 403 | passes |
POST services/localization/enable, PATCH chassis/services, PATCH chassis/auto-charge |
401 | 403 | passes |
POST chassis/moves/<n>, PATCH chassis/moves, POST chassis/movesx, POST chassis/moves/../pose, PATCH chassis/moves/abc |
401 | 403 — only the two exact drive shapes pass | passes |
DELETE maps/<n>, DELETE mappings/<n> |
401 | 403 | passes |
chassis/pose is deliberately not on the drive list: the operator interface never sets the pose; a pose set from the network sends the robot into a wall on its next move. "Passes" for the technician means: the answer is not 401/403 — behind the guard stands the chassis, and without one the answer is 502. BELEGT
Own routes¶
| Route | Driving / operating (session incl. service key) | Configuring (technician only) |
|---|---|---|
POST /api/touren/lauf (start, stop, pause, fortsetzen, bestaetigen) |
passes | — |
POST /api/regeln/aktion (ausloesen, taster, alarm, weiter, entfernen, alles-anhalten) |
passes | — |
GET of touren, regeln, einstellungen, fahrtenbuch, medien, nachbarn, funk, flotte, sprache |
passes | — |
PUT /api/touren, PUT /api/regeln, POST /api/einstellungen, POST /api/fahrwerte, POST /api/wlan, POST /api/kennwort (with session), POST /api/medien, POST /api/zonen, POST /api/kartenarchiv, POST /api/karte-aus-aufnahme, POST /api/sprache |
403 | passes |
POST /api/wolke (anmelden with join code) |
403 | passes |
/api/gebaeude/** — all methods, GET included |
403 | passes |
/api/dienstschluessel (GET, POST, DELETE) |
403 | passes |
/api/servicezugang (GET, POST, DELETE) |
403 | passes |
/api/uiot/<path> (read-only pass-through) |
403 | passes |
GET /api/protokolle, /api/protokolle/zeilen |
passes (session) | — |
GET /api/leben |
open — no session, no key | — |
6 GET /api/leben — liveness¶
For monitoring, install scripts and the self-test of a site computer. Always open, also in factory state, also on the maintenance tunnel. Contains nothing confidential: no chassis state, no serial number, no address, no password state. BELEGT
{ "ok": true, "fassung": "1.0.4" }
fassung is UGO_FASSUNG from the service environment, null if unset. There is no other health endpoint; the state reporter pushes to the cloud and is not polled.
7 /api/servicezugang — adb and remote support with a time window¶
Pass-through from the technician page to the network gate on the device (SCHRANKE_BASIS, default http://127.0.0.1:5302, 3 s). Technician only — the service key must not open a remote access. Not reachable through the maintenance tunnel. BELEGT
| Method | Body | Gate call | Response |
|---|---|---|---|
GET |
— | GET /zustand |
the gate's zustand.json unchanged, incl. betriebsart and servicezugang.{adb,fernwartung}.{offen,restSekunden,quelle} |
POST |
{"was":"adb"\|"fernwartung","minuten":15\|60\|240} |
POST /servicezugang with the same body |
the gate's answer unchanged — 200 {offen:true, was, restSekunden} or its refusal |
DELETE |
{"was":…} or empty (= everything) |
DELETE /servicezugang |
the gate's answer unchanged — 200 {offen:false[, was]} |
| Status | Body |
|---|---|
| 400 | Zu öffnen ist entweder „adb" oder „fernwartung". · Die Dauer ist 15, 60 oder 240 Minuten. — checked before the gate is asked |
| 502 | Netzschranke nicht erreichbar – Zugang bleibt wie er ist. — no answer, slower than 3 s, or no JSON; nothing has changed |
| 400 / 403 / 404 / 405 / 409 | the gate's own refusals, passed through unchanged (403 Aufrufer nicht prüfbar … when the gate cannot determine the caller UID; 409 without device owner; 405/404 from a gate older than 1.1.0) |
POST /neustart of the gate (device reboot) exists on the gate side and is not exposed by the uGo server in 1.0.4. OFFEN
8 What a technician has to do after the update¶
- Set the service password before the robot joins the customer network (
/einrichtungfrom the network, or the technician page at the kiosk). - Set an operator password if anybody is to see the operator interface or the logbook from the network (site-computer setup with a tablet: mandatory — from the network there is no login without a password, even with the operator set to "frei").
- Generate the service key and enter it into the site computer, test scripts and building services — it is shown once.
- Site computer: systemd unit on
node server.js; monitoring on/api/leben. - Everything that used to call the server without a login (
POST /api/regeln/aktion,POST /api/touren/lauf,Invoke-RestMethodprobes) sends the headerx-urg-dienstschluessel— without it,401.