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
1 change: 1 addition & 0 deletions Dockerfile.mock-convox
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ RUN --mount=type=cache,target=/go/pkg/mod,sharing=locked \

# Copy only the source needed for mock-convox
COPY cmd/mock-convox ./cmd/mock-convox
COPY internal/cmdenv ./internal/cmdenv
COPY internal/logging ./internal/logging

# Build mock binary with caches
Expand Down
31 changes: 21 additions & 10 deletions cmd/mock-convox/handlers_processes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/gorilla/websocket"

mclog "github.com/DocSpring/rack-gateway/cmd/mock-convox/logging"
"github.com/DocSpring/rack-gateway/internal/cmdenv"
)

func getProcesses(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -83,6 +84,13 @@ func serviceProcesses(w http.ResponseWriter, r *http.Request) {
rawBody,
)

// A rack runs whatever command it gets. This one refuses a command the gateway didn't wrap (cmdenv), so the
// CLI E2E tests catch a command reaching the rack with every env var.
if _, _, ok := cmdenv.Parse(r.Header.Get("Command")); !ok {
http.Error(w, "mock rack: the gateway didn't wrap the command", http.StatusBadRequest)
return
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusAccepted)
writeJSON(w, map[string]interface{}{
Expand Down Expand Up @@ -115,17 +123,20 @@ func execProcess(w http.ResponseWriter, r *http.Request) {
}()

_ = conn.WriteMessage(websocket.TextMessage, []byte("Connected to mock exec for app="+app+" pid="+id+"\n"))
cmd := r.Header.Get("command")
if cmd == "" {
cmd = r.URL.Query().Get("command")
// The rack runs the command the gateway wrapped (cmdenv): this one prints the command and the env vars it gets.
// A command the gateway didn't wrap fails, so the CLI E2E tests catch it.
plan, cmd, ok := cmdenv.Parse(r.Header.Get("Command"))
if !ok {
_ = conn.WriteMessage(websocket.TextMessage, []byte("mock rack: the gateway didn't wrap the command\n"))
_ = conn.WriteMessage(websocket.TextMessage, []byte(execExitMarker+"1\n"))
return
}
if cmd != "" {
_ = conn.WriteMessage(websocket.TextMessage, []byte("$ "+cmd+"\n"))
if strings.HasPrefix(cmd, "echo ") {
_ = conn.WriteMessage(websocket.TextMessage, []byte(strings.TrimPrefix(cmd, "echo ")+"\n"))
} else {
_ = conn.WriteMessage(websocket.TextMessage, []byte("(mock output)\n"))
}
_ = conn.WriteMessage(websocket.TextMessage, []byte("$ "+cmd+"\n"))
_ = conn.WriteMessage(websocket.TextMessage, []byte("env: "+plan.Describe()+"\n"))
if strings.HasPrefix(cmd, "echo ") {
_ = conn.WriteMessage(websocket.TextMessage, []byte(strings.TrimPrefix(cmd, "echo ")+"\n"))
} else {
_ = conn.WriteMessage(websocket.TextMessage, []byte("(mock output)\n"))
}
// A rack ends every exec stream with this marker and the exit code. The CLI strips the line, exits with the
// code, and fails when the marker is missing.
Expand Down
4 changes: 2 additions & 2 deletions convox.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ services:
# Pinned to an immutable release tag. scripts/bump-version.sh sets it to the new
# version (:vX.Y.Z); the release workflow publishes that tag when the v* git tag is pushed.
# :e327add is the v0.1.1 release (identical to :latest when this was pinned).
image: docker.io/docspringcom/rack-gateway:v0.1.25
image: docker.io/docspringcom/rack-gateway:v0.1.26
command: ./scripts/start-gateway.sh
environment:
- PORT=8080
Expand All @@ -54,7 +54,7 @@ services:

admin:
# Uses same image as gateway service (kept in sync by scripts/bump-version.sh)
image: docker.io/docspringcom/rack-gateway:v0.1.25
image: docker.io/docspringcom/rack-gateway:v0.1.26
command: echo "Use this service to run migrations, database admin tasks, etc."
environment:
- PORT=8080
Expand Down
6 changes: 6 additions & 0 deletions docs/src/content/docs/concepts/audit-logging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,12 @@ recorded with it as `justification` in the details. The gateway can require one
or in one app (`require_command_justification`): a command without one is refused with `400` and audited as a
denial.

The env vars a command got (see [Env vars](/user-guide/cli/commands/#env-vars)) are recorded with it: `env_vars`,
the names it got (only the ones the release sets reached it), or `all_env_vars: true` for every env var except
`RACK_URL`; `env_read_only`, each read-only env var it got with the env var it took its value from; and
`write: true` when it ran with `--write`, with the real values. `command` is the command as it was sent, without the
`sh` wrapper the gateway adds.

A command run with a script (`--script`, see [Scripts](/user-guide/cli/commands/#scripts)) is recorded readably:
`command` is the command as a person would run it locally (`rails runner check.rb`), `script` the script's text
(redacted like a command line), `script_name` its file name and `script_sha256` the SHA-256 of what was sent. The
Expand Down
20 changes: 14 additions & 6 deletions docs/src/content/docs/configuration/environment-variables.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -173,11 +173,15 @@ An environment variable pins its setting: the web UI shows it read-only, and the
- Numbers: whole numbers from 1 to the setting's maximum: 1440 (a day) for `session_timeout_minutes` and
`mfa_step_up_window_minutes`, 365 for `mfa_trusted_device_ttl_days`, and 10080 (a week) for
`deploy_approval_window_minutes`
- Lists: a JSON array of strings, or a comma-separated list (use the JSON form when a value contains a comma,
e.g. an approved deploy command). A quoted value such as `"A,B"` (what `jsonencode` writes for a string) is
refused: use `jsonencode` on a list. The env var lists (`protected_env_vars`, `secret_env_vars`,
`audit_visible_env_vars`) must hold valid env var names, here and when saved through the API.
- Maps (`service_image_patterns`): a JSON object of strings
- Lists: a JSON array of strings, or a comma-separated list (use the JSON form when a value contains a comma). A
quoted value such as `"A,B"` (what `jsonencode` writes for a string) is refused: use `jsonencode` on a list. The
env var lists (`protected_env_vars`, `secret_env_vars`, `audit_visible_env_vars`, `command_env_vars`) must hold
valid env var names, here and when saved through the API. `command_env_vars` keeps each name's case and can't
name `RACK_URL`.
- Maps (`service_image_patterns`, `command_env_read_only`): a JSON object of strings. `command_env_read_only` maps
env var names to env var names.
- `approved_deploy_commands`: a JSON array of objects, such as `[{"command": "bin/migrate", "write": true}]` (see
[Per-App Settings](/integrations/deploy-approvals/#per-app-settings)). A list of strings is refused.
- Strings: the plain value, or a JSON string (`"main"`, as Terraform's `jsonencode` writes it). Other quoting is
refused. `verify_git_commit_mode` must be `latest` or `branch`.

Expand Down Expand Up @@ -226,12 +230,16 @@ RGW_APP_MY_APP_SETTING_PROTECTED_ENV_VARS=DATABASE_URL,SECRET_KEY_BASE
RGW_APP_MY_APP_SETTING_AUDIT_VISIBLE_ENV_VARS=PORT,RAILS_ENV
RGW_APP_MY_APP_SETTING_REQUIRE_COMMAND_JUSTIFICATION=true
RGW_APP_MY_APP_SETTING_SERVICE_IMAGE_PATTERNS={"*":"registry\\.example\\.com/my-app:{{GIT_COMMIT}}"}
RGW_APP_MY_APP_SETTING_APPROVED_DEPLOY_COMMANDS=[{"command":"bin/migrate","write":true}]
RGW_APP_MY_APP_SETTING_COMMAND_ENV_VARS=PATH,HOME,RAILS_ENV,DATABASE_URL
RGW_APP_MY_APP_SETTING_COMMAND_ENV_READ_ONLY={"DATABASE_URL":"READ_ONLY_DATABASE_URL"}
```

See [Deploy Approvals](/integrations/deploy-approvals/) for common keys. `require_command_justification` makes
every command run in the app (`run`, `exec`) carry a `--justification` (see
[Justifications](/user-guide/cli/commands/#justifications)). `RGW_SETTING_REQUIRE_COMMAND_JUSTIFICATION` does the
same for every app, and an app can't opt out of it.
same for every app, and an app can't opt out of it. `command_env_vars` and `command_env_read_only` decide the env
vars of the app's commands (see [Env vars](/user-guide/cli/commands/#env-vars)).

## Audit Logging

Expand Down
5 changes: 4 additions & 1 deletion docs/src/content/docs/deployment/production-checklist.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,10 @@ Use this checklist before deploying Rack Gateway to production. Each item addres
- Approval job names match
- GitHub integration tested
- `service_image_patterns` set for each app, pinning the image repository (approval-bound builds are refused without it)
- `approved_deploy_commands` lists the exact commands CI runs (an empty list allows none)
- `approved_deploy_commands` lists the exact commands CI runs (an empty list allows none), each with the env
vars it needs (`env`, `write`)
- `command_env_vars` lists the env vars every command gets, and `command_env_read_only` points the database URL
at a read-only user (see [Env vars](/user-guide/cli/commands/#env-vars))
- `vcs_repo` set to `owner/repo` for CircleCI auto-approval

- [ ] **CI/CD tokens created**
Expand Down
21 changes: 19 additions & 2 deletions docs/src/content/docs/integrations/deploy-approvals/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ Every approval is tied to a specific full (40-character) git commit SHA. An API
- Build the archive it uploaded under that approval (one upload and one build per approval)
- Send no `git-sha`, or one equal to the approved commit (the gateway always uses the approved commit, never the client's)
- Reference only pre-built images, each tagged with the approved commit (`<sha>` or `<sha>-<suffix>`, e.g. `<sha>-amd64`), with no digest (`@sha256:...`: the runtime would pull the digest whatever the tag says) and matching the app's `service_image_patterns`. Every image a service runs is checked (its `image` and its `initContainer.image`); resource image overrides and any other `image` key are refused, and `${...}` can't change an image or the manifest's structure (see [What the Manifest Check Covers](/integrations/deploy-approvals/workflow/#what-the-manifest-check-covers))
- Run only commands listed in `approved_deploy_commands` (an empty list allows none)
- Run only commands listed in `approved_deploy_commands` (an empty list allows none), with the env vars their
entries give them

`service_image_patterns` is required for approval-bound builds: the commit tag alone does not pin the image repository.

Expand Down Expand Up @@ -229,7 +230,23 @@ Configure via UI or environment variables:
| `circleci_approval_job_name` | CircleCI approval job name |
| `circleci_auto_approve_on_approval` | Enable auto-approval (the gateway first checks the workflow's pipeline builds the approved commit of `vcs_repo`) |
| `service_image_patterns` | **Required.** Map of service name (or `*`) to an anchored regex; `{{GIT_COMMIT}}` is replaced with the approved commit, e.g. `{"*": "docker\\.io/org/app:{{GIT_COMMIT}}-amd64"}` |
| `approved_deploy_commands` | Exact commands a token may run under an approval (e.g. migrations); empty allows none |
| `approved_deploy_commands` | The exact commands a token may run under an approval (e.g. migrations), each with the env vars it gets (see below); empty allows none |

Each entry of `approved_deploy_commands` is an object: `command`, the exact command, and the env vars it gets on top
of the app's `command_env_vars` (see [Env vars](/user-guide/cli/commands/#env-vars)): `env`, a list of names;
`all_env_vars: true` for every env var except `RACK_URL`; and `write: true` for the real values of the app's
read-only env vars (`command_env_read_only`), so it can change data.

```json
[
{ "command": "bin/pre_release_checks" },
{ "command": "bundle exec rake db:migrate", "write": true },
{ "command": "bundle exec rake stripe:prepare", "env": ["STRIPE_SECRET_KEY"], "write": true }
]
```

A command run under an approval can't ask for env vars itself (`--env`, `--all-env-vars` and `--write` are
refused): its entry decides. A command listed twice, an unknown field, or `env` with `all_env_vars` is refused.

<Aside type="tip">
Configure per-app settings via environment variables:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,7 +346,7 @@ A promote the gateway refuses is answered with 403 and isn't forwarded, and one
| Build create | `POST /apps/{app}/builds` | The approval's archive, manifest check |
| Build and build log read | `GET /apps/{app}/builds/{id}`, build logs | The approval's build |
| Release read and promote | `GET /apps/{app}/releases/{id}`, `POST .../promote` | The approval's release |
| Process start | `POST /apps/{app}/services/{service}/processes` | The approval's release, command in `approved_deploy_commands` |
| Process start | `POST /apps/{app}/services/{service}/processes` | The approval's release, command in `approved_deploy_commands` (run with the env vars its entry gives it) |
| Process exec and stop | `SOCKET /apps/{app}/processes/{pid}/exec`, `DELETE /apps/{app}/processes/{pid}` | A process started under the approval |

Each check also requires that the token's owner could do the action themselves: an approval never gives a token
Expand Down
23 changes: 23 additions & 0 deletions docs/src/content/docs/operations/upgrades.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,29 @@ A restore to before the upgrade loses the audit events written since; see

## Release Notes That Need Action

### Each command gets only the env vars it may have (v0.1.26)

The gateway now runs every `run` and `exec` command inside a short `sh` wrapper that gives it only the env vars it
may have (see [Env vars](/user-guide/cli/commands/#env-vars)). Until you set `command_env_vars`, commands get every
env var as before, except `RACK_URL`.

1. **Rewrite `approved_deploy_commands` as objects.** Each command becomes `{"command": "..."}`, with `env`,
`all_env_vars` and `write` for the env vars it needs. The migration rewrites values stored in the database (the
old gateway, still running until the promote, then approves no commands, so don't deploy with CI meanwhile). A
`RGW_APP_<APP>_SETTING_APPROVED_DEPLOY_COMMANDS` env var that still holds a list of strings stops the new gateway
from starting, and the object form stops the old one, so change it in the same release as the new image: set
it without promoting (`convox env set ... --app rack-gateway` without `--promote`), then build, migrate and
promote v0.1.26.
2. **Set `command_env_vars` and `command_env_read_only` after the upgrade**, and give each approved deploy command
the env vars it needs at the same time: once `command_env_vars` is set, an approved command gets only those
and its entry's `env`, and a command that writes to a read-only database needs `"write": true`.
3. **Images need `sh` and `env`** to run commands. A process start without a command (one that would run the
service's default command) and a command that starts with `NAME=value` are refused.
4. **Upgrade the CLI after every gateway.** An older gateway ignores the v0.1.26 CLI's `--env`, `--all-env-vars`
and `--write`, and gives every command every env var. `run --env KEY=VALUE` never set anything (the gateway
doesn't forward a process start's environment option); `--env` now names env vars from the release, and a value
is refused.

### Gateway-wide command justifications (v0.1.24)

`require_command_justification` is now a gateway-wide setting as well as a per-app one (the Settings page, or
Expand Down
13 changes: 13 additions & 0 deletions docs/src/content/docs/security/hardening.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,18 @@ Changing either list needs MFA on that request, even within the step-up window,
Audit log entries (and so Slack) never show env var values unless the app lists the key in
`audit_visible_env_vars`; secret and protected keys stay hidden even then.

### Commands' Env Vars

A command run with `run` or `exec` gets every env var of the app unless the app limits them. Set
`command_env_vars` to the env vars the app needs to boot, and let each command ask for the others it needs
(`--env`). Point the database URL at a read-only user with `command_env_read_only`, so a command can change data
only when it's run with `--write`. See [Env vars](/user-guide/cli/commands/#env-vars).

Convox gives every app process `RACK_URL`, which holds the rack's API password, unless the rack's
`system-env-disable` feature gate is on. The gateway keeps it from every command it runs, but the app's own
processes still have it, and an `exec` can read it from the process it runs in. The gate also removes `RACK`, which
the gateway reads, so set `RACK` on the gateway app before turning the gate on.

### Secret Storage

Store secrets securely:
Expand Down Expand Up @@ -294,6 +306,7 @@ audit write fails, so a load balancer takes an instance with a broken audit log
- [ ] Session timeout ≤ 15 minutes
- [ ] Admin users ≤ 3
- [ ] CI/CD uses CI/CD role tokens
- [ ] `command_env_vars` and `command_env_read_only` set for each app
- [ ] Database encrypted and VPC-only
- [ ] Audit logging enabled
- [ ] S3 WORM configured (if required)
Expand Down
4 changes: 2 additions & 2 deletions docs/src/content/docs/security/rbac/permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -344,8 +344,8 @@ So these always need MFA on the request itself: running a command in an app (`co
deleting users, revoking sessions, creating, changing and deleting API tokens, approving, rejecting and extending deploy approvals, removing an MFA method, refreshing the rack TLS certificate,
changing protected env vars, privileged process options, and the settings that protect secrets, loosen deploy
checks or require justifications (`mfa_configuration`, `allow_destructive_actions`, `deploy_approvals`,
`vcs_ci_deploy`, `protected_env_vars`, `secret_env_vars`, `github_verification`, `require_pr_for_branch`, and
`require_command_justification`, gateway-wide and per app).
`vcs_ci_deploy`, `protected_env_vars`, `secret_env_vars`, `command_env_vars`, `command_env_read_only`,
`github_verification`, `require_pr_for_branch`, and `require_command_justification`, gateway-wide and per app).

API tokens can't verify MFA, so MFA levels don't apply to them; what a token may do is limited by its permissions and
its owner's role. The levels are defined in `internal/gateway/rbac/mfa_requirements.go`.
Expand Down
Loading