364 lines
14 KiB
Markdown
364 lines
14 KiB
Markdown
# 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=<queue_timeout>`
|
|
- `RATE_LIMIT_COUNT=<rate_limit_count>`
|
|
- `RATE_LIMIT_DURATION=<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
|
|
|
|
- <https://github.com/tamcore/stelloauth/tree/v0.6.0>
|
|
- <https://github.com/CloakHQ/CloakBrowser/tree/v0.5.10>
|
|
- <https://github.com/andreadegiovine/homeassistant-stellantis-vehicles/releases/tag/2026.9.4>
|
|
- <https://developers.home-assistant.io/docs/apps/configuration/>
|
|
- <https://developers.home-assistant.io/docs/apps/communication/>
|
|
- <https://developers.home-assistant.io/docs/apps/repository/>
|
|
- <https://developers.home-assistant.io/docs/apps/testing/>
|