Ruby on Rails
Monitor a Rails app: a Rack middleware writing one JSON line per request with status, Devise and Warden hooks for sign-ins, and beam monitor with the Rails rules.
On this page
How Beam reads a Rails app
There's no in-process SDK for this stack, so the app writes what Beam needs to its logs and beam monitor reads them on the same server or container. Only redacted findings leave it. Two kinds of line:
- One JSON line per request, with
method,path(with the query string),status,ip,user_agent,hostandduration_ms. The status is what lets Beam tell an attack your app refused from one it answered. - Security events as plain lines, worded like this (Beam matches the wording and reads
user=andip=):
login failed user=42 ip=203.0.113.7
login succeeded user=42 ip=203.0.113.7
granted admin role to user=99 by user=12
API key created user=12
{"event":"llm_usage","user":"42","model":"gpt-5","total_tokens":1834}
Use an opaque user id, never an email. New outbound hosts are only reported by the Node.js SDK; for this stack, restrict outbound traffic at the network instead.
Before you start
Do steps 1 and 2 of the general cookbook first: App monitoring is on, the app is added, and its beam_app_ token is in your secret store as BEAM_APP_TOKEN. Everything below needs @agent-beam/beam 0.2.32 or newer.
Log each request with its status
# lib/beam_request_log.rb
require "json"
require "logger"
class BeamRequestLog
LOG = Logger.new("/var/log/app/requests.log", formatter: ->(*, msg) { "#{msg}\n" })
def initialize(app) = @app = app
def call(env)
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
status, headers, body = @app.call(env)
request = ActionDispatch::Request.new(env)
LOG.info(JSON.generate(
method: request.request_method,
path: request.fullpath,
status: status,
ip: request.remote_ip,
user_agent: request.user_agent.to_s,
host: request.host,
duration_ms: ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - started) * 1000).round,
))
[status, headers, body]
end
end
# config/application.rb
require_relative "../lib/beam_request_log"
config.middleware.insert_before 0, BeamRequestLog
request.remote_ip honors your trusted proxies (config.action_dispatch.trusted_proxies).
Log sign-ins, admins and keys
With Devise (Warden):
# config/initializers/beam_security.rb
BEAM_SECURITY = Logger.new("/var/log/app/security.log", formatter: ->(*, msg) { "#{msg}\n" })
Warden::Manager.after_authentication do |user, auth, _opts|
BEAM_SECURITY.info("login succeeded user=#{user.id} ip=#{auth.request.remote_ip}")
end
Warden::Manager.before_failure do |env, _opts|
request = ActionDispatch::Request.new(env)
user_id = User.find_by(email: request.params.dig("user", "email"))&.id
BEAM_SECURITY.warn("login failed user=#{user_id} ip=#{request.remote_ip}")
end
Where you grant admin rights or issue keys:
BEAM_SECURITY.info("granted admin role to user=#{target.id} by user=#{current_user.id}")
BEAM_SECURITY.info("API key created user=#{current_user.id}")
BEAM_SECURITY.info({ event: "llm_usage", user: current_user.id.to_s, model: model, total_tokens: usage["total_tokens"] }.to_json)
Run beam monitor next to the app
npm install -g @agent-beam/beam
beam monitor --stack rails --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app
--stack rails turns on this framework's rules and rolls up attacks on frameworks you don't run; --app also checks the app's lockfiles daily (and would detect the stack on its own). Keep it running with systemd:
# /etc/systemd/system/beam-monitor.service
[Unit]
Description=AgentBeam App monitoring
After=network-online.target
[Service]
EnvironmentFile=/etc/beam/app.env
ExecStart=/usr/bin/env beam monitor --stack rails --file /var/log/app/requests.log --file /var/log/app/security.log --app /srv/app
Restart=always
User=beam
[Install]
WantedBy=multi-user.target
sudo install -m 600 /dev/null /etc/beam/app.env && echo "BEAM_APP_TOKEN=beam_app_..." | sudo tee /etc/beam/app.env >/dev/null
sudo systemctl daemon-reload && sudo systemctl enable --now beam-monitor
In Docker or Kubernetes, run the same command in a sidecar that mounts the log directory read-only, with BEAM_APP_TOKEN from a secret.
Prove it works
beam monitor status # app, level, stack, usage
beam monitor test # a test finding through your alert workflows
curl -s "https://your-app.example.com/?id=1%20UNION%20SELECT%201" -o /dev/null
Within a minute, the SQL injection test shows on App monitoring: under Needs action if the app answered 2xx, or counted under "needed nothing from you" if it refused it. Then sign in to a test account with a wrong password ten times and the right one once: Sign-in succeeded after 10 failed attempts appears as critical.
Check dependencies on every pull request
# .github/workflows/beam-cve.yml
name: Dependency check
on: [pull_request]
jobs:
cve:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx -y @agent-beam/beam cve check . --fail-on high
env:
BEAM_APP_TOKEN: ${{ secrets.BEAM_APP_TOKEN }}
Gemfile.lock is checked.
What Beam watches for on this stack
Besides every built-in rule and the attack-in-progress signals:
| Rule | Means | Severity |
|---|---|---|
| Rails info pages served publicly | /rails/info/properties or /rails/info/routes answered 2xx (development-only pages) | High |
| Template injection | ERB-style <%= 7*7 %> payloads | High |
Attacks on other frameworks (WordPress probes, Laravel or Spring exploits, and so on) are rolled up as Not your stack when there's no status to say otherwise, and never alert.
Close what Beam would catch
Beam raises these when they're found; better that there's nothing to find:
- Run production with
RAILS_ENV=production;/rails/infomust never answer there. - Keep
config.consider_all_requests_local = falsein production. - Rate-limit sign-ins (Rack::Attack) and enable Devise's
lockable.
Coverage checklist
| Feature | Done when |
|---|---|
| Request log with status | Findings show Answered, Refused or Errored |
| Stack declared | beam monitor prints the stack at start |
| Sign-in events | login failed and login succeeded lines are written |
| Admin and key events | Role grants and new keys are logged |
| LLM spend | An llm_usage line follows each model call |
| Dependency checks | The CI job runs on pull requests |
| Alerts | Critical pages someone, high reaches a channel (step 8) |
| Routine | Someone reads Needs action daily and Attack analytics weekly |
Set this up with your coding agent
Install the agentbeam-setup skill for Claude Code, Codex, Cursor, Gemini CLI, OpenCode and Copilot, then ask your agent to set up AgentBeam on this machine. Beam scans the skill before it installs.
npx -y @agent-beam/beam add skill agentbeam-setup -g --all-agents