Self-hosting

Running the control plane yourself

Run the whole control plane yourself—everything below is your own box. eitri is three pieces: a server, an agent on every box that runs VMs, and your laptop. 192.0.2.10 is the server below. Substitute yours.

What you need

Every Linux VM host needs KVM (ls -l /dev/kvm); a Mac needs brew install vfkit (see "Join a Mac"). Guest images are decoded in the agent, so there is no image toolchain to install. On Linux the agent fetches cloud-hypervisor and the guest firmware itself on first start, sha-verified against the release. To manage them by hand instead, disable it in /etc/default/eitri-agent: EITRI_AGENT_FLAGS="--bootstrap-url=".

Tarballs live at https://eitri.sh/dl/latest/. The Linux host bundle (eitri-server_<version>_linux_amd64.tar.gz) has eitri-server, eitri-agent, and their systemd units—a box that only runs VMs takes the same tarball and installs just the agent from it. A Mac host has a bundle of its own (eitri-agent_<version>_darwin_arm64.tar.gz): the agent and its LaunchAgent installer, no server. The issuer bundle (eitri-oidc_<version>_linux_amd64.tar.gz) has eitri-oidc—the bundled sign-in provider—and its unit. The client bundle (eitri-cli_<version>_<os>_<arch>.tar.gz) is the single eitri binary for your laptop, built for linux and macOS. arm64 boxes take the arm64 bundle.

Download and verify. SHA256SUMS comes first: it verifies the rest, and its filenames name the release, so V is read from it rather than typed:

curl -fsSLO https://eitri.sh/dl/latest/SHA256SUMS
V=$(sed -n 's/.*eitri-server_\(v[^_]*\)_linux_amd64\.tar\.gz$/\1/p' SHA256SUMS)
curl -fsSLO "https://eitri.sh/dl/latest/eitri-server_${V}_linux_amd64.tar.gz"
curl -fsSLO "https://eitri.sh/dl/latest/eitri-oidc_${V}_linux_amd64.tar.gz"
curl -fsSLO "https://eitri.sh/dl/latest/eitri-cli_${V}_$(uname -s | tr A-Z a-z)_amd64.tar.gz"
sha256sum -c SHA256SUMS --ignore-missing

Every artifact also has an immutable home at /dl/<version>/, which is what /dl links; /dl/latest/ is the moving alias, and only the current release is served.

Plain files at stable URLs—no install script. sha256sum -c must say OK for every tarball before you unpack anything.

The server

tar xzf eitri-server_*_linux_amd64.tar.gz && cd eitri-server_*_linux_amd64
sudo install -m 0755 eitri-server /usr/local/bin/eitri-server
sudo install -m 0644 eitri-server.service /etc/systemd/system/eitri-server.service
sudo useradd --system --home-dir /var/lib/eitri --shell /usr/sbin/nologin eitri
sudo mkdir -p /etc/eitri

The server runs as the dedicated eitri user, with no privilege and every listener on an unprivileged port; its state directory /var/lib/eitri is created and owned for it by the unit.

Set SERVER_ADDR, paste the rest:

SERVER_ADDR=192.0.2.10
IMAGE_DIR=https://cloud-images.ubuntu.com/resolute/current
HOST_SECRET=$(openssl rand -hex 32)
KEK=$(openssl rand -hex 32)
sha() { curl -fsSL "$IMAGE_DIR/SHA256SUMS" | awk -v f="resolute-server-cloudimg-$1.img" '$2 == "*" f {print $1}'; }
AMD64_SHA=$(sha amd64)
ARM64_SHA=$(sha arm64)

sudo tee /etc/eitri/server.json >/dev/null <<EOF
{
  "http_listen": ":8080",
  "quic_listen": ":8443",
  "advertise_http": "http://$SERVER_ADDR:8080",
  "advertise_quic": "$SERVER_ADDR:8443",
  "db_path": "/var/lib/eitri/eitri.db",
  "cidr_pool": "10.100.0.0/16",
  "oidc": {
    "issuer": "http://127.0.0.1:9111",
    "client_id": "eitri-console",
    "public_url": "http://$SERVER_ADDR:8080"
  },
  "host_secret": "$HOST_SECRET",
  "key_encryption_key": "$KEK",
  "default_images": {
    "amd64": {"url": "$IMAGE_DIR/resolute-server-cloudimg-amd64.img", "sha256": "$AMD64_SHA"},
    "arm64": {"url": "$IMAGE_DIR/resolute-server-cloudimg-arm64.img", "sha256": "$ARM64_SHA"}
  },
  "ssh_listen": ":2222",
  "ssh_gate_domain": "$SERVER_ADDR",
  "ssh_ca_key": "/var/lib/eitri/ssh_ca",
  "ssh_host_key": "/var/lib/eitri/ssh_host_key"
}
EOF
sudo chgrp eitri /etc/eitri/server.json && sudo chmod 0640 /etc/eitri/server.json

The chmod matters: server.json carries host_secret and key_encryption_key, so it is root-owned and readable only through the eitri group—never world-readable.

Keep a copy of server.json somewhere other than the machine it runs on, and somewhere other than your backups of /var/lib/eitri. Separating the two is what makes a stolen disk useless; losing the config while keeping the disk is what makes your own backups useless. Restoring this server elsewhere needs both.

advertise_* is what hosts and your laptop dial. Not 127.0.0.1. Any cloud-init disk image works as a default image; the Ubuntu one boots out of the box.

ssh_listen here binds every interface, which is why ssh_gate_domain is set beside it: a wildcard bind (:2222, 0.0.0.0:2222) gives the gate no name, so the server requires one and refuses to start without it. That name is what clients dial and the one principal on the gate's host certificate. A gate bound to one concrete address (127.0.0.1:2222 for a single-machine plane) names itself and may leave ssh_gate_domain out.

default_images is keyed by the architecture of the host a VM lands on, and a VM created without an explicit image takes the entry for its host. Configure every architecture in your fleet: a host can only run a guest built for its own CPU, and a create for an architecture you have not listed is refused rather than served an image that cannot boot. One entry is plenty for a single-architecture fleet—both are shown because an Apple silicon host takes arm64.

The oidc block points the console's sign-in at the bundled issuer you start next. public_url is where browsers reach the console (the callback lands at $public_url/auth/callback), so keep it equal to advertise_http. Bringing your own IdP instead of the bundled issuer: see byo-idp.md.

Sign-in

The console always signs in through OIDC. The bundled eitri-oidc issuer runs next to the server on loopback. Install it, write its config, and add yourself:

tar xzf eitri-oidc_*_linux_amd64.tar.gz && cd eitri-oidc_*_linux_amd64
sudo install -m 0755 eitri-oidc /usr/local/bin/eitri-oidc
sudo install -m 0644 eitri-oidc.service /etc/systemd/system/eitri-oidc.service
sudo useradd --system --home-dir /var/lib/eitri-oidc --shell /usr/sbin/nologin eitri-oidc

sudo tee /etc/eitri/eitri-oidc.json >/dev/null <<EOF
{
  "listen": "127.0.0.1:9111",
  "issuer": "http://127.0.0.1:9111",
  "users_file": "/var/lib/eitri-oidc/users.json",
  "signing_key": "/var/lib/eitri-oidc/signing.key",
  "clients": [
    {"id": "eitri-console", "redirect_url": "http://$SERVER_ADDR:8080/auth/callback"}
  ]
}
EOF

sudo systemctl daemon-reload
sudo systemctl enable --now eitri-oidc
sudo -u eitri-oidc eitri-oidc user add you@example.com    # prompts for a password

The issuer runs as its own eitri-oidc user, separate from the server's, with its user file and signing key in /var/lib/eitri-oidc, created and owned for it by the unit. Run user add through sudo -u eitri-oidc so the files it writes stay readable by the service.

redirect_url must equal the server's oidc.public_url + /auth/callback. On this single box the loopback issuer/public_url work because your browser is on the same machine; anything multi-machine needs a routable issuer.

Now start the server:

sudo systemctl enable --now eitri-server
journalctl -u eitri-server -f    # watch it come up

It speaks plain HTTP, so keep it on your LAN or put TLS in front. Open 8080/tcp (console, enroll), 8443/udp (sync), 2222/tcp (SSH gate). The issuer stays on loopback.

Sign in at http://192.0.2.10:8080 with the user you added. Your first sign-in creates your tenant.

Join your hosts

Once per box that runs VMs, and the server's box counts. + Add host in your console prints the join command; the steps are the same ones joining covers, run against your plane rather than the hosted one. Self-hosting asks one extra thing of a Mac host: an arm64 entry in the server's default_images.

Start a client, and a first VM

eitri init against your own plane needs to be told where that plane is— EITRI_URL=http://192.0.2.10:8080 eitri init—and writes it to ~/.eitri/config.json with your tenant and the gate address the server names on /api/v1/me, so later runs need no variable. Everything else about registering a CA, creating the VM and reaching it is on connecting.