Skip to content

Add session attribution and command audit log (inspired by console1984) #5

@raghubetina

Description

@raghubetina

Context

We currently provide a remote Rails console at /rails/console with a single shared HTTP Basic credential in production and no auth in development. The README is upfront that this is "toy apps only." After comparing the gem to console1984 (Basecamp's IRB-based audit/protection layer), there are two changes that would meaningfully reduce the recklessness of using slash_console without abandoning its "drop-in convenience" positioning.

Why not just depend on console1984?

We considered it. It's a bad fit:

  • console1984 installs its protections inside Rails' console do ... end block, which fires when bin/rails console boots IRB. slash_console doesn't use IRB — web-console evaluates input by eval-ing strings inside a stored Binding. The IRB::Context prepend, the decrypt!/encrypt! commands, and the TracePoint-based forbidden-method shield all have nothing to attach to in a /rails/console request.
  • It carries real weight: hard Rails 7 + Active Record Encryption requirement, four migration tables, eager loading on session start, a parser dep, monkeypatches into TCPSocket/SSLSocket/all three AR adapters, and Refrigerator#freeze_all which freezes classes process-wide. That conflicts with our "toy apps only" framing — and if you're willing to take all that on, you should be SSHing in and running rails console the normal way.

The ideas transfer cleanly, though. The two below are the high-value, low-cost subset.

Proposal

1. A real user concept (prerequisite for #2)

Today there is no notion of "who is using the console" — ADMIN_USERNAME is a shared label and dev mode has no auth at all. Before audit logging can mean anything, we need attribution.

Add a configurable resolver with a sensible fallback:

SlashConsole.configure do |c|
  c.user_resolver = -> { Current.user&.email }   # host app injects identity
end
  • If configured, the result is stamped on every logged command.
  • If not configured, fall back to the Basic auth username (production) or a one-time per-session "your name" prompt stored in the session cookie (development). Unverified, but at least an auditable string — same posture as console1984's unverified CONSOLE_USER.

This is small, composes with whatever auth the host app already has, and avoids slash_console pretending to be an auth system.

2. Session reason prompt + per-command audit log

Borrowing the most valuable part of console1984:

  • On first access in a session, ask the user why they're opening the console. Store with created_at + resolved user.
  • Persist every submitted command and its rendered output to a DB table. Encrypt at rest with Active Record Encryption if the host app has it configured; plain text otherwise (with a README warning).
  • Suggested schema (mirrors console1984 but flatter — no separate users table, no sensitive-access concept):
    • slash_console_sessions: id, user, reason, ip, user_agent, created_at
    • slash_console_commands: id, session_id, statements (encrypted), output (encrypted), created_at

web-console already hands us the input string in the controller, so this is straightforward — no IRB plumbing needed.

Explicitly out of scope

  • Refrigerator-style class freezing. A web-console attacker can Object.send(:remove_const, ...) or otherwise bypass; selling a defense we can't deliver is worse than not selling it.
  • decrypt! / encrypt! two-mode model. Awkward in a web form; if we ever want it, do it as a per-request flag, not a stateful mode toggle.
  • Socket-level egress blocking to protected_urls. Too invasive for this gem's positioning.

README update

Add a section pointing readers to console1984 + audits1984 + SSH for serious use cases, and reframe slash_console as: convenience for low-stakes apps, with attribution and an audit trail so misuse is at least traceable.

Open question

Should the audit log be opt-in via a generator (rails generate slash_console:install) or auto-create on first boot? Leaning opt-in to keep the "just add the gem, it works" path unchanged for toy apps that don't want a DB migration.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions