How to keep the Powernode platform — including its AI agent fleet — secure end to end.
- What this guide covers
- Prerequisites
- Security model
- Authentication
- Session security
- Permissions and authorization
- Cryptographic material safety
- Data protection
- Webhook security
- Audit logging
- Network security and rate limiting
- AI security guardrails
- Quarantine system
- Supply chain security
- SBOM and license compliance
- Incident response
- Compliance posture
- Related guides
- Materials previously at
This is the working security reference for engineers and operators of the Powernode platform. It covers application security (JWT, sessions, permissions), data protection (encryption, key management, PII), AI-specific security (guardrails, anomaly detection, quarantine, security gates), and supply chain security (dependency scanning, SBOM, license compliance).
PCI-relevant material (payment processing, cardholder data) lives in the extensions/business private submodule — this guide covers only the core platform's security posture.
- Familiarity with backend (
docs/guides/backend.md) and devops (docs/guides/devops.md) conventions - For Vault integration: a deployed Vault instance per
docs/operations/production-deployment.md - For AI guardrails: understanding the agent execution model in
docs/concepts/agents-and-autonomy.md
flowchart TB
Edge[Edge: Traefik + Rate Limiting]
Auth[Auth Layer: JWT + Permissions]
App[Application: Controllers + Services]
Data[Data Layer: Encrypted at Rest]
Vault[Vault: Keys, Tokens, Secrets]
Audit[Audit Trail]
AI[AI Security Gates + Guardrails]
SC[Supply Chain Scanner]
Edge --> Auth
Auth --> App
App --> Data
App --> Vault
App --> AI
App --> Audit
SC -.scans.-> Data
SC -.scans.-> App
Defense-in-depth: each layer assumes the layer in front of it has been bypassed and protects against the next class of failure.
JWT_CONFIG = {
algorithm: 'HS256',
access_token_expiry: 15.minutes,
refresh_token_expiry: 7.days,
issuer: 'powernode-api',
audience: ['powernode-frontend', 'powernode-mobile'],
require_iss: true,
require_aud: true,
require_exp: true,
require_nbf: true,
leeway: 10.seconds,
# Key rotation
kid: -> { TokenService.current_key_id },
verify_iss: true,
verify_aud: true,
}.freezeTokens carry kid (key ID) so the platform can rotate signing keys without invalidating outstanding tokens — see docs/operations/production-deployment.md for the rotation runbook.
- Access token — short-lived (15min), carries user_id + account_id + permissions claims
- Refresh token — long-lived (7d), used to mint new access tokens; revocable per session
- Impersonation token — admin-only, references an
ImpersonationSessionthat must be active and not expired - Worker API token — service-to-service, stored in
/etc/powernode/worker-*.conf
TOTP with backup codes:
class MfaService
TOTP_SETTINGS = {
digest: 'sha1', digits: 6, interval: 30,
drift_ahead: 15, drift_behind: 15
}.freeze
def self.generate_backup_codes(user)
codes = 10.times.map { SecureRandom.alphanumeric(8).upcase }
user.update!(backup_codes: codes.map { |c| BCrypt::Password.create(c) })
codes
end
def self.verify_backup_code(user, code)
user.backup_codes.any? { |hashed| BCrypt::Password.new(hashed) == code }
end
end# config/initializers/session_security.rb
Rails.application.config.session_store :cookie_store,
key: '_powernode_session',
domain: Rails.env.production? ? '.powernode.example.com' : nil,
secure: Rails.env.production?,
httponly: true,
same_site: :strict,
expire_after: 4.hoursSession middleware additionally:
- Clears sessions containing suspicious patterns (XSS payloads, admin/system probes)
- Rate-limits per-session at 100 requests/hour
- Logs session anomalies to the audit trail
The platform uses permission-based access control, never role-based. Roles are containers for permissions; controllers check permissions.
# Controller
before_action -> { require_permission('widgets.update') }, only: :update
# Model
class User < ApplicationRecord
def has_permission?(name)
all_permissions.include?(name)
end
def all_permissions
@all_permissions ||= roles.flat_map(&:permissions).map(&:name).uniq
end
endFrontend uses currentUser?.permissions?.includes('foo.bar') — never roles.includes('admin'). Backend uses current_user.has_permission?('foo.bar') — never permissions.include?() (returns objects, not strings).
See docs/concepts/permissions.md for the registry.
Every account-scoped query MUST start from current_account. Shared example 'scopes to current account' (in spec/support/shared_examples/) verifies this for every resource. Cross-account data leaks are gated at the spec level — adding a new index for testability isn't optional.
These rules are absolute — they override convenience, they override speed, they override "just this once":
| Rule | Detail |
|---|---|
| No key output | NEVER output, log, display, echo, or transmit private keys, API secrets, seed phrases, mnemonics, or signing material in any form |
| No keys in code | NEVER store keys, secrets, or credentials in source code files, scripts, configs, env files, or documentation |
| No CLI key generation | NEVER generate private keys via CLI commands (rails runner, rake, irb) where they could appear in shell history |
| Vault-only storage | ALL key generation MUST happen inside Vault or WalletKeyService (which stores directly to Vault) |
| Audit all key ops | ALL key operations (generate, import, revoke, sign) MUST be logged to Trading::AuditLog |
| No key arguments in logs | NEVER pass private keys as function arguments that could appear in logs, error messages, or exception traces |
| Guide, don't handle | When assisting with wallet setup, guide the user through the UI/API — never handle key material directly |
If you find yourself wanting to violate one of these rules, stop and route through the existing key management surface instead. There is no scenario where Claude or a human contributor should be staring at a private key in a terminal.
Sensitive data is encrypted with AES-256-GCM and a context-specific key derived from the master key via PBKDF2 (10,000 iterations, SHA-256):
class DataEncryptionService
ENCRYPTION_ALGORITHM = 'AES-256-GCM'.freeze
class << self
def encrypt_sensitive_data(data, context: nil)
return nil if data.nil?
cipher = OpenSSL::Cipher.new(ENCRYPTION_ALGORITHM)
cipher.encrypt
cipher.key = derive_key_for_context(context)
iv = SecureRandom.random_bytes(16)
cipher.iv = iv
encrypted = cipher.update(data.to_s) + cipher.final
auth_tag = cipher.auth_tag
Base64.strict_encode64(iv + auth_tag + encrypted)
end
def decrypt_sensitive_data(encrypted_data, context: nil)
return nil if encrypted_data.nil?
data = Base64.strict_decode64(encrypted_data)
iv, auth_tag, encrypted = data[0..15], data[16..31], data[32..]
cipher = OpenSSL::Cipher.new(ENCRYPTION_ALGORITHM)
cipher.decrypt
cipher.key = derive_key_for_context(context)
cipher.iv = iv
cipher.auth_tag = auth_tag
cipher.update(encrypted) + cipher.final
end
private
def derive_key_for_context(context)
base_key = Rails.application.credentials.master_key
salt = Rails.application.credentials.encryption_salt
OpenSSL::PKCS5.pbkdf2_hmac("#{base_key}:#{context}", salt, 10_000, 32, OpenSSL::Digest::SHA256.new)
end
end
endUse it via the VaultCredential concern on models that hold encrypted attributes:
class Ai::ProviderCredential < ApplicationRecord
include VaultCredential
vault_credential :api_key
end- Never log PII (emails, phone numbers, credit card fragments) at info level
- Use the
SensitiveDatalog filter to redact known PII fields from request/response logs - PII in user-supplied input passes through the AI guardrail pipeline's
pii_detectionrail - Export endpoints strip PII unless the requesting user has
analytics.export_pii
Inbound webhooks (provider callbacks, GitOps push, third-party integrations) MUST verify their signature before processing and MUST return 200/202 on processing failures — never 500.
class Api::V1::Webhooks::ProviderController < ApplicationController
skip_before_action :authenticate_request
before_action :verify_signature
def receive
WorkerJobService.enqueue('provider_event', request.raw_post)
head :accepted
rescue StandardError => e
Rails.logger.error("Webhook processing failed: #{e.message}")
head :ok # avoid retry storm
end
private
def verify_signature
expected = OpenSSL::HMAC.hexdigest('sha256', ENV['PROVIDER_WEBHOOK_SECRET'], request.raw_post)
received = request.headers['X-Provider-Signature']
head :unauthorized unless ActiveSupport::SecurityUtils.secure_compare(expected, received)
end
endReturning a 500 to a webhook provider triggers exponential-backoff retries — they pile up, exhaust your workers, and break the system. ALWAYS return 200/202 even when processing fails.
The platform maintains structured audit logs across multiple subsystems:
AuditLog— application-wide audit events (CRUD operations, admin actions)Ai::SecurityAuditTrail— agent execution security decisionsTrading::AuditLog— financial operations and key material handlingDevops::*Activity— Docker/Swarm/pipeline operations
Each entry captures: timestamp, user_id, account_id, ip_address, request_id, action, resource, before/after diff (for mutations), and outcome.
Audit logs surface in the admin UI under Activity Feed, are queryable via MCP tools (see docs/reference/auto/mcp-tools.md), and are retained per the platform's data retention policy.
class Rack::Attack
# 60 req/min per IP
throttle('req/ip', limit: 60, period: 1.minute) { |req| req.ip }
# 5 login attempts per minute per IP
throttle('login/ip', limit: 5, period: 1.minute) do |req|
req.ip if req.path == '/api/v1/auth/login' && req.post?
end
# 1000 API req/hour per authenticated user
throttle('api/user', limit: 1000, period: 1.hour) do |req|
req.env['current_user']&.id if req.path.start_with?('/api/')
end
blocklist('malicious-ips') do |req|
Rails.cache.read("blocked_ip:#{req.ip}")
end
endclass SecurityHeadersMiddleware
def call(env)
status, headers, response = @app.call(env)
headers['X-Frame-Options'] = 'DENY'
headers['X-Content-Type-Options'] = 'nosniff'
headers['X-XSS-Protection'] = '1; mode=block'
headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
headers['Content-Security-Policy'] = "default-src 'self'"
headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
[status, headers, response]
end
endThe AI subsystem ships with a multi-layered security pipeline. Every agent execution passes through:
flowchart TB
Req[Request]
PreGate[Pre-Execution Security Gate]
InputRails[Input Rails: token limit, prompt injection, PII, topic restriction, language]
Exec[Agent Execution]
PostGate[Post-Execution Security Gate]
OutputRails[Output Rails: toxicity, hallucination, format, structured output, credential leak]
Resp[Response]
Audit[Audit Trail]
Req --> PreGate
PreGate --> InputRails
InputRails --> Exec
Exec --> PostGate
PostGate --> OutputRails
OutputRails --> Resp
PreGate -.-> Audit
PostGate -.-> Audit
Ai::Security::SecurityGateService.pre_execution_gate runs six checks:
| Check | ASI ref | Criticality | What it does |
|---|---|---|---|
quarantine_gate |
ASI08 | Hard | Blocks if agent is quarantined |
anomaly_precheck |
ASI01 | Hard | Behavioral fingerprint anomaly |
privilege_check |
ASI05 | Hard | Action vs. privilege policy |
conformance_check |
ASI03 | Soft | Event sequence rules |
prompt_injection |
ASI02 | Hard | Pattern-based injection detection |
pii_input_scan |
ASI04 | Flag | Records but doesn't block |
Criticality levels: hard blocks execution; soft logs warning and marks degraded; flag records for audit only.
| Check | ASI ref | Criticality | What it does |
|---|---|---|---|
pii_output_redact |
ASI04 | Hard | Redacts PII from output |
output_safety |
ASI09 | Hard | Validates output safety |
Ai::Guardrails::Pipeline applies configurable input, output, and retrieval rails.
Input rails: token_limit, prompt_injection, pii_detection, topic_restriction, language_detection
Output rails: token_limit, toxicity, pii_detection, hallucination_check, format_validation, structured_output, credential_leak
Retrieval rails: apply input rails to each retrieved document — prevents poisoned retrieval results.
Per-account or per-agent guardrail config:
Ai::GuardrailConfig.create!(
account: account,
agent: agent, # nil for account-wide default
name: 'Production Guardrails',
toxicity_threshold: 0.7,
pii_sensitivity: 0.9,
max_input_tokens: 8000,
max_output_tokens: 4000,
protected_branches: ['main', 'master', 'release/*'],
is_active: true
)Ai::SecurityAuditTrail.log!(
action: 'agent_execution',
outcome: 'allowed',
account: account,
agent_id: agent.id,
asi_reference: 'ASI05',
csa_pillar: 'identity',
risk_score: 0.2,
severity: 'info',
source_service: 'SecurityGateService',
context: { tool_name: 'read_file' },
details: { checks_passed: 6 }
)Outcomes: allowed | denied | blocked | quarantined | escalated. Severities: info | warning | critical. ASI references: ASI01–ASI10. CSA pillars: identity | behavior | data_governance | segmentation | incident_response.
| File | Path |
|---|---|
| Security Gate Service | server/app/services/ai/security/security_gate_service.rb |
| Guardrail Pipeline | server/app/services/ai/guardrails/pipeline.rb |
| Input Rail | server/app/services/ai/guardrails/input_rail.rb |
| Output Rail | server/app/services/ai/guardrails/output_rail.rb |
| Guardrail Config Model | server/app/models/ai/guardrail_config.rb |
| Audit Trail Model | server/app/models/ai/security_audit_trail.rb |
| Quarantine Record Model | server/app/models/ai/quarantine_record.rb |
| Anomaly Detection | server/app/services/ai/security/agent_anomaly_detection_service.rb |
| PII Redaction | server/app/services/ai/security/pii_redaction_service.rb |
| Privilege Enforcement | server/app/services/ai/security/privilege_enforcement_service.rb |
| Quarantine Service | server/app/services/ai/security/quarantine_service.rb |
Ai::QuarantineRecord isolates misbehaving agents from execution. Severities low | medium | high | critical. Statuses active | escalated | restored | expired. Triggers anomaly_detection | manual | policy_violation | budget_exceeded.
record = Ai::QuarantineRecord.create!(
agent: agent,
severity: 'high',
status: 'active',
trigger_reason: 'Anomalous tool usage pattern',
trigger_source: 'anomaly_detection',
cooldown_minutes: 60,
scheduled_restore_at: 1.hour.from_now
)
record.past_cooldown? # true once cooldown elapsed
record.auto_restorable? # true if active and past scheduled_restore_atA nightly job (AiQuarantineRestoreJob) sweeps restorable records and lifts the quarantine if no further incidents are recorded during cooldown.
flowchart LR
Repos[Repositories + Packages]
Scanner[Dependency Scanner]
SBOM[SBOM Generator]
Analysis[Security Analysis Engine]
Alerts[Alert Manager]
Dash[Dashboard]
Policy[Policy Enforcement]
Repos --> Scanner
Repos --> SBOM
Scanner --> Analysis
SBOM --> Analysis
Analysis --> Alerts
Analysis --> Dash
Analysis --> Policy
class SecurityScannerService {
async scanDependencies(config: ScanConfig): Promise<ScanResult>
async getVulnerabilities(filters?: VulnerabilityFilters): Promise<Vulnerability[]>
async getDependencyTree(packageName?: string): Promise<Dependency[]>
}
interface ScanConfig {
scanType: 'full' | 'quick' | 'targeted';
includeDevDependencies: boolean;
severityThreshold: 'critical' | 'high' | 'medium' | 'low';
autoFix: boolean;
ignoredVulnerabilities: string[];
repositories: string[];
}| Severity | CVSS | Response time | Example |
|---|---|---|---|
| Critical | 9.0-10.0 | Immediate | RCE, data exfiltration |
| High | 7.0-8.9 | 24 hours | Privilege escalation |
| Medium | 4.0-6.9 | 1 week | Information disclosure |
| Low | 0.1-3.9 | 30 days | Minor issues |
The platform uses gitleaks configured via .gitleaks.toml. Scans run:
- Pre-commit hook (gates local commits)
scripts/validate.sh(pre-push)- Full history scan in CI on release-candidate tags
When gitleaks reports a true positive, rotate the leaked credential immediately and rewrite git history if it's recent enough to scrub without breaking downstream consumers.
interface SBOM {
format: 'cyclonedx' | 'spdx';
version: string;
createdAt: string;
components: SBOMComponent[];
}
interface SBOMComponent {
type: 'library' | 'application' | 'framework';
name: string;
version: string;
purl: string;
licenses: string[];
hashes: { algorithm: string; value: string }[];
}
async generateSBOM(format: 'cyclonedx' | 'spdx'): Promise<SBOM>;SBOMs are signed and stored per release tag for downstream consumers and compliance audits.
The license scanner produces a LicenseReport covering total packages, license breakdown, incompatible licenses (with reason), and an overall compliance score. The OSS policy bans GPL/AGPL in production paths and warns on weak copyleft (LGPL, MPL).
SecurityPolicy records define rules (license restrictions, severity thresholds, EOL package detection) and actions (block install, warn, auto-fix). The pre-commit hook and CI both evaluate active policies and gate builds accordingly.
When a security incident is detected:
- Triage — Determine scope: data exfiltration, unauthorized access, key compromise, dependency vulnerability
- Contain — Revoke compromised credentials/tokens, quarantine affected agents, isolate affected hosts
- Investigate — Pull audit trails, correlate with
Ai::SecurityAuditTrailandAuditLog, capture forensics - Remediate — Apply patch, rotate keys, update guardrail policies, blocklist offending IPs
- Report — File issue with
[SEC]prefix, notify affected users if data was exposed - Post-mortem — Document root cause, update threat model, add detection coverage
The escalation surface is the platform's Escalation model and intervention policies — see docs/concepts/agents-and-autonomy.md for the autonomy/intervention flow.
The core platform's compliance posture covers:
- GDPR / privacy — PII detection and redaction, export-with-consent, data subject access rights via admin tools
- SOC 2 Type II — Audit logging, access control, incident response procedures
- Supply chain (SLSA) — Build provenance, signed releases, SBOM generation
PCI DSS compliance (for payment processing) lives in extensions/business — when that extension is loaded, additional PCI-specific guardrails activate (tokenized PAN storage, HMAC-signed payment requests, restricted audit log access). See the business submodule's docs for the PCI matrix.
- Backend — Rails patterns and authentication flow
- DevOps — secrets management, Vault deployment
- Extensions — extension-level security boundaries
docs/concepts/permissions.md— permission registrydocs/concepts/agents-and-autonomy.md— agent execution model the guardrails protectdocs/reference/auto/mcp-tools.md— security-related MCP tools
This guide consolidates content from these legacy paths (preserved in git history for one release cycle):
docs/infrastructure/SECURITY_SPECIALIST.mddocs/platform/AI_SECURITY_GUARDRAILS.mddocs/platform/SUPPLY_CHAIN_SECURITY.md
Last verified: 2026-05-17