Skip to content

Security hardening: token exposure, resource limits, WebSocket deadlines#13

Merged
peg merged 4 commits intomainfrom
staging
Feb 12, 2026
Merged

Security hardening: token exposure, resource limits, WebSocket deadlines#13
peg merged 4 commits intomainfrom
staging

Conversation

@peg
Copy link
Copy Markdown
Owner

@peg peg commented Feb 12, 2026

Security Fixes

Two commits addressing 9 findings from automated security review:

Commit 1: Token exposure, resource limits, permissions

  • Shell shim no longer embeds token in script (written to 0600 .tok file, read at runtime)
  • Audit directories created with 0700 permissions
  • Base64 decode capped at 1MB to prevent memory exhaustion
  • MCP pendingCalls: 5-minute TTL + 1000 entry cap
  • Webhook timeout capped at 30s max

Commit 2: WebSocket deadlines, approval cleanup, dir perms

  • WebSocket: 90s read deadline + 30s ping/pong heartbeat (prevents goroutine leaks)
  • Approval watchExpiry goroutines exit on Store.Close()
  • Shell wrapper temp directory created with 0700
  • Token startup log prefix reduced from 8 to 4 chars

104 lines changed across 9 files. No functional changes — hardening only.

peg added 4 commits February 12, 2026 05:46
…ssions

- Token no longer embedded in shell shim scripts. Written to separate
  0600 file, read at runtime via cat. Not visible in /proc or ps output.
- Audit directories use 0700 (was 0755) in daemon and wrap commands.
- Base64 command decoding capped at 1MB to prevent memory exhaustion.
- MCP proxy pendingCalls: TTL eviction (5min) + hard cap (1000 entries)
  prevents unbounded memory growth from orphaned JSON-RPC requests.
- Webhook timeout capped at 30s maximum regardless of policy config.
…ging

- WebSocket: 90s read deadline + 30s ping/pong heartbeat detects dead
  connections. Goroutines no longer hang indefinitely on broken pipes.
- Approval store: watchExpiry goroutines now exit on Store.Close(),
  preventing goroutine leak during daemon shutdown.
- Shell wrapper temp directory restricted to 0700 (was default).
- Token startup log reduced from 8-char prefix to 4-char prefix.
Agent frameworks (OpenClaw, etc.) prepend descriptive comments to shell
commands (e.g. '# Check status\nkubectl get pods'). These comments caused
command_matches patterns to miss because matching started at the '#' character
instead of the actual command.

Now strips leading comment lines and blank lines in enrichParams before
commands are evaluated against policy rules. Inline comments are preserved.

Includes 10 test cases covering edge cases (empty, all-comments, multiline).
@peg peg merged commit c348c34 into main Feb 12, 2026
1 check passed
peg added a commit that referenced this pull request Feb 18, 2026
- Dedup identical pending approvals within 60s window using sha256(tool+command+agent)
  to handle agent retries on timeout/reconnect (#13)
- Write hash-chained audit events on approval resolution (approve/deny/always-allow)
  with full metadata: tool, command, resolution, resolved_by, approval_id, persist (#22)
- Add integration test for hook fallback to native prompt on unreachable serve (#15)
peg added a commit that referenced this pull request Feb 18, 2026
* feat: persist-to-policy (Always Allow) for approval resolve

- Add 'persist' field to resolve request; when true + approved,
  auto-generates an allow rule and appends to auto-allowed.yaml
- New internal/engine/persist.go: GeneralizeCommand, GenerateAllowRule,
  AppendAllowRule functions
- Command generalization: keeps first 2 tokens, wildcards rest
- Paths kept exact (security-sensitive)
- MCP tools matched by tool name with default condition
- Auto-allowed rules written to ~/.rampart/policies/auto-allowed.yaml
- Full test coverage in persist_test.go
- All existing tests pass

* feat(watch): interactive approval keybindings

Add --serve-url and --serve-token flags to 'rampart watch'. When set,
watch polls the serve API for pending approvals and displays them in a
highlighted section at the top of the TUI.

Keybindings:
  1-9  select pending approval by number
  a    approve selected
  d    deny selected
  A    approve + always allow (persist)
  q    quit

Without --serve-url, watch works exactly as before (read-only audit tail).

Includes ApprovalClient for API communication, countdown timers,
ANSI-colored pending section, and comprehensive tests.

* feat: dashboard 'Always Allow' button + persist support

Add 'Always Allow' button between Approve and Deny in the dashboard.
Sends resolve with persist:true to auto-generate an allow rule in
auto-allowed.yaml. Same functionality as 'A' keybinding in watch.

* docs: update rampart.sh messaging — guardrails not firewall

- Hero: 'Guardrails for AI agents. Works when sandboxes can't.'
- Replace 'policy engine/firewall' with 'guardrails' throughout
- Soften sandbox comparison language
- Fix tamper-proof → tamper-evident
- Update meta descriptions and structured data

* fix: critical issues for unified approval system

1. Empty when: clause now matches all tool calls (catch-all behavior)
   - matchCondition returns true for empty conditions
   - Lint message changed to INFO level

2. Watch TUI 'A' keybinding sends persist:true in resolve request
   - ApprovalClient.Resolve now accepts persist parameter
   - 'a' = approve (persist:false), 'A' = always allow (persist:true)

3. --serve-token flag deprecated in favor of RAMPART_TOKEN env var
   - Warning printed when flag is used directly (visible in ps aux)
   - Flag marked deprecated but kept for backward compatibility

4. GeneralizeCommand: dangerous commands never generalized
   - Blocklist: rm, chmod, chown, kill, dd, mkfs, reboot, shutdown, etc.
   - Single-token commands kept exact (ls -> ls, not ls *)

5. Atomic persist writes using temp file + rename pattern
   - Prevents corruption from concurrent Always Allow actions

6. Polling loop accepts context.Context for clean cancellation
   - requestApprovalCtx and pollApprovalCtx with context support
   - HTTP requests use NewRequestWithContext

* feat: approval deduplication, audit trail for resolutions, fallback test

- Dedup identical pending approvals within 60s window using sha256(tool+command+agent)
  to handle agent retries on timeout/reconnect (#13)
- Write hash-chained audit events on approval resolution (approve/deny/always-allow)
  with full metadata: tool, command, resolution, resolved_by, approval_id, persist (#22)
- Add integration test for hook fallback to native prompt on unreachable serve (#15)

* feat: directory-based policy loading and periodic reload

Add support for loading policies from multiple YAML files in a directory,
enabling the persist-to-policy feature to work end-to-end.

New components:
- PolicyStore interface (Load/Path) implemented by FileStore, DirStore, MultiStore
- DirStore: loads all *.yaml files from a directory in sorted order
- MultiStore: combines a primary config file with a policy directory
- Engine.StartPeriodicReload(interval): re-reads policies on a timer
- Engine.Stop(): terminates the reload goroutine

CLI changes:
- 'rampart serve' and 'rampart hook' now accept --config-dir flag
- Default: ~/.rampart/policies/ is included if it exists
- 'rampart serve' accepts --reload-interval (default 30s)

Merge behavior:
- Files loaded in sorted order (deterministic)
- default_action taken from first file that specifies one
- Duplicate policy names: first wins, later skipped with warning
- Invalid YAML files: logged and skipped (don't break other files)

This closes the gap where AppendAllowRule writes to
~/.rampart/policies/auto-allowed.yaml but the engine never loaded it.

* feat: configurable approval timeout (--approval-timeout flag)

* feat: dashboard redesign with brand colors + dangerous command detection

- Rampart brand colors (#FF6392 accent, zinc palette)
- Inter + JetBrains Mono fonts matching rampart.sh
- Stats bar: pending/approved/denied counters
- Dangerous command detection with orange warning badges
- Improved card layout with better visual hierarchy
- Responsive mobile design
- CSP updated for Google Fonts
- Configurable approval timeout (--approval-timeout flag)

* fix: dashboard timer flicker + more specific timeout message

- Update countdown timers in-place via data-id targeting instead of
  full DOM rebuild every second (eliminates card jumping)
- Timeout message now says 'no response received within' for clarity

* dashboard: fix layout shifts, add virtual scrolling, optimize for scale

- Virtual scrolling: only renders visible cards + buffer (5 above/below).
  Supports thousands of pending approvals with ~20 DOM nodes max.
- Efficient diffing: Map keyed by approval ID, only add/remove/update
  cards that actually changed between polls. Never rebuilds entire list.
- Timer updates: pure textContent swaps on visible cards only, with
  classList.toggle guard to avoid unnecessary DOM touches.
- Cards built with createElement (no innerHTML), escape results cached.
- Event delegation on container (no per-card listeners).
- Adaptive polling: 2s with pending, 5s idle, exponential backoff on error.
- Visibility API: pauses polling when tab is hidden, resumes on focus.
- requestAnimationFrame batching for DOM mutations after poll.
- CSS: added contain:layout style on cards, min-width on timer to prevent
  layout shift from text width changes. Replaced pulse animation with
  one-shot cardPulse that only fires on genuinely new cards.
- History only re-renders when dirty flag is set.
- Stats use textContent with change guards.

* feat(api): add audit history endpoints

Add four new authenticated API endpoints for querying historical audit data:

- GET /v1/audit/events — query events with pagination, filtering by
  tool, action, and agent. Returns most-recent-first.
- GET /v1/audit/dates — list available audit log dates
- GET /v1/audit/export — download a day's JSONL as attachment (streamed)
- GET /v1/audit/stats — aggregate stats across a date range

Includes WithAuditDir server option and comprehensive test coverage.

* fix: wire audit dir to proxy server for audit API endpoints

* feat: dashboard v2 — table layout, audit log, zero layout shift

* fix: command column showing ID instead of actual command

extractCmd() checked a.request.command (audit event format) before
a.command (approval API format). Approvals have command at top level.

* fix: Always Allow button requires confirmation + clearer label

- Clicking 'Always Allow' now shows a confirm dialog explaining it creates
  a permanent auto-approve rule, showing the command that will be allowed
- Button label changed from bare '★' to '★ Always' with descriptive tooltip
- Cleaned up duplicate test rules from auto-allowed.yaml

* fix: approval ordering (newest first) + action buttons column width

- Sort approvals by created_at before prepending so newest appears on top
- Widen actions column to 140px to fit all three buttons
- Actions container uses nowrap to prevent line breaking

* fix: persist dedup + clean YAML output

- Skip appending rule if identical pattern already exists (same tool + conditions)
- Use clean YAML structs with omitempty to eliminate verbose empty fields
- auto-allowed.yaml now outputs minimal, readable YAML

* fix: sort pending approvals by creation time for deterministic ordering

Store.List() iterated a map, producing random order on each call.
Now sorts by CreatedAt (oldest first) so the dashboard shows consistent
ordering across refreshes.

* feat: resizable table columns via drag handles

Drag the right edge of any column header to resize. Uses mousedown/
mousemove tracking with table-layout:fixed for predictable sizing.
Accent-colored handle appears on hover.

* feat: dashboard polish — history persistence, theme toggle, bulk actions

* fix: audit findings — double Stop() panic, audit dir leak, CHANGELOG v0.3.0

- Engine.Stop() uses sync.Once to prevent panic on double close
- /v1/audit/dates no longer exposes server filesystem path
- Comprehensive CHANGELOG for v0.3.0 with breaking change callout
  for empty when: clause behavior

* chore: version to v0.2.29

* chore: version to v0.2.3
peg added a commit that referenced this pull request Feb 18, 2026
* feat: persist-to-policy (Always Allow) for approval resolve

- Add 'persist' field to resolve request; when true + approved,
  auto-generates an allow rule and appends to auto-allowed.yaml
- New internal/engine/persist.go: GeneralizeCommand, GenerateAllowRule,
  AppendAllowRule functions
- Command generalization: keeps first 2 tokens, wildcards rest
- Paths kept exact (security-sensitive)
- MCP tools matched by tool name with default condition
- Auto-allowed rules written to ~/.rampart/policies/auto-allowed.yaml
- Full test coverage in persist_test.go
- All existing tests pass

* feat(watch): interactive approval keybindings

Add --serve-url and --serve-token flags to 'rampart watch'. When set,
watch polls the serve API for pending approvals and displays them in a
highlighted section at the top of the TUI.

Keybindings:
  1-9  select pending approval by number
  a    approve selected
  d    deny selected
  A    approve + always allow (persist)
  q    quit

Without --serve-url, watch works exactly as before (read-only audit tail).

Includes ApprovalClient for API communication, countdown timers,
ANSI-colored pending section, and comprehensive tests.

* feat: dashboard 'Always Allow' button + persist support

Add 'Always Allow' button between Approve and Deny in the dashboard.
Sends resolve with persist:true to auto-generate an allow rule in
auto-allowed.yaml. Same functionality as 'A' keybinding in watch.

* docs: update rampart.sh messaging — guardrails not firewall

- Hero: 'Guardrails for AI agents. Works when sandboxes can't.'
- Replace 'policy engine/firewall' with 'guardrails' throughout
- Soften sandbox comparison language
- Fix tamper-proof → tamper-evident
- Update meta descriptions and structured data

* fix: critical issues for unified approval system

1. Empty when: clause now matches all tool calls (catch-all behavior)
   - matchCondition returns true for empty conditions
   - Lint message changed to INFO level

2. Watch TUI 'A' keybinding sends persist:true in resolve request
   - ApprovalClient.Resolve now accepts persist parameter
   - 'a' = approve (persist:false), 'A' = always allow (persist:true)

3. --serve-token flag deprecated in favor of RAMPART_TOKEN env var
   - Warning printed when flag is used directly (visible in ps aux)
   - Flag marked deprecated but kept for backward compatibility

4. GeneralizeCommand: dangerous commands never generalized
   - Blocklist: rm, chmod, chown, kill, dd, mkfs, reboot, shutdown, etc.
   - Single-token commands kept exact (ls -> ls, not ls *)

5. Atomic persist writes using temp file + rename pattern
   - Prevents corruption from concurrent Always Allow actions

6. Polling loop accepts context.Context for clean cancellation
   - requestApprovalCtx and pollApprovalCtx with context support
   - HTTP requests use NewRequestWithContext

* feat: approval deduplication, audit trail for resolutions, fallback test

- Dedup identical pending approvals within 60s window using sha256(tool+command+agent)
  to handle agent retries on timeout/reconnect (#13)
- Write hash-chained audit events on approval resolution (approve/deny/always-allow)
  with full metadata: tool, command, resolution, resolved_by, approval_id, persist (#22)
- Add integration test for hook fallback to native prompt on unreachable serve (#15)

* feat: directory-based policy loading and periodic reload

Add support for loading policies from multiple YAML files in a directory,
enabling the persist-to-policy feature to work end-to-end.

New components:
- PolicyStore interface (Load/Path) implemented by FileStore, DirStore, MultiStore
- DirStore: loads all *.yaml files from a directory in sorted order
- MultiStore: combines a primary config file with a policy directory
- Engine.StartPeriodicReload(interval): re-reads policies on a timer
- Engine.Stop(): terminates the reload goroutine

CLI changes:
- 'rampart serve' and 'rampart hook' now accept --config-dir flag
- Default: ~/.rampart/policies/ is included if it exists
- 'rampart serve' accepts --reload-interval (default 30s)

Merge behavior:
- Files loaded in sorted order (deterministic)
- default_action taken from first file that specifies one
- Duplicate policy names: first wins, later skipped with warning
- Invalid YAML files: logged and skipped (don't break other files)

This closes the gap where AppendAllowRule writes to
~/.rampart/policies/auto-allowed.yaml but the engine never loaded it.

* feat: configurable approval timeout (--approval-timeout flag)

* feat: dashboard redesign with brand colors + dangerous command detection

- Rampart brand colors (#FF6392 accent, zinc palette)
- Inter + JetBrains Mono fonts matching rampart.sh
- Stats bar: pending/approved/denied counters
- Dangerous command detection with orange warning badges
- Improved card layout with better visual hierarchy
- Responsive mobile design
- CSP updated for Google Fonts
- Configurable approval timeout (--approval-timeout flag)

* fix: dashboard timer flicker + more specific timeout message

- Update countdown timers in-place via data-id targeting instead of
  full DOM rebuild every second (eliminates card jumping)
- Timeout message now says 'no response received within' for clarity

* dashboard: fix layout shifts, add virtual scrolling, optimize for scale

- Virtual scrolling: only renders visible cards + buffer (5 above/below).
  Supports thousands of pending approvals with ~20 DOM nodes max.
- Efficient diffing: Map keyed by approval ID, only add/remove/update
  cards that actually changed between polls. Never rebuilds entire list.
- Timer updates: pure textContent swaps on visible cards only, with
  classList.toggle guard to avoid unnecessary DOM touches.
- Cards built with createElement (no innerHTML), escape results cached.
- Event delegation on container (no per-card listeners).
- Adaptive polling: 2s with pending, 5s idle, exponential backoff on error.
- Visibility API: pauses polling when tab is hidden, resumes on focus.
- requestAnimationFrame batching for DOM mutations after poll.
- CSS: added contain:layout style on cards, min-width on timer to prevent
  layout shift from text width changes. Replaced pulse animation with
  one-shot cardPulse that only fires on genuinely new cards.
- History only re-renders when dirty flag is set.
- Stats use textContent with change guards.

* feat(api): add audit history endpoints

Add four new authenticated API endpoints for querying historical audit data:

- GET /v1/audit/events — query events with pagination, filtering by
  tool, action, and agent. Returns most-recent-first.
- GET /v1/audit/dates — list available audit log dates
- GET /v1/audit/export — download a day's JSONL as attachment (streamed)
- GET /v1/audit/stats — aggregate stats across a date range

Includes WithAuditDir server option and comprehensive test coverage.

* fix: wire audit dir to proxy server for audit API endpoints

* feat: dashboard v2 — table layout, audit log, zero layout shift

* fix: command column showing ID instead of actual command

extractCmd() checked a.request.command (audit event format) before
a.command (approval API format). Approvals have command at top level.

* fix: Always Allow button requires confirmation + clearer label

- Clicking 'Always Allow' now shows a confirm dialog explaining it creates
  a permanent auto-approve rule, showing the command that will be allowed
- Button label changed from bare '★' to '★ Always' with descriptive tooltip
- Cleaned up duplicate test rules from auto-allowed.yaml

* fix: approval ordering (newest first) + action buttons column width

- Sort approvals by created_at before prepending so newest appears on top
- Widen actions column to 140px to fit all three buttons
- Actions container uses nowrap to prevent line breaking

* fix: persist dedup + clean YAML output

- Skip appending rule if identical pattern already exists (same tool + conditions)
- Use clean YAML structs with omitempty to eliminate verbose empty fields
- auto-allowed.yaml now outputs minimal, readable YAML

* fix: sort pending approvals by creation time for deterministic ordering

Store.List() iterated a map, producing random order on each call.
Now sorts by CreatedAt (oldest first) so the dashboard shows consistent
ordering across refreshes.

* feat: resizable table columns via drag handles

Drag the right edge of any column header to resize. Uses mousedown/
mousemove tracking with table-layout:fixed for predictable sizing.
Accent-colored handle appears on hover.

* feat: dashboard polish — history persistence, theme toggle, bulk actions

* fix: audit findings — double Stop() panic, audit dir leak, CHANGELOG v0.3.0

- Engine.Stop() uses sync.Once to prevent panic on double close
- /v1/audit/dates no longer exposes server filesystem path
- Comprehensive CHANGELOG for v0.3.0 with breaking change callout
  for empty when: clause behavior

* chore: version to v0.2.29

* chore: version to v0.2.3

* fix: goreleaser writes formula to Formula/ directory
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.

1 participant