Incus (required for triggering deployments)

The default deploy config targets Incus containers on localhost via core/setups/tf-incus-hosts.nix. Incus is a NixOS system service; add the following to your NixOS configuration and run nixos-rebuild switch:

virtualisation.incus = {
  enable = true;
  preseed = {
    config."core.https_address" = ":8443";
    networks = [
      {
        name = "incusbr0";
        type = "bridge";
        config = {
          "ipv4.address" = "10.0.100.1/20";
          "ipv4.dhcp.ranges" = "10.0.100.2-10.0.100.254";
          "ipv4.nat" = "true";
          "ipv6.address" = "none";
        };
      }
    ];
    storage_pools = [
      {
        name = "default";
        driver = "dir";
      }
    ];
    profiles = [
      {
        name = "default";
        devices = {
          eth0 = {
            type = "nic";
            name = "eth0";
            network = "incusbr0";
          };
          root = {
            type = "disk";
            path = "/";
            pool = "default";
          };
        };
      }
    ];
  };
};
# Incus maps container uids/gids into a host sub-range; without these
# /etc/subuid + /etc/subgid entries `incusd forkstart` fails with
# `newuidmap: cannot open /etc/subuid`. Matches core/components/hosting-provider/incus.nix.
users.users.root.subUidRanges = [ { startUid = 1000000; count = 1000000000; } ];
users.users.root.subGidRanges = [ { startGid = 1000000; count = 1000000000; } ];
networking.firewall.allowedTCPPorts = [ 8443 ];
networking.nftables.enable = true;

After activation, issue a trust token for the Terraform provider:

incus config trust add --name terraform

Which Incus host the API targets is a deployment-method setting, not an env var: it defaults to localhost, a HOSTING_CONFIG host leaf pins it, and an operator submission overrides it at deploy time as TF_VAR_host. If Incus is not available, set DEPLOY_CONFIG_FILE to a custom config targeting reachable machines instead.

Provisioning the deployer's incus credentials

Even with virtualisation.incus enabled, a deploy fails unless a base image has been uploaded under the alias the deploy looks up -- data "incus_image" "base" then errors with Image alias not found -- and the deploy gets three TF_VAR_* variables: TF_VAR_token, TF_VAR_image_fingerprint and TF_VAR_incus_config_dir.

For the local-dev case (incus on the same host as the api), run the dev-tf-incus-bootstrap program of core once. It holds the incus + OpenTofu recipe: it mints two trust tokens (deployer-image, deployer-hosts), uploads the generic NixOS base images via dev-tf-incus-image, and writes the TF_VAR_* env file to OUT_TF_ENV. It reads the incus socket, so run it as root or as a member of the incus-admin group.

On a colocated node, the fediversity-api service reads the file when services.fediversity-api.environmentFiles names it, for example [ "-/var/lib/fediversity-api/tf-env" ]:

doas env OUT_TF_ENV=/var/lib/fediversity-api/tf-env INCUS_CONFIG_DIR=/etc/incus-client \
  nix run .#dev-tf-incus-bootstrap
doas systemctl restart fediversity-api.service

On a developer workstation, write the file to the XDG state path, which api-http-stack sources on start:

OUT_TF_ENV="${XDG_STATE_HOME:-$HOME/.local/state}/fediversity/tf-env" \
INCUS_CONFIG_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/fediversity/incus-config" \
  nix run .#dev-tf-incus-bootstrap

dev-tf-incus-bootstrap is a local-dev convenience (api and windmill on one host). The machine-agnostic REMOTE path -- deploy runners and incus on separate machines -- is the documented flow in notes/bootstrap.md: the base-image upload and the -tf-incus-all deploy read the incus host from TF_VAR_host (the deploy-runtime override the hosts graph already uses, issue #723) plus TF_VAR_incus_config_dir / TF_VAR_token, so the same commands serve local dev (host defaults to localhost) and a remote provider (TF_VAR_host=<ip>). The image's tofu state is backend-agnostic (a keyed local file by default), so a remote upload needs no state backend and a re-upload replaces the image in place.

For a multi-node provider, the runner nodes get their TF_VAR_token from the incus host through OpenBao rather than a local file (the #1013 follow-up, gap #38). The incus host's NixOS module (core/components/hosting-provider/incus.nix) mints a one-time trust token pair per deploy-runner node (windmill worker, api) at activation -- authenticated to the central OpenBao by its TPM-attested SVID -- and publishes each to its incus-deploy vars KV instance under a per-runner subpath. Each runner's openbao agent reads back ITS OWN token and hands it to the tf provider, which self-generates its client.crt/client.key LOCALLY and self-fetches the servercert, so only the one-time token travels. This is what lets windmill and api run on nodes separate from the incus host.