Anyone who can talk to the LXD or Incus daemon can attach host file systems and devices to an instance, so the daemon socket, its trust store and its group membership are effectively root on the host. Both managers run full Linux system containers and virtual machines from one API, and both default to unprivileged containers, but a few settings (privileged mode, nesting, raw options, device passthrough, a remote API bound to every interface) undo that isolation quickly. This guide covers the security model the two projects share and where they differ: local and remote access, user namespaces and idmaps, projects and restrictions, fine-grained authorisation, network spoofing controls, logging and a hardening baseline. It is for Linux platform engineers, homelab and lab operators moving to production, and security teams reviewing container hosts. Settings were checked against the LXD 7.0 and current Incus documentation on 8 November 2026; general container and image security is in the Container Security Guide.
Incus is the community-driven alternative to Canonical's LXD, maintained under the Linux Containers project by many of LXD's original developers, and it keeps most configuration keys. Where the two differ, tables below list them alphabetically (Incus, then LXD); the order is not a ranking.
This is an independent guide from NHI Mgmt Group, based on Canonical's public documentation as of November 2026. It is not sponsored or reviewed by Canonical. Product features change, so confirm settings against Canonical's current documentation.
Key takeaways
- The admin group is a root equivalent. Members of
incus-adminorlxdget full API control over the Unix socket, including mounting host paths into instances. Treat membership like sudo and audit it. - Keep containers unprivileged and isolated.
security.privileged=falseis the default; addsecurity.idmap.isolated=trueso containers do not share a UID/GID range, and never combinesecurity.nesting=truewith privileged mode. - Use projects with restrictions for anyone who is not an admin. Setting
restricted=trueon a project blocks privileged containers, low-level raw options, nesting and most device passthrough by default. - Bind the remote API to one address and trust only what you issue. Set
core.https_addressto a full IP and port, keepcore.trust_ca_certificates=false, shorten trust token expiry and prefer OIDC with MFA for people. - Bridged instances can spoof by default. Turn on
security.mac_filtering,security.ipv4_filteringandsecurity.ipv6_filteringin profiles for untrusted workloads, and use network ACLs.
How identity and access work in LXD and Incus
Both daemons run as root and expose a REST API over two channels: a local Unix socket, always on, and an optional HTTPS listener for remote clients. Inside each instance a third channel, the guest API socket (DevLXD in LXD, /dev/incus in Incus), answers only queries about the requesting instance.
| Access path | Incus | LXD | Security meaning |
|---|---|---|---|
| Local socket, full access | incus-admin group and root | lxd group (daemon.group) and root | Unrestricted API, host path and device attachment |
| Local socket, confined | incus group: each user is restricted to a single project tied to their user | daemon.user.group (snap) confines members to a project via the socket | The supported way to give non-admins local access |
| Remote, certificate | Trusted TLS clients added by token; incus config trust list | Trusted TLS clients added by token or PKI; lxc config trust list | Unrestricted certificates are full admins |
| Remote, people | OpenID Connect (oidc.*) | OpenID Connect (oidc.issuer, oidc.audience, oidc.groups.claim) | Lets the identity provider enforce MFA and offboarding |
| Fine-grained authorisation | External OpenFGA server (authorization.openfga.*), applied to client classes routed to it by authorization.client.* | Built-in groups, entitlements and permissions (lxc auth) | Per-resource rights instead of all or nothing |
Remote trust in practice
- Trust tokens: a client runs
remote add, sees the server certificate fingerprint, and presents a one-time token; the server then adds the client certificate to its trust store. The LXD hardening guide recommends cuttingcore.remote_token_expiryandcluster.join_token_expiryto 15 minutes (the cluster join token default is 3 hours). - PKI mode: clients with certificates from your CA can connect, but
core.trust_ca_certificatesisfalseby default and should stay that way so a CA-signed certificate does not become trusted automatically. Use a certificate revocation list and short-lived certificates. - Fingerprint verification: the first connection is trust on first use. Compare the fingerprint shown with the one on the server before accepting, or an interception at that moment becomes permanent.
- Revocation: remove trust with
config trust remove <fingerprint>; regular review of the trust store is the only thing that catches laptops and CI runners that were retired without cleanup.
User namespaces and idmaps
An unprivileged container runs in a user namespace: its UID 0 maps to an unprivileged UID on the host, so most container escapes become an ordinary unprivileged-user kernel bug rather than host root. The LXD docs require allocations of at least 65,536 UIDs and GIDs; on hosts without /etc/subuid and /etc/subgid LXD assumes a range starting at base UID 1,000,000 (the snap ignores those files entirely). By default every unprivileged container shares the same map, which means a process in one container has the same host UID as the matching process in another.
| Mode | Setting | Isolation | When to use |
|---|---|---|---|
| Privileged | security.privileged=true | Container root is host root; the LXD docs state these are not root-safe | Trusted legacy workloads only, ideally in a dedicated project or VM instead |
| Unprivileged, shared map | Default | Container root is an unprivileged host UID, shared across containers | Single-tenant hosts |
| Unprivileged, isolated map | security.idmap.isolated=true | Each container gets a non-overlapping range, limiting cross-container DoS | Default for multi-tenant and untrusted workloads |
| Custom mapping | raw.idmap | Maps specific host IDs into the container (for shared data) | Only where needed; restrict with restricted.idmap.uid and restricted.idmap.gid in projects |
| Virtual machine | Instance type VM | Separate guest kernel under QEMU | Untrusted code, kernel modules, different OS |
For the kernel mechanisms underneath, see the Linux Namespaces and cgroups Security Guide.
Admin roles and privileged access
There are only two safe shapes for human access: a small admin group with full rights, and everyone else confined to restricted projects.
- Separate the groups: both
daemon.groupanddaemon.user.groupdefault tolxdin the LXD snap, which gives every member full access. The hardening guide recommends a separate group such aslxdusersfor non-admins. Incus ships the split already (incus-adminversusincus). - Restricted TLS certificates: set
restricted: trueand a project list withconfig trust edit. A restricted client cannot change global configuration or the limits and restrictions of its own projects; an empty project list means no access. - LXD fine-grained authorisation: identities (OIDC users and fine-grained TLS identities) join groups, and groups receive permissions. Useful entitlements:
adminonserver(full access),project_manageronserver(projects but not server, storage pool or identity configuration),operatoron aproject(all resources in it, not the project config),useron aninstance(view, files, console, terminal). Map identity provider groups withoidc.groups.claim. - Incus OpenFGA: run your own OpenFGA server; Incus writes its model and queries it for every request from client classes routed to OpenFGA. Unrestricted certificates still get full access, so route remote clients deliberately.
Project restrictions that matter
Key (with restricted=true) | Default when restricted | Blocks |
|---|---|---|
restricted.containers.privilege | unprivileged | Privileged containers |
restricted.containers.nesting | block | Running nested container managers |
restricted.containers.lowlevel and restricted.virtual-machines.lowlevel | block | raw.lxc, raw.apparmor, raw.seccomp, raw.qemu style options |
restricted.containers.interception | block | System call interception options |
restricted.devices.disk and .disk.paths | managed | Arbitrary host path mounts |
restricted.devices.pci, usb, gpu, unix-char, unix-block, proxy | block | Device passthrough |
restricted.devices.nic and restricted.networks.* | managed / block | Attaching to host interfaces or uplinks |
restricted.idmap.uid and .gid | None allowed | Mapping host IDs with raw.idmap |
Combine restrictions with project limits (limits.instances, limits.cpu, limits.memory, limits.processes, limits.disk): instance CPU, memory and process limits are unset by default, which leaves the host open to resource exhaustion from one tenant.
Integrations, API tokens and service accounts
Automation (Terraform providers, Ansible, CI runners, cluster members, monitoring) usually authenticates with a client certificate, which makes each certificate a long-lived non-human identity with whatever scope its trust entry grants.
- One certificate per automation, restricted: give each tool its own certificate, restricted to the projects it manages; never reuse an admin's client certificate in a pipeline. Name trust entries so the owner is obvious.
- Protect the private key: the client key in the user's config directory is a full credential for that remote. Store pipeline keys in a secrets manager and inject them at run time. See the Secrets Management Guide.
- Bearer tokens from certificates: Incus can derive a JWT bearer token from a client certificate (
remote get-client-token) for browser clients; it carries the same rights as the certificate, so handle it as a secret. - OIDC client secret: LXD's hardening guide says to set
oidc.client.secretonly if the identity provider requires it, and to configure short-lived access tokens.oidc.session.expirydefaults to one week in LXD. - Metrics:
core.metrics_authenticationdefaults totrue; give Prometheus a dedicated metrics certificate rather than disabling authentication. - Image downloads as a probe: LXD notes that a user who can create images can make the daemon fetch from internal addresses, and error messages may reveal internal services. Route outbound requests through a filtering proxy with both
core.proxy_httpandcore.proxy_https. - Guest API exposure:
security.devlxdis on by default but returns only the instance's own data; keepsecurity.devlxd.imagesandsecurity.devlxd.management.volumesoff unless a workload needs them.
Where identity fits: the trust store is an inventory of machine identities. Review it on the same cadence as service accounts; see the SSH Key Management Guide for the equivalent discipline on SSH keys.
Logging and detection
Recent LXD releases emit a dedicated security event stream (lxc monitor --type=security, or the /1.0/events WebSocket with type=security) and both projects can forward events to Loki (loki.api.url, loki.types, which defaults to lifecycle and logging events, so add security events explicitly where supported). Lifecycle events record instance creation, configuration changes and deletion.
| Signal | Source | Why it matters |
|---|---|---|
authn_login_fail:tls | LXD security events | Untrusted certificates probing the remote API (T1133) |
authn_token_created, authn_token_reuse, authn_token_revoked | LXD security events | Trust tokens issued outside change windows; reuse suggests leakage |
authn_certificate_change, user_created, user_updated, user_deleted | LXD security events | Trust store and identity changes (T1098 Account Manipulation) |
Config changes to security.privileged, security.nesting, raw.*, new disk or PCI devices | Lifecycle events in both projects | The classic route to host escape (T1611) |
| New instances outside automation | Lifecycle events | Unauthorised workloads (T1610 Deploy Container) |
Execution of lxc and lxd binaries | auditd rules, as in the LXD hardening guide | Host-side record of who used the CLI |
Group membership changes for lxd or incus-admin | Host auth logs | Silent privilege escalation to host root |
For the snap, LXD documents daemon.syslog=true and daemon.verbose=true. Network ACLs can log unmatched traffic with security.acls.default.ingress.logged and security.acls.default.egress.logged.
Metrics
- Privileged instances: count of instances with
security.privileged=trueorsecurity.nesting=true(target zero outside an approved exception list). - Isolation coverage: percentage of containers with
security.idmap.isolated=trueon multi-tenant hosts. - Trust store hygiene: unrestricted certificates (target: admins only) and certificates unused for 90 days.
- Confinement: percentage of non-admin users confined to restricted projects.
- Patch lag: days between an LXD or Incus security release and deployment on all hosts.
Hardening baseline
- Inventory first: list instances with
security.privileged,security.nesting, anyraw.*key, disk devices with host sources and PCI or USB devices; list trust store entries and admin group members. - Lock down access: reduce admin group membership, create the confined group, restrict every automation certificate to its projects, and shorten token expiry to 15 minutes.
- Remote API: bind
core.https_addressto a management IP and port (for example192.0.2.10:8443, or127.0.0.1:8443for local HTTPS only), never:8443, which listens on every interface. Firewall it. If a proxy fronts it, list it incore.https_trusted_proxyonly. - Build a hardened profile: CPU, memory and process limits,
security.idmap.isolated=true,security.nesting=false, NICsecurity.mac_filtering,ipv4_filteringandipv6_filtering, read-only disk devices where possible. Apply it to a pilot project first and confirm workloads still start (isolated maps change file ownership on existing containers, so test on copies). - Restricted projects for tenants: set
restricted=trueand loosen individual keys only with a documented reason. - Network: apply network ACLs with default ingress and egress reject; the bridge otherwise accepts IPv6 router advertisements from instances (
accept_rais set to 2 on the bridge). - Logging: forward lifecycle and security events to Loki or your SIEM, add
auditdrules, alert on the signals above. - Roll back safely: keep the previous profile version, snapshot instances before applying idmap or device changes, and use
security.protection.delete=trueon critical instances.
Decision guidance
- Container or VM: use system containers for trusted Linux workloads that benefit from density; use VMs for untrusted code, anything needing kernel modules or privileged mode, and tenants you do not control.
- Incus or LXD fine-grained access: LXD's authorisation is built in and managed with
lxc auth; Incus delegates to an OpenFGA server you operate. Small teams with one or two admins can rely on restricted certificates and projects in either. - Single host or cluster: clusters add join tokens and inter-member certificates; shorten join token expiry and treat each member as a tier zero asset, as you would a hypervisor. See the Proxmox VE Security Hardening Guide for comparable hypervisor-level practice.
Common misconfigurations
- Developers in the admin group for convenience: any member can attach
/from the host into an instance. Use the confined group and restricted projects instead. - Privileged plus nesting: the LXD hardening guide warns that
security.nesting=truewithsecurity.privileged=trueprovides root access to the host. - Remote API on every interface:
core.https_address=:8443on an internet-facing host exposes the trust token flow and API to the world. - Auto-trusting a CA: enabling
core.trust_ca_certificatesmakes every certificate your CA ever issued an admin unless restricted. - Shared idmaps on multi-tenant hosts: containers share host UIDs, so a compromised container can exhaust resources owned by that UID in others.
- Bridges without filtering: untrusted instances can spoof MAC and IP addresses on
lxdbr0orincusbr0and send router advertisements.
Framework mapping
| Control | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ATT&CK addressed |
|---|---|---|---|
| Admin group and trust store review | AC-2, AC-6, IA-5 | PR.AA-05 | T1078 Valid Accounts, T1098 Account Manipulation |
| Restricted projects and fine-grained authorisation | AC-3, AC-6 | PR.AA-05 | T1610 Deploy Container |
| Unprivileged containers, isolated idmaps, no nesting | SC-39, CM-7 | PR.PS-01 | T1611 Escape to Host, T1068 Exploitation for Privilege Escalation |
| Remote API binding, TLS, firewall | SC-7, SC-8 | PR.IR-01 | T1133 External Remote Services |
| Hardened profiles and configuration baselines | CM-6 | PR.PS-01 | T1611 Escape to Host |
| Security and lifecycle events, auditd | AU-2, AU-6 | PR.PS-04, DE.CM-09 | T1610 Deploy Container |
Practitioner checklist
- Limit the
lxdorincus-admingroup to people trusted with sudo, and audit it monthly. - Confine non-admin users with a separate group and restricted projects.
- Restrict every automation certificate to named projects; remove unused certificates.
- Keep
core.trust_ca_certificates=falseand set trust and join token expiry to 15 minutes. - Bind
core.https_addressto one management IP and port behind a firewall. - Apply a hardened profile: limits,
security.idmap.isolated=true, nesting off, NIC spoofing filters. - Run untrusted or privileged workloads as VMs, not privileged containers.
- Route image downloads through a filtering proxy.
- Forward security and lifecycle events to your SIEM and alert on privilege and device changes.
- Stay on a supported LTS or current feature release and patch promptly.
Standards and references
- Canonical: How to harden security for LXD
- Canonical: LXD security
- Canonical: LXD remote API authorization
- Linux Containers: Incus authorization
- Linux Containers: Incus remote API authentication
- Linux Containers: About Incus security
- MITRE ATT&CK: Escape to Host
- NIST: SP 800-53 Rev. 5
Related NHI Mgmt Group resources: Container Security Guide · Linux Namespaces and cgroups Security Guide · Proxmox VE Security Hardening Guide · Secrets Management Guide