Feature Specification: Component incus
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/components/hosting-provider/incus/default.nix, core/components/hosting-provider/incus/oidc.nix, core/components/hosting-provider/incus/runtime/module.nix, core/components/hosting-provider/incus.nix, core/components/hosting-provider/incus-attest.nix, core/components/hosting-provider/incus-bridge.nix, core/components/hosting-provider/incus-bridge-dnsmasq.nix, core/components/hosting-provider/incus-hwrng-udev.nix, core/components/hosting-provider/incus-tpm-udev.nix, core/components/hosting-provider/incus-store-identity.nix, core/components/hosting-provider/incus-store-identity-test.nix, core/components/hosting-provider/incus-store-serve.nix, core/components/hosting-provider/incus-test.nix).
User Scenarios & Testing (mandatory)
User Story 1 - Run the hypervisor for the guest nodes (Priority: P1)
A hosting provider enables incus on a host node. The host runs the Incus
daemon with a bridge, a storage pool and a default profile. The deployment
methods then create the other nodes of the group as Incus instances on this
host.
Why this priority: The tf-incus deployment methods need a hypervisor.
Without it, no guest node exists.
Independent Test: test-incus-service (core/components/hosting-provider/incus-test.nix). The bridge DNS answers have integrations-bridge-dns.
Acceptance Scenarios:
- Given a node that imports
incus.nix, When it boots, Thenincus listsucceeds and theincusbr0network, thedefaultstorage pool and thedefaultprofile exist. - Given
fediversity.incus.uiDomainis set, When the firewall loads, Then ports8443and443are in theinput-allowchain. - Given a guest on the bridge, When it resolves
dnsmasq.<apex>or a name inbridgeCnames, Then dnsmasq on the bridge answers it.
User Story 2 - Sign in to the Incus web UI (Priority: P2)
An administrator opens https://incus.<apex>/. nginx terminates TLS and
proxies to the daemon. The daemon accepts an OIDC login from the identity
provider of the group.
Why this priority: The UI is the operator-visible face of the
hypervisor. The overview page links to it through uiSubdomain.
Independent Test: test-incus-service checks that a client reaches https://incus/. nix-unit-setups-hypervisor-config checks the served UI port and the published UI name. nix-unit-components-incus-oidc checks the OIDC settings of the daemon. test-incus-oidc-login (core/components/hosting-provider/incus/oidc-login-test.nix) does the OIDC login through authelia and its lldap directory, with core/testers/authelia-oidc-login.py in place of the browser.
Acceptance Scenarios:
- Given the component is enabled, When a browser opens
https://<domain>/, Then nginx proxies the request tohttps://127.0.0.1:8443with websocket upgrade. - Given an OIDC provider answers the
incus-oidcrequest, When the node activates, Then the server config ofvirtualisation.incus.runtimecarriesoidc.issuer,oidc.client.idandoidc.scopes. - Given a user of the directory of the identity provider, When the user logs in at
https://<domain>/oidc/loginand accepts the consent, ThenGET /1.0with the session cookies of incus givesauth: trustedandauth_user_method: oidc, and without themauth: untrusted.
User Story 3 - Let guests share the host store (Priority: P2)
A deployer builds a guest closure once on the host. Guests in shared-store
mode read the host /nix/store. A scoped SSH identity lets the deployer copy
signed closures in and set per-instance profiles.
Why this priority: Shared-store mode saves disk space and build time on
the host. It is the default nixStore mode of a component.
Independent Test: integrations-incus-shared-store, nix-unit-setups-shared-store and nix-unit-components-incus-store-identity.
Acceptance Scenarios:
- Given
fediversity.incus.storeServe.enable, When theincus-storeuser logs in over SSH, ThenForceCommandrunsincus-store-serve, which dispatchesincus-store-profile set|removeor runsnix-daemon --stdio. - Given the minted public halves change, When the path unit fires, Then
incus-store-serve-trustrewritesauthorized_keysand the trusted signing key, and stopsnix-daemon. - Given the host SSH key, When
incus-store-host-key-publishruns, Then OpenBao KV<prefix>/public/incus-store/sshHostPublicKeyholds that key.
User Story 4 - Attest the host and publish deploy tokens (Priority: P3)
The host registers its TPM endorsement key with SPIRE. It also mints Incus trust tokens for the deploy runners and publishes them to OpenBao KV.
Why this priority: These tasks let remote deployers and guest SPIRE agents trust the host. A host without SPIRE or runners works without them.
Independent Test: integrations-spire-incus, integrations-register-ek-evidence, integrations-incus-token-publish and nix-unit-components-incus-deploy-tokens.
Acceptance Scenarios:
- Given
spireInstanceandspireHostNameare set, When the host boots, Thenhost-swtpmprovides a vTPM andhost-register-ekregistersspiffe://fediversity/host/<network.name>. - Given
deployRunnersnames a runner, Whenincus-deploy-token-publishruns and a KV entry or trust name is missing, Then it mintsdeployer-hosts-<runner>anddeployer-image-<runner>and writestokenHostsandtokenImageto KV.
Edge Cases
- If the KV probe write fails,
incus-deploy-token-publishexits with an error and retries every 30 seconds without a start limit. - A runner whose KV entries and trust names all exist gets no new token.
virtualisation.incus.packageispkgs.incus(not LTS). Its database migration cannot go back to an older version.- The host has no TPM resource manager other than
tpmrm0in use. A udev rule clearsSYSTEMD_WANTSfor othertpmrmdevices, anddev-tpmrm0.devicewaits at most 10 seconds. - A TPM never stays the hardware RNG source. A udev rule sets
rng_currenttononefortpm-rng-*. /run/varsis tmpfs, so the store-serve keys are minted again at boot. The KV pair then replaces them, and the path unit applies the change.- Incus has no fine-grained authorization. Every OIDC user is an administrator, so admission control is at the identity provider.
Requirements (mandatory)
Functional Requirements
- FR-001: The component MUST declare
name = "Incus",package = pkgs.incus, the Incus logo as theiconof itsartwork,uiSubdomain = "incus",certAudience = "browser"and a defaultsubdomainofincus. - FR-002: When enabled, the component MUST request
ssl.domains = [domain]in grouptlsandspiffe.enablein groupspiffe. Seesslandspiffe. - FR-003: When enabled, the component MUST request an
oidcclientincusin groupincus-oidc, public, with JWT access tokens, scopesopenid profile email offline_accessand redirect URIs on<domain>:8443and<domain>. - FR-004: The
oidcrequest MUST importincus/oidc.nixwith the issuer fromcontracts.oidc.results.incus.default. - FR-005: If the issuer is
https://andfediversity.openbao-pki.caCertFileis set, theincusservice MUST order afteropenbao-pki-trust.serviceand useSSL_CERT_FILE=/etc/ssl/certs/openbao-pki-bundle.crt. - FR-006: The
coderequest MUST importincus.nixwithfediversity.incus.uiDomain = domainandfediversity.incus.storeServe.enable = true. Seenixos-module. - FR-007:
incus.nixMUST preseed only bridgeincusbr0at10.0.100.1/20with the DHCP range10.0.100.2-10.0.100.254, NAT and no IPv6, and adirstorage pooldefault. Services on the bridge address need these two objects at boot, and the preseed applies them even when the reconciler fails. - FR-007a:
incus.nixMUST declare the other runtime state through thedeclarative-incusreconciler of thelxc/incusdeclarative-runtime pairing (virtualisation.incus.runtime):dns.domainandraw.dnsmasqonincusbr0, theeth0and root disk devices of thedefaultprofile,core.https_address = ":8443"and theoidc.*keys in the server config, and the trusted metrics certificate. The reconciler removes what is no longer declared. - FR-007b: The reconciler MUST set
adopt = trueonincusbr0anddefault, and MUST declare each key that the preseed sets with the same value (bridgeBase). Otherwise the preseed and the reconciler change the key back and forth on each start of incus. - FR-007c:
declarative-incusMUST restart on failure, with no start limit. - FR-008: The bridge MUST serve DNS for the apex through
raw.dnsmasqfromincus-bridge-dnsmasq.nix, with acname=line perbridgeCnamesentry and ahost-recordforuiDomain. - FR-009:
incus-bridge.nixMUST be the single source of the bridge address and subnet.core/setups/hosts-common.nixreads it. - FR-010: The host MUST put the bridge address first in
networking.nameservers, and dhcpcd MUST ignoreveth*andincusbr*. - FR-011: The host MUST open TCP port
8443, and also443whenuiDomainis not empty. - FR-012: The nginx vhost for
uiDomainMUST proxy/tohttps://127.0.0.1:8443with websockets and no body size limit. - FR-012a: The nginx vhost for
uiDomainMUST verify the daemon with/var/lib/incus/server.crtas its only trust anchor, and it MUST check the DNS name that is in that cert. nginx MUST start afterincus.service. - FR-013: The location
= /1.0/metricsMUST admit only the bridge subnet and present the client certificate thatincus-metrics-certmints. The reconciler MUST trust it as themetricscertificateotel-collector-metrics. - FR-014: The host MUST run
nix-serveon the bridge address, port5000, withnet.ipv4.ip_nonlocal_bind = 1.nix-serveMUST sign each narinfo with the key thatincus-bridge-cache-keymints once under/var/lib/incus-bridge-cache. - FR-014a: The
defaultprofile MUST bind/var/lib/incus-bridge-cache/public, which holds the public key as anix.conffragment, read-only into each container atcacheKeyDirofincus-bridge.nix, withrequired=false. A node that substitutes from the bridge cache MUST!includethat fragment and MUST keeprequire-sigson (core/setups/nixos/incus-bridge-cache.nix). - FR-015: When
deployRunnersis not empty,incus-deploy-token-publishMUST gate onincus config trust listand a KV probe write, then mint and publish missing tokens per runner under<prefix>/secret/incus-deploy/<runner>/. It MUST skip the mint for a runner only when both KV entries and both trust names of that runner are present, and the mint MUST write the same trust names and KV paths that the check reads.incus-deploy-tokens.nixgives these checks, names and paths. It MUST start afterdeclarative-incus, because incus issues a token only when it listens on the network, and the reconciler setscore.https_address. - FR-016: A timer MUST start
incus-deploy-token-publish-refreshevery 5 minutes. - FR-017: When
storeServe.enable, the host MUST mint theincus-storefile set throughgenerateFiles: an ed25519 SSH key pair and a Nix signing key pair namedincus-store-<hostName>. - FR-018: The
incus-storeuser MUST linger, MUST NOT be a trusted Nix user, and MUST have no forwarding, TTY or tunnel in itsMatchblock. - FR-019:
incus-store-profileMUST validate the instance name and the store path before it sets or removes a profile under/nix/var/nix/profiles/inx. - FR-020:
incus-store-identity.nixMUST hold the user name and key file paths.windmill/f/fediversity/_jobenv.pyMUST restate the same values. - FR-021: When
spireInstanceandspireHostNameare both set, the host MUST runhost-swtpm,host-swtpm-perms,host-register-ekand a 5-minutehost-register-ek-refreshtimer. - FR-022: When the SPIRE agent is enabled, its failure MUST start
host-register-ek-refresh. - FR-023: The component MUST declare
provides = [ "instances" ]. The setups MUST give the public uplink, the bridge firewall, the bridge CNAMEs, the deploy-runner list, the SPIRE instance and the public UI record to theinstancesprovider node, not to a node by name.
Key Entities
fediversity.incusoptions:uiDomain,bridgeCnames,deployRunners,spireInstance,spireHostName,storeServe.enable,storeServe.user.- Bridge data: address
10.0.100.1, subnet10.0.96.0/20, the provider's DHCP range10.0.100.2-10.0.100.254and the prefix of the operator deployments10.0.104.0/21, fromincus-bridge.nix. - Deploy tokens: Incus trust names
deployer-hosts-<runner>anddeployer-image-<runner>, and KV keystokenHostsandtokenImage. - Store-serve identity: user
incus-store, keys at/run/incus-deploy/incus-store-ssh.keyand/run/incus-deploy/incus-store-signing.key. - Host SPIFFE ID:
spiffe://fediversity/host/<network.name>.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
test-incus-servicepasses: the daemon, network, storage pool and profile devices exist, the bridge has adns.domain, the metrics certificate is trusted, the bridge cache signs with the key thedefaultprofile binds into the containers, a reapply after a restart of incus changes nothing, ports 22, 443 and 8443 are open, a client reacheshttps://incus/, and nginx verifies the daemon and proxies/1.0and/1.0/metricsto it. - SC-002:
integrations-bridge-dns,nix-unit-setups-hypervisor-configandnix-unit-setups-apex-resolutionpass. - SC-003:
integrations-incus-shared-store,nix-unit-setups-shared-storeandnix-unit-components-incus-store-identitypass. - SC-004:
integrations-incus-token-publish,integrations-spire-incus,integrations-register-ek-evidenceandnix-unit-components-incus-deploy-tokenspass. - SC-005:
nix-unit-components-incus-oidcandtest-incus-oidc-loginpass.
Assumptions
- The node vars use the
vars-openbaoservice, which gives the OpenBao address, token, mount, prefix and CA to the publish units. - The far end of the host key publish is
.forgejo/workflows/cd-env.sh, which reads the key to pin the host. - The deploy-side overlay of the deployment method gives the API trust token and the TLS material.
nginxand its TLS certificate come from thesslresource.
Known Deviations
deployRunnersholds runnernetwork.namelabels, and the trust and KV names derive from them.- Incus has no fine-grained authorization, so every OIDC user is an administrator.
- When
declarative-incusfails on a fresh host, the host has the bridge and the pool, but no:8443listener, nodefaultprofile devices, no bridge zone and no OIDC. The unit tries again every 30 seconds. A preseed of these keys would hide the failure, but it would also apply again on each start of incus, so a key removed from the configuration would come back. - The provider identifies a certificate by its fingerprint, so the reconciler cannot adopt a trust entry. A host whose trust store has the
otel-collector-metricsentry from before the reconciler needsincus config trust remove <fingerprint>once, before the first apply.