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.
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.
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.
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.
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.
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.