Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion apps/agor-cli/src/commands/admin/ensure-user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,12 @@ export default class EnsureUser extends Command {
// Create the user
try {
this.log(`Creating Unix user: ${username}`);
await executor.exec(UnixUserCommands.createUser(username, '/bin/bash', homeBase));
// Only pass --home when the user explicitly overrides the default
// home base; otherwise let useradd use the system default. The
// wrapper validates the supplied path (see docker/sudoers/agor-user-admin).
const homeDir =
homeBase && homeBase !== AGOR_HOME_BASE ? `${homeBase}/${username}` : undefined;
await executor.exec(UnixUserCommands.createUser(username, homeDir));
this.log(`✅ Created Unix user: ${username}`);

// Setup ~/agor/worktrees directory
Expand Down
43 changes: 24 additions & 19 deletions apps/agor-docs/pages/guide/multiplayer-unix-isolation.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,24 @@ storage:

### 3. Sudoers Configuration

The daemon requires sudo access for Unix user/group management. Agor provides a comprehensive, production-ready sudoers file with extensive documentation and security scoping.
The daemon requires sudo access for Unix user/group management. Instead of granting wildcard sudo rules for `useradd`, `usermod`, `chpasswd`, `groupadd`, `gpasswd`, and `find` — each a well-known root-shell path — Agor funnels all privileged operations through a single validated wrapper script: **`agor-user-admin`**.

The wrapper:

- Accepts a fixed set of verbs (`add-user`, `delete-user`, `add-group`, `set-password`, `setgid-tree`, `list-symlinks`, …) with strict argument validation
- Re-validates usernames, groupnames, and filesystem paths before calling any tool (regex check, system-account deny-list, `readlink -f` + allowlist)
- Canonicalizes paths so symlinks cannot escape `/home/*/agor/*`, `/home/*/.agor/*`, or `/var/agor/*`
- Rejects chpasswd stdin containing NUL/CR/LF/`:` or non-printable bytes
- Audits every invocation to syslog (`logger -t agor-user-admin`)

**Installation:**

```bash
# Download the sudoers file
# 1) Install the wrapper (owned by root, mode 0755)
curl -O https://raw.githubusercontent.com/preset-io/agor/main/docker/sudoers/agor-user-admin
sudo install -o root -g root -m 0755 ./agor-user-admin /usr/local/sbin/agor-user-admin

# 2) Install the sudoers rule (single NOPASSWD line, no wildcards)
curl -O https://raw.githubusercontent.com/preset-io/agor/main/docker/sudoers/agor-daemon.sudoers

# CRITICAL: Always validate syntax before installing
Expand All @@ -127,11 +139,12 @@ sudo visudo -c -f ./agor-daemon.sudoers
# Install with correct permissions
sudo install -m 0440 ./agor-daemon.sudoers /etc/sudoers.d/agor

# Verify configuration
# 3) Verify
sudo -l -U agor
sudo -n /usr/local/sbin/agor-user-admin # should exit 64 "usage: …"
```

**View the complete file:** [`docker/sudoers/agor-daemon.sudoers`](https://github.com/preset-io/agor/blob/main/docker/sudoers/agor-daemon.sudoers)
**View the files:** [`docker/sudoers/agor-user-admin`](https://github.com/preset-io/agor/blob/main/docker/sudoers/agor-user-admin) · [`docker/sudoers/agor-daemon.sudoers`](https://github.com/preset-io/agor/blob/main/docker/sudoers/agor-daemon.sudoers)

<details>
<summary>**What the sudoers file enables**</summary>
Expand All @@ -144,23 +157,15 @@ sudo -l -U agor
agor ALL=(%agor_users) NOPASSWD: ALL
```

**User Management:**

```bash
# Create/manage Unix users for Agor users
agor ALL=(ALL) NOPASSWD: /usr/sbin/useradd *
agor ALL=(ALL) NOPASSWD: /usr/sbin/userdel *
agor ALL=(ALL) NOPASSWD: /usr/sbin/usermod *
agor ALL=(ALL) NOPASSWD: /usr/sbin/chpasswd
```

**Group Management (Worktree Isolation):**
**User / group management and password sync — routed through the wrapper:**

```bash
# Create Unix groups per worktree (agor_wt_<id>)
agor ALL=(ALL) NOPASSWD: /usr/sbin/groupadd *
agor ALL=(ALL) NOPASSWD: /usr/sbin/groupdel *
agor ALL=(ALL) NOPASSWD: /usr/sbin/gpasswd *
# ONE NOPASSWD line covers add/delete/lock/unlock users, add/delete groups,
# add/remove user-from-group, set-password, setgid-tree, and symlink prune.
# The wrapper re-validates every argument and only shells out to the
# matching real tool (useradd, usermod, chpasswd, groupadd, groupdel,
# gpasswd, find) with pinned, safe argv.
agor ALL=(root) NOPASSWD: /usr/local/sbin/agor-user-admin
```

**Filesystem Operations (SCOPED TO AGOR PATHS ONLY):**
Expand Down
9 changes: 9 additions & 0 deletions docker/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ RUN useradd -m -s /bin/bash agor_executor && \
mkdir -p /home/agor_executor/.agor && \
chown -R agor_executor:agor_executor /home/agor_executor

# Install the agor-user-admin wrapper. This is the single sudoers entry point
# for user/group/password/find operations on installations that opt into the
# hardened sudoers file (docker/sudoers/agor-daemon.sudoers). It is installed
# in every image so the restrictive sudoers config can be enabled at any time
# without rebuilding.
COPY docker/sudoers/agor-user-admin /usr/local/sbin/agor-user-admin
RUN chown root:root /usr/local/sbin/agor-user-admin && \
chmod 0755 /usr/local/sbin/agor-user-admin

# Copy Zellij config for both users
COPY docker/zellij-config.kdl /tmp/zellij-config.kdl
RUN mkdir -p /home/agor/.config/zellij && \
Expand Down
10 changes: 6 additions & 4 deletions docker/docker-entrypoint-postgres.sh
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,13 @@ create_unix_user() {

echo "👤 Creating Unix user: $username"

# Create user with home directory
sudo -n useradd -m -s /bin/bash "$username"
# Create user with home directory. Routed through agor-user-admin so the
# entrypoint exercises the same path as production hardened installs —
# flag-smuggling regressions are caught in dev, not in prod.
sudo -n /usr/local/sbin/agor-user-admin add-user "$username"

# Set password to 'admin'
echo "$username:admin" | sudo -n chpasswd
# Set password to 'admin' (stdin to wrapper; wrapper runs chpasswd internally)
printf 'admin' | sudo -n /usr/local/sbin/agor-user-admin set-password "$username"

# Create .agor directory
sudo -n mkdir -p "/home/$username/.agor"
Expand Down
50 changes: 28 additions & 22 deletions docker/sudoers/agor-daemon.sudoers
Original file line number Diff line number Diff line change
Expand Up @@ -49,26 +49,33 @@ Defaults:agor !requiretty
agor ALL=(%agor_users) NOPASSWD: ALL

# ============================================================================
# USER MANAGEMENT - Create/Delete Agor Users
# USER + GROUP MANAGEMENT - via agor-user-admin wrapper
# ============================================================================
# These commands are used when users sign up or are onboarded.
# The daemon creates a corresponding Unix user for each Agor user.

agor ALL=(ALL) NOPASSWD: /usr/sbin/useradd *
agor ALL=(ALL) NOPASSWD: /usr/sbin/userdel *
agor ALL=(ALL) NOPASSWD: /usr/sbin/usermod *
agor ALL=(ALL) NOPASSWD: /usr/sbin/chpasswd

# ============================================================================
# GROUP MANAGEMENT - Worktree Isolation
# ============================================================================
# Each worktree gets a Unix group (agor_wt_<short-id>).
# Users with 'all' permission are added to the group.
# Filesystem permissions on the worktree directory enforce access.
# The daemon's user/group/password operations all route through a single
# root-owned wrapper script that validates verbs and arguments internally.
#
# This replaces five previous wildcard NOPASSWD rules that were each a
# root-shell path under sudo (any of them would have let a compromised
# daemon smuggle flags into useradd/usermod/gpasswd/chpasswd/find):
#
# # Superseded by /usr/local/sbin/agor-user-admin — see
# # docker/sudoers/agor-user-admin for the validated verb interface.
# agor ALL=(ALL) NOPASSWD: /usr/sbin/useradd *
# agor ALL=(ALL) NOPASSWD: /usr/sbin/userdel *
# agor ALL=(ALL) NOPASSWD: /usr/sbin/usermod *
# agor ALL=(ALL) NOPASSWD: /usr/sbin/chpasswd
# agor ALL=(ALL) NOPASSWD: /usr/sbin/groupadd *
# agor ALL=(ALL) NOPASSWD: /usr/sbin/groupdel *
# agor ALL=(ALL) NOPASSWD: /usr/sbin/gpasswd *
# agor ALL=(ALL) NOPASSWD: /usr/bin/find * (see below — also collapsed)
#
# Verbs supported by the wrapper (see script for validators + audit log):
# add-user / delete-user [--remove-home] / lock-user / unlock-user
# add-group / delete-group / add-to-group / remove-from-group
# set-password (password on stdin)
# setgid-tree / list-symlinks / prune-all-symlinks / prune-broken-symlinks

agor ALL=(ALL) NOPASSWD: /usr/sbin/groupadd *
agor ALL=(ALL) NOPASSWD: /usr/sbin/groupdel *
agor ALL=(ALL) NOPASSWD: /usr/sbin/gpasswd *
agor ALL=(root) NOPASSWD: /usr/local/sbin/agor-user-admin

# ============================================================================
# FILESYSTEM OPERATIONS - Worktree Permissions (SCOPED TO AGOR PATHS)
Expand Down Expand Up @@ -120,14 +127,13 @@ agor ALL=(ALL) NOPASSWD: /usr/bin/tee /var/agor/*
# QUERY AND TRAVERSAL COMMANDS
# ============================================================================
# id, getent, readlink, test: read-only state checks, no security risk.
# find: used with -exec chmod g+s for setgid propagation. Note that
# find with -exec can run arbitrary commands as root — command construction
# is controlled in our codebase (group-manager.ts), not from user input.
# `find` is NOT granted here — all legitimate find uses (setgid propagation
# and symlink cleanup) now route through agor-user-admin's setgid-tree /
# list-symlinks / prune-all-symlinks / prune-broken-symlinks verbs.

agor ALL=(ALL) NOPASSWD: /usr/bin/id *
agor ALL=(ALL) NOPASSWD: /usr/bin/getent *
agor ALL=(ALL) NOPASSWD: /usr/bin/readlink *
agor ALL=(ALL) NOPASSWD: /usr/bin/find *
agor ALL=(ALL) NOPASSWD: /usr/bin/test *


Expand Down
Loading
Loading