Skip to content

Refuse sandboxed consoles, which roll back their own audit trail - #6

Merged
grantcox merged 1 commit into
mainfrom
stagility/disable-sandbox
Sep 1, 2026
Merged

Refuse sandboxed consoles, which roll back their own audit trail#6
grantcox merged 1 commit into
mainfrom
stagility/disable-sandbox

Conversation

@grantcox

@grantcox grantcox commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

A sandboxed console (rails console --sandbox) wraps the whole session in a transaction that is rolled back on exit. Audit records are enqueued through ActiveJob, so with a database-backed queue on the primary database — Solid Queue, GoodJob, Delayed Job — the enqueue is inside that transaction, and the rollback discards the audit trail along with the operator's changes.

The result is a fully interactive console with no record of a single statement. That is precisely what this gem exists to prevent.

This is console1984#91 reached by a different route. Upstream loses DB-stored audit rows; we store none, but the queue row is the casualty instead — so any host app with a DB-backed ActiveJob adapter on the primary database has this, regardless of where records eventually ship.

Reproduced

Against a Solid Queue app on Heroku: the api:dyno webhook arrived and no console audit records did — no session_start, no command. production.rb sets :solid_queue and database.yml defines no separate queue database, so the solid_queue_jobs row is written on the primary connection inside the sandbox transaction and discarded on exit.

The change

Rails already has the gate, so this uses it rather than building a second delivery path:

Rails.application.config.disable_sandbox = true

Rails::Console#initialize checks it and exit 1s before the console boots. Setting it from the railtie (gated on the same cfg.enabled? as everything else in that block) means installing the gem is what protects an app, rather than every app having to remember a config line.

Ordering is sound: Rails::Command::ConsoleCommand#perform calls boot_application! before Rails::Console.start, so the railtie's after_initialize runs first.

Alternatives considered

  • Deliver inline when sandboxed. Reintroduces exactly the trade-off DeliveryJob's class comment rejects — in-dyno HTTP with a retry budget measured in seconds, blocking each statement — and adds a second delivery path that only runs in the rare case.
  • Give the queue its own database. Closes it, and is the Rails 8 default app layout. Not relied on here because it makes auditing silently contingent on queue topology: a later database consolidation reopens the hole with no signal.

No opt-out is exposed. An unaudited console is the one outcome the gem cannot tolerate.

Tests

Specs boot a real (tiny) Rails application, since the ordering is the point — only a booted app proves the setting lands before Rails::Console reads it. Each case forks, because a Rails application can only be initialized once per process. Verified non-vacuous: the enabled case fails when the railtie line is removed.

65 examples, 0 failures; standardrb clean.

Companion change

heroku-buildpack-console-guard denies --sandbox/-s at the wrapper as a backstop (scoped to console/c, since -s is rake's silent flag).

The two layers cover the same dynos, not different ones: this change activates on CONSOLE_AUDIT_ENABLED, which that buildpack exports only for one-off, scheduler and release dynos. Its value is independence, not reach — it holds even when the command never reaches the wrapper, and it applies on any host that sets the variable. A console opened with heroku ps:exec on a long-running dyno is covered by neither, and remains a documented gap governed outside both projects.

🤖 Generated with Claude Code

@grantcox
grantcox force-pushed the stagility/disable-sandbox branch from ed98233 to bae64ee Compare August 27, 2026 07:41
@grantcox
grantcox marked this pull request as ready for review August 27, 2026 07:42
@grantcox
grantcox requested a review from becky-ynab August 27, 2026 07:42
@grantcox
grantcox changed the base branch from main to stagility/session-end-at-exit August 27, 2026 07:43
@grantcox
grantcox force-pushed the stagility/session-end-at-exit branch from 81fb292 to 71a3226 Compare August 27, 2026 07:44
`rails console --sandbox` wraps the session in a transaction that is rolled
back on exit. Records are enqueued through ActiveJob, so with a DB-backed
queue on the primary database — Solid Queue, GoodJob, Delayed Job — the
enqueue is inside that transaction and the rollback discards the audit trail
along with the operator's changes.

Reproduced against a Solid Queue app: the Heroku api:dyno webhook arrived and
no console audit records did. A fully interactive console with no record of a
single statement is precisely what this gem exists to prevent.

Rails already has the gate, so use it rather than building a second delivery
path: `config.disable_sandbox = true` is checked in Rails::Console#initialize
before the console boots. Setting it from the railtie means installing the gem
is what protects an app, instead of every app having to remember.

Giving the queue its own database would also close this, but it would make
auditing depend silently on queue topology — a later consolidation would
reopen the hole with no signal.

Specs fork per case, since a Rails application can only be initialized once
per process.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@grantcox
grantcox force-pushed the stagility/disable-sandbox branch from bae64ee to 9f84581 Compare August 27, 2026 07:45
@grantcox
grantcox marked this pull request as draft August 27, 2026 07:50
@grantcox
grantcox changed the base branch from stagility/session-end-at-exit to main August 27, 2026 22:51
@grantcox
grantcox marked this pull request as ready for review August 27, 2026 22:51

@becky-ynab becky-ynab left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't even know about the --sandbox option

@grantcox

grantcox commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator Author

I didn't even know about the --sandbox option

I didn't either! I was just looking over the upstream console1984 issues and PRs to see how responsive they'd likely be to feature requests (like making the "reason" source configurable), and I found this still-open PR from 2024 (basecamp/console1984#101) about this --sandbox issue.

FWIW they already have an issue reported for the configurable reason (basecamp/console1984#132), and for "also cover rails runner" (basecamp/console1984#126). But considering the age of open PRs, and that I don't see any "new feature" PRs merged in the last year, I'm not hopeful.

@grantcox
grantcox merged commit 3ee0ee4 into main Sep 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants