docs/self-hosting.md
Ref: Size: 8.7 KiB History
# 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:
```sh
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](https://eitri.sh/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
```sh
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:
```sh
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](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:
```sh
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:
```sh
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](joining.md) 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](connecting.md).
## Related
- [byo-idp](byo-idp.md)—your own IdP instead of the bundled issuer
- [upgrade](upgrade.md)—upgrading agents, the server, cloud-hypervisor
- [cert rotation](cert-rotation.md)—rotating the server certificate
- [credential revocation](credential-revocation.md)—when something leaks