Skip to content
Merged
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
8 changes: 1 addition & 7 deletions cmd/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -223,13 +223,7 @@ func writeAuditAnchor() error {
}
defer closeDatabase(database)

worker := jobaudit.NewAnchorWriterWorker(
database,
anchorConfig.S3Client,
anchorConfig.Bucket,
anchorConfig.ChainID,
anchorConfig.RetentionDays,
)
worker := jobaudit.NewAnchorWriterWorker(database, anchorConfig.S3Client, anchorConfig.Bucket, anchorConfig.ChainID)

// Write this hour's anchor directly (synchronously), for the hour by the database's clock. It takes the same
// lock as the periodic job, and fails rather than waits while another anchor write holds it.
Expand Down
2 changes: 1 addition & 1 deletion convox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ environment:
- AUDIT_HMAC_SECRET # Secret key used for HMAC-SHA256 cryptographic chain of audit logs
- AUDIT_ANCHOR_BUCKET # S3 bucket name for WORM audit anchors
- AUDIT_ANCHOR_CHAIN_ID # Chain identifier (typically rack alias, e.g., staging, eu, us)
- AUDIT_ANCHOR_RETENTION_DAYS # Object Lock retention in days (default: 400)
- AUDIT_ANCHOR_RETENTION_DAYS # Least default COMPLIANCE retention the bucket must have, in days (default: 400)

services:
gateway:
Expand Down
5 changes: 1 addition & 4 deletions docs/src/content/docs/concepts/mfa-security.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -267,10 +267,7 @@ flowchart LR

- **TOTP seeds and YubiKey public IDs** are encrypted with AES-256-GCM under `COLUMN_ENCRYPTION_KEY` and
bound to their user, so a copy of the database alone doesn't reveal them, and a value copied to another
user's row is rejected. A plaintext seed is never used as it is, but while the upgrade sweep for column
encryption exists (it will be removed in a later release), a plaintext seed written into the database is
encrypted by the next sweep, at startup or 10 minutes after it, and then works. Each value the sweep encrypts
is logged as a warning, and the sweep 10 minutes after startup reports any to Sentry. See
user's row is rejected. A plaintext seed written into the database is never used. See
[Column Encryption](/configuration/environment-variables/#column-encryption).
- **Backup codes** are stored only as keyed hashes.
- **WebAuthn** stores only each credential's public key.
Expand Down
17 changes: 6 additions & 11 deletions docs/src/content/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ against a database, it marks the database as `production`, or `development` in a
`COLUMN_ENCRYPTION_KEY` encrypts the secrets the gateway stores in its database: TOTP seeds and YubiKey
public IDs (`mfa_methods.secret`) and the Slack bot token. Values are encrypted with AES-256-GCM and bound
to where they belong (the user and MFA method type, or the Slack workspace), so a value copied to another
user's row, or to a method of another type, can't be decrypted there. A plaintext value is never used as it is,
but the upgrade sweep encrypts any it finds (see **Existing data is upgraded at startup** below).
user's row, or to a method of another type, can't be decrypted there. A plaintext value is never used: reading
one is an error, as for a value that doesn't decrypt.

- **Generate it once** with `openssl rand -base64 32` and store it with your other secrets. Use a
different key per environment. It is separate from `APP_SECRET_KEY`, so rotating the session secret
Expand All @@ -73,14 +73,9 @@ but the upgrade sweep encrypts any it finds (see **Existing data is upgraded at
automatic account lock.
Slack must be reconnected. The gateway logs (and reports to Sentry, if configured) every value it fails
to decrypt when it tries to use it.
- **Existing data is upgraded at startup.** Secrets stored by releases before column encryption (before
v0.1.7: plaintext TOTP seeds, base64 Slack tokens) are encrypted when the server starts, before it serves
requests, and once more 10 minutes later, in case the previous release stored more during the rollout. The
sweep runs on every start and is safe to repeat. It logs a warning naming each row it encrypts (the user
and MFA method type, or the Slack workspace, never the value). It will be removed in a later release; until then it encrypts any
plaintext value it finds, so someone who can write to the database could plant a TOTP seed they know and
have it encrypted. After the upgrade nothing else stores plaintext, so the sweep 10 minutes after startup
encrypting anything is reported to Sentry, if configured, and logged as an error.
- **Upgrading from before v0.1.7** (plaintext TOTP seeds, base64 Slack tokens): deploy a release from v0.1.7 to
v0.1.10 first. Those releases encrypt the existing secrets when they start; later releases don't, and can't
read secrets stored before column encryption.
- **Rotation isn't supported yet.** Every encrypted value records which key encrypted it, so rotation can
be added later without changing the stored format.

Expand Down Expand Up @@ -225,7 +220,7 @@ See [Deploy Approvals](/integrations/deploy-approvals/) for common keys.
| `AUDIT_HMAC_SECRET` | (required in prod) | HMAC chain secret: at least 32 bytes (`openssl rand -base64 32`). Without it (`DEV_MODE` only) the gateway doesn't verify its audit log. |
| `AUDIT_ANCHOR_BUCKET` | | S3 bucket for anchors. Unset, no anchors are written |
| `AUDIT_ANCHOR_CHAIN_ID` | | Chain identifier, the anchors' key prefix. Required with `AUDIT_ANCHOR_BUCKET` |
| `AUDIT_ANCHOR_RETENTION_DAYS` | `400` | Not used for retention: anchors are locked by the bucket's default Object Lock rule, so set the retention there. The gateway only shows this value in anchor write errors |
| `AUDIT_ANCHOR_RETENTION_DAYS` | `400` | The least the anchor bucket's default Object Lock retention may be. The bucket's rule locks the anchors; the gateway checks it at startup and in every verification, and refuses to start (verification fails) unless the bucket locks new objects in COMPLIANCE mode for at least this many days. Set it to the bucket's retention. Needs `s3:GetBucketObjectLockConfiguration` |
| `AUDIT_ANCHOR_INTERVAL_MINUTES` | `60` | Anchor interval |
| `AUDIT_HASH_V2_FROM` | | Chain index where hash v2 starts, recorded after upgrading, for the hourly verification and `verify-audit` (see [Verification](/deployment/terraform/s3-worm-storage/#pinning-where-hash-v2-starts)). The gateway refuses to start when it isn't a chain index. |
| `AWS_ENDPOINT_URL_S3` | | S3 endpoint override, e.g. MinIO (`testbypass` builds only) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,9 @@ resource "aws_iam_role_policy" "gateway_s3" {
"s3:ListBucket",
# verify-audit reads every version of each anchor
"s3:GetObjectVersion",
"s3:ListBucketVersions"
"s3:ListBucketVersions",
# The gateway checks the bucket's default retention at startup and in every verification
"s3:GetBucketObjectLockConfiguration"
]
Resource = [
aws_s3_bucket.audit_anchors.arn,
Expand Down
21 changes: 14 additions & 7 deletions docs/src/content/docs/deployment/terraform/s3-worm-storage.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,9 @@ resource "aws_iam_role_policy" "anchor_writer" {
"s3:GetObject",
"s3:GetObjectVersion",
"s3:ListBucket",
"s3:ListBucketVersions"
"s3:ListBucketVersions",
# The gateway checks the bucket's default retention at startup and in every verification
"s3:GetBucketObjectLockConfiguration"
]
Resource = [
aws_s3_bucket.audit_anchors.arn,
Expand Down Expand Up @@ -347,7 +349,8 @@ resource "aws_iam_role_policy" "anchor_verifier" {
"s3:GetObjectVersion",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:GetObjectRetention"
"s3:GetObjectRetention",
"s3:GetBucketObjectLockConfiguration"
]
Resource = [
aws_s3_bucket.audit_anchors.arn,
Expand Down Expand Up @@ -409,8 +412,10 @@ It prints a summary and exits non-zero if it finds any integrity failure. Warnin
- `RGW_DATABASE_URL`, `GATEWAY_DATABASE_URL` or `DATABASE_URL`, for a role with `SELECT` on `audit.audit_event`
(the `audit_reader` role is enough).
- Unless `--no-anchors` is given: `AUDIT_ANCHOR_BUCKET`, `AUDIT_ANCHOR_CHAIN_ID` and AWS credentials allowed
`s3:ListBucket`, `s3:ListBucketVersions`, `s3:GetObject` and `s3:GetObjectVersion` on the bucket, and
`kms:Decrypt` on its key (the verifier role above).
`s3:ListBucket`, `s3:ListBucketVersions`, `s3:GetObject`, `s3:GetObjectVersion` and
`s3:GetBucketObjectLockConfiguration` on the bucket, and `kms:Decrypt` on its key (the verifier role above).
It fails unless the bucket's default retention is COMPLIANCE mode for at least `AUDIT_ANCHOR_RETENTION_DAYS`
(default 400).
- On a gateway that was upgraded from a version before hash v2: `AUDIT_HASH_V2_FROM` (or `--hash-v2-from`), see
below.

Expand Down Expand Up @@ -497,9 +502,11 @@ version before hash v2 has no legacy events and needs no pin.
### Hourly Verification in the Gateway

The gateway runs these checks itself at half past every hour (UTC), with the configuration `verify-audit` reads:
`AUDIT_HMAC_SECRET`, `AUDIT_HASH_V2_FROM` and the anchor settings. Its role needs `s3:ListBucketVersions` and
`s3:GetObjectVersion` on the bucket ([IAM Role for the Gateway](#iam-role-for-the-gateway)); without them every run
fails and alerts.
`AUDIT_HMAC_SECRET`, `AUDIT_HASH_V2_FROM` and the anchor settings. Its role needs `s3:ListBucketVersions`,
`s3:GetObjectVersion` and `s3:GetBucketObjectLockConfiguration` on the bucket
([IAM Role for the Gateway](#iam-role-for-the-gateway)); without them every run fails and alerts. The gateway also
reads the bucket's Object Lock configuration before it starts, and refuses to start unless the default retention is
COMPLIANCE mode for at least `AUDIT_ANCHOR_RETENTION_DAYS`.

- **What a run checks.** The first run after the gateway starts, every run while verification is failing, and one
run a day check every event. The hourly runs in between check the events written since the last passing run,
Expand Down
23 changes: 16 additions & 7 deletions docs/src/content/docs/operations/upgrades.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,18 @@ A restore to before the upgrade loses the audit events written since; see

## Release Notes That Need Action

### Anchor bucket retention checked at startup

`AUDIT_ANCHOR_RETENTION_DAYS` is now checked: the gateway reads the anchor bucket's Object Lock configuration at
startup and in every audit verification, and refuses to start (verification fails and alerts) unless the bucket's
default retention is COMPLIANCE mode for at least that many days.

1. **Give the gateway's role `s3:GetBucketObjectLockConfiguration` on the anchor bucket before deploying** (see
[IAM Role for the Gateway](/deployment/terraform/s3-worm-storage/#iam-role-for-the-gateway)), and the role
`verify-audit` runs with elsewhere. Without it the new release doesn't start.
2. **Check `AUDIT_ANCHOR_RETENTION_DAYS` matches the bucket's retention** (`worm_retention_days`). It defaults to
400.

### Audit hash v2 and `verify-audit`

This release changes how audit events are hashed (hash v2: every stored column, recomputable from the row) and
Expand Down Expand Up @@ -260,20 +272,17 @@ itself every hour and alerts when they fail.
run, and the hourly verification keeps failing (with a daily reminder), until the anchors' retention ends and
they're deleted.

### Column encryption key required
### Column encryption key required (v0.1.7)

TOTP seeds, YubiKey public IDs and the Slack bot token are now stored encrypted with a dedicated key. See
[Column Encryption](/configuration/environment-variables/#column-encryption).

1. **Set `COLUMN_ENCRYPTION_KEY` before deploying** (`openssl rand -base64 32`) and back it up. Every command
that opens the database, including `migrate`, refuses to run without it, so a deploy without the key
fails before the new release is promoted.
2. **Existing secrets are encrypted when the new release starts**, before it serves requests. The log shows
a `Column encryption:` line with counts and a warning for each row encrypted. This happens at startup rather
than in `migrate` because the previous release keeps serving while `migrate` runs. It runs again 10 minutes
after startup, for anything the previous release stored during the rollout (a TOTP enrollment or a Slack
reconnect); if it encrypts anything then, it reports it to Sentry. During this upgrade that can be a real
enrollment from the rollout; after it, it means a secret was written straight into the database.
2. **Existing secrets are encrypted when the new release starts** (v0.1.7 to v0.1.10 only), before it serves
requests. Releases after v0.1.10 don't encrypt existing secrets, so upgrade a gateway from before v0.1.7 to
v0.1.10 first.
3. **Expect a short gap for TOTP and YubiKey OTP.** Once the new release has upgraded the data, any process
still running the previous release can't read the encrypted values, so its TOTP and YubiKey OTP checks
and Slack notifications fail until it is replaced. WebAuthn and backup codes keep working.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ See [Roles](/security/rbac/roles/) for detailed permission lists.
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.
- **Stops working with its owner.** Tokens of a locked 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.
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/security/authentication/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Users from other domains receive an error during OAuth callback.
The gateway verifies that:
- Email is verified in Google's ID token
- Email domain matches the allowed domain
- User account exists and is not locked/suspended
- User account exists and is not locked

### Token Hashing

Expand Down
3 changes: 1 addition & 2 deletions docs/src/content/docs/security/authentication/sessions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,6 @@ sequenceDiagram
| Not idle | `last_seen_at` + TTL > now | 401 + auto-revoke |
| User exists | Associated user found | 401 + auto-revoke |
| User not locked | `locked_at` is null | 401 + auto-revoke |
| User not suspended | `suspended` is false | 401 + auto-revoke |

## Session Expiration

Expand Down Expand Up @@ -169,7 +168,7 @@ sessions: **Revoke** for one, or **Revoke All**. Users can revoke their own; rev
Sessions stop working when:

- The session expires (idle timeout)
- The user is locked (locking revokes every session) or suspended
- The user is locked (locking revokes every session)
- The user is deleted

## Session Metadata
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/security/compliance/audit-trail.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ it to stdout as one JSON line for CloudWatch:
| `user_email` | User's email address (a token's owner for API tokens) | `alice@example.com` |
| `user_name` | User's display name | `Alice Developer` |
| `api_token_id`, `api_token_name` | The API token, when one made the request | `ci-deploy` |
| `action_type` | The scope of the action: `convox`, `gateway`, `auth` or `security`. Results that admin handlers record use `users`, `tokens` or `admin` | `convox` |
| `action_type` | The scope of the permission the action needs: `convox`, `gateway`, `auth` or `security`. A request's attempt and result entries have the same type | `convox` |
| `action` | `<resource>.<verb>`, named after the route's permission | `release.promote`, `user.create`, `login.oauth_failed` |
| `resource` | Target resource | `myapp` |
| `resource_type` | Resource category | `app`, `build`, `release` |
Expand Down Expand Up @@ -335,7 +335,7 @@ ORDER BY day DESC;
| **Requests** | Every gateway API and rack request, reads included, with its RBAC decision |
| **Deployments** | Build, release, promote, deploy approvals |
| **Environment** | Read, set, unset (values redacted), and secret reveals |
| **User Management** | Create, update, delete, lock |
| **User Management** | Create, update, delete, lock, MFA reset |
| **Token Management** | Create, delete tokens |
| **Settings** | Configuration changes |

Expand Down
6 changes: 0 additions & 6 deletions docs/src/content/docs/security/rbac/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -42,17 +42,14 @@ Resources that map to Convox API operations:
|----------|-------------|
| `app` | Applications |
| `build` | Docker image builds |
| `cert` | SSL/TLS certificates |
| `deploy` | Deployment operations |
| `env` | Environment variables |
| `instance` | EC2/container instances |
| `log` | Application logs |
| `object` | Build artifacts and objects |
| `process` | Running processes/containers |
| `rack` | Rack configuration |
| `registry` | Docker registries |
| `release` | Application releases |
| `resource` | Convox resources (databases, etc.) |
| `secret` | Secret env var values (`convox:secret:read` reveals them) |

The gateway forwards only the rack API routes it has a permission for (see
Expand Down Expand Up @@ -98,20 +95,17 @@ Authentication and MFA resources:

| Action | Description |
|--------|-------------|
| `add` | Add item to a collection |
| `approve` | Approve a request |
| `create` | Create a new resource |
| `delete` | Delete a resource |
| `deploy_with_approval` | Deploy requiring prior approval |
| `exec` | Execute commands |
| `generate` | Generate credentials/codes |
| `import` | Import external resources |
| `keyroll` | Rotate keys/credentials |
| `list` | List resources |
| `manage` | General management operations |
| `promote` | Promote a release |
| `read` | Read/view a resource |
| `remove` | Remove item from collection |
| `restart` | Restart a service |
| `run_privileged` | Start a process with privileged options |
| `set` | Set a value |
Expand Down
2 changes: 1 addition & 1 deletion docs/src/content/docs/security/rbac/roles.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ Beyond all Ops permissions:
| `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.
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 or locked.

### Use Cases

Expand Down
11 changes: 7 additions & 4 deletions docs/src/content/docs/user-guide/web-ui/audit-logs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,13 @@ Use the filter panel to narrow results:
Filter by user, resource, or action text.

### Action Type
- Convox API
- Authentication
- User Management (results of user changes)
- Token Management (results of API token changes)

The scope of the permission the action needs. A request's attempt and result entries have the same type.

- Convox API (`convox`): requests to the rack
- Gateway (`gateway`): users, API tokens, deploy approvals, settings and the gateway's other features
- Authentication (`auth`): login, logout and MFA
- Security (`security`): secrets, rate limits and audit log verification

### Status
- Success
Expand Down
5 changes: 3 additions & 2 deletions internal/cli/gateway_test_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import (
"strings"

"github.com/spf13/cobra"

"github.com/DocSpring/rack-gateway/internal/cli/webauthn"
)

// TestAuthCommand returns the cobra command for testing authentication and MFA flows.
Expand Down Expand Up @@ -235,8 +237,7 @@ func testOTPAuth(cmd *cobra.Command, baseURL, bearer string, status *MFAStatusRe
}

func testWebAuthnAuth(baseURL, bearer string, status *MFAStatusResponse) error {
// Check WebAuthn availability
if !checkWebAuthnAvailability() {
if !webauthn.CheckAvailability() {
return fmt.Errorf("no WebAuthn device detected")
}

Expand Down
9 changes: 0 additions & 9 deletions internal/cli/mfa_verify.go
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,6 @@ func submitMFAVerification(baseURL, sessionToken, code string) error {
return fmt.Errorf("gateway error: %s", RenderGatewayError(bodyBytes))
}

func checkWebAuthnAvailability() bool {
defer func() {
if r := recover(); r != nil {
_ = r
}
}()
return webauthn.CheckAvailability()
}

func hasMethodType(methods []MFAMethodResponse, methodType string) bool {
for _, m := range methods {
if m.Type == methodType && !m.IsEnrolling {
Expand Down
Loading