From 20551e8a6345baaf22a4fc7548578d64511dcad3 Mon Sep 17 00:00:00 2001 From: Dennis Juhler Aagaard Date: Thu, 24 Sep 2026 12:23:35 +0200 Subject: [PATCH] docs: add stelloauth addon design --- ...-home-assistant-stelloauth-addon-design.md | 363 ++++++++++++++++++ 1 file changed, 363 insertions(+) create mode 100644 docs/superpowers/specs/2026-09-24-home-assistant-stelloauth-addon-design.md diff --git a/docs/superpowers/specs/2026-09-24-home-assistant-stelloauth-addon-design.md b/docs/superpowers/specs/2026-09-24-home-assistant-stelloauth-addon-design.md new file mode 100644 index 0000000..ec93867 --- /dev/null +++ b/docs/superpowers/specs/2026-09-24-home-assistant-stelloauth-addon-design.md @@ -0,0 +1,363 @@ +# Home Assistant Stelloauth Add-on Design + +## Purpose + +Build a maintainable Home Assistant OS custom add-on repository that runs +Stelloauth locally for the Stellantis Vehicles integration. The add-on replaces +the shared OAuth worker without changing Home Assistant OS, bypassing +Supervisor, or installing Docker Compose. + +The finished repository must install from Home Assistant's add-on store, start +and stop through Supervisor, survive restarts, keep CloakBrowser's CDP endpoint +private, and provide one exact Login service URL for the integration. + +## Verified target environment + +The target was inspected read-only in Home Assistant on 2026-09-24: + +- Home Assistant OS 18.2 +- Home Assistant Core 2026.9.3 +- Home Assistant Supervisor 2026.09.2 +- Container architecture `amd64` +- CPU architecture `x86_64` +- Machine type `ova` on KVM +- Home Assistant host address `192.168.1.20` +- Approximately 4 GB free disk space at inspection time + +The add-on will also declare and test `aarch64`, because both required upstream +images publish Linux arm64 manifests. + +## Sources and pinned versions + +The initial add-on version is `0.1.0` and pins these upstream inputs: + +| Component | Version | Source commit or OCI index digest | +| --- | --- | --- | +| Stelloauth | `v0.6.0` | `367d4f8c02a3b072c59142c49dffc129edc8548b` | +| Stelloauth image contract | `v0.6.0` | `sha256:51b2194ec9b80cc484d11c016ec5436a12161277a493afdab7078959966d5aa9` | +| CloakBrowser wrapper/image | `0.5.10` | `f04c23da285b3b3d3cf10c8f9d282e7adc1d52ce` / `sha256:2ed5b2d047cbdde22cde7ef1a796526c716aadaa5bccbe1db5ade49282b64a76` | +| Go builder | `1.27.1-bookworm` | `sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195` | +| Stellantis Vehicles integration | `2026.9.4` | `9e0ef96f8fe478af291da4c38c923ada78d0ebf6` | + +The Stelloauth and CloakBrowser OCI indexes contain both `linux/amd64` and +`linux/arm64` manifests. The Dockerfile will use index digests so Docker selects +the manifest matching the Supervisor build architecture. + +## Architecture decision + +Use one add-on container with two supervised application processes: + +```text +Home Assistant Core + | + | POST http://0031621f-stelloauth:8080/worker + v +Stelloauth 0.0.0.0:8080 + | + | CDP http://127.0.0.1:9222 + v +CloakBrowser 127.0.0.1:9222 +``` + +A small Python process manager is the container command. Supervisor's Docker +init remains enabled and acts as PID 1/subreaper. The manager starts +CloakBrowser, proves CDP readiness, starts Stelloauth, monitors both processes, +forwards termination, and performs bounded graceful shutdown. If either +process exits unexpectedly, the manager stops the other process and exits +nonzero so Supervisor can restart the add-on. + +This design keeps both upstream applications as separate processes while +giving them one lifecycle and a loopback-only CDP connection. + +### Rejected alternatives + +Two add-ons were rejected because CloakBrowser would have to listen on the +shared Supervisor network, installation order and cross-add-on discovery would +be more fragile, and shutdown/readiness would span two Supervisor lifecycles. + +A derived prebuilt public image was rejected because the CloakBrowser Binary +License prohibits redistributing or repackaging the binary. The repository +therefore contains build instructions only. The user's Supervisor obtains the +official CloakBrowser image and creates an image solely for internal use. + +A host port enabled by default was rejected because `/worker` accepts MyOpel +credentials and has no authentication. The supported internal Supervisor +network does not expose the listener on the LAN. + +## Repository structure + +```text +repository.yaml +README.md +LICENSE +stelloauth/ + config.yaml + Dockerfile + README.md + DOCS.md + CHANGELOG.md + translations/ + da.yaml + en.yaml + rootfs/ + usr/local/bin/addon-supervisor + patches/ + stelloauth-security.patch + cloakserve-loopback.patch +tests/ + test_addon_metadata.py + test_supervisor.py + test_security_patches.py + test_runtime.sh +``` + +No `build.yaml` is used. Current Home Assistant documentation states that the +legacy builder no longer reads it. The Dockerfile provides an explicit base +image and the required `io.hass.*` labels. + +## Build design + +The Dockerfile has two stages: + +1. A pinned Go builder clones the Stelloauth `v0.6.0` tag, verifies that HEAD is + exactly `367d4f8c02a3b072c59142c49dffc129edc8548b`, applies the maintained + security patch, runs the relevant Go tests, and builds a static binary. +2. The runtime stage derives from the official pinned CloakBrowser `0.5.10` + image, applies the MIT-licensed wrapper patch, copies the patched Stelloauth + binary and process manager, clears the upstream entrypoint, and starts the + manager as the container command. + +The proprietary CloakBrowser Chromium binary is not modified. The official +image already downloads it during CloakBrowser's own build and is consumed as +an upstream dependency. + +## Supervisor metadata + +`stelloauth/config.yaml` will use: + +- `slug: stelloauth` +- `version: 0.1.0` +- `arch: [amd64, aarch64]` +- `startup: application` +- `boot: auto` +- `init: true` +- no `privileged`, `full_access`, `host_network`, Docker API, devices, or maps +- `8080/tcp: null` by default +- an HTTP watchdog against `/` on the internal port +- no ingress + +Stelloauth's UI uses absolute paths such as `/oauth` and `/configs`, so it does +not work correctly beneath Home Assistant's dynamic ingress prefix without a +proxy/rewriting layer. Ingress adds no value to the integration's machine to +machine worker call and is omitted from version 0.1.0. + +## Networking and Login service URL + +The custom repository URL is: + +```text +https://git.radixadm.dk/dennis/homeassistant-stelloauth-addon.git +``` + +Supervisor derives repository ID `0031621f` from the first eight hexadecimal +characters of SHA-1 over the lowercased exact repository URL. With add-on slug +`stelloauth`, Home Assistant Core can resolve the add-on as +`0031621f-stelloauth` on the internal Supervisor network. + +The exact Stellantis Vehicles value is: + +```text +http://0031621f-stelloauth:8080/worker +``` + +The integration uses this value verbatim and does not append `/worker`. + +For troubleshooting only, the user may set the disabled host mapping to port +8080 in the add-on Network panel. The corresponding fallback is: + +```text +http://192.168.1.20:8080/worker +``` + +The documentation will tell the user to disable the host mapping again after +testing. + +## Configuration + +The default configuration works without user input: + +```yaml +queue_timeout: 60s +rate_limit_count: 5 +rate_limit_duration: 1h +``` + +The process manager maps these values to: + +- `CLOAK_CDP_URL=http://127.0.0.1:9222` +- `CLOAK_MAX_SESSIONS=1` +- `CLOAK_QUEUE_TIMEOUT=` +- `RATE_LIMIT_COUNT=` +- `RATE_LIMIT_DURATION=` +- `HTTP_ADDRESS=0.0.0.0` +- `PORT=8080` +- `METRICS_ADDRESS=127.0.0.1` +- `METRICS_PORT=9090` + +`CLOAK_MAX_SESSIONS` is fixed at one because the free CloakBrowser tier allows +one concurrent login and the add-on is designed for one household. The queue +absorbs overlapping setup/reauth attempts without launching extra browsers. + +No option contains MyOpel credentials, tokens, cookies, or OAuth codes. + +## Security hardening + +Upstream Stelloauth `v0.6.0` cannot be used unmodified because it logs email +addresses and full current/redirect URLs. A redirect URL contains the OAuth +code. Its worker also accepts an arbitrary authorize URL, creating a +browser-based SSRF surface. + +The minimal maintained patch will: + +1. Remove email addresses from `/oauth` and `/worker` log lines. +2. Replace full redirect/current/error URLs with fixed event messages. +3. Require worker URLs to use HTTPS, the default HTTPS port, and exact host/path + combinations for these current Stellantis identity providers: + - `idpcvs.citroen.com/am/oauth2/authorize` + - `idpcvs.driveds.com/am/oauth2/authorize` + - `idpcvs.opel.com/am/oauth2/authorize` + - `idpcvs.peugeot.com/am/oauth2/authorize` + - `idpcvs.vauxhall.co.uk/am/oauth2/authorize` +4. Limit worker request bodies to 64 KiB. +5. Use the direct remote address for rate limiting instead of trusting a + caller-supplied `X-Forwarded-For` header. + +The CloakBrowser wrapper patch changes the container listener from +`0.0.0.0:9222` to `127.0.0.1:9222`. Port 9222 is not declared in Supervisor +metadata and cannot be mapped through the add-on UI. + +Only Stelloauth listens on the Supervisor network. Its port is not mapped to +the HAOS host by default. No TLS is required for the worker call because the +request stays on the private container network; the subsequent Stellantis +traffic uses HTTPS. + +## Credential and temporary-data lifecycle + +Email and password exist only in the incoming request body and in process +memory while the OAuth flow runs. They are passed to the page through CDP and +are not written to `/data`. + +Each OAuth attempt uses a unique CloakBrowser fingerprint and profile under +`/tmp/cloakserve`. Stelloauth closes the browser target when the flow ends. +CloakBrowser runs with `--idle-timeout=30`, and the process manager removes +stale `/tmp/cloakserve` contents before startup. Container shutdown terminates +CloakBrowser, which closes remaining Chromium processes. + +No persistent credential, cookie, token, profile, screenshot, HTML dump, or +debug dump is created. The only persistent add-on data is Supervisor's +`/data/options.json` containing the non-secret options above. + +## Startup, readiness, health, and shutdown + +Startup order: + +1. Validate `/data/options.json` and reject malformed values without echoing + its content. +2. Remove stale CloakBrowser profiles from `/tmp/cloakserve`. +3. Start `cloakserve --idle-timeout=30`. +4. Wait with bounded backoff for `GET http://127.0.0.1:9222/`. +5. Request `/json/version?fingerprint=addon-readiness` and require a valid + `webSocketDebuggerUrl`. +6. Close the readiness profile with + `POST /fingerprint/addon-readiness/close`. +7. Start Stelloauth and wait for `GET http://127.0.0.1:8080/`. +8. Enter the process-monitor loop. + +Startup logs use fixed messages such as `Starting CloakBrowser`, +`CloakBrowser ready`, `Starting Stelloauth`, and +`Stelloauth listening on 0.0.0.0:8080`. + +The Supervisor watchdog checks `GET /`. This proves the Stelloauth listener is +alive. The process manager supplies the missing combined health property: if +CloakBrowser exits, it immediately stops Stelloauth and exits, causing the +watchdog/Supervisor restart path to recover both processes together. + +On SIGTERM or SIGINT, the manager sends SIGTERM to Stelloauth and CloakBrowser, +waits up to ten seconds, sends SIGKILL only to remaining children, reaps them, +and exits with the original shutdown status. + +## Testing and acceptance evidence + +Automated validation will cover: + +1. YAML syntax and required/current Home Assistant metadata fields. +2. Default options matching their schemas. +3. Repository ID and documented internal hostname derivation. +4. Patch application to the exact pinned upstream commits. +5. Unit tests for options, startup ordering, readiness, child failure, signal + forwarding, bounded shutdown, and no secret values in manager logs. +6. Stelloauth security tests for URL allowlisting, request-size limits, + forwarded-header spoofing, and redacted logs. +7. An amd64 Docker build and container run on the development host. +8. CloakBrowser startup and a real CDP `/json/version` probe. +9. Stelloauth startup and HTTP `GET /` response. +10. `POST /worker` contract behavior with invalid/non-Stellantis URLs, without + real MyOpel credentials. +11. Verification from outside the container that port 9222 is unreachable. +12. Graceful stop and restart with no remaining child processes. +13. Log scanning with sentinel email, password, cookie, OAuth code, access + token, and refresh token values. +14. Base-image manifest checks for amd64 and arm64 plus an arm64 build when the + local Docker builder supports emulation. +15. Idle RAM and image/disk size measurement. Login-flow RAM will be documented + as unmeasured unless real credentials are explicitly supplied for testing. + +Home Assistant's devcontainer/Supervisor validation is preferred when it is +available. Standalone Docker tests remain the reproducible minimum. + +## Documentation and release maintenance + +The root README explains repository installation. `stelloauth/DOCS.md` +contains the user guide shown by Supervisor: install, start-on-boot, watchdog, +log readiness, endpoint test, integration setup, Opel/DK selection, +troubleshooting, security model, resource results, and removal. + +`CHANGELOG.md` starts at `0.1.0` and names both pinned upstream versions. +Version updates require changing the pins/digests, reapplying security patches, +running the full validation, and reviewing upstream logging, worker validation, +license, endpoints, and image manifests before release. + +## Known limitations + +- The first local build is large because CloakBrowser contains Chromium. The + target currently has only about 4 GB free, so installation may require disk + cleanup after the measured build size is known. +- A complete MyOpel login and peak login RAM measurement cannot be performed + without real credentials. No credentials will be requested or stored merely + to make the build pass. +- The fixed allowlist must be updated if the Stellantis Vehicles integration + adds or changes identity-provider hosts. +- Changing the exact Gitea repository URL changes Supervisor's repository ID, + add-on hostname, and Login service URL. +- The add-on uses the free CloakBrowser tier and intentionally serializes login + flows. + +## Out of scope + +- Installation or deployment to the production Home Assistant host +- Docker Compose, Docker-in-Docker, Portainer, host networking, or HAOS system + partition changes +- Publishing a prebuilt image containing the CloakBrowser binary +- Modifying the Stellantis Vehicles integration +- Automated use of real MyOpel credentials +- Pushing commits to Gitea + +## Primary references + +- +- +- +- +- +- +-