diff --git a/scripts/sudoers-t3-autopair b/scripts/sudoers-t3-autopair new file mode 100644 index 00000000..fe517b74 --- /dev/null +++ b/scripts/sudoers-t3-autopair @@ -0,0 +1,6 @@ +# The t3-dispatch service (unprivileged user t3-dispatch) may run ONLY the +# t3-mint wrapper, as root. t3-mint validates the target user against +# /etc/ttyd-user-map and mints a one-time t3 pairing token as that user. +# A compromise of the network-facing dispatch service can therefore mint +# pairing tokens for already-mapped users at most — never arbitrary root. +t3-dispatch ALL=(root) NOPASSWD: /usr/local/bin/t3-mint diff --git a/scripts/t3-dispatch.service b/scripts/t3-dispatch.service index dc37cc03..426948fe 100644 --- a/scripts/t3-dispatch.service +++ b/scripts/t3-dispatch.service @@ -4,7 +4,9 @@ After=network.target [Service] Type=simple -User=wizard +# Unprivileged dedicated user; the only privileged action is `sudo t3-mint` +# (scoped in /etc/sudoers.d/t3-autopair). Compromise => mint tokens at most. +User=t3-dispatch ExecStart=/usr/local/bin/t3-dispatch Restart=on-failure RestartSec=5 diff --git a/scripts/t3-dispatch/main.go b/scripts/t3-dispatch/main.go index baf43d64..3a254be4 100644 --- a/scripts/t3-dispatch/main.go +++ b/scripts/t3-dispatch/main.go @@ -62,9 +62,10 @@ func lookup(ak string) (entry, bool) { // user, via the scoped sudoers entry) and exchanges it at the instance's // /api/auth/bootstrap, relaying the returned t3_session Set-Cookie to the browser. func autoPair(e entry, w http.ResponseWriter, r *http.Request) { - out, err := exec.Command("sudo", "-n", "-u", e.OsUser, - "t3", "auth", "pairing", "create", - "--base-dir", "/home/"+e.OsUser+"/.t3", "--ttl", "5m", "--json").Output() + // t3-mint (root, via scoped sudoers) validates the OS user is in + // /etc/ttyd-user-map, then mints as that user. The dispatch service itself + // runs unprivileged and can invoke nothing else. + out, err := exec.Command("sudo", "-n", "/usr/local/bin/t3-mint", e.OsUser).Output() if err != nil { log.Printf("mint for %s failed: %v", e.OsUser, err) http.Error(w, "pairing mint failed", http.StatusInternalServerError) diff --git a/scripts/t3-mint b/scripts/t3-mint new file mode 100644 index 00000000..078f89e8 --- /dev/null +++ b/scripts/t3-mint @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Mint a one-time t3 pairing token for a mapped OS user. +# Runs as root via the scoped sudoers entry for the t3-dispatch service user. +# Validates the requested user is an actual t3 OS user (a value on the RHS of +# /etc/ttyd-user-map) before minting as that user. Prints the t3 CLI JSON. +set -euo pipefail +os_user="${1:-}" +[[ "$os_user" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]] || { echo "invalid user" >&2; exit 2; } +# Must be a mapped t3 OS user (RHS of a non-comment "authentik=os" line). +awk -F= '!/^[[:space:]]*#/ && NF==2 { gsub(/[[:space:]]/, "", $2); print $2 }' /etc/ttyd-user-map \ + | grep -qxF "$os_user" || { echo "user not mapped" >&2; exit 3; } +exec runuser -u "$os_user" -- /usr/bin/t3 auth pairing create \ + --base-dir "/home/${os_user}/.t3" --ttl 5m --json