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
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# Notifications and recovery delivery
|
|
2
|
+
|
|
3
|
+
Beskar supplies opt-in plain-text email for confirmed account locks (Devise and
|
|
4
|
+
Rails-native), Rails-native emergency password resets, and security-team reset
|
|
5
|
+
alerts. Delivery uses the host's Active Job and Action Mailer configuration.
|
|
6
|
+
These notices describe precautionary actions, not proof of account compromise.
|
|
7
|
+
|
|
8
|
+
## Enable explicitly
|
|
9
|
+
|
|
10
|
+
All three notification flags now default to **false**. Their former true defaults
|
|
11
|
+
only logged intent; enabling them now requires real delivery configuration:
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
# config/initializers/beskar.rb
|
|
15
|
+
Beskar.configure do |config|
|
|
16
|
+
config.notifications = {
|
|
17
|
+
from: "security@your-domain.example",
|
|
18
|
+
recovery_url: "https://your-domain.example/account-recovery",
|
|
19
|
+
security_team_recipients: ["security-team@your-domain.example"]
|
|
20
|
+
}
|
|
21
|
+
config.risk_based_locking[:notify_user] = true
|
|
22
|
+
config.emergency_password_reset[:send_notification] = true
|
|
23
|
+
config.emergency_password_reset[:notify_security_team] = true
|
|
24
|
+
end
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Replace the example mailboxes and URL with your own. Notification flags do not
|
|
28
|
+
enable risk locking or emergency resets; configure those policies separately.
|
|
29
|
+
Enabling notifications is independent of optional audit logging and background
|
|
30
|
+
analysis. No sender fallback or hardcoded production recipient is used.
|
|
31
|
+
|
|
32
|
+
The sender and recipients must be plain, single mailbox addresses, without display
|
|
33
|
+
names, comma-separated lists, or header-control characters. Up to 20 security-team
|
|
34
|
+
recipients are supported, with one separate message/job per list entry. An enabled
|
|
35
|
+
team flag requires a nonempty list. User messages require a configured recovery
|
|
36
|
+
URL. Invalid settings stop startup/configure publication; delivery workers also
|
|
37
|
+
validate the notification settings they use.
|
|
38
|
+
|
|
39
|
+
The recovery URL must be an absolute HTTPS **entry page**, without userinfo,
|
|
40
|
+
query parameters, or a fragment. It must not contain a password-reset/sign-in token
|
|
41
|
+
in its path either; syntax validation cannot determine whether a path contains a
|
|
42
|
+
secret. Beskar never constructs this URL from a request's Host header. For multiple
|
|
43
|
+
authentication models, provide a host page that routes users to the appropriate
|
|
44
|
+
recovery/support flow. URL ownership, availability, and recovery correctness are
|
|
45
|
+
host responsibilities, not validated by a network request at boot.
|
|
46
|
+
|
|
47
|
+
Configure `config.active_job.queue_adapter` for your host and run workers consuming
|
|
48
|
+
`beskar_notifications` (including any host queue prefix). Beskar jobs inherit the
|
|
49
|
+
engine's `ApplicationJob < ActiveJob::Base`, not a host-defined `ApplicationJob`;
|
|
50
|
+
host-only subclass callbacks/settings do not automatically apply. The normal Rails
|
|
51
|
+
application-wide queue settings do apply. Configure Action Mailer transport/sender
|
|
52
|
+
authorization, with `perform_deliveries` and `raise_delivery_errors` enabled.
|
|
53
|
+
Disabled/suppressed delivery is treated as a failure, not a successful notice.
|
|
54
|
+
|
|
55
|
+
## Transaction and failure contract
|
|
56
|
+
|
|
57
|
+
Notification scheduling follows a confirmed lock/reset and waits for every
|
|
58
|
+
enclosing Active Record transaction to commit. Outer rollbacks and rolled-back
|
|
59
|
+
savepoints cancel callbacks. With no open transaction, enqueueing happens
|
|
60
|
+
immediately after the operation. Enqueue attempts never occur inside retryable
|
|
61
|
+
security-state mutation blocks.
|
|
62
|
+
|
|
63
|
+
Monitor mode and IP whitelist policy suppress the automatic security action and
|
|
64
|
+
its notification. A failed lock or rolled-back reset produces no notice. A failed
|
|
65
|
+
optional account-lock audit does not prevent notification of the actual lock.
|
|
66
|
+
Password invalidation, session revocation, and the mandatory emergency-reset audit
|
|
67
|
+
remain transactional; notification delivery is outside that transaction.
|
|
68
|
+
|
|
69
|
+
The user-reset and security-team hooks are independent: one failing hook does not
|
|
70
|
+
suppress the other. Initial enqueue exceptions/aborted enqueues are logged without
|
|
71
|
+
changing admission or rolling back committed work. There is no durable pending
|
|
72
|
+
row to retry an initial enqueue failure automatically.
|
|
73
|
+
|
|
74
|
+
`Beskar::NotificationJob` retries delivery failures up to five total attempts with
|
|
75
|
+
Active Job's polynomial backoff. Exhausted failures remain raised for the backend
|
|
76
|
+
to retain/report. A failed retry enqueue also raises visibly. Configure backend
|
|
77
|
+
failure monitoring and reconciliation; backend-level retries may add attempts.
|
|
78
|
+
|
|
79
|
+
This is **not an outbox or exactly-once delivery system**. A process crash between
|
|
80
|
+
commit and enqueue can lose a notice. SMTP acceptance followed by a timeout,
|
|
81
|
+
backend retries, or host delivery-observer failures can duplicate email. Separate
|
|
82
|
+
recipient jobs avoid retrying all team members for one member's delivery failure.
|
|
83
|
+
Successful execution means the configured transport returned successfully, not
|
|
84
|
+
that the recipient received or read the message. No delivery-status audit/table,
|
|
85
|
+
bounce handling, or verified recipient confirmation is supplied.
|
|
86
|
+
|
|
87
|
+
## Data and logging boundaries
|
|
88
|
+
|
|
89
|
+
Jobs contain only `user_type`, `user_id`, `kind`, and an optional
|
|
90
|
+
`recipient_index`. No email, password, reset token, model object, raw request
|
|
91
|
+
metadata, or risk explanation is serialized into job arguments. IDs are still
|
|
92
|
+
personal identifiers and require appropriate queue access/retention controls.
|
|
93
|
+
|
|
94
|
+
Workers load the current account and email (`email_address` for native accounts,
|
|
95
|
+
`email` for Devise), and current sender/recovery URL/team list. They recheck the
|
|
96
|
+
notification flag. Deleted accounts and removed recipient indices are skipped.
|
|
97
|
+
A reordered/replaced team list changes the recipient at a queued index; drain
|
|
98
|
+
pending jobs before changing the list if a fixed recipient snapshot is required.
|
|
99
|
+
User address changes similarly affect pending delivery. These jobs are historical
|
|
100
|
+
notices, not checks that the user is still locked when mail is delivered.
|
|
101
|
+
|
|
102
|
+
User bodies contain only fixed explanatory text and the configured recovery page.
|
|
103
|
+
Team bodies contain the model name and account ID, not the user's email or raw
|
|
104
|
+
security evidence. Review the authenticated dashboard for incident details.
|
|
105
|
+
|
|
106
|
+
Job argument logging is disabled for the notification job. Preparation/delivery
|
|
107
|
+
errors are replaced with static class-based errors, without their original causes,
|
|
108
|
+
before the normal job retry/failure reporting. This mailer uses
|
|
109
|
+
`deliver.beskar_notification` instrumentation with the mailer name, timing, and
|
|
110
|
+
sanitized exception information instead of Action Mailer's encoded-message and
|
|
111
|
+
recipient delivery payload. Subscribe to that event if your monitoring normally
|
|
112
|
+
depends on `deliver.action_mailer`. Other host mailers are unchanged.
|
|
113
|
+
|
|
114
|
+
Host queue-adapter logging, custom instrumentation/interceptors/observers, SMTP
|
|
115
|
+
debugging, and external error-reporting systems have their own privacy boundaries.
|
|
116
|
+
Beskar does not globally filter them, protect a transport's internal logs, or
|
|
117
|
+
anonymize mail recipients. Review those settings before production rollout.
|
|
118
|
+
|
|
119
|
+
## Recovery remains a host flow
|
|
120
|
+
|
|
121
|
+
The notice links to your existing recovery page; it does not issue a bearer token,
|
|
122
|
+
expose the random replacement password, authenticate the recipient, unlock an
|
|
123
|
+
account, or establish trusted-device/IP evidence. The host must supply secure
|
|
124
|
+
password-reset issuance, expiry, redemption, anti-enumeration/rate limiting,
|
|
125
|
+
identity verification, and authorized manual unlock where required.
|
|
126
|
+
|
|
127
|
+
For native accounts, emergency invalidation changes the password digest, revokes
|
|
128
|
+
sessions, and invalidates Rails' previous password-reset tokens. A later successful
|
|
129
|
+
password reset does **not** remove a Beskar manual lock. Follow the authorized
|
|
130
|
+
unlock procedure in [Authentication](authentication.md). Devise owns its own
|
|
131
|
+
Recoverable/Lockable recovery semantics; this repair adds Devise lock notices,
|
|
132
|
+
not a second Devise password-reset or unlock implementation.
|
|
133
|
+
|
|
134
|
+
The public native `send_emergency_reset_notification(reason)` and
|
|
135
|
+
`notify_security_team_of_reset(reason, event)` hooks remain overridable. Their
|
|
136
|
+
defaults now enqueue these emails. Overrides still run independently after commit,
|
|
137
|
+
but own their delivery, retry, and privacy behavior; do not call `super` if that
|
|
138
|
+
would duplicate your delivery. Enabling their flags still requires the validated
|
|
139
|
+
notification settings above. The optional analysis job is a separate read-only
|
|
140
|
+
extension point, not a recovery-delivery adapter.
|
|
141
|
+
|
|
142
|
+
## Verification and rollout
|
|
143
|
+
|
|
144
|
+
Local Ruby 4.0.6 tests exercise real outer commits/savepoint rollbacks, both
|
|
145
|
+
authentication paths, user/team hook and queue failures, all five delivery
|
|
146
|
+
attempts, invalid recipients, disabled delivery, and sanitized logging. Mail is
|
|
147
|
+
sent only to Rails' **test** delivery backend. The native integration test follows
|
|
148
|
+
the recovery notice through the dummy host's recovery form, reset-token email and
|
|
149
|
+
redemption, confirms token invalidation, and requires manual unlock before login.
|
|
150
|
+
|
|
151
|
+
Real SMTP delivery, production worker durability, email-client rendering, host
|
|
152
|
+
support processes, and broader host recovery integrations have not been verified.
|
|
153
|
+
The dummy Devise reset-email/token handoff now verifies its email-unlock policy,
|
|
154
|
+
one-time token use, and that old sessions remain revoked. Configure and
|
|
155
|
+
exercise those before enabling emergency resets in production. This batch adds
|
|
156
|
+
no database migration and does not change the any-`Rails.cache` coordination
|
|
157
|
+
contract. Remaining work is tracked in [Repair status](../audits/repair-status.md).
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Authentication risk evidence
|
|
2
|
+
|
|
3
|
+
Authentication now uses one `RiskAssessment` snapshot for the decision, login
|
|
4
|
+
audit, and lock context. `metadata["risk_assessment"]` contains a version, assessment
|
|
5
|
+
time, observation/enforcement mode, score, and named factors with points/evidence.
|
|
6
|
+
Factor points, including negative cap adjustments, sum to the recorded score.
|
|
7
|
+
`device_info` and `geolocation` carry the same facts used by that assessment.
|
|
8
|
+
|
|
9
|
+
## Factors and limits
|
|
10
|
+
|
|
11
|
+
| Factor | Points |
|
|
12
|
+
| --- | ---: |
|
|
13
|
+
| Successful / failed credentials | 1 / 10 |
|
|
14
|
+
| Missing User-Agent | 20 |
|
|
15
|
+
| Bot-like User-Agent claim | 30 |
|
|
16
|
+
| User-Agent shorter than 20 or longer than 500 characters | 15 |
|
|
17
|
+
| Test/debug/script marker in User-Agent | 10 |
|
|
18
|
+
| More than three parentheses in User-Agent | 5 |
|
|
19
|
+
| Chrome/Firefox major version below the legacy cutoff of 90 | 5 |
|
|
20
|
+
| Mobile User-Agent, application-local hour 22:00–05:59 | 5 |
|
|
21
|
+
| At least two recent account failures in ten minutes | 20 |
|
|
22
|
+
| Private or unavailable geographic location | 10 |
|
|
23
|
+
| Impossible-travel heuristic | 25 |
|
|
24
|
+
| Changed known country | 10 |
|
|
25
|
+
|
|
26
|
+
User-Agent factors are capped at 50, geographic factors at 30, and total risk at
|
|
27
|
+
100. The version-90 cutoff is a retained heuristic, not a browser support policy.
|
|
28
|
+
Password contents and length are no longer used as risk factors. User-Agent
|
|
29
|
+
assessment/storage is bounded to 500 sanitized characters; the length factor uses
|
|
30
|
+
the original length. User-Agent risk logging no longer includes the raw header.
|
|
31
|
+
Other audit surfaces still need the privacy work tracked under F11.
|
|
32
|
+
|
|
33
|
+
These are heuristic weights, not calibrated probabilities. A travel signal alone
|
|
34
|
+
does not reach the default locking threshold of 75. IP geolocation can describe a
|
|
35
|
+
VPN, proxy, mobile carrier, or shared egress; User-Agent values can be forged. None
|
|
36
|
+
of these factors proves the identity, intent, or physical position of a person.
|
|
37
|
+
|
|
38
|
+
## Timestamped geographic history
|
|
39
|
+
|
|
40
|
+
The geographic assessment considers up to the latest 20 success records in four
|
|
41
|
+
hours, explicitly ordered by `created_at` and ID. A history record must have
|
|
42
|
+
`authentication.allowed == true` and must not report `locked_now == true`.
|
|
43
|
+
Blocked outcomes, legacy successes without explicit admission evidence, future
|
|
44
|
+
events, and malformed records are not travel baselines. Observation-mode records
|
|
45
|
+
are excluded from enforcement assessments. Monitor assessments may inspect
|
|
46
|
+
enforced history as well as observations.
|
|
47
|
+
|
|
48
|
+
Every usable location is paired with its own event timestamp and ID. Travel uses
|
|
49
|
+
the newest comparable coordinate observation, actual elapsed seconds, and a
|
|
50
|
+
1,000 km/h heuristic. Evidence includes the previous event ID/time, elapsed
|
|
51
|
+
seconds, distance, and speed threshold. Country change independently uses the
|
|
52
|
+
newest known-country observation. JSON string/symbol keys and numeric coordinate
|
|
53
|
+
strings are normalized; missing, non-finite, or out-of-range coordinates cannot
|
|
54
|
+
produce travel evidence. Equal, future, or malformed times are ignored.
|
|
55
|
+
|
|
56
|
+
History remains optional audit data, not an independent travel-enforcement store.
|
|
57
|
+
Disabled/missing audits reduce available history. Concurrent authentications do
|
|
58
|
+
not serialize geographic observations, and records outside the bounded window
|
|
59
|
+
are not considered. The recent-failure factor similarly examines the latest 20
|
|
60
|
+
failures within ten minutes, excluding observations in enforcement mode. These
|
|
61
|
+
limits bound database work; they are not exhaustive forensic analysis.
|
|
62
|
+
|
|
63
|
+
The integer `calculate_location_risk` compatibility method accepts timestamped
|
|
64
|
+
observations (`location`, `occurred_at`, optional `event_id`), or one previous
|
|
65
|
+
location with a positive elapsed duration. An untimestamped collection no longer
|
|
66
|
+
shares one duration across all entries. Prefer `assess_location` for evidence.
|
|
67
|
+
|
|
68
|
+
## Providers and configuration changes
|
|
69
|
+
|
|
70
|
+
The default `:mock` provider produces synthetic data for development. Synthetic
|
|
71
|
+
locations never establish country/travel evidence or add geographic risk. Private
|
|
72
|
+
addresses still receive the explicitly labeled unavailable-location factor.
|
|
73
|
+
Configure `:maxmind` with an actual city database for geographic observations.
|
|
74
|
+
Unknown provider names and the unimplemented IP2Location provider are rejected
|
|
75
|
+
by configuration validation and service construction. Valid MaxMind lookups may
|
|
76
|
+
still return unknown data when enrichment is unavailable; this is not fabricated
|
|
77
|
+
geographic evidence. See [Configuration](configuration.md).
|
|
78
|
+
|
|
79
|
+
Cache keys include provider and MaxMind database identity (path, inode, size,
|
|
80
|
+
modification time). New service instances/readers follow configuration or database
|
|
81
|
+
generation changes. An in-flight service instance can finish using its snapshot.
|
|
82
|
+
Old cache entries expire under their TTL; they are not reused across generations.
|
|
83
|
+
Any Rails.cache backend remains supported, including NullStore and unavailable
|
|
84
|
+
optional caches. Replacements that preserve every identity attribute require an
|
|
85
|
+
explicit reader reset and cache invalidation, or a new database path.
|
|
86
|
+
|
|
87
|
+
## Locks, observation, and trust
|
|
88
|
+
|
|
89
|
+
The actual computed travel, country-change, and bot/suspicious flags drive lock
|
|
90
|
+
reasons. Lock audits include the same risk factors and authentication attempt ID.
|
|
91
|
+
When risk locking is enabled, login metadata includes `lock_decision` with the
|
|
92
|
+
threshold, adapter availability, `would_lock`, and whether policy permits
|
|
93
|
+
enforcement. `would_lock` describes eligibility, not successful persistence;
|
|
94
|
+
`authentication.locked_now` records the actual result. Monitor mode/whitelists do
|
|
95
|
+
not mutate accounts. Unknown/custom strategies are not advertised as available.
|
|
96
|
+
|
|
97
|
+
There is **no automatic trust discount**. Repeated IP use, lock attempts, and
|
|
98
|
+
manual/automatic unlocks do not prove a verified device or confirmed recovery.
|
|
99
|
+
The former 30% scoring discount and complete geographic bypass have been removed.
|
|
100
|
+
A future trust mechanism needs explicit host-verified identity/recovery evidence.
|
|
101
|
+
|
|
102
|
+
## Upgrade and validation
|
|
103
|
+
|
|
104
|
+
No new migration is required for this batch. Legacy login records without explicit
|
|
105
|
+
admission evidence are not promoted into geographic history. Scores can rise when
|
|
106
|
+
unsafe trust discounts disappear, and fall when mock geography or erroneous old-
|
|
107
|
+
browser penalties disappear. Review recorded factors in monitor mode before
|
|
108
|
+
enabling risk locking or opt-in emergency resets; do not assume old thresholds
|
|
109
|
+
have been calibrated for the corrected inputs.
|
|
110
|
+
|
|
111
|
+
Local coverage includes real Devise/native logins, persisted JSON history,
|
|
112
|
+
chronological ordering, midnight boundaries, modern browsers, malformed locations,
|
|
113
|
+
provider/cache isolation, monitor/whitelist policy, and matching lock evidence.
|
|
114
|
+
Real MaxMind database accuracy, production false-positive rates, and verified
|
|
115
|
+
recovery/notification delivery remain unverified or unfinished. See
|
|
116
|
+
[Authentication](authentication.md) and [Repair status](../audits/repair-status.md).
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Monitor-only mode
|
|
2
|
+
|
|
3
|
+
Monitor mode lets you observe WAF and rate-limit decisions without rejecting
|
|
4
|
+
requests or automatically creating IP bans:
|
|
5
|
+
|
|
6
|
+
```ruby
|
|
7
|
+
Beskar.configure do |config|
|
|
8
|
+
config.monitor_only = true
|
|
9
|
+
config.waf[:enabled] = true
|
|
10
|
+
config.waf[:auto_block] = true
|
|
11
|
+
end
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The installer defaults to monitor mode in every environment. Turn it off explicitly
|
|
15
|
+
only after reviewing traffic and tuning thresholds.
|
|
16
|
+
|
|
17
|
+
## What is recorded
|
|
18
|
+
|
|
19
|
+
WAF violations remain available as `Beskar::SecurityEvent` records when
|
|
20
|
+
`config.waf[:create_security_events]` is enabled. Their metadata includes:
|
|
21
|
+
|
|
22
|
+
- `monitor_only_mode`: whether the observation was made in monitor mode.
|
|
23
|
+
- `would_be_blocked`: whether the threshold and auto-block policy would deny a
|
|
24
|
+
non-whitelisted IP.
|
|
25
|
+
- `current_score`, `score_threshold`, and matched patterns.
|
|
26
|
+
|
|
27
|
+
Matched patterns contain rule identifiers and static descriptions, not URLs,
|
|
28
|
+
query strings, raw headers, or exception messages. Default exception scoring
|
|
29
|
+
requires independent scanner-path/format evidence (with resolved IP-spoof signals
|
|
30
|
+
handled separately). See [Audit data and WAF](../guides/audit-and-waf.md) before changing this
|
|
31
|
+
policy or interpreting exports.
|
|
32
|
+
|
|
33
|
+
Monitor WAF history, authentication counters, and rate-denial counters are stored
|
|
34
|
+
separately from enforcement state. Switching to enforcement does not promote
|
|
35
|
+
monitor observations into active counters or bans. Switching modes does not delete
|
|
36
|
+
either history; unexpired enforcement state from an earlier enforcement period
|
|
37
|
+
remains effective when enforcement resumes.
|
|
38
|
+
|
|
39
|
+
Existing manually created or previously enforced bans remain in the database, but
|
|
40
|
+
middleware does not enforce them while monitoring. Monitor mode does not prevent an
|
|
41
|
+
administrator from explicitly creating or modifying a ban.
|
|
42
|
+
|
|
43
|
+
## Review and enable enforcement
|
|
44
|
+
|
|
45
|
+
Use the dashboard, or inspect recent observations in the console:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
Beskar::SecurityEvent.where(event_type: "waf_violation")
|
|
49
|
+
.where("created_at >= ?", 24.hours.ago)
|
|
50
|
+
.find_each do |event|
|
|
51
|
+
next unless event.metadata["monitor_only_mode"]
|
|
52
|
+
puts [event.ip_address, event.metadata["would_be_blocked"],
|
|
53
|
+
event.metadata["current_score"]].inspect
|
|
54
|
+
end
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Review false positives, configure trusted proxies and whitelist entries, then set:
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
Beskar.configure do |config|
|
|
61
|
+
config.monitor_only = false
|
|
62
|
+
end
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Use `config.waf[:score_threshold]` to tune cumulative WAF scores.
|
|
66
|
+
`block_threshold` and nested `config.waf[:monitor_only]` are not supported options.
|
|
67
|
+
|
|
68
|
+
## Important limitations during remediation
|
|
69
|
+
|
|
70
|
+
Beskar's automatic account locks, current-attempt sign-outs, and emergency password
|
|
71
|
+
resets now honor monitor mode and the IP whitelist. Rails-native applications must
|
|
72
|
+
use the [admission and session-reader guards](../guides/authentication.md). Host application
|
|
73
|
+
restrictions—such as Devise's own failed-attempt Lockable policy or Rails' own rate
|
|
74
|
+
limiter—remain independent of Beskar monitor mode. Risk-enabled login audits now
|
|
75
|
+
include `lock_decision.would_lock`, adapter availability, enforcement policy, and
|
|
76
|
+
the scored evidence. Observed successes/failures do not supply enforced risk
|
|
77
|
+
history. See [Risk scoring](../guides/risk-scoring.md). Production calibration and real
|
|
78
|
+
notifications remain open; observation does not prove the signals are accurate.
|
|
79
|
+
|
|
80
|
+
Older versions created bans during monitoring. Existing bans cannot reliably be
|
|
81
|
+
classified as monitor-only after the fact. Review and explicitly remove any
|
|
82
|
+
unwanted legacy bans before enabling enforcement; this release does not silently
|
|
83
|
+
delete ban records.
|
|
84
|
+
|
|
85
|
+
See [state storage and upgrade notes](state-storage.md) for database requirements.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Audit findings 2–6: hardening and deployment contract
|
|
2
|
+
|
|
3
|
+
This batch extends the previous repairs. It does not claim that arbitrary host
|
|
4
|
+
authentication code is automatically secured, or that production capacity has
|
|
5
|
+
been validated. Use the coverage checklist below as a deployment gate.
|
|
6
|
+
|
|
7
|
+
## Authentication and revocation coverage
|
|
8
|
+
|
|
9
|
+
| Entry point | Beskar enforcement | Host requirement / boundary |
|
|
10
|
+
| --- | --- | --- |
|
|
11
|
+
| Devise database password, including HTTP Basic | IP/account admission before password verification; risk/lock decision before session establishment | Include SecurityTrackable on every protected model; preserve the standard strategy/serialization pipeline |
|
|
12
|
+
| Standard custom Warden strategies | IP admission before `_run!`; account admission when identity becomes known; failed strategy outcomes audited | Scope must resolve to a protected model. An opaque token's target cannot be counted before its verifier identifies it |
|
|
13
|
+
| OAuth/manual Warden sign-in | `set_user` guarded, even with `run_callbacks: false` | External provider verification happens before Beskar sees the identity. Use the generic gateway if pre-verification admission is needed |
|
|
14
|
+
| Devise cookie and remember-me resumption | Durable generation in both credential salts; locks rotate generations; fetch checks locks | New salt format invalidates old cookies once. Custom serializers/remember verifiers require review |
|
|
15
|
+
| Rails-native session creation and resumption | Transactional session guard; locks delete sessions; resumption validates the persisted session and lock | Adopt both controller and resumption hooks in docs/guides/authentication.md; arbitrary host `Current`/cookie readers are not discoverable automatically |
|
|
16
|
+
| Custom API/token issuance | Framework-neutral admission gateway and issued generation | Host verifies/cryptographically binds identity and generation and honors the result before issuing anything |
|
|
17
|
+
| API token use | Shared base-controller guard checks account and signed generation per request | Include `Controllers::SessionSecurity` after host identity resolution; implement both readers below |
|
|
18
|
+
| Action Cable | Base-channel guard checks subscribe, inbound actions, and normal outbound channel transmissions | Prepend `Channels::SessionSecurity`; connection implements the same readers. Direct connection writes/custom dispatchers bypass this adapter |
|
|
19
|
+
|
|
20
|
+
Risk-based locking remains opt-in and requires a supported lock strategy. A
|
|
21
|
+
confirmed lock always rejects the attempt; legacy `immediate_signout: false` no
|
|
22
|
+
longer provides an exception. Ordinary Devise `locked_at` writes rotate the
|
|
23
|
+
account generation in the user transaction. Explicit `user.revoke_beskar_sessions!`
|
|
24
|
+
rotates it without needing a lock; native accounts also destroy database sessions.
|
|
25
|
+
Unlock never restores old generations. Persistent generation rows must not be
|
|
26
|
+
purged; they deliberately have no TTL. Bulk SQL updates bypass model callbacks.
|
|
27
|
+
|
|
28
|
+
All users, sessions and security state must share the writer connection pool.
|
|
29
|
+
Generation/native-lock reads bypass the query cache, not just Rails.cache. A
|
|
30
|
+
database failure denies access; it does not use an old allow decision. In-flight
|
|
31
|
+
work cannot be retroactively canceled; idle sockets close on their next guarded
|
|
32
|
+
action or transmission, not via a background disconnect broadcast.
|
|
33
|
+
|
|
34
|
+
### Framework-neutral credential issuance
|
|
35
|
+
|
|
36
|
+
```ruby
|
|
37
|
+
attempt = Beskar::Services::Authentication.authenticate(
|
|
38
|
+
request, model: User, scope: :api,
|
|
39
|
+
credentials: {email_address: params[:email_address]} # Identity only; never passwords/tokens
|
|
40
|
+
) do
|
|
41
|
+
User.authenticate_by(params.permit(:email_address, :password))
|
|
42
|
+
end
|
|
43
|
+
# On denial, return attempt.response; on Unavailable, return the standard 503.
|
|
44
|
+
# Only when attempt.allowed? is true may the host issue a credential containing:
|
|
45
|
+
# subject: attempt.user.id, beskar_generation: attempt.session_token
|
|
46
|
+
# Sign/store these values using the host's existing authenticated token mechanism.
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
`scope` is a stable host-selected name, never a client-selected partition. The
|
|
50
|
+
gateway does not create tokens, sessions, endpoints, or verify OAuth assertions.
|
|
51
|
+
Credential verifiers must return the authenticated persisted model or nil.
|
|
52
|
+
If the subject is unknown before verification, use an empty identity hash; IP
|
|
53
|
+
admission still precedes the verifier and account admission follows it.
|
|
54
|
+
|
|
55
|
+
For protected API controllers:
|
|
56
|
+
|
|
57
|
+
```ruby
|
|
58
|
+
class Api::BaseController < ActionController::API
|
|
59
|
+
before_action :verify_host_token! # Existing host verifier; never trust unverified claims
|
|
60
|
+
include Beskar::Controllers::SessionSecurity
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def beskar_authenticated_user = @verified_user
|
|
65
|
+
def beskar_authenticated_generation = @verified_claims["beskar_generation"]
|
|
66
|
+
end
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
For Action Cable, prepend `Beskar::Channels::SessionSecurity` to the shared
|
|
70
|
+
`ApplicationCable::Channel` base. The connection must expose public
|
|
71
|
+
`beskar_authenticated_user` and `beskar_authenticated_generation` methods from a
|
|
72
|
+
verified credential, plus its normal `request`. Missing readers, missing/old
|
|
73
|
+
generations, locks and unavailable state fail closed. **Never fill a missing token
|
|
74
|
+
generation with `user.beskar_session_token` on resumption**: doing so would give
|
|
75
|
+
an old credential a new generation and bypass revocation. Native DB-session
|
|
76
|
+
connections can use the persisted-session guard instead, through a host adapter.
|
|
77
|
+
|
|
78
|
+
Inventory every login, impersonation, recovery auto-login, API base, native
|
|
79
|
+
session reader and Cable base before enabling enforcement. Verify each with a
|
|
80
|
+
lock/revoke/unlock replay test. Beskar cannot guarantee coverage for omitted
|
|
81
|
+
hooks, unprotected model classes, direct `connection.transmit`, or host overrides
|
|
82
|
+
that intentionally bypass the supported pipeline.
|
|
83
|
+
|
|
84
|
+
## Administrative history and permissions
|
|
85
|
+
|
|
86
|
+
Security events survive account deletion unchanged and now reject ordinary
|
|
87
|
+
instance rewrites/deletes. Administrative history is also append-only at the
|
|
88
|
+
model layer; raw SQL/bulk APIs and privileged Ruby code remain outside this
|
|
89
|
+
guarantee. No historical rows were rewritten or backfilled.
|
|
90
|
+
|
|
91
|
+
Dashboard authentication grants no capabilities by itself. Configure
|
|
92
|
+
`authorize_admin(request, permission)` in controller context to return exactly
|
|
93
|
+
true for separately assigned `:read`, `:manage_bans`, `:export`, or `:read_audit`
|
|
94
|
+
grants. Missing callbacks/grants deny access. Do not infer permissions from the
|
|
95
|
+
request; consult the host's trusted role/permission store.
|
|
96
|
+
|
|
97
|
+
Exports require a trusted `audit_actor`, a reason, and a committed journal record
|
|
98
|
+
before sending a body. The record includes resource, format, filtered query,
|
|
99
|
+
result count/ID bounds and truncation. A log of preparation does not prove client
|
|
100
|
+
receipt. Every cursor page needs a reason. See docs/guides/audit-lifecycle.md for ban history
|
|
101
|
+
and atomic all-or-nothing mutations.
|
|
102
|
+
|
|
103
|
+
Configuration is sealed after startup. For exceptional process-local runtime
|
|
104
|
+
changes, configure a separate `authorize_configuration(actor)` callback at boot,
|
|
105
|
+
then call `Beskar.configure(actor:, reason:, request_id:) { |candidate| ... }`.
|
|
106
|
+
It serializes local publication, validates a copy and requires a filtered
|
|
107
|
+
before/after journal before publishing. Open database transactions are rejected.
|
|
108
|
+
The record includes changed top-level settings; callbacks are represented as
|
|
109
|
+
`[CALLBACK]`, never serialized code. Per-entry filtering/bounds still apply.
|
|
110
|
+
|
|
111
|
+
This is not distributed configuration: restart all workers from reviewed
|
|
112
|
+
initializers for normal deployments. A crash between journal commit/publication
|
|
113
|
+
can leave an intent record without publication. Source-controlled boot edits,
|
|
114
|
+
environment changes, and trusted callback code changes require host deployment
|
|
115
|
+
auditing; they cannot safely require a migrated database during initial boot.
|
|
116
|
+
|
|
117
|
+
## Advertised features
|
|
118
|
+
|
|
119
|
+
The nonexistent versioned API routes remain removed. No built-in automatic
|
|
120
|
+
pattern analyzer is advertised: enabling it requires a real host Active Job and
|
|
121
|
+
startup validates that dependency. Notifications have opt-in Action Mailer jobs,
|
|
122
|
+
bounded retries, and explicit sender/recipient/recovery-page configuration.
|
|
123
|
+
Recovery itself uses host/Devise token and unlock workflows, not a Beskar token
|
|
124
|
+
issuer. Production delivery, an outbox, replay/idempotence policy and support
|
|
125
|
+
identity verification remain host/operational work; see docs/guides/notifications-and-recovery.md.
|
|
126
|
+
|
|
127
|
+
## Availability and validation gate
|
|
128
|
+
|
|
129
|
+
The global login budget now defaults off, removing its shared lock hot spot and
|
|
130
|
+
distributed-attacker kill switch. `global_attempts[:enabled] = true` explicitly
|
|
131
|
+
restores both. IP/account admission and backoff remain enabled. Authentication
|
|
132
|
+
quotas no longer deny unrelated traffic or auto-ban shared egress by default;
|
|
133
|
+
`ip_attempts[:block_requests] = true` restores that opt-in policy.
|
|
134
|
+
|
|
135
|
+
Shared-NAT users can still share a login quota, and attackers can target one
|
|
136
|
+
account's quota. Tune thresholds with observed traffic. There is no unconditional
|
|
137
|
+
DoS resistance: password hashing, audit growth, writer availability, pool/lock
|
|
138
|
+
timeouts, WAF storage and ingress bandwidth remain finite resources. Required
|
|
139
|
+
pre-request state failures return a no-store 503/Retry-After response; arbitrary
|
|
140
|
+
host database exceptions retain their host handling.
|
|
141
|
+
|
|
142
|
+
The SQLite suite includes independently leased connections, lost-update/admission
|
|
143
|
+
races, rollback/retry, null/unavailable caches, distributed-budget isolation, NAT
|
|
144
|
+
page access and dependency failures. PostgreSQL 17/MySQL 8.4 CI jobs run the full
|
|
145
|
+
suite using `BESKAR_TEST_DATABASE_URL`; **not yet executed here**. Docker daemon
|
|
146
|
+
access was denied, including outside the sandbox. Installed pg/mysql2 adapters
|
|
147
|
+
alone are not evidence of tested servers. No production throughput claim is made.
|
|
148
|
+
|
|
149
|
+
Before production, run those jobs and host load/soak tests across multiple app
|
|
150
|
+
workers: successful/failed/distributed logins, concentrated account/NAT traffic,
|
|
151
|
+
WAF bursts, exports, concurrent locks/session creation, database loss/recovery,
|
|
152
|
+
pool exhaustion and bounded-retry exhaustion. Measure p50/p95/p99 latency, error
|
|
153
|
+
rate, password CPU, query/lock waits, pool utilization and table growth. Set host
|
|
154
|
+
ingress limits, database statement/lock timeouts and operational alerts from the
|
|
155
|
+
measured capacity. Never use a production database URL with Rails test tasks.
|
|
156
|
+
|
|
157
|
+
## Rollout
|
|
158
|
+
|
|
159
|
+
1. Install/apply all engine migrations, including `ExpandAdministrativeActionTargets`.
|
|
160
|
+
2. Configure separate dashboard permissions and actor resolution; update exports
|
|
161
|
+
to send a reason. Runtime settings now require the audited path.
|
|
162
|
+
3. Deploy the host authentication adapters and signed generation claims; reject
|
|
163
|
+
or reissue legacy tokens without them. Plan the one-time Devise sign-out.
|
|
164
|
+
4. Drain old workers and restart from the same reviewed configuration. Mixed
|
|
165
|
+
versions can otherwise issue stale credentials or mutate/delete old audit rows.
|
|
166
|
+
5. Keep the adapter/load gate open until actually run. Only the local test
|
|
167
|
+
database was migrated; no production database, remote CI or mail service was changed.
|