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.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +274 -0
  3. data/README.md +412 -204
  4. data/app/channels/concerns/beskar/channels/session_security.rb +46 -0
  5. data/app/controllers/beskar/administrative_actions_controller.rb +16 -0
  6. data/app/controllers/beskar/application_controller.rb +214 -0
  7. data/app/controllers/beskar/banned_ips_controller.rb +255 -0
  8. data/app/controllers/beskar/dashboard_controller.rb +62 -0
  9. data/app/controllers/beskar/security_events_controller.rb +164 -0
  10. data/app/controllers/concerns/beskar/controllers/audit_export.rb +54 -0
  11. data/app/controllers/concerns/beskar/controllers/security_tracking.rb +76 -48
  12. data/app/controllers/concerns/beskar/controllers/session_security.rb +29 -0
  13. data/app/jobs/beskar/notification_job.rb +33 -0
  14. data/app/mailers/beskar/security_mailer.rb +59 -0
  15. data/app/models/beskar/administrative_action.rb +41 -0
  16. data/app/models/beskar/banned_ip.rb +105 -105
  17. data/app/models/beskar/security_event.rb +51 -4
  18. data/app/models/beskar/security_state.rb +58 -0
  19. data/app/services/beskar/banned_ip_manager.rb +88 -0
  20. data/app/views/beskar/administrative_actions/index.html.erb +33 -0
  21. data/app/views/beskar/administrative_actions/show.html.erb +21 -0
  22. data/app/views/beskar/banned_ips/edit.html.erb +195 -0
  23. data/app/views/beskar/banned_ips/index.html.erb +319 -0
  24. data/app/views/beskar/banned_ips/new.html.erb +190 -0
  25. data/app/views/beskar/banned_ips/review.html.erb +24 -0
  26. data/app/views/beskar/banned_ips/show.html.erb +304 -0
  27. data/app/views/beskar/dashboard/index.html.erb +280 -0
  28. data/app/views/beskar/security_events/index.html.erb +302 -0
  29. data/app/views/beskar/security_events/show.html.erb +293 -0
  30. data/app/views/beskar/shared/_export_form.html.erb +10 -0
  31. data/app/views/layouts/beskar/_behavior.html.erb +121 -0
  32. data/app/views/layouts/beskar/application.html.erb +581 -6
  33. data/config/routes.rb +30 -0
  34. data/db/migrate/20251016000001_create_beskar_security_events.rb +3 -3
  35. data/db/migrate/20260910000001_create_beskar_security_states.rb +14 -0
  36. data/db/migrate/20260911000001_create_beskar_administrative_actions.rb +22 -0
  37. data/db/migrate/20260911000002_expand_administrative_action_targets.rb +6 -0
  38. data/docs/README.md +73 -0
  39. data/docs/archive/project-documentation.md +659 -0
  40. data/docs/audits/project-review.md +437 -0
  41. data/docs/audits/repair-status.md +216 -0
  42. data/docs/guides/audit-and-waf.md +175 -0
  43. data/docs/guides/audit-lifecycle.md +172 -0
  44. data/docs/guides/authentication.md +213 -0
  45. data/docs/guides/configuration.md +182 -0
  46. data/docs/guides/dashboard-and-search.md +251 -0
  47. data/docs/guides/notifications-and-recovery.md +157 -0
  48. data/docs/guides/risk-scoring.md +116 -0
  49. data/docs/operations/monitor-only-mode.md +85 -0
  50. data/docs/operations/security-hardening.md +167 -0
  51. data/docs/operations/state-storage.md +144 -0
  52. data/docs/research/rust-performance-assessment.md +69 -0
  53. data/lib/beskar/configuration.rb +105 -20
  54. data/lib/beskar/configuration_validator.rb +188 -0
  55. data/lib/beskar/devise_authentication.rb +24 -0
  56. data/lib/beskar/engine.rb +21 -88
  57. data/lib/beskar/logger.rb +288 -0
  58. data/lib/beskar/middleware/request_analyzer.rb +133 -99
  59. data/lib/beskar/models/security_trackable_authenticable.rb +76 -97
  60. data/lib/beskar/models/security_trackable_devise.rb +34 -25
  61. data/lib/beskar/models/security_trackable_generic.rb +171 -214
  62. data/lib/beskar/risk_level.rb +22 -0
  63. data/lib/beskar/services/account_locker.rb +90 -81
  64. data/lib/beskar/services/administrative_audit.rb +36 -0
  65. data/lib/beskar/services/administrative_bans.rb +104 -0
  66. data/lib/beskar/services/audit_data.rb +72 -0
  67. data/lib/beskar/services/authentication.rb +31 -0
  68. data/lib/beskar/services/authentication_attempt.rb +141 -0
  69. data/lib/beskar/services/ban_expiry.rb +28 -0
  70. data/lib/beskar/services/device_detector.rb +32 -41
  71. data/lib/beskar/services/event_search.rb +58 -0
  72. data/lib/beskar/services/geolocation_service.rb +83 -114
  73. data/lib/beskar/services/ip_whitelist.rb +31 -40
  74. data/lib/beskar/services/location_assessment.rb +109 -0
  75. data/lib/beskar/services/native_account_lock.rb +82 -0
  76. data/lib/beskar/services/notifications.rb +46 -0
  77. data/lib/beskar/services/rate_limiter.rb +99 -125
  78. data/lib/beskar/services/request_context.rb +64 -0
  79. data/lib/beskar/services/risk_assessment.rb +58 -0
  80. data/lib/beskar/services/session_revocation.rb +62 -0
  81. data/lib/beskar/services/waf.rb +311 -198
  82. data/lib/beskar/services/waf_request.rb +60 -0
  83. data/lib/beskar/version.rb +1 -1
  84. data/lib/beskar/warden_authentication.rb +53 -0
  85. data/lib/beskar.rb +54 -4
  86. data/lib/generators/beskar/install/install_generator.rb +158 -0
  87. data/lib/generators/beskar/install/templates/initializer.rb.tt +261 -0
  88. data/lib/tasks/beskar_tasks.rake +25 -20
  89. metadata +93 -12
  90. 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.