6c663030
feat: server and issuer run as dedicated non-root users
a73x 2026-07-29 05:50
Commit message
docs/quickstart.md
| Old | New | ||
|---|---|---|---|
| @@ -15,7 +15,7 @@ manage them by hand instead, disable it in `/etc/default/eitri-agent`: | |||
| 15 | 15 | ||
| 16 | Tarballs live at <https://eitri.sh/dl/latest/>. The host bundle | 16 | Tarballs live at <https://eitri.sh/dl/latest/>. The host bundle |
| 17 | (`eitri_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and | 17 | (`eitri_<version>_linux_amd64.tar.gz`) has `eitri-server`, `eitri-agent`, and |
| 18 | the agent's systemd unit. The issuer bundle | 18 | their systemd units. The issuer bundle |
| 19 | (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc` — the bundled | 19 | (`eitri-oidc_<version>_linux_amd64.tar.gz`) has `eitri-oidc` — the bundled |
| 20 | sign-in provider — and its unit. The client bundle | 20 | sign-in provider — and its unit. The client bundle |
| 21 | (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for | 21 | (`eitri-cli_<version>_<os>_<arch>.tar.gz`) is the single `eitri` binary for |
| @@ -26,9 +26,15 @@ your laptop, built for linux and macOS. arm64 boxes take the arm64 bundle. | |||
| 26 | ```sh | 26 | ```sh |
| 27 | tar xzf eitri_*_linux_amd64.tar.gz && cd eitri_*_linux_amd64 | 27 | tar xzf eitri_*_linux_amd64.tar.gz && cd eitri_*_linux_amd64 |
| 28 | sudo install -m 0755 eitri-server /usr/local/bin/eitri-server | 28 | sudo install -m 0755 eitri-server /usr/local/bin/eitri-server |
| 29 | sudo mkdir -p /etc/eitri /var/lib/eitri | 29 | sudo install -m 0644 eitri-server.service /etc/systemd/system/eitri-server.service |
| 30 | sudo useradd --system --home-dir /var/lib/eitri --shell /usr/sbin/nologin eitri | ||
| 31 | sudo mkdir -p /etc/eitri | ||
| 30 | ``` | 32 | ``` |
| 31 | 33 | ||
| 34 | The server runs as the dedicated `eitri` user — it needs no privilege (every | ||
| 35 | listener is on an unprivileged port); its state directory `/var/lib/eitri` is | ||
| 36 | created and owned for it by the unit. | ||
| 37 | |||
| 32 | Set `SERVER_ADDR`, paste the rest: | 38 | Set `SERVER_ADDR`, paste the rest: |
| 33 | 39 | ||
| 34 | ```sh | 40 | ```sh |
| @@ -60,8 +66,12 @@ sudo tee /etc/eitri/server.json >/dev/null <<EOF | |||
| 60 | "ssh_host_key": "/var/lib/eitri/ssh_host_key" | 66 | "ssh_host_key": "/var/lib/eitri/ssh_host_key" |
| 61 | } | 67 | } |
| 62 | EOF | 68 | EOF |
| 69 | sudo chgrp eitri /etc/eitri/server.json && sudo chmod 0640 /etc/eitri/server.json | ||
| 63 | ``` | 70 | ``` |
| 64 | 71 | ||
| 72 | The chmod matters: `server.json` carries `host_secret`, so it is root-owned | ||
| 73 | and readable only via the `eitri` group — not world-readable. | ||
| 74 | |||
| 65 | `advertise_*` is what hosts and your laptop dial. Not `127.0.0.1`. Any | 75 | `advertise_*` is what hosts and your laptop dial. Not `127.0.0.1`. Any |
| 66 | cloud-init disk image works as the default image; the Ubuntu one boots out of | 76 | cloud-init disk image works as the default image; the Ubuntu one boots out of |
| 67 | the box. | 77 | the box. |
| @@ -80,13 +90,14 @@ next to the server on loopback. Install it, write its config, and add yourself: | |||
| 80 | tar xzf eitri-oidc_*_linux_amd64.tar.gz && cd eitri-oidc_*_linux_amd64 | 90 | tar xzf eitri-oidc_*_linux_amd64.tar.gz && cd eitri-oidc_*_linux_amd64 |
| 81 | sudo install -m 0755 eitri-oidc /usr/local/bin/eitri-oidc | 91 | sudo install -m 0755 eitri-oidc /usr/local/bin/eitri-oidc |
| 82 | sudo install -m 0644 eitri-oidc.service /etc/systemd/system/eitri-oidc.service | 92 | sudo install -m 0644 eitri-oidc.service /etc/systemd/system/eitri-oidc.service |
| 93 | sudo useradd --system --home-dir /var/lib/eitri-oidc --shell /usr/sbin/nologin eitri-oidc | ||
| 83 | 94 | ||
| 84 | sudo tee /etc/eitri/eitri-oidc.json >/dev/null <<EOF | 95 | sudo tee /etc/eitri/eitri-oidc.json >/dev/null <<EOF |
| 85 | { | 96 | { |
| 86 | "listen": "127.0.0.1:9111", | 97 | "listen": "127.0.0.1:9111", |
| 87 | "issuer": "http://127.0.0.1:9111", | 98 | "issuer": "http://127.0.0.1:9111", |
| 88 | "users_file": "/etc/eitri/oidc-users.json", | 99 | "users_file": "/var/lib/eitri-oidc/users.json", |
| 89 | "signing_key": "/etc/eitri/oidc-signing.key", | 100 | "signing_key": "/var/lib/eitri-oidc/signing.key", |
| 90 | "clients": [ | 101 | "clients": [ |
| 91 | {"id": "eitri-console", "redirect_url": "http://$SERVER_ADDR:8080/auth/callback"} | 102 | {"id": "eitri-console", "redirect_url": "http://$SERVER_ADDR:8080/auth/callback"} |
| 92 | ] | 103 | ] |
| @@ -95,23 +106,30 @@ EOF | |||
| 95 | 106 | ||
| 96 | sudo systemctl daemon-reload | 107 | sudo systemctl daemon-reload |
| 97 | sudo systemctl enable --now eitri-oidc | 108 | sudo systemctl enable --now eitri-oidc |
| 98 | sudo eitri-oidc user add you@example.com # prompts for a password | 109 | sudo -u eitri-oidc eitri-oidc user add you@example.com # prompts for a password |
| 99 | ``` | 110 | ``` |
| 100 | 111 | ||
| 112 | The issuer handles passwords and holds the sign-in signing key, so it runs as | ||
| 113 | its own `eitri-oidc` user — separate even from the server's `eitri` user, | ||
| 114 | keeping the key material unreadable by any other service. Its mutable state | ||
| 115 | (user file, autogenerated signing key) lives in `/var/lib/eitri-oidc`, created | ||
| 116 | and owned for it by the unit; that's also why `user add` runs via | ||
| 117 | `sudo -u eitri-oidc` — files it writes stay readable by the service. | ||
| 118 | |||
| 101 | `redirect_url` must equal the server's `oidc.public_url` + `/auth/callback`. On | 119 | `redirect_url` must equal the server's `oidc.public_url` + `/auth/callback`. On |
| 102 | this single box the loopback `issuer`/`public_url` work because your browser is | 120 | this single box the loopback `issuer`/`public_url` work because your browser is |
| 103 | on the same machine; anything multi-machine needs a routable issuer. | 121 | on the same machine; anything multi-machine needs a routable issuer. |
| 104 | 122 | ||
| 105 | Now run the server: | 123 | Now start the server: |
| 106 | 124 | ||
| 107 | ```sh | 125 | ```sh |
| 108 | sudo eitri-server --config /etc/eitri/server.json | 126 | sudo systemctl enable --now eitri-server |
| 127 | journalctl -u eitri-server -f # watch it come up | ||
| 109 | ``` | 128 | ``` |
| 110 | 129 | ||
| 111 | It runs in the foreground. nohup, tmux, or write a unit. It speaks plain | 130 | It speaks plain HTTP, so keep it on your LAN or put TLS in front. Open |
| 112 | HTTP, so keep it on your LAN or put TLS in front. Open `8080/tcp` (console, | 131 | `8080/tcp` (console, enroll), `8443/udp` (sync), `2222/tcp` (SSH gate). The |
| 113 | enroll), `8443/udp` (sync), `2222/tcp` (SSH gate). The issuer stays on | 132 | issuer stays on loopback. |
| 114 | loopback. | ||
| 115 | 133 | ||
| 116 | Sign in at `http://192.0.2.10:8080` with the user you added. Your first | 134 | Sign in at `http://192.0.2.10:8080` with the user you added. Your first |
| 117 | sign-in creates your tenant. | 135 | sign-in creates your tenant. |
scripts/eitri-agent.service
| Old | New | ||
|---|---|---|---|
| @@ -33,7 +33,11 @@ RestartSec=5 | |||
| 33 | # running VM on a mere agent stop. Only the agent itself may be signalled — | 33 | # running VM on a mere agent stop. Only the agent itself may be signalled — |
| 34 | # VMs survive agent restarts by design. | 34 | # VMs survive agent restarts by design. |
| 35 | KillMode=process | 35 | KillMode=process |
| 36 | # The agent manages KVM guests, bridges, and taps — it needs root. | 36 | # Root is deliberate and scoped to THIS unit (eitri-server and eitri-oidc run |
| 37 | # as dedicated users): the agent opens /dev/kvm and /dev/net/tun, creates the | ||
| 38 | # bridge and taps, sets net.ipv4.ip_forward, installs the nftables masquerade, | ||
| 39 | # binds DHCP on :67, and self-upgrades by swapping its own binary in | ||
| 40 | # /usr/local/bin. | ||
| 37 | User=root | 41 | User=root |
| 38 | 42 | ||
| 39 | [Install] | 43 | [Install] |
scripts/eitri-oidc.service
| Old | New | ||
|---|---|---|---|
| @@ -1,11 +1,21 @@ | |||
| 1 | # eitri bundled OIDC issuer systemd unit. Install on the box that hosts it: | 1 | # eitri bundled OIDC issuer systemd unit. Install on the box that hosts it: |
| 2 | # | 2 | # |
| 3 | # cp eitri-oidc.service /etc/systemd/system/ | 3 | # sudo useradd --system --home-dir /var/lib/eitri-oidc --shell /usr/sbin/nologin eitri-oidc |
| 4 | # systemctl daemon-reload && systemctl enable --now eitri-oidc | 4 | # sudo cp eitri-oidc.service /etc/systemd/system/ |
| 5 | # sudo systemctl daemon-reload && sudo systemctl enable --now eitri-oidc | ||
| 5 | # | 6 | # |
| 6 | # Sibling to eitri-server: deploy it only for a bundled-OIDC install; a fleet | 7 | # Sibling to eitri-server: deploy it only for a bundled-OIDC install; a fleet |
| 7 | # fronted by an external IdP never needs this unit. Config and the flat user | 8 | # fronted by an external IdP never needs this unit. |
| 8 | # file live under /etc/eitri (see docs and `eitri-oidc user add`). | 9 | # |
| 10 | # Runs as the dedicated 'eitri-oidc' user — this daemon handles passwords and | ||
| 11 | # holds the identity signing key, so it must never run as root, and a separate | ||
| 12 | # user from eitri-server keeps that key material unreadable by the server | ||
| 13 | # process. Config is root-owned in /etc/eitri; the mutable pieces (the flat | ||
| 14 | # user file, the autogenerated signing key) live in /var/lib/eitri-oidc, which | ||
| 15 | # StateDirectory= creates and owns. Manage users as the service user so files | ||
| 16 | # stay readable by it: | ||
| 17 | # | ||
| 18 | # sudo -u eitri-oidc eitri-oidc user add you@example.com | ||
| 9 | 19 | ||
| 10 | [Unit] | 20 | [Unit] |
| 11 | Description=eitri bundled OIDC issuer | 21 | Description=eitri bundled OIDC issuer |
| @@ -14,9 +24,16 @@ After=network-online.target | |||
| 14 | Wants=network-online.target | 24 | Wants=network-online.target |
| 15 | 25 | ||
| 16 | [Service] | 26 | [Service] |
| 27 | User=eitri-oidc | ||
| 28 | Group=eitri-oidc | ||
| 17 | ExecStart=/usr/local/bin/eitri-oidc -config /etc/eitri/eitri-oidc.json | 29 | ExecStart=/usr/local/bin/eitri-oidc -config /etc/eitri/eitri-oidc.json |
| 30 | StateDirectory=eitri-oidc | ||
| 18 | Restart=on-failure | 31 | Restart=on-failure |
| 19 | RestartSec=5 | 32 | RestartSec=5 |
| 33 | NoNewPrivileges=yes | ||
| 34 | ProtectSystem=strict | ||
| 35 | ProtectHome=yes | ||
| 36 | PrivateTmp=yes | ||
| 20 | 37 | ||
| 21 | [Install] | 38 | [Install] |
| 22 | WantedBy=multi-user.target | 39 | WantedBy=multi-user.target |
scripts/eitri-server.service
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,32 @@ | |||
| 1 | # eitri control plane systemd unit. Install on the server box: | ||
| 2 | # | ||
| 3 | # sudo useradd --system --home-dir /var/lib/eitri --shell /usr/sbin/nologin eitri | ||
| 4 | # sudo cp eitri-server.service /etc/systemd/system/ | ||
| 5 | # sudo systemctl daemon-reload && sudo systemctl enable --now eitri-server | ||
| 6 | # | ||
| 7 | # Runs as the dedicated 'eitri' user: the server needs no privilege — every | ||
| 8 | # listener (console 8080/tcp, sync 8443/udp, SSH gate 2222/tcp) sits on an | ||
| 9 | # unprivileged port and its state (sqlite, SSH CA keys) lives in | ||
| 10 | # /var/lib/eitri, which StateDirectory= creates and owns for it. Config stays | ||
| 11 | # root-owned in /etc/eitri, readable via the 'eitri' group. | ||
| 12 | |||
| 13 | [Unit] | ||
| 14 | Description=eitri control plane | ||
| 15 | Documentation=https://eitri.sh | ||
| 16 | After=network-online.target | ||
| 17 | Wants=network-online.target | ||
| 18 | |||
| 19 | [Service] | ||
| 20 | User=eitri | ||
| 21 | Group=eitri | ||
| 22 | ExecStart=/usr/local/bin/eitri-server --config /etc/eitri/server.json | ||
| 23 | StateDirectory=eitri | ||
| 24 | Restart=on-failure | ||
| 25 | RestartSec=5 | ||
| 26 | NoNewPrivileges=yes | ||
| 27 | ProtectSystem=strict | ||
| 28 | ProtectHome=yes | ||
| 29 | PrivateTmp=yes | ||
| 30 | |||
| 31 | [Install] | ||
| 32 | WantedBy=multi-user.target | ||
scripts/release.sh
| Old | New | ||
|---|---|---|---|
| @@ -55,7 +55,7 @@ for arch in amd64 arm64; do | |||
| 55 | -o "$stage/$bundle/eitri-server" ./cmd/eitri-server | 55 | -o "$stage/$bundle/eitri-server" ./cmd/eitri-server |
| 56 | CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build -trimpath -ldflags "$LDFLAGS" \ | 56 | CGO_ENABLED=0 GOOS=linux GOARCH="$arch" go build -trimpath -ldflags "$LDFLAGS" \ |
| 57 | -o "$stage/$bundle/eitri-agent" ./cmd/eitri-agent | 57 | -o "$stage/$bundle/eitri-agent" ./cmd/eitri-agent |
| 58 | cp scripts/eitri-agent.service "$stage/$bundle/" | 58 | cp scripts/eitri-agent.service scripts/eitri-server.service "$stage/$bundle/" |
| 59 | tar -C "$stage" -czf "$OUT/$bundle.tar.gz" "$bundle" | 59 | tar -C "$stage" -czf "$OUT/$bundle.tar.gz" "$bundle" |
| 60 | cp "$stage/$bundle/eitri-agent" "$OUT/eitri-agent_linux_${arch}" | 60 | cp "$stage/$bundle/eitri-agent" "$OUT/eitri-agent_linux_${arch}" |
| 61 | done | 61 | done |