Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2c4fe53
Authorize every gateway API route; scope API tokens to their own perm…
ndbroadbent Oct 9, 2026
d72f7dc
Close MFA bypass via factor enrollment from a pre-MFA session
ndbroadbent Oct 9, 2026
e4e5bc8
Reject env var names and values that can break the KEY=VALUE format
ndbroadbent Oct 9, 2026
918c39b
Merge branch 'nathan/security-mfa-pending' into nathan/security-authz
ndbroadbent Oct 9, 2026
d648908
Proxy: forward an allowlist of client headers and set the rack actor
ndbroadbent Oct 9, 2026
2d97393
Scrub credentials from Sentry events; refuse test-only switches in pr…
ndbroadbent Oct 9, 2026
54f8cdd
Merge branch 'nathan/security-header-allowlist' into nathan/security-…
ndbroadbent Oct 9, 2026
49cc881
Require verified, hosted-domain Google identities; RS256 only
ndbroadbent Oct 9, 2026
46bf350
Fix approval list MFA level and E2E fallout from the MFA session gate
ndbroadbent Oct 9, 2026
f1ccd58
Refuse websocket redirects that downgrade wss to ws
ndbroadbent Oct 9, 2026
b791e00
Proxy: refuse unknown query parameters; cap approval tokens at owner …
ndbroadbent Oct 9, 2026
8cb80ab
Clear MFA verification on other sessions at first enrollment
ndbroadbent Oct 9, 2026
bb1d867
Accept backup codes in the web UI; verify the browser on CLI login MFA
ndbroadbent Oct 9, 2026
11acd99
Self-service for non-admin roles; route table checked against the router
ndbroadbent Oct 9, 2026
0f2407e
Keep agent worktrees out of build contexts; close stdin for E2E CLI c…
ndbroadbent Oct 9, 2026
d3db28f
Address DeepSource and CodeRabbit findings on the review fixes
ndbroadbent Oct 9, 2026
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
3 changes: 3 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ docs
# other repos checked out for reference
reference/

# Local agent state (Claude Code settings and worktrees)
.claude/

# Web app artifacts
web/node_modules/
web/dist
Expand Down
8 changes: 7 additions & 1 deletion cmd/mock-convox/handlers_apps.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,13 @@ func updateService(w http.ResponseWriter, r *http.Request) {
app := vars["app"]
service := vars["service"]

updated, err := updateServiceState(app, service, r.URL.Query())
// Like the real rack, read options from the form body (where the SDK sends them) as well as the query.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
updated, err := updateServiceState(app, service, r.Form)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
Expand Down
16 changes: 16 additions & 0 deletions docs/src/content/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ description: Complete reference of all environment variables for Rack Gateway.
This page provides a complete reference of Rack Gateway environment variables.
For a shorter overview, see [Configuration](/configuration/).

## Production Safety Check

The first time a gateway starts against a database, it marks the database as `production` (or `development` when `DEV_MODE=true`). A gateway refuses to start against a production database while any development or test-only setting is present, because each one weakens a security control:

| Variable | Why it's refused in production |
|----------|--------------------------------|
| `DEV_MODE=true` | Relaxes cookies, CSP and secret requirements |
| `E2E_TEST_MODE=true` | Skips WebAuthn assertion checks |
| `AWS_ENDPOINT_URL` | Sends every AWS call (S3 audit anchors, STS, KMS) to another endpoint |
| `AWS_ENDPOINT_URL_S3` | Sends audit anchors to another S3 endpoint |
| `AWS_ENDPOINT_URL_STS`, `AWS_ENDPOINT_URL_KMS` | Sends AWS credential exchange or key operations to another endpoint |
| `POSTMARK_API_BASE` | Sends the Postmark token to another server |
| `GOOGLE_OAUTH_BASE_URL` without `https://` | Accepts identity tokens over plain HTTP |

The error names every offending variable. Remove them from the production environment and restart. `GOOGLE_ALLOWED_DOMAIN` is required whenever `DEV_MODE` is off.

## Core Server

| Variable | Default | Description |
Expand Down
4 changes: 3 additions & 1 deletion docs/src/content/docs/development/api-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ The OpenAPI spec is the source of truth, but this page summarizes the current su

## Authentication

Most endpoints require either a session cookie (browser/CLI login) or an API token (automation).
Gateway endpoints require a session (browser cookie or CLI login). API tokens (automation) can use the rack proxy (`/api/v1/rack-proxy/*`) and only four gateway endpoints: `GET /info`, `GET /rack`, `POST /deploy-approval-requests` and `GET /deploy-approval-requests/{id}`. Every other gateway endpoint returns `403 API tokens cannot use this endpoint`.

Each gateway endpoint declares the permission it needs; callers without it get `403 insufficient permissions: requires <permission>`. Endpoints under `/users/{email}` that read or sign out a user's own sessions, profile or audit log also accept that user themselves.

### Session Authentication

Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/getting-started/architecture.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ rack-gateway apps
4. Gateway validates session token
5. Gateway checks MFA requirements (if enabled)
6. Gateway checks RBAC permissions for `convox:app:list`
7. If authorized, gateway forwards to real Convox rack
7. If authorized, gateway forwards to real Convox rack. Only the request headers the Convox API uses (an allowlist) are forwarded; cookies, CSRF and MFA headers and any client-supplied identity headers are dropped. The gateway authenticates to the rack with its own credential and sets `X-Convox-Actor` to the signed-in user (or `token:<name>`), so the rack's own logs name the real caller. Query parameters are limited to the ones the Convox SDK sends; a request with any other is refused, because the rack would read options such as an exec command or release env from the query string without the gateway checking them.
8. Gateway logs the action to audit log
9. Response returned to user

Expand Down
28 changes: 19 additions & 9 deletions docs/src/content/docs/security/authentication/api-tokens.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ API tokens provide authentication for automated systems like CI/CD pipelines, sc
| Aspect | API Tokens | Sessions |
|--------|-----------|----------|
| **Use Case** | Automation, CI/CD | Human users |
| **Creation** | Admin/user creates | OAuth flow |
| **Lifetime** | Until deleted | Idle timeout |
| **Creation** | Deployers (for themselves) or admins | OAuth flow |
| **Lifetime** | Until deleted or the optional `expires_at` | Idle timeout |
| **MFA** | Not applicable | Supported |
| **CSRF** | Not required | Required |
| **Revocation** | Manual delete | Logout/admin |
Expand Down Expand Up @@ -58,14 +58,24 @@ API tokens store explicit permissions. Roles are a shortcut at creation time:
| `ops` | Emergency scripts | Restart, exec, view env |
| `deployer` | Deploy automation | Full deploy capabilities |
| `cicd` | CI/CD pipelines | Minimal deploy + approval |
| `admin` | Full automation | All operations |
| `admin` | Rack automation | All Convox operations, capped by the owner's role |

<Aside type="tip" title="Recommended: CI/CD Role">
For CI/CD pipelines, use the dedicated `cicd` role instead of `deployer`. It has minimal permissions and integrates with deploy approvals for additional security.
</Aside>

See [Roles](/security/rbac/roles/) for detailed permission lists.

## What a Token Can Do

A token is always limited by the person who owns it:

- **Capped by the owner's current role.** Every request needs the permission in the token's own list *and* in the owner's current role. Demote the owner and their tokens lose the extra access on the next request.
- **Stops working with its owner.** Tokens of a locked, suspended or deleted user are rejected.
- **Created within the owner's role.** When a token is created or edited, each permission must be one the owner's role grants. Deployers can create tokens for themselves; only admins can create tokens for other users.
- **Rack access only, plus a few gateway endpoints.** Tokens can use the rack proxy (`/api/v1/rack-proxy/*`) and only these gateway endpoints: `GET /api/v1/info`, `GET /api/v1/rack`, `POST /api/v1/deploy-approval-requests` and `GET /api/v1/deploy-approval-requests/:id`. User, token, settings, audit-log and approval endpoints refuse tokens, even admin-owned ones.
- **No privileged runs.** Tokens can never start a process with a custom image, host volumes, privileged mode or node placement options.

## Creating Tokens

### Web UI
Expand Down Expand Up @@ -227,7 +237,7 @@ Always use the minimum required role:
| Monitoring | `viewer` | Read-only sufficient |
| Health checks | `viewer` | Read-only sufficient |
| Emergency restart | `ops` | Only restart, no deploy |
| Admin automation | `admin` | Full access needed |
| Rack automation | `admin` | Only if it really needs every Convox operation |

### Token Rotation

Expand Down Expand Up @@ -322,16 +332,16 @@ Token-based requests are distinguishable from session-based requests in audit lo

API tokens don't support MFA because:
- Automation can't complete interactive MFA
- Token role determines permissions
- The token's permissions, capped by its owner's role, determine access
- Audit logging provides accountability

For sensitive operations requiring human approval, use [Deploy Approvals](/integrations/deploy-approvals/).

### No Expiration
### Optional Expiration

Tokens don't expire automatically:
- Useful for long-running automation
- Manual rotation/deletion required
Tokens don't expire unless you set `expires_at` when creating them (for example `rack-gateway api-token create --expires-at 2027-01-01T00:00:00Z`):
- Without an expiry, tokens suit long-running automation
- Manual rotation/deletion is still required
- Regular review process recommended

### No CSRF
Expand Down
41 changes: 30 additions & 11 deletions docs/src/content/docs/security/rbac/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Resources specific to the gateway:
| Resource | Description |
|----------|-------------|
| `api_token` | API tokens for automation |
| `audit_log` | The gateway audit log |
| `deploy_approval_request` | Deploy approval requests |
| `integration` | Third-party integrations |
| `job` | Background jobs |
Expand Down Expand Up @@ -170,6 +171,11 @@ Environment variables often contain secrets (API keys, database passwords). Only
| `convox:process:start` | Start a process | ops, deployer, admin |
| `convox:process:exec` | Execute command in container | ops, deployer, admin |
| `convox:process:terminate` | Terminate a process | ops, deployer, admin |
| `convox:process:run_privileged` | Start a process with options that escape the release (`Image`, `Volumes`, `Privileged`, node placement) | admin |

<Aside type="note">
`run_privileged` is checked in addition to `process:start` whenever a `run` request carries one of those options. API tokens can never send them, whatever their permissions.
</Aside>

</TabItem>
<TabItem label="Infrastructure">
Expand All @@ -192,24 +198,31 @@ Environment variables often contain secrets (API keys, database passwords). Only

| Permission | Description | Roles |
|------------|-------------|-------|
| `gateway:user:list` | List all users | admin |
| `gateway:user:read` | View user details | admin |
| `gateway:user:list` | View the team directory (names, emails, roles, locked status) | viewer, ops, deployer, admin |
| `gateway:user:read` | View any user's details, sessions, lock reasons and MFA preferences | admin |
| `gateway:user:create` | Create new user | admin |
| `gateway:user:update` | Update user (roles, status) | admin |
| `gateway:user:update` | Update user (roles, lock), revoke any user's sessions | admin |
| `gateway:user:update_name` | Change a user's display name | admin |
| `gateway:user:delete` | Delete user | admin |
| `gateway:audit_log:read` | Read and export every user's audit log | admin |

<Aside type="note">
Every user may view their own details, list and revoke their own sessions, and read their own audit log without these permissions. API tokens can't use these endpoints at all.
</Aside>

</TabItem>
<TabItem label="API Tokens">

| Permission | Description | Roles |
|------------|-------------|-------|
| `gateway:api_token:list` | List all API tokens | admin |
| `gateway:api_token:read` | View token details | admin |
| `gateway:api_token:create` | Create new token | admin |
| `gateway:api_token:delete` | Delete/revoke token | admin |
| `gateway:api_token:read` | List and view your own tokens | viewer, ops, deployer, admin |
| `gateway:api_token:create` | Create tokens for yourself | deployer, admin |
| `gateway:api_token:update` | Rename or change permissions of your own tokens | deployer, admin |
| `gateway:api_token:delete` | Delete your own tokens | deployer, admin |
| `gateway:api_token:manage` | See and change every user's tokens, and create tokens for other users | admin |

<Aside type="note">
Users can always manage their own API tokens. These permissions control access to other users' tokens.
A token's permissions must be within its owner's current role when it is created or edited, and every request a token makes is checked against both the token's permissions and the owner's current role. Tokens never manage tokens.
</Aside>

</TabItem>
Expand All @@ -219,8 +232,13 @@ Users can always manage their own API tokens. These permissions control access t
|------------|-------------|-------|
| `gateway:deploy_approval_request:create` | Create approval request | deployer, cicd, admin |
| `gateway:deploy_approval_request:read` | View approval status | deployer, cicd, admin |
| `gateway:deploy_approval_request:list` | List every request and its audit trail | admin |
| `gateway:deploy_approval_request:approve` | Approve/reject request | admin |
| `convox:deploy:deploy_with_approval` | Execute approved deploy | cicd, admin |
| `convox:deploy:deploy_with_approval` | Execute approved deploy (API tokens) | deployer, cicd, admin |

<Aside type="note">
`deploy_with_approval` only matters for API tokens; it lets a token run the build, release and process actions of an approved deploy. Deployers hold it so they can issue CI tokens for themselves; it gates actions a deployer already has directly.
</Aside>

</TabItem>
<TabItem label="Settings">
Expand Down Expand Up @@ -272,6 +290,7 @@ Admin role uses wildcard permissions:
|------------|---------|
| `convox:*:*` | All Convox operations |
| `gateway:*:*` | All Gateway operations |
| `security:*:*` | Security operations (e.g. refreshing the rack TLS certificate) |

Wildcards match any value in that position:
- `convox:app:*` would match all app actions
Expand All @@ -286,8 +305,8 @@ Wildcard permissions are reserved for the admin role. Custom roles cannot use wi
When a request arrives, the gateway:

1. **Identifies the user** from session or API token
2. **Maps the endpoint** to a permission (e.g., `DELETE /apps/myapp` → `convox:app:delete`)
3. **Checks role** against the required permission
2. **Maps the endpoint** to a permission (e.g., `DELETE /apps/myapp` → `convox:app:delete`). Every gateway endpoint under `/api/v1` is declared in a route policy table with the permissions it needs; an endpoint that isn't in the table is refused.
3. **Checks the caller's current roles** against the required permission. Roles are read from the database on every request, so a demotion takes effect immediately. An API token must hold the permission itself *and* its owner's current role must allow it.
4. **Logs the decision** to audit trail

```mermaid
Expand Down
32 changes: 25 additions & 7 deletions docs/src/content/docs/security/rbac/roles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ The hierarchy uses inheritance: each role automatically includes all permissions
| `convox:build:list` | List builds |
| `convox:build:read` | View build details |
| `convox:rack:read` | View rack configuration |
| `gateway:user:list` | View the team directory |
| `gateway:api_token:read` | See API tokens an admin has issued to them |

<Aside type="note" title="Every role">
Every user, whatever their role, can view their own profile, list and sign out their own sessions, read their own audit trail, and manage their own MFA methods.
</Aside>

### Use Cases

Expand Down Expand Up @@ -146,6 +152,12 @@ Beyond all Ops permissions:
| `convox:app:update` | Update application settings |
| `gateway:deploy_approval_request:create` | Request deploy approval |
| `gateway:deploy_approval_request:read` | View approval requests |
| `gateway:api_token:create` | Create API tokens for themselves |
| `gateway:api_token:update` | Edit their own API tokens |
| `gateway:api_token:delete` | Delete their own API tokens |
| `convox:deploy:deploy_with_approval` | Lets their CI tokens deploy with approval |

Deployers can issue CI/CD tokens for their own pipelines. A token can't hold more than the deployer's role, and stops working if the deployer is demoted, locked, or suspended.

### Use Cases

Expand Down Expand Up @@ -176,8 +188,8 @@ rack-gateway releases promote RABCDEF -a myapp
- Delete applications
- Manage users and roles
- Configure gateway settings
- Access audit logs
- Create API tokens for other users
- Access other users' audit logs
- Create or manage API tokens for other users

---

Expand Down Expand Up @@ -261,6 +273,7 @@ The Deployer role includes permissions CI/CD pipelines don't need:
|------------|-------------|
| `convox:*:*` | All Convox operations |
| `gateway:*:*` | All Gateway operations |
| `security:*:*` | Security operations (rack TLS certificate refresh) |

The wildcard permissions grant access to everything, including:

Expand All @@ -285,9 +298,11 @@ The wildcard permissions grant access to everything, including:
| Create users | `gateway:user:create` |
| Delete users | `gateway:user:delete` |
| Change user roles | `gateway:user:update` |
| View all API tokens | `gateway:api_token:list` |
| Delete any API token | `gateway:api_token:delete` |
| View audit logs | `gateway:*:*` |
| View, edit or delete any user's API tokens | `gateway:api_token:manage` |
| View other users' details and sessions | `gateway:user:read` |
| View and export everyone's audit logs | `gateway:audit_log:read` |
| List deploy approval requests and approve them | `gateway:deploy_approval_request:list`, `:approve` |
| Run processes with privileged options | `convox:process:run_privileged` |
| Delete applications | `convox:app:delete` |
| Configure gateway settings | `gateway:setting_group:*` |

Expand All @@ -311,9 +326,12 @@ Follow the principle of least privilege. Most teams should have only 2-3 admins.
| Promote releases | No | No | Yes | No | Yes |
| Set environment variables | No | No | Yes | No | Yes |
| Request deploy approval | No | No | Yes | Yes | Yes |
| Deploy with approval | No | No | No | Yes | Yes |
| Deploy with approval (API tokens) | No | No | Own tokens | Yes | Yes |
| View team directory | Yes | Yes | Yes | No | Yes |
| View own profile, sessions and activity | Yes | Yes | Yes | No | Yes |
| Create API tokens for themselves | No | No | Yes | No | Yes |
| Manage users | No | No | No | No | Yes |
| View audit logs | No | No | No | No | Yes |
| View everyone's audit logs | No | No | No | No | Yes |
| Delete applications | No | No | No | No | Yes |
| Configure settings | No | No | No | No | Yes |

Expand Down
12 changes: 7 additions & 5 deletions docs/src/content/docs/user-guide/web-ui/api-tokens.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Aside, Steps } from '@astrojs/starlight/components';
API tokens provide programmatic access to the gateway for CI/CD pipelines and automation.

<Aside type="note">
The API Tokens page is visible to all users. Creating or editing tokens requires the **Admin** or **Deployer** role.
Every user sees the tokens they own. **Deployers** can create, edit, and delete their own tokens. **Admins** see and manage every user's tokens and can create tokens for other users. A token can never be given more permissions than its owner's role grants, and it loses access as soon as its owner is demoted, locked, or suspended.
</Aside>

## Viewing Tokens

Navigate to **API Tokens** in the sidebar to see all tokens:
Navigate to **API Tokens** in the sidebar to see your tokens (admins see every token):

| Column | Description |
|--------|-------------|
Expand All @@ -30,8 +30,8 @@ Navigate to **API Tokens** in the sidebar to see all tokens:

1. Click **Create Token**
2. Enter a descriptive name
3. Choose permissions (or use a role shortcut like Viewer, Ops, Deployer, CI/CD, Admin)
4. Click **Create**
3. Choose permissions (or use a role shortcut like Viewer, Ops, Deployer, CI/CD, Admin). You can only choose permissions your own role has.
4. Click **Create** and complete the MFA prompt
5. Copy the token secret immediately

</Steps>
Expand Down Expand Up @@ -69,9 +69,11 @@ curl -H "Authorization: Bearer rgw_..." \

## Editing or Deleting Tokens

- **Edit** lets admins (or deployers who own the token) update name and permissions.
- **Edit** lets the token's owner (if they are a deployer) or an admin update the name and permissions. New permissions must still be within the owner's role.
- **Delete Token** permanently removes the token.

Both need a fresh MFA check.

## Best Practices

- Use **CI/CD** role shortcuts for automation tokens
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/user-guide/web-ui/audit-logs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Aside, Steps } from '@astrojs/starlight/components';
The Audit Logs section provides a complete record of all gateway activity for security monitoring and compliance.

<Aside type="note">
Audit logs are read-only. If you don’t have access in your environment, ask an admin to review logs on your behalf.
Audit logs are read-only. Admins (`gateway:audit_log:read`) see everyone's activity and can export it. Everyone else sees **My Activity** in the sidebar instead: the same view, limited to their own actions, without export.
</Aside>

## Viewing Logs

Navigate to **Audit Logs** in the sidebar to see the activity log:
Navigate to **Audit Logs** (or **My Activity**) in the sidebar to see the activity log:

| Column | Description |
|--------|-------------|
Expand Down
Loading
Loading