Skip to main content

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:

DeclarationEffect
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: truerating 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:

  1. Default — no file shipped, apparmor: true. Docker's standard profile.
  2. Custom profile — ship an apparmor.txt in the add-on directory. On install/update the Supervisor loads it into the host:
    • The file must contain exactly one profile declaration — zero or multiple profiles are rejected as invalid.
    • The profile is renamed to the add-on slug before loading (the profile line 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.
  3. Disabledapparmor: 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:

  1. Blocklist — proxying Supervisor API calls through the Core API (/{core-proxy}/api/supervisor/...) is always forbidden, for everyone.
  2. No-check paths — ingress traffic (/ingress/<token>/...), frontend asset paths (add-on logos/icons), and /supervisor/ping skip token validation entirely.
  3. 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 why bashio::config works without any API grant. (/auth additionally requires auth_api: true at the endpoint level.)
  4. Core-only paths/addons/<slug>/sys_options can only be called by ApexOS Core itself, never by an add-on token.
  5. Role check — everything else is matched against the add-on's apexos_role:
RoleReachable Supervisor API surface
default/<anything>/info endpoints only.
apexosdefault + the Core control/proxy trees (/core/..., /apexos/...).
backupdefault + /backups....
managerdefault + 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.
adminEverything.

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 access
  • host_pid — host PID namespace
  • docker_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.protected returns the current state,
  • bashio::require.unprotected aborts with a clear explanation if the user has not disabled protection,
  • bashio::require.protected does 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 combine ingress_port: 0 with mapped ports in 62000–65500 (enforced).
  • Keep apexos_role at default unless you call management endpoints.
  • Ship an apparmor.txt for anything with elevated privileges.
  • Gate dangerous functionality behind bashio::require.unprotected so it fails loudly with protection mode on.
  • Avoid full_access; declare the specific devices/uart/usb/gpio you need (declaring both triggers a validation warning).
  • Passwords in options: use the password schema type and consider bashio::config.require.safe_password.