Security posture
ha-paneld runs a foreground agent on a wall panel exposing an HTTP API (:8888), an MQTT control plane, a root helper daemon, and an opt-in CDP/DevTools relay. Relaxed mode keeps the turnkey LAN-trust, no-token pairing by default. Optional Hardened mode requires physical access for selected high-impact network operations: someone must approve them on the panel’s screen, and they cannot be approved remotely. The remaining unauthenticated API is an accepted residual risk, with network segmentation as the broader control and Home Assistant’s authentication model as the chosen route if general authentication is added. This page records the threat model and the decisions taken.
Trust model
Section titled “Trust model”The panel and Home Assistant normally sit on a trusted LAN, and the app is built around a turnkey, no-token pairing experience. A root or file-level attacker on the panel itself is out of scope (they already own the device). Software already running on the panel is also trusted for the Hardened-mode boundary because loopback requests cannot be attributed securely to the ha-paneld UI rather than another local Android app. Hardened mode is an opt-in physical-presence boundary for networks where not every remote client should be allowed to perform destructive or credential-bearing operations; it is not a complete authentication layer.
Attack surface
Section titled “Attack surface”| Surface | Exposure | Notes |
|---|---|---|
HTTP API :8888 |
unauthenticated; binds dual-stack (::) |
Relaxed mode trusts LAN callers; Hardened mode adds physical approval to selected high-impact routes |
| Runtime profile import/activation | trusted-LAN HTTP workflow; activation is approval-gated in Hardened mode | untrusted YAML is previewed, validated, stored as immutable revisions and explicitly activated; it can select only compiled drivers and cannot grant authority |
CDP / WebView DevTools relay :9222 |
opt-in in Relaxed mode (started via /inspect/start), binds 0.0.0.0, unauthenticated |
full dashboard-session access if started; Hardened mode stops and verifies the process and listener before activation, then prevents restart |
| MQTT | governed by the broker | the intended control plane; inherent to MQTT discovery |
Camera RTSP :8554 |
off by default; while the camera setting is on it binds every interface and serves the video-only stream unauthenticated | enabling it from Home Assistant needs approval at the panel in every mode, and the camera cannot open unless the on-screen indicator can be drawn; treat the stream as LAN-visible for as long as it is on |
Root helper daemon (abstract UNIX socket @hapaneld-helper) |
mutually peer-uid authenticated (SO_PEERCRED: server must be root; client must be the current ha-paneld uid or root), command-allowlisted, character-sanitised, bounded parsing, connection caps |
The app rejects a non-root process that claims the abstract name, and the generic Android shell uid is excluded so Shizuku’s shell-only boundary cannot become root. Its only private-app operation is an explicit Home Assistant mobile app login backup/restore confined to an allowlisted package and fixed descriptor-relative files, with size bounds, staged validation and a recovery journal. |
| Shizuku enhanced access (see the command-line install guide) | optional; enabled and approved only on the panel | Official manager signer is pinned; the UserService must report UID 2000 and protocol v2; methods and inputs are typed and bounded, with no generic command or filesystem access. Shell is not root. Consent is excluded from Android backup, config bundles, restore, and pushes to several panels. An ADB-started service may need rearming after reboot. |
| Network ADB | opt-in switch; opens 5555 to the LAN when enabled; Android may separately expose paired Wireless debugging on a dynamic TLS port |
documented and off by default; classic TCP ADB and Android Wireless debugging are mutually exclusive with Hardened mode because an ADB client could inject input into the approval screen |
| Accessibility service | key-capture only (no window-content retrieval) | Sound |
| su call-sites | CPU governor regex-sanitised; relay/adb/cdp use constants; Zigbee role allowlisted before shell interpolation | no remaining injection paths |
The state-changing routes on :8888 are where the exposure concentrates:
POST /configsets MQTT broker, credentials and panel ID. In Hardened mode a changed broker or Home Assistant origin cannot inherit the credential stored for the previous endpoint: a replacement credential must arrive in the same save or the old one is cleared. Repointing remains a denial-of-service risk because routine configuration is not generally approval-gated.POST /playdownloads and plays an arbitrary HTTP(S) URL; HTTPS uses the platform trust store and normal hostname verification, while cleartext HTTP remains available for LAN sources. Remote media requests require physical approval in Hardened mode.POST /api/v1/inspect/startstarts the CDP relay (the:9222surface above) in Relaxed mode; Hardened mode closes and disables this surface rather than approving it.- Profile save/activate/rollback routes can change which compiled hardware drivers and capability candidates the panel uses after a controlled service restart. Preview tokens bind save to the exact inspected YAML, activation pins an exact revision/hash, risky declarations are highlighted, and startup falls back to the last-known-good revision. Activation and rollback require physical approval in Hardened mode.
GET /diagreturns device and capability information (useful for reconnaissance). It is intentionally a support tool.GET /api/v1/screenshot.pngandPOST /api/v1/inputview and inject input on the panel. Root/helper routes already expose these; locally approved Shizuku makes the same trusted-LAN controls available on a non-root panel. Hardened mode keeps screenshots available but rejects non-loopback input so a remote caller cannot operate the native approval UI.POST /api/v1/install/componentcan install signer-pinned ha-paneld or minimal Home Assistant mobile app builds. Shizuku enables those verified updates on a non-root panel, but does not enable arbitrary APK upload or WebView replacement. Component installation and an inspected uploaded APK’s commit are protected by Hardened mode.- Full backup export and restore routes can carry configuration secrets and, when explicitly selected on a rooted or helper-backed panel, the allowlisted Home Assistant mobile app login files. Hardened mode approval protects network export and restore; encrypted
.hpbarchives remain the recommended portable format. The helper has no general private-filesystem command and the mobile app login transfer is descriptor-confined, size-bounded and transactionally restored.
Provisioning credentials
Section titled “Provisioning credentials”Host-side provisioning accepts MQTT passwords, Home Assistant long-lived tokens and Home Assistant login passwords through --mqtt-pass-file, --ha-token-file and --ha-pass-file. The one-line installer rewrites the literal-value flags to private temporary files before it starts the authenticated provisioner, preventing an avoidable second copy in the child command line. It cannot portably erase the literal from the user’s original shell history or installer process, so public instructions use the file options and retain literal flags only for compatibility. Secret values are not exported through the helper installer: adb, su and the root transaction journals carry only authenticated artifact hashes, build identities, nonces and staging paths.
Credential files address exposure on the provisioning host, not transport confidentiality. The management endpoint remains cleartext http://<panel>:8888 under the trusted-LAN model. The Home Assistant password is submitted directly from the host to the configured Home Assistant login endpoint and never reaches the panel; an http:// Home Assistant URL sends it without transport encryption, so credential login should use HTTPS. Supplied or newly minted access/refresh tokens, the MQTT password, secret config export and config restore cross the host-to-panel management connection without TLS. Hardened mode can require physical approval for protected requests; it does not encrypt them. Operators must keep provisioning and the management API on a trusted, segmented network.
Browser-mediated attacks
Section titled “Browser-mediated attacks”Browser-mediated attacks are guarded by OriginGuard:
- CSRF: OriginGuard refuses a state-changing request (
POST/PUT/PATCH/DELETE) whoseOrigin/Refereris present and doesn’t match the requestHost, so a malicious LAN web page can’t silently drive these endpoints. Same-origin UIfetches and header-less API clients (curl, Home Assistantrest_command) are unaffected. - DNS rebinding: the
Hostheader must be an IP literal,localhost,*.local(mDNS), or an operator-configured name (http_allowed_hosts); any other hostname is refused (all methods), so an attacker who rebinds their own DNS name to the panel can’t pose as same-origin to read secrets (GET /config/export) or drive the surface. Reaching a panel by IP, which is the norm, is always allowed and is inherently immune to rebinding.
Every request passes the rebinding check first; what happens next depends on whether it changes state, starts privileged work, or is an ordinary read:
- Host check. A request with no
Hostheader passes, because there is no name to rebind. A presentHostmust be an IP literal,localhost,*.localor listed inhttp_allowed_hosts, or the request is refused. - State-changing methods (
POST,PUT,PATCH,DELETE). With noOriginorReferer, the request is allowed as a header-less LAN API client. With one present, it must be parseable and its authority must equal theHost; otherwise it is refused as a cross-origin write. - Active
GETs (those that start capture, a subprocess, sampling or a network refresh). ASec-Fetch-Siteofcross-siteorsame-siteis refused. Otherwise, a presentOriginorReferergoes through the same parse-and-match check as a write. With neither, a request whose user agent looks like a browser but which carries no Fetch Metadata is refused; anything else is allowed as automation. - Ordinary
GETs are allowed, because the browser’s same-origin policy already blocks reading the response cross-origin.
Three behaviours there are easy to misread. A missing Host passes the rebinding check, but a state-changing request with no Host is refused, as is one whose Origin is present but unparseable. Ordinary GETs are deliberately not origin-guarded; it is the side effect of a write, or of an active read, that needs guarding. And active GETs take a third path keyed on Fetch Metadata, failing closed for a browser-shaped request that supplies no positive same-origin evidence while leaving header-less automation working.
Neither guard authenticates the caller. Hardened mode can require physical approval for a protected operation, but decision 3 describes the route for general caller authentication.
APK-installer downloads through AppInstaller are HTTPS-only: the initial URL and every redirect hop must be https, as defence in depth on top of the post-download signer and package pin, which already blocks installing a substituted APK.
Runtime profile files are treated as untrusted data, not plugins. The closed schema can select only drivers compiled into ha-paneld; it carries no shell commands, helper verbs, native code or general scripting. Privileged paths are restricted to core allowlists, and WebView recommendations select only core-owned artifact IDs whose HTTPS URL, version and signer hash are compiled into the app. Security-sensitive driver parameters are validated by the owning driver, and declarations never substitute for live root/helper/Shizuku or hardware probes. A Shizuku recommendation cannot install the Manager, record local consent or approve ha-paneld.
Decisions
Section titled “Decisions”- No bespoke API token. A per-panel secret is throwaway and breaks the easy setup. (Rejected.)
- No in-app network allowlist. Restricting who can reach
:8888is delegated to the network layer (router, VLAN or firewall segmentation) rather than reinvented in the app, consistent with leaning on existing platform and infrastructure capabilities. Home Assistant’s own IP allowlist andip_bansecure Home Assistant’s HTTP server, not the panel’s separate:8888, so they do not apply here; network segmentation is the existing control. The in-app path, when warranted, is the Home Assistant authentication model (decision 3). - No general caller authentication. The panel app does not authenticate callers on the local network; the trust model above and Hardened mode are the controls.
- Credential-at-rest encryption is out of scope. MQTT credentials live in app SharedPreferences; encrypting them only defends against a root or file-level attacker who already owns the device, at the cost of a deprecated
security-cryptodependency and a credential migration on installed panels. The value is low for the cost. /playkeeps standard HTTPS verification while allowing cleartext HTTP for LAN audio sources under the LAN-trust model.- Hardened mode is opt-in and device-local. Relaxed mode remains the default so Home Assistant workflows and tooling that manages several panels do not acquire unattended prompts. Hardened approval is process-local, bound to the HTTP peer or shared MQTT command channel and the exact protected request, valid for ten minutes and consumed by one matching retry. It cannot be enabled by a network request, backup, restore or an operation across several panels, and it cannot coexist with classic TCP ADB, persistent or explicitly addressed ADB listeners, Android Wireless debugging, or the LAN WebView developer-tools relay. Entry fails closed unless those remote-control paths are verified inactive. Non-loopback tap injection is unavailable while it is active. Loopback software already running on the panel remains trusted. Hardened mode covers credential-bearing export and restore, software and package mutation including a download bound to one exact URL, profile activation, reboot, display and renderer maintenance, database recovery maintenance, and remote media. Enabling the panel camera is approved in every mode rather than only in Hardened mode.
Comments
Comments are kept in this page's discussion on GitHub, so loading them connects your browser to GitHub. This browser remembers the choice and loads comments on other pages when you scroll down to them.
Comment without a GitHub account