Add-on security
Every privilege an add-on can request is declared in config.yaml, enforced by
the Supervisor, and reflected in a security rating from 1 (not secure) to 8
(highly secure) shown to users. This page explains the enforcement mechanisms
and how each declaration affects the rating.
The security rating
The rating starts at 5 and is adjusted:
| Declaration | Effect |
|---|---|
Custom AppArmor profile (apparmor.txt shipped) | +1 |
apparmor: false | −1 |
ingress: true | +2 |
auth_api: true (without ingress) | +1 |
| Signed image | +1 (signing is currently not supported, so this never applies) |
Any of the capabilities BPF, CHECKPOINT_RESTORE, DAC_READ_SEARCH, NET_ADMIN, NET_RAW, PERFMON, SYS_ADMIN, SYS_MODULE, SYS_PTRACE, SYS_RAWIO — or kernel_modules: true | −1 |
apexos_role: manager | −1 |
apexos_role: admin | −2 |
host_network: true | −1 |
host_pid: true | −2 |
host_uts: true combined with SYS_ADMIN | −1 |
docker_api: true or full_access: true | rating forced to 1 |
The result is clamped to 1–8. Practical takeaway: ingress and a custom AppArmor profile are the two things a developer can do to raise trust; Docker access or full hardware access marks the add-on maximally risky no matter what else it does.
AppArmor
AppArmor confinement is on by default (apparmor: true), which runs the
container under the Docker default profile. You have three postures:
- Default — no file shipped,
apparmor: true. Docker's standard profile. - Custom profile — ship an
apparmor.txtin the add-on directory. On install/update the Supervisor loads it into the host:- The file must contain exactly one
profiledeclaration — zero or multiple profiles are rejected as invalid. - The profile is renamed to the add-on slug before loading (the
profileline is rewritten), so the container is always started with the profile named after its slug; internal sub-profile references are adjusted to match. - When the add-on is uninstalled, or a new version no longer ships the file, the profile is removed from the host.
- A custom profile raises the security rating by 1.
- The file must contain exactly one
- Disabled —
apparmor: false. Lowers the rating; only for cases where confinement genuinely breaks the payload.
Roles and the Supervisor API
Every add-on container receives a SUPERVISOR_TOKEN environment variable. The
Supervisor's security middleware maps that token back to the add-on on every
request and enforces, in order:
- Blocklist — proxying Supervisor API calls through the Core API
(
/{core-proxy}/api/supervisor/...) is always forbidden, for everyone. - No-check paths — ingress traffic (
/ingress/<token>/...), frontend asset paths (add-on logos/icons), and/supervisor/pingskip token validation entirely. - Self-service bypass — an installed add-on may always call its own
endpoints:
/addons/self/<anything except security and update>and/addons/self/options/config, plus/info,/services...,/discovery..., and/auth. This is whybashio::configworks without any API grant. (/authadditionally requiresauth_api: trueat the endpoint level.) - Core-only paths —
/addons/<slug>/sys_optionscan only be called by ApexOS Core itself, never by an add-on token. - Role check — everything else is matched against the add-on's
apexos_role:
| Role | Reachable Supervisor API surface |
|---|---|
default | /<anything>/info endpoints only. |
apexos | default + the Core control/proxy trees (/core/..., /apexos/...). |
backup | default + /backups.... |
manager | default + add-on management (/addons/... — except other add-ons' security endpoints), /audio, /auth/cache, /available_updates, /backups, /cli, /core, /dns, /docker, /jobs, /hardware, /apexos, /host, /mounts, /multicast, /network, /observer, /os (except /os/datadisk/wipe), /refresh_updates, /resolution, /security, /snapshots, /store, /supervisor. |
admin | Everything. |
Requests an add-on's role does not permit return 403. Note the deliberate
carve-outs even for manager: it cannot change another add-on's security
settings and cannot wipe the data disk. The same policy applies to the /v2
API tree with the same role semantics.
Declaring apexos_api: true is what authorizes the add-on to use the API at
all beyond the self-service surface; apexos_core_api: true separately gates
the Core API proxy. Use the least role that works — manager and admin cost
rating points and reviewer scrutiny.
Ingress vs. exposed ports
Two ways to offer a web UI, with very different security properties:
Exposed ports (ports:) publish container ports on the host. Anyone who
can reach the host's network can reach the port — authentication is entirely
your add-on's problem. Users can remap or disable each port.
Ingress (ingress: true) keeps the add-on's web server internal. The
platform UI proxies requests to ingress_port on the add-on's internal
network address after validating the user's session; the add-on never listens
on the host. Session establishment, validation, and panel registration are
handled by the Supervisor (/ingress/session, /ingress/validate_session,
/ingress/panels), and each add-on gets a random per-installation ingress
token in its URL (/api/supervisor_ingress/<token>/). Ingress raises the
security rating by 2.
Dynamic ingress ports: ingress_port: 0 asks the Supervisor to assign a
port from the reserved range 62000–65500. To keep ingress authentication
airtight, an add-on using dynamic ingress may not map any host port from that
range via ports: — the config is rejected if it does, because a collision
would expose the ingress endpoint directly on the host, bypassing session
checks.
host_network implications
With host_network: true the container joins the host's network namespace
instead of the internal add-on network (172.30.32.0/23):
- Every port the add-on opens is open on the host itself; the
ports:map and the user's port overrides no longer mediate anything. - The add-on bypasses the network isolation between add-ons.
- The security rating drops by 1.
Use it only for services that genuinely need host-level networking (e.g. SMB
browsing/NetBIOS in the samba add-on); prefer ports: or ingress otherwise.
The related namespace shares (host_pid, host_ipc, host_uts, host_dbus)
each grant a specific slice of host visibility — host_pid is the most
sensitive (−2 rating) and, like full access, only takes effect when protection
mode is off.
Protection mode
Every installed add-on has a user-controlled protected flag, on by
default. It is not set by the developer — the user must deliberately disable
it per add-on. Protection mode is the last gate in front of the most dangerous
grants; even if config.yaml declares them, the Supervisor only applies these
while protection is disabled:
full_access— full device accesshost_pid— host PID namespacedocker_api— access to the Docker socket
When an add-on starts with protection disabled, the Supervisor logs a warning
("... running with disabled protected mode!"). From inside the add-on you
can check the flag and fail cleanly:
bashio::addon.protectedreturns the current state,bashio::require.unprotectedaborts with a clear explanation if the user has not disabled protection,bashio::require.protecteddoes the inverse.
Note that an add-on can never flip its own protection flag through the API:
the self-service bypass explicitly excludes security endpoints, and even
manager-role tokens cannot reach other add-ons' security settings — only
admin (or the user in the UI) can.
Hardening checklist for add-on authors
- Request the narrowest
map:entries, read-only unless you must write. - Prefer ingress over
ports:; never combineingress_port: 0with mapped ports in 62000–65500 (enforced). - Keep
apexos_roleatdefaultunless you call management endpoints. - Ship an
apparmor.txtfor anything with elevated privileges. - Gate dangerous functionality behind
bashio::require.unprotectedso it fails loudly with protection mode on. - Avoid
full_access; declare the specificdevices/uart/usb/gpioyou need (declaring both triggers a validation warning). - Passwords in options: use the
passwordschema type and considerbashio::config.require.safe_password.