infra/stacks/terminal/files/devvm/README.md
Viktor Barzin f63f10f7fa terminal: per-Authentik-user OS-user isolation; deny unmapped users
Restores the kernel-level isolation the pre-cutover ttyd-session.sh had,
but keeps the multi-session lobby UX:

- ttyd.service gets `-H X-authentik-username` back. `tmux-attach.sh` reads
  $TTYD_USER, looks up the local part in /etc/ttyd-user-map, denies the
  connection (no fallback to wizard) if there's no mapping, otherwise
  `sudo -n -H -u <os_user> tmux …`. Each Authentik identity → its own
  Unix user → its own `/tmp/tmux-<uid>/default` socket.
- tmux-api scopes every request to the same OS user via the same header.
  Adds /whoami so the lobby HTML can preflight access and render
  "logged in as <os_user> (<authentik>)" instead of leaving the user to
  discover the deny via a reconnect loop.
- Commits /etc/ttyd-user-map and the matching /etc/sudoers.d/ttyd-users
  fragment under files/devvm/ so future operators see one canonical
  source of truth. Current mappings: vbarzin → wizard, emil.barzin → emo.

Adding a user is now: append a line to ttyd-user-map + a NOPASSWD
sudoers line + `useradd -m`. README walks through it.

No Terraform changes — this is all DevVM-side + lobby JS.
2026-05-13 19:25:55 +00:00

108 lines
4.7 KiB
Markdown

# DevVM terminal files
ttyd + tmux-api on the DevVM (`10.0.10.10`). ttyd serves the multi-session
lobby on port 7681 and attaches each Authentik identity into its own OS
user's tmux server. tmux-api (port 7684) backs the lobby's list/kill
actions, scoped to the same OS user.
`terminal-ro.service` (port 7682, single read-only session) and
`clipboard-upload` (port 7683) are unchanged by these files.
## Per-user isolation
The Authentik forward-auth middleware injects `X-authentik-username` on
every authenticated request:
1. **ttyd** is started with `-H X-authentik-username`, so the header value
lands as `$TTYD_USER` in each launched `tmux-attach.sh` invocation.
2. **`tmux-attach.sh`** looks up `$TTYD_USER` in `/etc/ttyd-user-map`,
denies the connection if there is no mapping, and otherwise
`sudo -n -H -u <os_user> /usr/bin/tmux …`.
3. **`tmux-api`** reads `X-authentik-username` on every request and runs
tmux as the mapped OS user too — so the lobby's session list is the
intersection of "your Authentik identity" and "what tmux on that OS
user's socket reports".
Different Authentik identities map to different Unix users, which means
different `/tmp/tmux-<uid>/default` sockets — kernel-level isolation,
not "the API filtered the list".
Adding a new user:
1. Append a line to `/etc/ttyd-user-map` (canonical at
`files/devvm/ttyd-user-map`).
2. Append `wizard ALL=(<os_user>) NOPASSWD: /usr/bin/tmux` to
`/etc/sudoers.d/ttyd-users` (canonical at
`files/devvm/sudoers.d-ttyd-users`).
3. Ensure the OS user exists (`useradd -m <os_user>`).
## Layout
| Source | Destination on DevVM | Mode |
|--------|----------------------|------|
| `tmux-attach.sh` | `/usr/local/bin/tmux-attach.sh` | 0755 |
| `ttyd.service` | `/etc/systemd/system/ttyd.service` | 0644 |
| `tmux-api.service` | `/etc/systemd/system/tmux-api.service` | 0644 |
| `ttyd-user-map` | `/etc/ttyd-user-map` | 0644 |
| `sudoers.d-ttyd-users` | `/etc/sudoers.d/ttyd-users` | 0440, root:root |
| `../index.html` (one dir up) | `/usr/local/share/ttyd/index.html` | 0644 |
| `../../tmux-api/` Go binary | `/usr/local/bin/tmux-api` | 0755 |
## Apply
From the workstation (`infra/` repo root):
```bash
DEVVM=10.0.10.10 # SSH config provides the user
# 1. Build the tmux-api binary for linux/amd64
( cd infra/stacks/terminal/tmux-api && GOOS=linux GOARCH=amd64 go build -o /tmp/tmux-api . )
# 2. HTML + config files
scp infra/stacks/terminal/files/index.html $DEVVM:/tmp/index.html
scp infra/stacks/terminal/files/devvm/tmux-attach.sh $DEVVM:/tmp/tmux-attach.sh
scp infra/stacks/terminal/files/devvm/ttyd-user-map $DEVVM:/tmp/ttyd-user-map
scp infra/stacks/terminal/files/devvm/sudoers.d-ttyd-users $DEVVM:/tmp/sudoers.d-ttyd-users
ssh $DEVVM "
sudo install -m 0644 /tmp/index.html /usr/local/share/ttyd/index.html
sudo install -m 0755 /tmp/tmux-attach.sh /usr/local/bin/tmux-attach.sh
sudo install -m 0644 /tmp/ttyd-user-map /etc/ttyd-user-map
sudo install -m 0440 -o root -g root /tmp/sudoers.d-ttyd-users /etc/sudoers.d/ttyd-users
sudo visudo -cf /etc/sudoers.d/ttyd-users
rm /tmp/index.html /tmp/tmux-attach.sh /tmp/ttyd-user-map /tmp/sudoers.d-ttyd-users
"
# 3. tmux-api binary
scp /tmp/tmux-api $DEVVM:/tmp/tmux-api
ssh $DEVVM "sudo install -m 0755 /tmp/tmux-api /usr/local/bin/tmux-api && rm /tmp/tmux-api"
# 4. systemd units
scp infra/stacks/terminal/files/devvm/ttyd.service $DEVVM:/tmp/
scp infra/stacks/terminal/files/devvm/tmux-api.service $DEVVM:/tmp/
ssh $DEVVM "
sudo mv /tmp/ttyd.service /etc/systemd/system/
sudo mv /tmp/tmux-api.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now tmux-api
sudo systemctl restart ttyd
"
# 5. Sanity checks
ssh $DEVVM "systemctl status ttyd tmux-api --no-pager"
ssh $DEVVM "curl -sf -H 'X-Authentik-Username: vbarzin' localhost:7684/whoami"
ssh $DEVVM "curl -sf -H 'X-Authentik-Username: emil.barzin' localhost:7684/whoami"
ssh $DEVVM "curl -si -H 'X-Authentik-Username: nobody' localhost:7684/whoami | head -3"
```
## Notes
- **ttyd ≥ 1.7** required for the `-a` flag (URL args → argv). DevVM has 1.7.7.
- **Argv flow**: `?arg=foo` → ttyd appends `foo` as `$1` to `tmux-attach.sh`
→ wrapper regex-validates and runs `tmux new-session -A -s "$name"`. ttyd
uses argv, never a shell string — no injection path.
- **No external exposure of 7681/7684** — DevVM is internal-VLAN-only;
Authentik forward-auth is the access gate.
- **Cutover history** — `term.viktorbarzin.me` and `ttyd-multi.service`
(port 7685) were the staging surface for this design; both retired
when the multi-session config was promoted to port 7681. The
per-Authentik-user isolation followed in a separate change.