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.
This commit is contained in:
Viktor Barzin 2026-05-13 19:25:55 +00:00 committed by Viktor Barzin
parent aff4f67671
commit 9fce3c7b09
7 changed files with 316 additions and 65 deletions

View file

@ -1,12 +1,53 @@
#!/usr/bin/env bash
# Invoked by ttyd-multi.service. ttyd's -a flag forwards ?arg=<value> as $1.
# Defence-in-depth: ttyd uses argv (never shell strings) and we re-validate
# here before handing the name to tmux as a quoted argv slot.
# Invoked by ttyd.service per WebSocket connection. ttyd's `-a` flag
# forwards `?arg=<value>` as $1; `-H X-authentik-username` sets
# $TTYD_USER to the Authentik identity.
#
# We map TTYD_USER → OS user via /etc/ttyd-user-map and sudo into that
# user before running tmux, so each Authentik identity gets its own
# kernel-isolated tmux server (one socket per uid). Authentik users
# without a mapping are denied — no fallback to a shared account.
set -euo pipefail
name="${1:-main}"
if ! [[ "$name" =~ ^[a-zA-Z0-9_-]{1,32}$ ]]; then
name=main
MAP=/etc/ttyd-user-map
NAME_RE='^[a-zA-Z0-9_-]{1,32}$'
auth_user="${TTYD_USER:-}"
auth_local="${auth_user%%@*}"
os_user=""
if [[ -n "$auth_local" && -r "$MAP" ]]; then
os_user=$(awk -F= -v k="$auth_local" '
/^[[:space:]]*(#|$)/ {next}
$1==k {sub(/:.*$/, "", $2); print $2; exit}
' "$MAP")
fi
exec tmux new-session -A -s "$name" -c /home/wizard/code
if [[ -z "$os_user" ]] || ! id "$os_user" >/dev/null 2>&1; then
cat <<EOF
Access denied
─────────────
No terminal account for Authentik user '${auth_user:-<missing header>}'.
This DevVM maps Authentik identities to OS users via
/etc/ttyd-user-map. Ask Viktor to add a mapping (and a matching
/etc/sudoers.d/ttyd-users entry) if you should have access.
EOF
sleep 10
exit 1
fi
# Session name from URL ?arg=<name>; default to the OS user's own name.
name="${1:-$os_user}"
[[ "$name" =~ $NAME_RE ]] || name="$os_user"
home_dir=$(getent passwd "$os_user" | cut -d: -f6)
home_dir="${home_dir:-/}"
if [[ "$os_user" == "$(id -un)" ]]; then
exec tmux new-session -A -s "$name" -c "$home_dir"
else
exec sudo -n -H -u "$os_user" tmux new-session -A -s "$name" -c "$home_dir"
fi