beskar 0.0.2 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +274 -0
- data/README.md +412 -204
- data/app/channels/concerns/beskar/channels/session_security.rb +46 -0
- data/app/controllers/beskar/administrative_actions_controller.rb +16 -0
- data/app/controllers/beskar/application_controller.rb +214 -0
- data/app/controllers/beskar/banned_ips_controller.rb +255 -0
- data/app/controllers/beskar/dashboard_controller.rb +62 -0
- data/app/controllers/beskar/security_events_controller.rb +164 -0
- data/app/controllers/concerns/beskar/controllers/audit_export.rb +54 -0
- data/app/controllers/concerns/beskar/controllers/security_tracking.rb +76 -48
- data/app/controllers/concerns/beskar/controllers/session_security.rb +29 -0
- data/app/jobs/beskar/notification_job.rb +33 -0
- data/app/mailers/beskar/security_mailer.rb +59 -0
- data/app/models/beskar/administrative_action.rb +41 -0
- data/app/models/beskar/banned_ip.rb +105 -105
- data/app/models/beskar/security_event.rb +51 -4
- data/app/models/beskar/security_state.rb +58 -0
- data/app/services/beskar/banned_ip_manager.rb +88 -0
- data/app/views/beskar/administrative_actions/index.html.erb +33 -0
- data/app/views/beskar/administrative_actions/show.html.erb +21 -0
- data/app/views/beskar/banned_ips/edit.html.erb +195 -0
- data/app/views/beskar/banned_ips/index.html.erb +319 -0
- data/app/views/beskar/banned_ips/new.html.erb +190 -0
- data/app/views/beskar/banned_ips/review.html.erb +24 -0
- data/app/views/beskar/banned_ips/show.html.erb +304 -0
- data/app/views/beskar/dashboard/index.html.erb +280 -0
- data/app/views/beskar/security_events/index.html.erb +302 -0
- data/app/views/beskar/security_events/show.html.erb +293 -0
- data/app/views/beskar/shared/_export_form.html.erb +10 -0
- data/app/views/layouts/beskar/_behavior.html.erb +121 -0
- data/app/views/layouts/beskar/application.html.erb +581 -6
- data/config/routes.rb +30 -0
- data/db/migrate/20251016000001_create_beskar_security_events.rb +3 -3
- data/db/migrate/20260910000001_create_beskar_security_states.rb +14 -0
- data/db/migrate/20260911000001_create_beskar_administrative_actions.rb +22 -0
- data/db/migrate/20260911000002_expand_administrative_action_targets.rb +6 -0
- data/docs/README.md +73 -0
- data/docs/archive/project-documentation.md +659 -0
- data/docs/audits/project-review.md +437 -0
- data/docs/audits/repair-status.md +216 -0
- data/docs/guides/audit-and-waf.md +175 -0
- data/docs/guides/audit-lifecycle.md +172 -0
- data/docs/guides/authentication.md +213 -0
- data/docs/guides/configuration.md +182 -0
- data/docs/guides/dashboard-and-search.md +251 -0
- data/docs/guides/notifications-and-recovery.md +157 -0
- data/docs/guides/risk-scoring.md +116 -0
- data/docs/operations/monitor-only-mode.md +85 -0
- data/docs/operations/security-hardening.md +167 -0
- data/docs/operations/state-storage.md +144 -0
- data/docs/research/rust-performance-assessment.md +69 -0
- data/lib/beskar/configuration.rb +105 -20
- data/lib/beskar/configuration_validator.rb +188 -0
- data/lib/beskar/devise_authentication.rb +24 -0
- data/lib/beskar/engine.rb +21 -88
- data/lib/beskar/logger.rb +288 -0
- data/lib/beskar/middleware/request_analyzer.rb +133 -99
- data/lib/beskar/models/security_trackable_authenticable.rb +76 -97
- data/lib/beskar/models/security_trackable_devise.rb +34 -25
- data/lib/beskar/models/security_trackable_generic.rb +171 -214
- data/lib/beskar/risk_level.rb +22 -0
- data/lib/beskar/services/account_locker.rb +90 -81
- data/lib/beskar/services/administrative_audit.rb +36 -0
- data/lib/beskar/services/administrative_bans.rb +104 -0
- data/lib/beskar/services/audit_data.rb +72 -0
- data/lib/beskar/services/authentication.rb +31 -0
- data/lib/beskar/services/authentication_attempt.rb +141 -0
- data/lib/beskar/services/ban_expiry.rb +28 -0
- data/lib/beskar/services/device_detector.rb +32 -41
- data/lib/beskar/services/event_search.rb +58 -0
- data/lib/beskar/services/geolocation_service.rb +83 -114
- data/lib/beskar/services/ip_whitelist.rb +31 -40
- data/lib/beskar/services/location_assessment.rb +109 -0
- data/lib/beskar/services/native_account_lock.rb +82 -0
- data/lib/beskar/services/notifications.rb +46 -0
- data/lib/beskar/services/rate_limiter.rb +99 -125
- data/lib/beskar/services/request_context.rb +64 -0
- data/lib/beskar/services/risk_assessment.rb +58 -0
- data/lib/beskar/services/session_revocation.rb +62 -0
- data/lib/beskar/services/waf.rb +311 -198
- data/lib/beskar/services/waf_request.rb +60 -0
- data/lib/beskar/version.rb +1 -1
- data/lib/beskar/warden_authentication.rb +53 -0
- data/lib/beskar.rb +54 -4
- data/lib/generators/beskar/install/install_generator.rb +158 -0
- data/lib/generators/beskar/install/templates/initializer.rb.tt +261 -0
- data/lib/tasks/beskar_tasks.rake +25 -20
- metadata +93 -12
- data/lib/beskar/templates/beskar_initializer.rb +0 -107
data/README.md
CHANGED
|
@@ -1,23 +1,79 @@
|
|
|
1
1
|
# Beskar
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Implementation repairs are tracked in [Repair status](docs/audits/repair-status.md). Read [Security hardening and rollout](docs/operations/security-hardening.md) before upgrading: session revocation, explicit admin permissions, export auditing, sealed configuration, and safer availability defaults change integration requirements. The original findings remain in [Original project review](docs/audits/project-review.md).
|
|
4
|
+
|
|
5
|
+
Account deletion retains security events unchanged. Dashboard ban changes now require
|
|
6
|
+
a trusted actor and reason, recorded in a new administrative-history table. See
|
|
7
|
+
[Audit lifecycle](docs/guides/audit-lifecycle.md) for configuration and upgrade requirements.
|
|
8
|
+
|
|
9
|
+
**Beskar** is a Rails-native security engine for authentication admission limits, risk-based account locking, IP bans, scanner-path detection, and an administrative audit dashboard. It requires a shared writer database for coordinated state and explicit host authentication integration. Its heuristic signals do not replace application authorization, input validation, or recovery delivery.
|
|
10
|
+
|
|
11
|
+
## Documentation
|
|
12
|
+
|
|
13
|
+
The [documentation index](docs/README.md) organizes current guides, operational
|
|
14
|
+
contracts, audit findings, research, and archived reference material. Start with
|
|
15
|
+
[configuration](docs/guides/configuration.md) and
|
|
16
|
+
[authentication](docs/guides/authentication.md) for integration, or the
|
|
17
|
+
[rollout checklist](docs/operations/security-hardening.md#rollout) for an upgrade.
|
|
18
|
+
|
|
19
|
+
## Screenshots
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
| Security Events | Banned IPs |
|
|
24
|
+
|:---------------:|:----------:|
|
|
25
|
+
|  |  |
|
|
26
|
+
|
|
27
|
+
## Table of Contents
|
|
28
|
+
|
|
29
|
+
- [Documentation](#documentation)
|
|
30
|
+
- [Features](#features)
|
|
31
|
+
- [Installation](#installation)
|
|
32
|
+
- [Quick Start](#quick-start)
|
|
33
|
+
- [Dashboard Authentication (REQUIRED)](#dashboard-authentication-required)
|
|
34
|
+
- [Add to Your User Model](#add-to-your-user-model)
|
|
35
|
+
- [Configuration](#configuration)
|
|
36
|
+
- [Usage](#usage)
|
|
37
|
+
- [Risk-Based Account Locking](#risk-based-account-locking-with-devise-lockable)
|
|
38
|
+
- [Rate Limiting](#rate-limiting)
|
|
39
|
+
- [IP Whitelisting](#ip-whitelisting)
|
|
40
|
+
- [Web Application Firewall (WAF)](#web-application-firewall-waf)
|
|
41
|
+
- [IP Blocking and Banning](#ip-blocking-and-banning)
|
|
42
|
+
- [Security Events](#security-events)
|
|
43
|
+
- [Middleware Integration](#middleware-integration)
|
|
44
|
+
- [WAF Pattern Reference](#waf-pattern-reference)
|
|
45
|
+
- [Security Best Practices](#security-best-practices)
|
|
46
|
+
- [Troubleshooting](#troubleshooting)
|
|
47
|
+
- [Development](#development)
|
|
48
|
+
- [Contributing](#contributing)
|
|
49
|
+
- [License](#license)
|
|
4
50
|
|
|
5
51
|
## Features
|
|
6
52
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
53
|
+
For current reporting bands, search semantics, native-user presentation, UTC ban
|
|
54
|
+
forms, script CSP/native-navigation behavior, and dashboard/export routes, see
|
|
55
|
+
[Dashboard and search](docs/guides/dashboard-and-search.md).
|
|
56
|
+
No versioned administration API is implemented.
|
|
57
|
+
|
|
58
|
+
See [Configuration](docs/guides/configuration.md) for startup validation, supported
|
|
59
|
+
strategies/providers, and opt-in host background analysis. Opt-in lock/reset email
|
|
60
|
+
delivery and its host recovery requirements are documented in
|
|
61
|
+
[Notifications and recovery](docs/guides/notifications-and-recovery.md).
|
|
62
|
+
|
|
63
|
+
- **Devise Integration:** Admission, risk assessment, optional audit tracking and durable session/remember-cookie revocation on protected models.
|
|
64
|
+
- **Risk-Based Account Locking:** Opt-in rules lock supported accounts at configured thresholds. Scores are heuristics, not proof of account takeover.
|
|
65
|
+
- **Rate Limiting:** Database-coordinated IP/account and opt-in global admission limits with enforced backoff deadlines. Supports any Rails.cache backend. See [authentication integration](docs/guides/authentication.md) for required host guards.
|
|
66
|
+
- **Authentication Pattern Helpers:** Bounded account/IP failure-history helpers; automatic background analysis requires an explicitly configured host job.
|
|
67
|
+
- **IP Whitelisting:** Trusted IPs/CIDRs bypass automatic blocking while configured observations remain enabled; optional audit delivery is not guaranteed.
|
|
68
|
+
- **Persistent IP Blocking:** Database-authoritative blocking across application restarts. WAF rules and opt-in request-wide quota-abuse escalation can create automatic bans.
|
|
69
|
+
- **Web Application Firewall (WAF):** Bounded, decoded-path scanner signatures and narrowly scoped Rails exception signals, with cumulative scores, escalating bans, monitor-only mode, and method/path/category exclusions. This is not a general SQL injection or XSS filter.
|
|
70
|
+
- **Security Event Tracking:** Filtered authentication/WAF evidence and required administrative history. Events survive account deletion; ordinary instance rewrites/deletes are rejected.
|
|
15
71
|
- **IP Geolocation:** MaxMind GeoLite2-City database integration for country/city location, coordinates, timezone, and enhanced risk scoring (configurable, database not included due to licensing).
|
|
16
|
-
- **Geographic Anomaly Detection:** Haversine-based
|
|
17
|
-
- **
|
|
72
|
+
- **Geographic Anomaly Detection:** Timestamped, admitted-login history and Haversine-based travel heuristics with explicit evidence. Mock locations do not trigger geographic risk; see [risk scoring](docs/guides/risk-scoring.md).
|
|
73
|
+
- **User-Agent Heuristics:** Browser and bot-like User-Agent signals contribute to authentication risk. Headers are spoofable; JavaScript challenges and honeypots are not implemented.
|
|
18
74
|
- **Modular Architecture:** Devise-specific code is isolated in separate services for maintainability and extensibility.
|
|
19
|
-
- **Rails-Native Architecture:** Built as a mountable `Rails::Engine`,
|
|
20
|
-
- **
|
|
75
|
+
- **Rails-Native Architecture:** Built as a mountable `Rails::Engine`, with Active Record-backed security state and optional caching for enrichment.
|
|
76
|
+
- **Security Dashboard:** A mountable web interface for monitoring security events, managing IP bans, and viewing statistics. Features configurable authentication, real-time filtering, and export capabilities. See [Dashboard Authentication](#dashboard-authentication-required) section below.
|
|
21
77
|
|
|
22
78
|
## Installation
|
|
23
79
|
|
|
@@ -52,22 +108,118 @@ bin/rails db:migrate
|
|
|
52
108
|
|
|
53
109
|
### Quick Start
|
|
54
110
|
|
|
111
|
+
**1. Configure Dashboard Authentication (Required)**
|
|
112
|
+
|
|
113
|
+
Before using Beskar, you must configure authentication for the dashboard. See the [Dashboard Authentication](#dashboard-authentication-required) section below for details and examples.
|
|
114
|
+
|
|
115
|
+
**2. Enable WAF Monitoring**
|
|
116
|
+
|
|
55
117
|
By default, Beskar enables the **Web Application Firewall (WAF) in monitor-only mode**. This means:
|
|
56
118
|
- ✅ Vulnerability scans are detected and logged
|
|
57
|
-
- ✅ Security events are created for analysis
|
|
119
|
+
- ✅ Security events are created for analysis
|
|
58
120
|
- ⚠️ No requests are blocked yet (safe to enable in production)
|
|
59
121
|
|
|
60
122
|
After monitoring for 24-48 hours, review the logs and disable monitor-only mode to enable active blocking:
|
|
61
123
|
|
|
62
124
|
```ruby
|
|
63
125
|
# config/initializers/beskar.rb
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
126
|
+
Beskar.configure do |config|
|
|
127
|
+
config.monitor_only = true # Change this to false to enable blocking
|
|
128
|
+
config.waf[:enabled] = true
|
|
67
129
|
# ... rest of configuration
|
|
68
|
-
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Dashboard Authentication (REQUIRED)
|
|
134
|
+
|
|
135
|
+
**⚠️ IMPORTANT: Dashboard authentication must be configured for all environments.**
|
|
136
|
+
|
|
137
|
+
The Beskar dashboard requires authentication to prevent unauthorized access. You must configure how users authenticate to access the dashboard by setting up the `authenticate_admin` callback:
|
|
138
|
+
|
|
139
|
+
```ruby
|
|
140
|
+
# config/initializers/beskar.rb
|
|
141
|
+
Beskar.configure do |config|
|
|
142
|
+
# REQUIRED: Configure dashboard authentication
|
|
143
|
+
# The block is executed in the controller context and receives the request object.
|
|
144
|
+
# You have access to all controller methods (cookies, session, etc.) and helpers.
|
|
145
|
+
config.authenticate_admin = ->(request) do
|
|
146
|
+
# Return truthy to allow access, falsey to deny
|
|
147
|
+
|
|
148
|
+
# Example 1: Devise with admin role (recommended for production)
|
|
149
|
+
user = request.env['warden']&.authenticate(scope: :user)
|
|
150
|
+
user&.admin?
|
|
151
|
+
end
|
|
152
|
+
# REQUIRED: grant individual capabilities using your host permission store.
|
|
153
|
+
config.authorize_admin = ->(request, permission) do
|
|
154
|
+
user = request.env['warden']&.user(scope: :user)
|
|
155
|
+
user&.admin? && user.beskar_permissions.include?(permission.to_s)
|
|
156
|
+
end
|
|
157
|
+
# Adapt beskar_permissions to your host: read, manage_bans, export, read_audit.
|
|
158
|
+
# REQUIRED for dashboard writes and exports: identify the authenticated operator.
|
|
159
|
+
config.audit_actor = ->(request) do
|
|
160
|
+
user = request.env['warden']&.user(scope: :user)
|
|
161
|
+
"User:#{user.id}" if user&.admin?
|
|
162
|
+
end
|
|
163
|
+
end
|
|
69
164
|
```
|
|
70
165
|
|
|
166
|
+
**Why this is required:** Previous versions allowed unauthenticated access in development/test environments, which could lead to production security issues. Now, authentication must be explicitly configured for all environments to prevent accidental exposure.
|
|
167
|
+
|
|
168
|
+
`audit_actor` is separate from authorization. Without it, authenticated reads work
|
|
169
|
+
but ban mutations return 503. Every mutation also requires a nonblank `audit_reason`
|
|
170
|
+
(at most 1,000 characters), supplied by the dashboard forms. Apply the history
|
|
171
|
+
migration and adapt other authentication strategies to return a trusted opaque
|
|
172
|
+
operator ID; never use request parameters or credentials as that identity. See
|
|
173
|
+
[administrative history](docs/guides/audit-lifecycle.md).
|
|
174
|
+
|
|
175
|
+
**Other Authentication Strategies:**
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
# Token-based authentication
|
|
179
|
+
config.authenticate_admin = ->(request) do
|
|
180
|
+
token = ENV['BESKAR_ADMIN_TOKEN']
|
|
181
|
+
token.present? && Beskar::Services::RequestContext.secure_match?(request.headers['Authorization'], "Bearer #{token}")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# HTTP Basic Auth (uses controller method)
|
|
185
|
+
config.authenticate_admin = ->(request) do
|
|
186
|
+
authenticate_or_request_with_http_basic do |username, password|
|
|
187
|
+
Beskar::Services::RequestContext.secure_match?(username, ENV['BESKAR_USERNAME']) &&
|
|
188
|
+
Beskar::Services::RequestContext.secure_match?(password, ENV['BESKAR_PASSWORD'])
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Cookie-based authentication (uses controller cookies)
|
|
193
|
+
config.authenticate_admin = ->(request) do
|
|
194
|
+
Beskar::Services::RequestContext.secure_match?(cookies.signed[:admin_token], ENV['BESKAR_ADMIN_TOKEN'])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Development/Testing bypass (use with caution!)
|
|
198
|
+
config.authenticate_admin = ->(request) do
|
|
199
|
+
Rails.env.development? || Rails.env.test?
|
|
200
|
+
end
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
**Accessing the Dashboard:**
|
|
204
|
+
|
|
205
|
+
After configuring authentication, mount the engine in your routes:
|
|
206
|
+
|
|
207
|
+
```ruby
|
|
208
|
+
# config/routes.rb
|
|
209
|
+
Rails.application.routes.draw do
|
|
210
|
+
mount Beskar::Engine => "/beskar"
|
|
211
|
+
end
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Then visit `http://localhost:3000/beskar` to access the dashboard.
|
|
215
|
+
|
|
216
|
+
**Dashboard Features:**
|
|
217
|
+
- 📊 Security event monitoring with filtering and search
|
|
218
|
+
- 🚫 IP ban management (view, extend, unban)
|
|
219
|
+
- 📈 Statistics and risk distribution analysis
|
|
220
|
+
- 📥 Export capabilities (CSV/JSON)
|
|
221
|
+
- 🔒 CSRF protection and secure by default
|
|
222
|
+
|
|
71
223
|
### Add to Your User Model
|
|
72
224
|
|
|
73
225
|
Include the `SecurityTrackable` concern in your Devise user model:
|
|
@@ -75,8 +227,8 @@ Include the `SecurityTrackable` concern in your Devise user model:
|
|
|
75
227
|
```ruby
|
|
76
228
|
# app/models/user.rb
|
|
77
229
|
class User < ApplicationRecord
|
|
78
|
-
include Beskar::SecurityTrackable
|
|
79
|
-
|
|
230
|
+
include Beskar::Models::SecurityTrackable
|
|
231
|
+
|
|
80
232
|
devise :database_authenticatable, :registerable,
|
|
81
233
|
:recoverable, :rememberable, :validatable
|
|
82
234
|
# ... other Devise modules
|
|
@@ -85,18 +237,28 @@ end
|
|
|
85
237
|
|
|
86
238
|
## Configuration
|
|
87
239
|
|
|
88
|
-
You can configure Beskar in the initializer file created by the installer
|
|
240
|
+
You can configure Beskar in the initializer file created by the installer.
|
|
241
|
+
|
|
242
|
+
> **Note:** Dashboard authentication setup is covered in the [Dashboard Authentication](#dashboard-authentication-required) section above.
|
|
89
243
|
|
|
90
244
|
```ruby
|
|
91
245
|
# config/initializers/beskar.rb
|
|
92
246
|
Beskar.configure do |config|
|
|
247
|
+
# === Dashboard Authentication (REQUIRED) ===
|
|
248
|
+
# See "Dashboard Authentication" section above for examples and details
|
|
249
|
+
config.authenticate_admin = ->(request) do
|
|
250
|
+
user = request.env['warden']&.authenticate(scope: :user)
|
|
251
|
+
user&.admin?
|
|
252
|
+
end
|
|
253
|
+
|
|
93
254
|
# === Security Tracking ===
|
|
94
255
|
# Controls what security events are tracked and analyzed
|
|
95
256
|
config.security_tracking = {
|
|
96
257
|
enabled: true, # Master switch - disables all tracking when false
|
|
97
258
|
track_successful_logins: true, # Track successful authentication events
|
|
98
259
|
track_failed_logins: true, # Track failed authentication attempts
|
|
99
|
-
auto_analyze_patterns:
|
|
260
|
+
auto_analyze_patterns: false, # Opt in only with a host-owned Active Job
|
|
261
|
+
analysis_job: nil # Example: "SecurityReviewJob"; see docs/guides/configuration.md
|
|
100
262
|
}
|
|
101
263
|
|
|
102
264
|
# === Rate Limiting ===
|
|
@@ -112,6 +274,7 @@ Beskar.configure do |config|
|
|
|
112
274
|
exponential_backoff: true
|
|
113
275
|
},
|
|
114
276
|
global_attempts: {
|
|
277
|
+
enabled: false, # Opt-in: attackers can exhaust a shared login budget
|
|
115
278
|
limit: 100, # System-wide limit
|
|
116
279
|
period: 1.minute,
|
|
117
280
|
exponential_backoff: false
|
|
@@ -119,36 +282,37 @@ Beskar.configure do |config|
|
|
|
119
282
|
}
|
|
120
283
|
|
|
121
284
|
# === IP Whitelisting ===
|
|
122
|
-
#
|
|
123
|
-
#
|
|
124
|
-
config.ip_whitelist = [
|
|
125
|
-
"192.168.1.100", # Single IP address
|
|
126
|
-
"10.0.0.0/24", # CIDR notation - entire subnet
|
|
127
|
-
"172.16.0.0/16", # Larger CIDR range
|
|
128
|
-
"2001:db8::/32" # IPv6 support
|
|
129
|
-
]
|
|
285
|
+
# See "IP Whitelisting" section below for detailed examples
|
|
286
|
+
config.ip_whitelist = [] # Add trusted IPs here (supports CIDR notation)
|
|
130
287
|
|
|
131
288
|
# === Web Application Firewall (WAF) ===
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
289
|
+
# See "Web Application Firewall" section below for production examples
|
|
290
|
+
# Defaults shown here - use [:key] syntax to preserve other defaults
|
|
291
|
+
config.waf[:enabled] = true # Master switch for WAF
|
|
292
|
+
# config.waf[:auto_block] = true # Default: true
|
|
293
|
+
# config.waf[:score_threshold] = 150 # Default: 150 (cumulative risk score before blocking)
|
|
294
|
+
# config.waf[:violation_window] = 6.hours # Default: 6 hours (max time to track violations)
|
|
295
|
+
# config.waf[:block_durations] = [1.hour, 6.hours, 24.hours, 7.days] # Escalating bans
|
|
296
|
+
# config.waf[:permanent_block_after] = 500 # Permanent after cumulative score reaches 500
|
|
297
|
+
# config.waf[:create_security_events] = true # Log to SecurityEvent table
|
|
298
|
+
# config.waf[:record_not_found_exclusions] = [] # Regex patterns for false positives
|
|
299
|
+
# config.waf[:decay_enabled] = true # Enable exponential decay of violation scores
|
|
300
|
+
# config.waf[:decay_rates] = { # Decay rates by severity (half-life in minutes)
|
|
301
|
+
# critical: 360, # 6 hour half-life
|
|
302
|
+
# high: 120, # 2 hour half-life
|
|
303
|
+
# medium: 45, # 45 minute half-life
|
|
304
|
+
# low: 15 # 15 minute half-life
|
|
305
|
+
# }
|
|
306
|
+
# config.waf[:max_violations_tracked] = 50 # Maximum violations to track per IP
|
|
143
307
|
|
|
144
308
|
# === Risk-Based Account Locking ===
|
|
145
309
|
# Automatically lock accounts when authentication risk score exceeds threshold
|
|
146
310
|
config.risk_based_locking = {
|
|
147
311
|
enabled: false, # Master switch for risk-based locking
|
|
148
312
|
risk_threshold: 75, # Lock account if risk score >= this value (0-100)
|
|
149
|
-
lock_strategy: :devise_lockable, # Strategy: :devise_lockable, :
|
|
150
|
-
auto_unlock_time: 1.hour, #
|
|
151
|
-
notify_user:
|
|
313
|
+
lock_strategy: :devise_lockable, # Strategy: :devise_lockable, :rails_auth, :none
|
|
314
|
+
auto_unlock_time: 1.hour, # Native locks only; Devise owns unlock_in
|
|
315
|
+
notify_user: false, # Opt-in email; configure notifications first
|
|
152
316
|
log_lock_events: true # Create security event for locks
|
|
153
317
|
}
|
|
154
318
|
|
|
@@ -163,32 +327,14 @@ Beskar.configure do |config|
|
|
|
163
327
|
}
|
|
164
328
|
end
|
|
165
329
|
|
|
166
|
-
# Security Tracking Configuration Details
|
|
167
|
-
The security tracking system respects all configuration settings:
|
|
168
|
-
|
|
169
|
-
- **`enabled: false`** - Completely disables all security event tracking
|
|
170
|
-
- **`track_successful_logins: false`** - Stops tracking successful login events
|
|
171
|
-
- **`track_failed_logins: false`** - Stops tracking failed login attempts
|
|
172
|
-
- **`auto_analyze_patterns: false`** - Disables automatic threat pattern analysis
|
|
173
|
-
|
|
174
|
-
When tracking is disabled via configuration, no `SecurityEvent` records are created and no background analysis jobs are queued.
|
|
175
330
|
```
|
|
176
331
|
|
|
177
332
|
## Usage
|
|
178
333
|
|
|
179
|
-
|
|
334
|
+
For current admission, account-lock, and recovery behavior—including the required
|
|
335
|
+
Rails-native controller/session-reader upgrade—see [Authentication](docs/guides/authentication.md).
|
|
180
336
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
```ruby
|
|
184
|
-
class User < ApplicationRecord
|
|
185
|
-
devise :database_authenticatable, :registerable,
|
|
186
|
-
:recoverable, :rememberable, :validatable
|
|
187
|
-
|
|
188
|
-
# Add Beskar security tracking
|
|
189
|
-
include Beskar::Models::SecurityTrackable
|
|
190
|
-
end
|
|
191
|
-
```
|
|
337
|
+
> **Note:** If you haven't already, see the [Add to Your User Model](#add-to-your-user-model) section in Quick Start for setting up `SecurityTrackable`.
|
|
192
338
|
|
|
193
339
|
### Risk-Based Account Locking (with Devise Lockable)
|
|
194
340
|
|
|
@@ -203,7 +349,7 @@ class User < ApplicationRecord
|
|
|
203
349
|
devise :database_authenticatable, :registerable,
|
|
204
350
|
:recoverable, :rememberable, :validatable,
|
|
205
351
|
:lockable # Add this for risk-based locking
|
|
206
|
-
|
|
352
|
+
|
|
207
353
|
include Beskar::Models::SecurityTrackable
|
|
208
354
|
end
|
|
209
355
|
```
|
|
@@ -228,8 +374,8 @@ Beskar.configure do |config|
|
|
|
228
374
|
enabled: true, # Enable the feature
|
|
229
375
|
risk_threshold: 75, # Lock when risk >= 75
|
|
230
376
|
lock_strategy: :devise_lockable, # Use Devise's lockable module
|
|
231
|
-
auto_unlock_time: 1.hour, #
|
|
232
|
-
notify_user:
|
|
377
|
+
auto_unlock_time: 1.hour, # Native locks only; configure Devise's unlock_in separately
|
|
378
|
+
notify_user: false, # Opt-in email; see docs/guides/notifications-and-recovery.md
|
|
233
379
|
log_lock_events: true # Create security events
|
|
234
380
|
}
|
|
235
381
|
end
|
|
@@ -237,37 +383,24 @@ end
|
|
|
237
383
|
|
|
238
384
|
**How it works:**
|
|
239
385
|
|
|
240
|
-
-
|
|
241
|
-
|
|
242
|
-
- Device fingerprints (suspicious user agents, bot signatures)
|
|
243
|
-
- Login patterns (velocity, time of day, recent failures)
|
|
244
|
-
- IP reputation and geolocation risk
|
|
245
|
-
|
|
246
|
-
- **Adaptive Learning:** The system learns from user behavior:
|
|
247
|
-
- After 2+ successful logins from an IP, that location becomes "established"
|
|
248
|
-
- If a user unlocks and logs in successfully, that pattern is trusted
|
|
249
|
-
- Risk scores are reduced to 30% for established patterns (capped at 25)
|
|
250
|
-
- This prevents repeated locks after users validate their login context
|
|
386
|
+
- Each authentication assessment records a score (0–100), named factors, and evidence: timestamped geographic observations, unverified User-Agent claims, application-local mobile hours, and recent account failures.
|
|
387
|
+
- Repeated IP use and unlock events do not establish verified trust. The former automatic risk discount and geographic bypass have been removed.
|
|
251
388
|
|
|
252
389
|
- If the risk score meets or exceeds the configured threshold, the account is automatically locked
|
|
253
|
-
-
|
|
254
|
-
-
|
|
255
|
-
-
|
|
256
|
-
|
|
257
|
-
**Example Adaptive Flow:**
|
|
258
|
-
1. User travels to new location → High risk (85) → Account locked
|
|
259
|
-
2. User unlocks account → Validates legitimacy
|
|
260
|
-
3. User logs in from same location → Pattern established → Risk reduced to 25 → Login succeeds ✅
|
|
261
|
-
4. Future logins from this location → Normal risk → No more locks
|
|
390
|
+
- Confirmed locks reject the current attempt and revoke prior Devise sessions/remember cookies. Unlock does not resurrect them. `immediate_signout` defaults true; legacy false no longer bypasses a lock. Unrelated accounts remain signed in.
|
|
391
|
+
- Optional audit events record lock details; missing audit rows do not change enforcement.
|
|
392
|
+
- Devise controls its own unlock policy; Rails-native locks use Beskar's `auto_unlock_time` and persistent session guards.
|
|
262
393
|
|
|
263
|
-
See
|
|
394
|
+
See [Risk scoring](docs/guides/risk-scoring.md) for factor weights, bounded history, provider
|
|
395
|
+
behavior, and rollout limitations. Scores are heuristics, not calibrated
|
|
396
|
+
probabilities; validate the corrected inputs in monitor mode before enforcement.
|
|
264
397
|
|
|
265
398
|
**Lock Reasons:**
|
|
266
399
|
|
|
267
400
|
The system identifies specific reasons for locking:
|
|
268
401
|
- `:impossible_travel` - Login from location requiring impossible travel speed
|
|
269
402
|
- `:suspicious_device` - Bot signature or suspicious user agent detected
|
|
270
|
-
- `:geographic_anomaly` -
|
|
403
|
+
- `:geographic_anomaly` - Changed known country
|
|
271
404
|
- `:high_risk_authentication` - General high-risk authentication pattern
|
|
272
405
|
|
|
273
406
|
**Manual Lock/Unlock Operations:**
|
|
@@ -351,7 +484,7 @@ attack_type = rate_limiter.attack_pattern_type
|
|
|
351
484
|
case attack_type
|
|
352
485
|
when :brute_force_single_account
|
|
353
486
|
# Single IP attacking one account
|
|
354
|
-
when :distributed_single_account
|
|
487
|
+
when :distributed_single_account
|
|
355
488
|
# Multiple IPs attacking one account
|
|
356
489
|
when :single_ip_multiple_accounts
|
|
357
490
|
# One IP attacking multiple accounts (credential stuffing)
|
|
@@ -388,62 +521,162 @@ if Beskar::Services::IpWhitelist.whitelisted?(request.ip)
|
|
|
388
521
|
# IP is trusted - allow but log activity
|
|
389
522
|
end
|
|
390
523
|
|
|
391
|
-
#
|
|
524
|
+
# Optional compatibility method; configuration changes are detected automatically.
|
|
392
525
|
Beskar::Services::IpWhitelist.clear_cache!
|
|
393
526
|
```
|
|
394
527
|
|
|
395
528
|
### Web Application Firewall (WAF)
|
|
396
529
|
|
|
397
|
-
Beskar's WAF
|
|
530
|
+
Beskar's WAF uses a **score-based blocking system with exponential decay** for scanner-path signatures and selected Rails exceptions. See [the matching and privacy contract](docs/guides/audit-and-waf.md) for canonicalization, exclusions, and limitations.
|
|
398
531
|
|
|
399
532
|
**Attack Categories Detected:**
|
|
400
|
-
1. **WordPress Scans** (High
|
|
401
|
-
2. **
|
|
402
|
-
3. **
|
|
403
|
-
4. **
|
|
404
|
-
5. **
|
|
405
|
-
6. **
|
|
406
|
-
7. **
|
|
533
|
+
1. **WordPress Scans** (High: 80 points) - `/wp-admin`, `/wp-login.php`, `/wp-content/*.php`, `/xmlrpc.php`
|
|
534
|
+
2. **WordPress Static Files** (Low: 30 points) - `/wp-content/*.css`, `/wp-content/*.js`, `/wp-content/*.jpg` (broken links, not attacks)
|
|
535
|
+
3. **PHP Admin Panels** (High: 80 points) - `/phpmyadmin`, `/admin.php`, `/phpinfo.php`
|
|
536
|
+
4. **Config Files** (Critical: 95 points) - `/.env`, `/.git`, `/database.yml`
|
|
537
|
+
5. **Path Traversal** (Critical: 95 points) - `/../../../etc/passwd`, URL encoded variants
|
|
538
|
+
6. **Framework Debug** (Medium: 60 points) - `/rails/info/routes`, `/__debug__`, `/telescope`
|
|
539
|
+
7. **CMS Detection** (Medium: 60 points) - `/joomla`, `/drupal`, `/magento`
|
|
540
|
+
8. **Common Exploits** (Critical: 95 points) - `/shell.php`, `/c99.php`, `/webshell`
|
|
541
|
+
9. **Rails Format Paths** (Medium: 60 points) - Selected resource/extension signatures such as `/users/1.exe`, plus exact executable `format` query values
|
|
542
|
+
10. **Record Scanning Paths** (Low: 30 points) - Selected large-ID and scanner-name path signatures
|
|
543
|
+
11. **Rails Exceptions** - `UnknownFormat` and `InvalidType` (60), `RecordNotFound` (30), and `IpSpoofAttackError` (95). Ordinary Rails exceptions require independent path/format evidence by default; IP-spoof exceptions require safe resolved attribution in middleware. Exceptions alone can be scored by explicitly opting into `exception_detection: :all`.
|
|
544
|
+
|
|
545
|
+
**How Score-Based Blocking Works:**
|
|
546
|
+
|
|
547
|
+
Instead of counting violations (1, 2, 3...), Beskar tracks a **cumulative risk score** that decays over time:
|
|
548
|
+
|
|
549
|
+
- Each middleware pass records at most one violation, using the highest matched path severity (Critical=95, High=80, Medium=60, Low=30); a downstream exception does not add a second charge
|
|
550
|
+
- Violations **decay exponentially** based on severity (critical threats persist longer)
|
|
551
|
+
- IP is blocked when cumulative score reaches threshold (default: 150 points)
|
|
552
|
+
- Lower-severity violations decay faster; ordinary 404s do not add points by default, but signature-matching legitimate paths still can
|
|
553
|
+
|
|
554
|
+
**Example Scenarios:**
|
|
555
|
+
```ruby
|
|
556
|
+
# Scenario 1: Ordinary missing records, no matching scanner signature
|
|
557
|
+
# exception_detection: :suspicious (default)
|
|
558
|
+
10 × RecordNotFound = no WAF points
|
|
559
|
+
# Opting into :all changes this: ten rapid failures can cross the ban threshold.
|
|
560
|
+
|
|
561
|
+
# Scenario 2: Attacker scanning config files
|
|
562
|
+
2 × /.env access close together (95 points each) ≈ 190 points
|
|
563
|
+
→ Exceeds threshold (150) → Ban when enforcement and auto-block are enabled
|
|
564
|
+
→ One request alone is below the default threshold
|
|
565
|
+
→ Critical severity has a 6-hour half-life within the configured retention window
|
|
566
|
+
|
|
567
|
+
# Scenario 3: Mixed attack pattern
|
|
568
|
+
1 × WordPress scan (80) + 1 × Config access (95) = 175
|
|
569
|
+
→ Exceeds threshold → Ban triggered
|
|
570
|
+
→ Different decay rates for each violation type
|
|
571
|
+
```
|
|
407
572
|
|
|
408
|
-
**Configuration
|
|
573
|
+
**Configuration Profiles:**
|
|
409
574
|
|
|
410
575
|
```ruby
|
|
411
|
-
#
|
|
576
|
+
# 🔥 STRICT - High-security environment (financial, healthcare)
|
|
412
577
|
Beskar.configure do |config|
|
|
413
|
-
config.waf =
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
578
|
+
config.waf[:enabled] = true
|
|
579
|
+
config.waf[:auto_block] = true
|
|
580
|
+
config.waf[:score_threshold] = 100 # Lower threshold = faster blocking
|
|
581
|
+
config.waf[:violation_window] = 12.hours # Longer memory
|
|
582
|
+
config.waf[:permanent_block_after] = 300 # Permanent ban at 300 cumulative score
|
|
583
|
+
config.waf[:block_durations] = [6.hours, 24.hours, 7.days, 30.days]
|
|
584
|
+
|
|
585
|
+
# Slower decay = violations persist longer
|
|
586
|
+
config.waf[:decay_rates] = {
|
|
587
|
+
critical: 720, # 12 hour half-life (very persistent)
|
|
588
|
+
high: 360, # 6 hour half-life
|
|
589
|
+
medium: 120, # 2 hour half-life
|
|
590
|
+
low: 30 # 30 minute half-life
|
|
421
591
|
}
|
|
592
|
+
|
|
593
|
+
# Exclude legitimate 404-prone paths
|
|
594
|
+
config.waf[:record_not_found_exclusions] = [
|
|
595
|
+
%r{/posts/.*}, %r{/articles/\d+}, %r{/public/.*}
|
|
596
|
+
]
|
|
422
597
|
end
|
|
423
598
|
|
|
424
|
-
#
|
|
599
|
+
# ⚖️ BALANCED - Default production (recommended for most apps)
|
|
425
600
|
Beskar.configure do |config|
|
|
426
|
-
config.waf =
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
601
|
+
config.waf[:enabled] = true
|
|
602
|
+
config.waf[:auto_block] = true
|
|
603
|
+
config.waf[:score_threshold] = 150 # Default threshold
|
|
604
|
+
config.waf[:violation_window] = 6.hours # Standard window
|
|
605
|
+
config.waf[:permanent_block_after] = 500 # Permanent at 500 cumulative
|
|
606
|
+
config.waf[:decay_enabled] = true
|
|
607
|
+
# Uses default decay rates (critical: 360, high: 120, medium: 45, low: 15)
|
|
608
|
+
|
|
609
|
+
config.waf[:record_not_found_exclusions] = [
|
|
610
|
+
%r{/posts/.*}, %r{/products/[\\w-]+}
|
|
611
|
+
]
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# 🧪 PERMISSIVE - High-traffic public site with many 404s
|
|
615
|
+
Beskar.configure do |config|
|
|
616
|
+
config.waf[:enabled] = true
|
|
617
|
+
config.waf[:auto_block] = true
|
|
618
|
+
config.waf[:score_threshold] = 200 # Higher tolerance
|
|
619
|
+
config.waf[:violation_window] = 3.hours # Shorter memory
|
|
620
|
+
config.waf[:permanent_block_after] = 800 # Rare permanent bans
|
|
621
|
+
|
|
622
|
+
# Faster decay = violations forgotten quickly
|
|
623
|
+
config.waf[:decay_rates] = {
|
|
624
|
+
critical: 180, # 3 hour half-life
|
|
625
|
+
high: 60, # 1 hour half-life
|
|
626
|
+
medium: 20, # 20 minute half-life
|
|
627
|
+
low: 5 # 5 minute half-life (very forgiving)
|
|
430
628
|
}
|
|
431
|
-
|
|
432
|
-
|
|
629
|
+
|
|
630
|
+
# Extensive exclusions for public content
|
|
631
|
+
config.waf[:record_not_found_exclusions] = [
|
|
632
|
+
%r{/posts/.*}, %r{/articles/.*}, %r{/tags/.*},
|
|
633
|
+
%r{/search/.*}, %r{/public/.*}, %r{/assets/.*}
|
|
634
|
+
]
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
# 🔍 MONITOR ONLY - Testing/staging (recommended before going live)
|
|
638
|
+
Beskar.configure do |config|
|
|
639
|
+
config.monitor_only = true # Log violations but NEVER block
|
|
640
|
+
config.waf[:enabled] = true
|
|
641
|
+
config.waf[:create_security_events] = true
|
|
642
|
+
config.ip_whitelist = ["127.0.0.1", "::1"] # Whitelist localhost
|
|
433
643
|
end
|
|
434
644
|
```
|
|
435
645
|
|
|
436
646
|
**Blocking Behavior:**
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
- **
|
|
440
|
-
- **
|
|
441
|
-
- **
|
|
647
|
+
|
|
648
|
+
With **default settings** (score_threshold: 150):
|
|
649
|
+
- **Violations accumulate**: Each violation adds points based on severity
|
|
650
|
+
- **Score threshold reached**: IP automatically banned when cumulative score ≥ 150
|
|
651
|
+
- **Exponential decay**: Violations lose impact over time based on severity
|
|
652
|
+
- **Ban duration escalates**: Based on total score accumulated:
|
|
653
|
+
- 150-300 points → 1 hour ban
|
|
654
|
+
- 300-450 points → 6 hour ban
|
|
655
|
+
- 450-600 points → 24 hour ban
|
|
656
|
+
- 600+ points → 7 day ban
|
|
657
|
+
- 500+ cumulative score → **permanent ban**
|
|
658
|
+
|
|
659
|
+
**Key Advantages:**
|
|
660
|
+
- **Fewer false positives**: Low-severity violations (404s) decay quickly
|
|
661
|
+
- **Faster response to serious threats**: Critical violations persist longer
|
|
662
|
+
- **Adaptive blocking**: Mixed attack patterns properly weighted
|
|
663
|
+
- **Monitor mode compatible**: Set `config.monitor_only = true` to log without blocking
|
|
664
|
+
|
|
665
|
+
> **Production Tip:** Start with monitor mode for 24-48 hours to observe your traffic patterns, then adjust thresholds and exclusions before enabling blocking.
|
|
442
666
|
|
|
443
667
|
**Check WAF status:**
|
|
444
668
|
```ruby
|
|
445
|
-
#
|
|
669
|
+
# Get current risk score (with decay applied)
|
|
670
|
+
current_score = Beskar::Services::Waf.get_current_score(ip_address)
|
|
671
|
+
# => 145.3 (below threshold, not blocked)
|
|
672
|
+
|
|
673
|
+
# Get number of violations tracked
|
|
446
674
|
violation_count = Beskar::Services::Waf.get_violation_count(ip_address)
|
|
675
|
+
# => 3 (number of violations being tracked)
|
|
676
|
+
|
|
677
|
+
# Get detailed violation records
|
|
678
|
+
violations = Beskar::Services::Waf.get_violations(ip_address)
|
|
679
|
+
# => [{timestamp: ..., score: 95, severity: :critical, category: :config_files}, ...]
|
|
447
680
|
|
|
448
681
|
# Reset violations (admin action)
|
|
449
682
|
Beskar::Services::Waf.reset_violations(ip_address)
|
|
@@ -459,14 +692,19 @@ end
|
|
|
459
692
|
|
|
460
693
|
### IP Blocking and Banning
|
|
461
694
|
|
|
462
|
-
Beskar uses
|
|
695
|
+
Beskar uses indexed database ban checks. All workers must share the same authoritative database. Cache eviction, cache outages, and stale cache values cannot change ban enforcement.
|
|
463
696
|
|
|
464
|
-
**Automatic IP Banning:**
|
|
697
|
+
**Automatic IP Banning Thresholds:**
|
|
465
698
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
699
|
+
| Trigger | Threshold | Time Window | Ban Duration | Configurable |
|
|
700
|
+
|---------|-----------|-------------|--------------|--------------|
|
|
701
|
+
| **Authentication attempt limit** | Configured IP limit (default 10) | Configured period (default 1 hour) | HTTP 429 until the actual retry deadline | Via rate_limiting config |
|
|
702
|
+
| **Rate Limit Violations** | 5 denied requests | Fixed 1 hour window | Adds 1 hour to a temporary ban | Fixed middleware policy |
|
|
703
|
+
| **WAF Violations** | Cumulative score (default 150) | Configured window with optional decay | Score-based durations or permanent | Via waf configuration |
|
|
704
|
+
|
|
705
|
+
Explicit extensions without a duration escalate to 6h, 24h, 7d, then permanent.
|
|
706
|
+
Extensions with a duration add that duration; already permanent bans remain permanent.
|
|
707
|
+
Monitor mode does not create automatic bans.
|
|
470
708
|
|
|
471
709
|
**Manual IP Management:**
|
|
472
710
|
|
|
@@ -520,16 +758,11 @@ Beskar::BannedIp.where(reason: 'rate_limit_abuse')
|
|
|
520
758
|
removed_count = Beskar::BannedIp.cleanup_expired!
|
|
521
759
|
```
|
|
522
760
|
|
|
523
|
-
**
|
|
761
|
+
**State cleanup:**
|
|
524
762
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
# In config/initializers/beskar.rb (optional - happens automatically)
|
|
529
|
-
Rails.application.config.after_initialize do
|
|
530
|
-
Beskar::BannedIp.preload_cache!
|
|
531
|
-
end
|
|
532
|
-
```
|
|
763
|
+
Schedule `bin/rails beskar:cleanup_security_state` to reclaim expired coordination
|
|
764
|
+
rows. Audit-event retention is separate. Ban cache preloading is no longer required;
|
|
765
|
+
`Beskar::BannedIp.preload_cache!` remains a compatibility no-op.
|
|
533
766
|
|
|
534
767
|
### Security Events and Monitoring
|
|
535
768
|
|
|
@@ -574,10 +807,10 @@ class SecurityCleanupJob < ApplicationJob
|
|
|
574
807
|
# Remove expired bans from database
|
|
575
808
|
removed = Beskar::BannedIp.cleanup_expired!
|
|
576
809
|
Rails.logger.info "Cleaned up #{removed} expired IP bans"
|
|
577
|
-
|
|
810
|
+
|
|
578
811
|
# Archive old security events (optional)
|
|
579
812
|
Beskar::SecurityEvent.where('created_at < ?', 90.days.ago).delete_all
|
|
580
|
-
|
|
813
|
+
|
|
581
814
|
# Generate security report (example)
|
|
582
815
|
report = {
|
|
583
816
|
active_bans: Beskar::BannedIp.active.count,
|
|
@@ -587,7 +820,7 @@ class SecurityCleanupJob < ApplicationJob
|
|
|
587
820
|
created_at: 24.hours.ago..Time.current
|
|
588
821
|
).count
|
|
589
822
|
}
|
|
590
|
-
|
|
823
|
+
|
|
591
824
|
# Send to monitoring service
|
|
592
825
|
Rails.logger.info "Security Report: #{report}"
|
|
593
826
|
end
|
|
@@ -611,10 +844,7 @@ Every request passes through these security checks in order:
|
|
|
611
844
|
**Features:**
|
|
612
845
|
- **Early exit** - Banned IPs are blocked immediately for performance
|
|
613
846
|
- **Whitelist bypass** - Trusted IPs bypass all blocking but activity is logged
|
|
614
|
-
- **Auto-blocking** - Automatic IP banning
|
|
615
|
-
- 10+ failed authentication attempts (authentication abuse)
|
|
616
|
-
- 5+ rate limit violations in 1 hour (rate limit abuse)
|
|
617
|
-
- 3+ WAF violations (configurable, vulnerability scanning)
|
|
847
|
+
- **Auto-blocking** - See [Automatic IP Banning Thresholds](#ip-blocking-and-banning) section for details
|
|
618
848
|
- **Custom error pages** - Returns helpful 403/429 error responses
|
|
619
849
|
- **Response headers** - Adds `X-Beskar-Blocked` and `X-Beskar-Rate-Limited` headers
|
|
620
850
|
- **Graceful degradation** - Continues working if cache is unavailable
|
|
@@ -637,19 +867,27 @@ Security events are logged to the `beskar_security_events` table for analysis an
|
|
|
637
867
|
|
|
638
868
|
| Category | Severity | Example Patterns | Risk Score |
|
|
639
869
|
|----------|----------|------------------|------------|
|
|
640
|
-
| WordPress Scans | High | `/wp-admin`, `/wp-login.php`, `/
|
|
870
|
+
| WordPress Scans | High | `/wp-admin`, `/wp-login.php`, `/wp-content/*.php` | 80 |
|
|
871
|
+
| WordPress Static Files | Low | `/wp-content/*.css`, `/wp-content/*.jpg` | 30 |
|
|
641
872
|
| PHP Admin Panels | High | `/phpmyadmin`, `/admin.php`, `/phpinfo.php` | 80 |
|
|
642
873
|
| Config Files | **Critical** | `/.env`, `/.git`, `/database.yml`, `/config.php` | **95** |
|
|
643
874
|
| Path Traversal | **Critical** | `/../../../etc/passwd`, `%2e%2e/` | **95** |
|
|
644
875
|
| Framework Debug | Medium | `/rails/info/routes`, `/__debug__`, `/telescope` | 60 |
|
|
645
876
|
| CMS Detection | Medium | `/joomla`, `/drupal`, `/magento` | 60 |
|
|
646
877
|
| Common Exploits | **Critical** | `/shell.php`, `/c99.php`, `/webshell` | **95** |
|
|
878
|
+
| Rails Format Paths | Medium | `/users/1.exe`, `/reports?format=exe` | 60 |
|
|
879
|
+
| Record Scanning Paths | Low | `/account/999999` | 30 |
|
|
880
|
+
| IP Spoofing Exception | **Critical** | Conflicting IP headers | **95** |
|
|
881
|
+
| UnknownFormat / InvalidType Exceptions | Medium | Requires path/format evidence by default | 60 |
|
|
882
|
+
| RecordNotFound Exception | Low | Requires path/format evidence by default | 30 |
|
|
883
|
+
|
|
884
|
+
**Pattern matching:**
|
|
647
885
|
|
|
648
|
-
|
|
649
|
-
-
|
|
650
|
-
-
|
|
651
|
-
-
|
|
652
|
-
-
|
|
886
|
+
- Uses an at-most-8-KiB path with up to two percent-decoding passes; backslashes become slashes and dot segments are retained.
|
|
887
|
+
- Uses case-insensitive matching for most signatures, with explicit root/segment boundaries.
|
|
888
|
+
- Ignores arbitrary query text and request bodies; only the exact `format` query key is examined.
|
|
889
|
+
- Can match several rules but charges once per middleware pass; ordinary `.well-known` endpoints are not signatures.
|
|
890
|
+
- Supports explicit method/path/category exclusions. `exception_detection` defaults to `:suspicious`; `:all` opts into broad exception scoring and `:none` disables exception scoring.
|
|
653
891
|
|
|
654
892
|
## Security Best Practices
|
|
655
893
|
|
|
@@ -658,21 +896,17 @@ Security events are logged to the `beskar_security_events` table for analysis an
|
|
|
658
896
|
When first enabling WAF, use monitor-only mode to tune thresholds:
|
|
659
897
|
|
|
660
898
|
```ruby
|
|
661
|
-
config.
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
create_security_events: true
|
|
665
|
-
}
|
|
899
|
+
config.monitor_only = true # Log but don't block
|
|
900
|
+
config.waf[:enabled] = true
|
|
901
|
+
config.waf[:create_security_events] = true
|
|
666
902
|
```
|
|
667
903
|
|
|
668
904
|
After reviewing logs for false positives, enable blocking:
|
|
669
905
|
|
|
670
906
|
```ruby
|
|
671
|
-
config.
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
auto_block: true
|
|
675
|
-
}
|
|
907
|
+
config.monitor_only = false
|
|
908
|
+
config.waf[:enabled] = true
|
|
909
|
+
config.waf[:auto_block] = true
|
|
676
910
|
```
|
|
677
911
|
|
|
678
912
|
### 2. Whitelist Carefully
|
|
@@ -757,10 +991,10 @@ end
|
|
|
757
991
|
|
|
758
992
|
### Issue: Legitimate users being blocked
|
|
759
993
|
|
|
760
|
-
**Solution:**
|
|
994
|
+
**Solution:** Review matching rules in monitor mode, add narrow exclusions, or raise the cumulative score threshold:
|
|
761
995
|
|
|
762
996
|
```ruby
|
|
763
|
-
config.waf[:
|
|
997
|
+
config.waf[:score_threshold] = 250 # Increase from default 150; not a violation count
|
|
764
998
|
```
|
|
765
999
|
|
|
766
1000
|
Or whitelist specific IPs:
|
|
@@ -773,7 +1007,7 @@ config.ip_whitelist = ["user.ip.address.here"]
|
|
|
773
1007
|
**Solution:** Enable monitor-only mode and review patterns:
|
|
774
1008
|
|
|
775
1009
|
```ruby
|
|
776
|
-
config.
|
|
1010
|
+
config.monitor_only = true # This is a global setting, not WAF-specific
|
|
777
1011
|
|
|
778
1012
|
# Review what's being flagged
|
|
779
1013
|
Beskar::SecurityEvent.where(event_type: 'waf_violation').last(20).each do |event|
|
|
@@ -795,50 +1029,25 @@ Beskar::BannedIp.cleanup_expired!
|
|
|
795
1029
|
|
|
796
1030
|
### Issue: Performance concerns
|
|
797
1031
|
|
|
798
|
-
**Solution:**
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
# config/environments/production.rb
|
|
802
|
-
config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] }
|
|
803
|
-
```
|
|
804
|
-
|
|
805
|
-
Check cache health:
|
|
806
|
-
```ruby
|
|
807
|
-
Rails.cache.read("test_key") # Should work
|
|
808
|
-
Beskar::BannedIp.preload_cache! # Reload from database if needed
|
|
809
|
-
```
|
|
1032
|
+
**Solution:** Measure database query latency, connection-pool contention, and global
|
|
1033
|
+
counter throughput. Redis is not required and cannot replace the authoritative
|
|
1034
|
+
database. See [state-storage trade-offs](docs/operations/state-storage.md).
|
|
810
1035
|
|
|
811
1036
|
## Migration from Previous Versions
|
|
812
1037
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
# Preload cache with existing bans (if any)
|
|
820
|
-
rails runner "Beskar::BannedIp.preload_cache!"
|
|
821
|
-
|
|
822
|
-
# Test in development first
|
|
823
|
-
RAILS_ENV=development rails server
|
|
824
|
-
|
|
825
|
-
# Review logs for any issues
|
|
826
|
-
tail -f log/development.log | grep Beskar
|
|
827
|
-
```
|
|
1038
|
+
Copy engine migrations with `bin/rails beskar:install:migrations`, then run
|
|
1039
|
+
`bin/rails db:migrate` before starting new workers. Drain old cache-based workers:
|
|
1040
|
+
mixed versions do not share counters. Existing cache counters are not imported.
|
|
1041
|
+
Review legacy bans and schedule state cleanup as described in
|
|
1042
|
+
[the upgrade notes](docs/operations/state-storage.md#upgrade).
|
|
828
1043
|
|
|
829
1044
|
## Performance Characteristics
|
|
830
1045
|
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
**Scalability:**
|
|
838
|
-
- Handles 1000s of requests/second
|
|
839
|
-
- Cache-first architecture minimizes database queries
|
|
840
|
-
- Efficient pattern matching with compiled regexes
|
|
841
|
-
- Parallel test execution: 352 tests run in < 3 seconds
|
|
1046
|
+
Ordinary requests read indexed ban state. Request-wide IP quota checks are opt-in. Authentication
|
|
1047
|
+
attempts update applicable counters transactionally; WAF violations update per-IP
|
|
1048
|
+
history. The global counter is disabled by default; enabling it serializes authentication accounting. No fixed
|
|
1049
|
+
requests-per-second or latency guarantee is asserted; benchmark the deployed
|
|
1050
|
+
database and workload.
|
|
842
1051
|
|
|
843
1052
|
## Development
|
|
844
1053
|
|
|
@@ -853,7 +1062,7 @@ $ bin/rails test
|
|
|
853
1062
|
|
|
854
1063
|
## Contributing
|
|
855
1064
|
|
|
856
|
-
Bug reports and pull requests are welcome on GitHub at [https://github.com/prograis/beskar](https://github.com/prograils/beskar).
|
|
1065
|
+
Bug reports and pull requests are welcome on GitHub at [https://github.com/prograis/beskar](https://github.com/prograils/beskar).
|
|
857
1066
|
|
|
858
1067
|
## License
|
|
859
1068
|
|
|
@@ -862,4 +1071,3 @@ The gem is available as open source under the terms of the [MIT License](https:/
|
|
|
862
1071
|
## Code of Conduct
|
|
863
1072
|
|
|
864
1073
|
Just be nice to each other.
|
|
865
|
-
|