Warden Documentation
Warden is a self-hosted, browser-based terminal for SSH servers and AWS ECS Fargate containers. It pairs passwordless passkey authentication with time-boxed access approvals, encrypted session recording, live session sharing, and resumable sessions — all behind a full admin UI, with no browser extensions required.
A single Go HTTP server proxies WebSocket connections to real ssh and
aws ecs execute-command subprocesses over a PTY. The UI is static HTML with xterm.js from a CDN —
no build step. State lives in PostgreSQL; secrets and recordings are AES-256-GCM encrypted at rest.
Features #
Start with Quick Start (Docker) — Compose brings up Warden (with the AWS CLI + SSM plugin bundled) and PostgreSQL together. Then read How It Works to understand the session model.
Prerequisites #
Warden ships as Docker containers — everything it needs to run is inside the image and the Compose stack. On the host you only need Docker.
| Tool | Purpose |
|---|---|
Docker Engine 24+ | Runs the Warden container. |
Docker Compose v2 | Brings up Warden and its PostgreSQL together. |
Everything else is provided for you: the PostgreSQL database runs as a Compose service, and ssh/sshpass plus the AWS CLI v2 + session-manager-plugin (for ECS connections) are bundled inside the Warden image — nothing to install on the host.
Quick Start (Docker) #
Docker Compose starts the app (with the AWS CLI + session-manager-plugin bundled) and a PostgreSQL instance together.
cp .env.example .env # set ENCRYPTION_KEY, POSTGRES_PASSWORD, and review the rest
docker compose up -d # starts the provided Warden image + PostgreSQL
open http://localhost:8080
Compose refuses to start until POSTGRES_PASSWORD and ENCRYPTION_KEY are set in .env. Generate a key with openssl rand -base64 32 — changing it later makes existing encrypted secrets and recordings unreadable.
The image runs as non-root user warden (UID 1000) with a read-only root filesystem; /tmp and /home/warden are tmpfs, and /data (recordings + known_hosts) is a persistent volume.
First-Run Setup #
On first launch — when no users exist — Warden's bootstrapCheck() prints a one-time setup link to stdout (valid 24 hours):
WARDEN setup link (expires in 24 h): http://localhost:8080/...
Open that link to create the initial admin account with a passkey. Every subsequent user is invited from Admin → Users (an invite or reset link is emailed via SMTP, or printed to stdout if SMTP is not configured).
How It Works #
Browser (xterm.js)
│ WS binary frames — raw terminal I/O
│ WS text frames — JSON connect / resize / session_id / exit
▼
Go HTTP server (net/http + gorilla/websocket)
│ ── SSH path ── pty.Start(ssh / sshpass)
│ ── ECS path ── pty.Start(aws ecs execute-command --interactive)
│ one long-lived liveSession owns the PTY:
│ output → recorder (if enabled) + hub fan-out (owner + share subscribers) + scrollback
▼
Remote SSH server or AWS ECS task (via SSM session-manager-plugin)
- Browser authenticates via passkey; the server issues an
http_sessionscookie. - User opens a WebSocket to
/wsand sends{"type":"connect", "connType":"ssh"|"ecs", ...}(orresumeSessionIdto reattach). - Saved configs are resolved and the target is checked against the access-approval gate; inputs that become process args are validated.
- The PTY is owned by a liveSession that outlives the WebSocket. Detaching (logout / tab close) leaves it running; the owner reattaches later and the buffered scrollback is replayed.
- PTY output is fanned out to the owner and any share subscribers, and (when the environment opts in) appended to the recorder.
- On a clean remote exit the server sends
{"type":"exit"}and the tab auto-closes; the recording is finalized to an encrypted file on disk. - Sessions with no browser attached are reclaimed after
SESSION_IDLE_TIMEOUT; a session is also killed if its access grant expires.
The liveSession Model #
The load-bearing invariant: a liveSession owns the PTY — not the WebSocket. Registered in a sync.Map by sessionID, it owns the ptmx, the recorder, and a sessionHub, and it outlives any WebSocket.
- Start:
newLiveSessionlaunchesrun()(PTY → recorder + hub fan-out) and acmd.Wait()reaper. - Attach / detach: WebSockets attach via
serveOwnerorhandleShareWS. On WS close they detach — they never finalize or kill the session. - End: the session ends only when the PTY closes — process exit or
kill()(ptmx.Close(), called by the End endpoint, the idle sweeper, or when a grant expires). Thenfinish()runs once: reads the exit code, notifies a clean exit ({"type":"exit"}→ tab auto-closes), closes the hub, deletes from the registry, finalizes the recording, removes the temp SSH key. - Reclaim: detached sessions with no clients are reclaimed after
SESSION_IDLE_TIMEOUT; the sweeper also kills a session whose access grant has expired.
The session hub appends to a bounded scrollback (256 KiB) and fans output out to all clients; on attach it replays the scrollback to restore the screen. An owner-approved view→control upgrade takes effect live, with no reconnect.
Roles #
| Role | Terminal access | Admin panel | Use for |
|---|---|---|---|
user default | Yes | No | Engineers who open SSH/ECS terminals. |
admin | Yes | Yes | Operators who manage users, environments, AWS accounts, recordings. |
admin_only | No | Yes | Approvers / auditors — admin panel access without any terminal access. |
admin_only passes the admin guard but is blocked from /ws; the terminal page redirects it to /admin. Disabling a user immediately drops their browser sessions and kills their live terminals.
SSH Terminals #
Connect to any reachable SSH server. Warden launches a real ssh subprocess (wrapped by sshpass for password auth) attached to a PTY.
- In the UI, choose the SSH tab and enter host, username, port, and credentials — or pick a saved connection.
- Host, username, port, and any value that becomes a process argument are validated and rejected if they could be parsed as options (e.g.
-oProxyCommand=…). - SSH host keys are verified via
accept-new(trust-on-first-use) against a persistentknown_hostsby default; setSSH_HOST_KEY_CHECKING=yesfor strict verification against a pre-seeded file.
| Setting | Default | Description |
|---|---|---|
SSH_HOST_KEY_CHECKING | accept-new | accept-new (TOFU), yes (require known_hosts), or no (legacy insecure). |
SSH_KNOWN_HOSTS_FILE | known_hosts | Path to the SSH known_hosts file (Docker: /data/known_hosts). |
AWS ECS Terminals #
Exec into a Fargate container with aws ecs execute-command --interactive over the SSM session-manager-plugin.
- Enable ECS Exec on your task definition / service (
enableExecuteCommand: true). - Add an AWS account in Admin → AWS Accounts (access key ID + secret key, stored AES-256-GCM encrypted; optional default region/cluster/role ARN). Users then pick the account by name in the ECS connect form — no AWS credentials on the Warden host required. Selecting "host credentials" falls back to the host's ambient AWS config/instance role.
- The Docker image bundles the AWS CLI + session-manager-plugin — nothing to install on the host.
In the UI: switch to the AWS ECS tab, pick an AWS account, enter cluster + task ID (or ▾ List to browse running tasks), then connect.
Required IAM permissions
Credentials are shared by all Warden users, so apply least privilege — scope Resource to the specific cluster/task ARNs you expose rather than "*", and prefer a separate, narrowly scoped role per environment.
{
"Effect": "Allow",
"Action": [
"ecs:ExecuteCommand", "ecs:ListTasks", "ecs:DescribeTasks",
"ssm:StartSession", "ssm:TerminateSession", "ssm:DescribeSessions"
],
"Resource": [
"arn:aws:ecs:REGION:ACCOUNT:cluster/YOUR_CLUSTER",
"arn:aws:ecs:REGION:ACCOUNT:task/YOUR_CLUSTER/*"
]
}
ECS_ALLOW_UNGATED=false (default) means an AWS account or cluster must be placed in an environment the user can access before it may be used — the shared admin AWS credentials never reach a target no admin has assigned. Set it to true only to restore the legacy behavior where any authenticated user may use any ungated account/cluster.
AWS Accounts #
Admin-managed AWS credentials, encrypted at rest and shared across users — so engineers never handle raw AWS keys.
- Created in Admin → AWS Accounts: a name, access key ID, secret key (AES-256-GCM encrypted), and optional default region / cluster / role ARN.
- Users select an account by name in the ECS connect form; the secret never leaves the server and is passed only to the
awssubprocess via a minimal environment. - The non-admin list endpoint returns only names/metadata — never secret material.
So "host credentials" can't be used to dodge an account-scoped gate. See Access Approvals.
Saved Connections #
SSH and ECS connection profiles are stored server-side per user so common targets are one click away.
- SSH configs (
/api/configs): host, user, port, and credentials — secrets AES-256-GCM encrypted at rest. - ECS configs (
/api/ecs-configs): cluster + task references, pointing at a stored AWS account (account_id) rather than embedding credentials.
When you connect with a saved config, Warden resolves it server-side, then runs the same approval gate and argument validation as a manual connection.
Access Approvals #
Gate sensitive environments behind time-boxed, approver-granted access with a full audit trail.
Admins define environments and attach connection resources to them — an AWS account or ECS cluster (for ECS) and/or an SSH host glob. An environment marked restricted (requires_approval) needs an active, time-boxed grant to connect; each environment has a list of approvers.
- Users request access from the terminal page (My Access): environment + duration-or-until + justification. Approvers see the request (a ~5 s polling banner, or the admin Access Grants table) and approve / deny; admins can also grant directly.
- The gate runs at connect time and is re-checked on reattach; a running session is killed when its grant expires.
- ECS gating keys on both the AWS account and the cluster, so "host credentials" can't be used to dodge an account-scoped gate. SSH matches the host glob via
path.Match. - Every request and decision is auditable in Admin → Access Grants.
Environment fields
| Field | Description |
|---|---|
Resources (kind) | aws_account, ecs_cluster, or ssh_host (glob) attached to the environment. |
| Approvers | Users who may approve/deny requests for this environment. |
requires_approval | When true, an active grant is required to connect. |
record_sessions | Independently opt this environment into session recording (off by default). |
max_duration_secs | Optional cap on how long a grant can last. |
Grants are a state machine: pending → approved/denied/cancelled, approved → revoked; expired is derived. An active grant is approved and not expired. Approve / deny / revoke self-authorize (approver-for-env or admin).
Resumable Sessions #
A session keeps running server-side when you log out or close the tab. Reattach later with the screen restored.
- Because the liveSession owns the PTY, closing the browser only detaches — the remote process keeps running.
- Reconnect by sending
resumeSessionIdon the/wsconnect frame (the UI's Running Sessions list does this for you). Ownership and access are re-verified, then the buffered scrollback is replayed to restore the screen. - Detached sessions are reclaimed after
SESSION_IDLE_TIMEOUT(default2h); each user is capped atSESSION_MAX_PER_USERconcurrent live sessions (default 20), with a global cap ofMAX_LIVE_SESSIONS(default 500).
Live Session Sharing #
Share a running terminal with another user — view-only or with control — for pairing, debugging, or supervision.
- The owner invites a user to a running session; the invitee accepts and attaches as a subscriber to the session hub via
/ws/share/{id}, receiving the replayed scrollback. - A viewer can request control; the owner approves and the viewer is upgraded to control live — the share mode is re-checked per message, so no reconnect is needed.
- Revoking a share, or disabling the user, immediately disconnects active subscribers.
Sharing is managed through the /api/share/* endpoints (create, accept/decline, request/grant/deny control, revoke) — see the API Reference.
Session Recording #
Opt-in per environment. Captured as asciicast v2, gzipped and AES-256-GCM encrypted on disk, played back and downloaded by admins with speed control and scrubbing.
- Recording is off by default. A session is recorded only when its target's environment has
record_sessions=true— independent of whether the environment requires approval. - On finalize, output is gzipped → AES-GCM encrypted → written to
{RECORDINGS_DIR}/{id}.cast.gz(magic headerWER1), withrecording_path+recording_sizestored in the DB. On a file-write failure it falls back to an encrypted DB column. - Admins browse and play recordings in Admin → Session recordings (and via
/api/admin/sessions/{id}/recording). Playback handles legacy plaintext columns and legacy unencrypted gzip transparently.
| Setting | Default | Description |
|---|---|---|
RECORDINGS_DIR | recordings | Directory for encrypted recording files (Docker: /data/recordings). |
RECORDING_MAX_BYTES | 16777216 (16 MiB) | Per-recording in-memory buffer cap. |
RECORDING_RETENTION_DAYS | 0 (keep forever) | Delete recordings older than N days. Orphaned files with no DB row are always cleaned at startup. |
Recordings are encrypted with ENCRYPTION_KEY. Changing the key makes existing recordings (and stored secrets) unreadable.
Tabs, Tile & Broadcast #
Work many connections at once.
- Tabs — keep multiple SSH/ECS connections open and switch between them.
- Tile layout — view several terminals side-by-side.
- Broadcast — optionally mirror your keystrokes to every open terminal at once, for fan-out operations across a fleet.
Running and saved sessions are listed in the left rail; a running session shows a reattach (↻) indicator.
Authentication #
Warden is passkey-only (WebAuthn) — there are no passwords to phish, reuse, or leak.
- Registration is invite-driven: an admin sends a magic link (one-time token, 24 h) which the user opens to register a passkey. The very first admin is created from the first-run setup link.
- Login uses the registered passkey; the WebAuthn sign counter is persisted on each login.
- Browser sessions are
http_sessionscookies — 7-day rolling with an absolute 30-day cap.https://auto-enables Secure cookies + HSTS. - Auth endpoints are protected by a per-IP rate limiter; magic tokens are one-time; disabling a user drops their sessions and kills live terminals.
| Setting | Default | Description |
|---|---|---|
APP_URL | http://localhost:8080 | Public URL — the WebAuthn origin; also scopes WS/CSRF origin checks and CSP. https:// auto-enables Secure cookies + HSTS. |
RP_ID | (APP_URL host) | WebAuthn relying-party ID — must match the domain users browse to. |
Security Model #
Built-in protections across the request, subprocess, and storage boundaries.
- Input validation (the RCE boundary) — host/username/port and ECS fields that become process arguments are rejected if they could be parsed as options (e.g.
ssh -oProxyCommand=…). Any user value reachingexecgoes throughvalidate.go. - SSH host keys — verified via
accept-new(TOFU) against a persistentknown_hostsby default;SSH_HOST_KEY_CHECKING=yesfor strict. - WebSocket — origin checked against
APP_URL; per-connection read limit; keepalive with read deadlines. - Secrets at rest — SSH/AWS credentials and recordings are AES-256-GCM encrypted. Subprocesses receive a minimal environment — the AES key / DB URL / SMTP password are never passed to
ssh/aws. - Headers — CSP,
X-Frame-Options: DENY,nosniff,Referrer-Policy: no-referrer; HSTS whenSECURE_COOKIES. - Auth — passkey-only; per-IP rate limiting; one-time magic tokens; absolute session lifetime; disabling a user drops sessions and kills live terminals.
The Docker image runs as non-root warden (UID 1000) with a read-only root filesystem, all Linux capabilities dropped, and no-new-privileges. /tmp and /home/warden are tmpfs; /data is the only persistent, writable mount.
STRICT_AAD=true rejects legacy unauthenticated/unbound data — it disables the no-AAD decryption fallback and requires the WER1 header on recordings. Enable only after re-encrypting any pre-existing secrets/recordings.
Hardening Checklist #
Before exposing Warden to the internet:
- Put TLS in front (the bundled Caddy service, or Nginx) — see Deployment & HTTPS.
- Set
APP_URLto yourhttps://domain andRP_IDto its host. - Set
SECURE_COOKIES=trueandTRUST_PROXY=true(only when actually behind a trusted proxy that setsX-Forwarded-For/X-Real-IP). - Keep
BIND_ADDR=127.0.0.1unless you front it with TLS; never publish the app port directly without TLS. - Generate a strong
ENCRYPTION_KEYand back it up — losing it makes all encrypted secrets and recordings unreadable. - Use
sslmode=require(orverify-full) for any non-loopbackDATABASE_URL. - Prefer strict SSH host keys (
SSH_HOST_KEY_CHECKING=yeswith a seeded known_hosts) for sensitive fleets. - Scope AWS IAM to specific cluster/task ARNs; use a narrowly scoped role per environment.
Configuration #
All settings are environment variables. Copy .env.example to .env to get started; Docker Compose reads .env automatically.
Server & networking
| Variable | Default | Description |
|---|---|---|
ADDR or PORT | :8080 | Listen address / port. |
BIND_ADDR | 127.0.0.1 | (Docker) host interface the port binds to; 0.0.0.0 to expose directly (only behind TLS). |
APP_URL | http://localhost:8080 | Public URL — WebAuthn origin; scopes WS/CSRF origin checks and CSP. https:// auto-enables Secure cookies + HSTS. |
RP_ID | (APP_URL host) | WebAuthn relying-party ID. |
TZ | UTC | Container timezone (affects log timestamps). |
Database
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | required | PostgreSQL connection string. Use sslmode=require for a non-loopback DB. The server refuses to start if unset. |
POSTGRES_USER / POSTGRES_PASSWORD / POSTGRES_DB | warden / — / warden | Used by Docker Compose to configure the postgres service. Compose refuses to start until POSTGRES_PASSWORD is set. |
Encryption & cookies
| Variable | Default | Description |
|---|---|---|
ENCRYPTION_KEY | required | Base64 32-byte key encrypting stored credentials and recordings. Generate: openssl rand -base64 32. The server refuses to start without it. |
WARDEN_DEV | (unset) | 1 permits booting with an ephemeral encryption key — local dev only; data is lost on restart. |
SECURE_COOKIES | false | Force the cookie Secure flag + HSTS. Auto-enabled when APP_URL is https://. |
TRUST_PROXY | false | Trust X-Real-IP / rightmost X-Forwarded-For (for rate limiting) only when behind a trusted proxy. |
STRICT_AAD | false | Reject legacy unauthenticated/unbound data (requires the WER1 header). Enable only after re-encrypting legacy data. |
Sessions & limits
| Variable | Default | Description |
|---|---|---|
SESSION_IDLE_TIMEOUT | 2h | How long a detached session runs before the server kills it (Go duration: 30m, 2h, 8h). |
SESSION_MAX_PER_USER | 20 | Cap on concurrent live sessions per user. |
MAX_LIVE_SESSIONS | 500 | Global cap on concurrent live sessions across all users. |
CONN_RATE_PER_MIN | 30 | Per-source-IP rate limit for new connection/spawn attempts on /ws, /ws/share, and /api/ecs/tasks. |
SSH & ECS
| Variable | Default | Description |
|---|---|---|
SSH_HOST_KEY_CHECKING | accept-new | accept-new (TOFU), yes (require known_hosts), or no (legacy insecure). |
SSH_KNOWN_HOSTS_FILE | known_hosts | Path to the SSH known_hosts file (Docker: /data/known_hosts). |
ECS_ALLOW_UNGATED | false | When false (fail-closed), an AWS account/cluster must live in an accessible environment before use. true restores legacy ungated access. |
Recordings
| Variable | Default | Description |
|---|---|---|
RECORDINGS_DIR | recordings | Directory for encrypted recording files (Docker: /data/recordings). |
RECORDING_MAX_BYTES | 16777216 | Per-recording in-memory buffer cap (16 MiB). |
RECORDING_RETENTION_DAYS | 0 | Delete recordings older than N days. 0/unset keeps them indefinitely. |
SMTP (optional — magic links print to stdout if unset)
| Variable | Default | Description |
|---|---|---|
SMTP_HOST | — | SMTP server for magic-link emails. Unset → links printed to stdout. |
SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM | 587 / — | SMTP delivery settings. |
SMTP_TLS | require | Transport security: STARTTLS required by default (implicit TLS on port 465). disable for local testing only; implicit to force SMTPS on a non-465 port. |
Deployment & HTTPS #
The recommended deployment is Docker Compose with a TLS reverse proxy in front.
Compose services
The bundled docker-compose.yml runs the warden app (read-only root FS, all caps dropped, tmpfs /tmp + /home/warden, persistent /data volume) and a pinned postgres:16-alpine with healthchecks. The app port binds to ${BIND_ADDR:-127.0.0.1} so it isn't reachable off-host without a proxy.
HTTPS with the bundled Caddy
Uncomment the caddy service in docker-compose.yml and edit the Caddyfile with your domain — Caddy obtains and renews a Let's Encrypt certificate automatically. Requirements: a public domain pointing at the host, ports 80 + 443 open.
ssh.yourdomain.com {
reverse_proxy warden:8080 {
# Forward the real client IP (replace, don't append, so a client-supplied
# X-Forwarded-For cannot survive into the chain and spoof the rate-limiter).
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
flush_interval -1 # snappier WebSocket streaming
}
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
}
Then set APP_URL=https://ssh.yourdomain.com, RP_ID=ssh.yourdomain.com, SECURE_COOKIES=true, and TRUST_PROXY=true.
App↔DB traffic uses sslmode=disable in Compose because both containers share a private Docker network and the DB publishes no host port. If you run the database on a separate host, provision a server cert and set sslmode=require (or verify-full) so the DB password and encrypted columns aren't sent in cleartext.
Database Schema #
Schema is applied at startup via CREATE TABLE IF NOT EXISTS + ALTER TABLE … ADD COLUMN IF NOT EXISTS — there is no separate migration tool. All of it lives in db.go:schema().
| Table | Purpose |
|---|---|
users | Accounts (email, role user/admin/admin_only, disabled). |
passkeys | WebAuthn credentials (sign counter persisted on login). |
magic_tokens | Invite / reset / setup one-time tokens (24 h). |
http_sessions | Browser cookies — 7-day rolling, absolute 30-day cap. |
ssh_configs / ecs_configs | Saved connection profiles (SSH secrets encrypted; ECS refs an account_id). |
aws_accounts | Admin-managed AWS creds (secret AES-GCM encrypted), shared across users. |
ssh_sessions | Audit log of all terminal sessions (ECS too) + recording_path/recording_size. |
session_shares | Live share invitations + control_requested. |
environments / environment_resources / environment_approvers | Access-control configuration. |
access_grants | Time-boxed access requests / grants (audit trail). |
HTTP API Reference #
All /api/* routes require an authenticated session cookie; /api/admin/* requires an admin (or admin_only) role. State-changing requests are CSRF-guarded. WebSocket endpoints are origin-checked and connection-rate-limited.
WebSockets & pages
| Method | Path | Description |
|---|---|---|
| GET | /ws | Terminal WebSocket — connect or reattach (resumeSessionId). Auth + connection-rate-limited. |
| GET | /ws/share/{id} | Attach to a shared session as a subscriber (view or control). |
| GET | / · /login · /admin | Terminal UI · auth/invite page · admin panel (admin-gated). |
Auth & user
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/register/begin · /finish | WebAuthn registration (rate-limited). |
| POST | /api/auth/login/begin · /finish | WebAuthn login (rate-limited). |
| POST | /api/auth/logout | End the browser session. |
| GET | /api/auth/validate-token | Validate an invite / reset / setup token. |
| GET | /api/user/me · /api/user/passkeys | Current user; list own passkeys (DELETE /{id} to remove). |
Connections — SSH / ECS / AWS
| Method | Path | Description |
|---|---|---|
| GETPOST | /api/configs | List / create saved SSH configs (GET/PUT/DEL /{id}). |
| GET | /api/ecs/tasks | List running ECS tasks for an account/cluster (rate-limited). |
| GETPOST | /api/ecs-configs | List / create saved ECS configs (DEL /{id}). |
| GET | /api/aws-accounts | List AWS accounts (names/metadata only — no secrets). |
| GETPOST | /api/admin/aws-accounts | Admin: full CRUD over AWS accounts (PUT/DEL /{id}). |
Sharing
| Method | Path | Description |
|---|---|---|
| GET | /api/users | List users to share with. |
| POST | /api/share | Create a share invitation. |
| GET | /api/share/pending · /control-requests | Pending share invites; pending control requests. |
| POST | /api/share/{id}/accept · /decline | Respond to a share invite (DEL /{id} to revoke). |
| POST | /api/share/{id}/request-control · /grant-control · /deny-control | Live view→control upgrade flow. |
Environments & access grants
| Method | Path | Description |
|---|---|---|
| GET | /api/environments · /restricted | List environments; list restricted ones the user can request. |
| POST | /api/admin/environments | Admin: create environments (PUT/DEL /{id}, resources, approvers). |
| POST | /api/access/requests | Request access to an environment (GET /my, DEL /requests/{id}). |
| GET | /api/access/pending | Approver: list pending approvals. |
| POST | /api/access/{id}/approve · /deny · /revoke | Approve / deny / revoke a grant. |
| GETPOST | /api/admin/grants | Admin: list all grants; create a direct grant. |
Sessions & admin
| Method | Path | Description |
|---|---|---|
| GET | /api/sessions/active | List the user's active (running) sessions. |
| POST | /api/sessions/{id}/end | End a running session (kills the PTY). |
| GET | /api/admin/users | Admin: user CRUD (GET/PATCH/DEL /{id}). |
| POST | /api/admin/invite · /users/{id}/reset-passkey | Invite a user; reset a user's passkey. |
| GET | /api/admin/sessions · /{id}/recording | Admin: session audit log; fetch a recording for playback/download. |
AgileSecOps