robust_server_socket 0.3.1 → 0.3.3
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/README.en.md +652 -0
- data/README.md +366 -38
- data/lib/robust_server_socket/client_token.rb +21 -17
- data/lib/robust_server_socket.rb +0 -1
- data/lib/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 626993a0fda547dc940291eb09015b688a1f761cb5345664488996ed28a1906c
|
|
4
|
+
data.tar.gz: c5bb810563a9e6682c021b6553296477b3e34ecffeeecc2121534b8e397c7377
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9361de5026c683ce6e4a92aad34b8a3358248f4774bff96eb423148ee5daf2556fe1c16f7bd98658acba11218b1e6eae67f8f3af714970474070aa256132e2e7
|
|
7
|
+
data.tar.gz: a24681663c355f6117040fcaa538b73fa5bf9a4ff43edf6e962c9d8a3727e2d107bc222233efddffefd5fdb76a282c16c56e1c7eb323488168274175e0f29993
|
data/README.en.md
ADDED
|
@@ -0,0 +1,652 @@
|
|
|
1
|
+
# RobustServerSocket
|
|
2
|
+
|
|
3
|
+
Gem for inter-service authorization, used in pair with RobustClientSocket
|
|
4
|
+
|
|
5
|
+
### ⚠️ Not Production Tested (yet)
|
|
6
|
+
|
|
7
|
+
## 📋 Table of Contents
|
|
8
|
+
|
|
9
|
+
- [Security Features](#security-features)
|
|
10
|
+
- [Installation](#installation)
|
|
11
|
+
- [Configuration](#configuration)
|
|
12
|
+
- [Usage](#usage)
|
|
13
|
+
- [Error Handling](#error-handling)
|
|
14
|
+
|
|
15
|
+
## 🔒 Security Features
|
|
16
|
+
|
|
17
|
+
RobustServerSocket implements a multi-layered protection system for inter-service communications:
|
|
18
|
+
|
|
19
|
+
### 1. Cryptographic Protection
|
|
20
|
+
- **RSA-2048 Encryption**: Uses RSA key pairs with minimum 2048-bit length
|
|
21
|
+
- **Key Validation**: Automatic key size verification during configuration
|
|
22
|
+
- **Asymmetric Encryption**: Private key on server, public keys on clients
|
|
23
|
+
|
|
24
|
+
### 2. Token Reuse Protection
|
|
25
|
+
- **One-time Tokens**: Each token can only be used once
|
|
26
|
+
- **Redis Blacklist**: Used tokens are automatically added to blacklist
|
|
27
|
+
- **Atomic Verification**: Race conditions prevented via Redis Lua scripts
|
|
28
|
+
|
|
29
|
+
### 3. Time-based Restrictions
|
|
30
|
+
- **Expiration Time**: Configurable token lifetime
|
|
31
|
+
- **Automatic Expiration**: Tokens automatically become invalid after expiration
|
|
32
|
+
- **Replay Attack Protection**: Old tokens cannot be reused
|
|
33
|
+
|
|
34
|
+
### 4. Access Control
|
|
35
|
+
- **Client Whitelist**: Only authorized services can connect
|
|
36
|
+
- **Name-based Identification**: Each client must be explicitly listed in `allowed_services`
|
|
37
|
+
- **Token Format Validation**: Strict token structure verification
|
|
38
|
+
|
|
39
|
+
### 5. Rate Limiting (optional)
|
|
40
|
+
- **DDoS Protection**: Limit number of requests from each client
|
|
41
|
+
- **Sliding Window**: Fair distribution of requests over time
|
|
42
|
+
- **Fail-open Strategy**: If Redis is unavailable, requests are allowed (for reliability)
|
|
43
|
+
- **Per-client Limits**: Individual counters for each client
|
|
44
|
+
|
|
45
|
+
### 6. Injection Protection
|
|
46
|
+
- **Input Validation**: Type, length, and format verification of tokens
|
|
47
|
+
- **Maximum Token Length**: 2048 character limit
|
|
48
|
+
- **Empty Value Checks**: Rejection of empty or malformed tokens
|
|
49
|
+
|
|
50
|
+
## 📦 Installation
|
|
51
|
+
|
|
52
|
+
Add to Gemfile:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
gem 'robust_server_socket'
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Then execute:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
bundle install
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## ⚙️ Configuration
|
|
65
|
+
|
|
66
|
+
Create file `config/initializers/robust_server_socket.rb`:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
RobustServerSocket.configure do |c|
|
|
70
|
+
# REQUIRED PARAMETERS
|
|
71
|
+
|
|
72
|
+
# Service private key (RSA-2048 or higher)
|
|
73
|
+
# IMPORTANT: Store in environment variables, DO NOT commit to git!
|
|
74
|
+
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
75
|
+
|
|
76
|
+
# Token lifetime in seconds
|
|
77
|
+
# Recommendation: 1-3 seconds for production (time from client request to server)
|
|
78
|
+
c.token_expiration_time = 3
|
|
79
|
+
|
|
80
|
+
# List of allowed services (whitelist)
|
|
81
|
+
# Must match names in client keychain
|
|
82
|
+
c.allowed_services = %w[core payments notifications]
|
|
83
|
+
|
|
84
|
+
# Redis for storing used tokens
|
|
85
|
+
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
86
|
+
c.redis_pass = ENV['REDIS_PASSWORD'] # can be nil for local development
|
|
87
|
+
|
|
88
|
+
# OPTIONAL PARAMETERS: Rate Limiting
|
|
89
|
+
|
|
90
|
+
# Enable rate limiting (default: false)
|
|
91
|
+
c.rate_limit_enabled = true
|
|
92
|
+
|
|
93
|
+
# Maximum requests per time window (default: 100)
|
|
94
|
+
c.rate_limit_max_requests = 100
|
|
95
|
+
|
|
96
|
+
# Time window size in seconds (default: 60)
|
|
97
|
+
c.rate_limit_window_seconds = 60
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Load configuration with validation
|
|
101
|
+
RobustServerSocket.load!
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## 🚀 Usage
|
|
105
|
+
|
|
106
|
+
### Basic Authorization
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
# In controller or middleware
|
|
110
|
+
class ApiController < ApplicationController
|
|
111
|
+
before_action :authenticate_service!
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
def authenticate_service!
|
|
116
|
+
# Header configured in RobustClientSocket (SECURE-TOKEN default)
|
|
117
|
+
token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
|
|
118
|
+
|
|
119
|
+
@current_service = RobustServerSocket::ClientToken.validate!(token) # bang method (raises errors)
|
|
120
|
+
rescue RobustServerSocket::ClientToken::InvalidToken
|
|
121
|
+
render json: { error: 'Invalid token' }, status: :unauthorized
|
|
122
|
+
rescue RobustServerSocket::ClientToken::UnauthorizedClient
|
|
123
|
+
render json: { error: 'Unauthorized service' }, status: :forbidden
|
|
124
|
+
rescue RobustServerSocket::ClientToken::UsedToken
|
|
125
|
+
render json: { error: 'Token already used' }, status: :unauthorized
|
|
126
|
+
rescue RobustServerSocket::ClientToken::StaleToken
|
|
127
|
+
render json: { error: 'Token expired' }, status: :unauthorized
|
|
128
|
+
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
129
|
+
render json: { error: e.message }, status: :too_many_requests
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def authenticate_service
|
|
133
|
+
token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
|
|
134
|
+
@current_service = RobustServerSocket::ClientToken.new(token)
|
|
135
|
+
|
|
136
|
+
if @current_service.valid? # doesn't raise errors
|
|
137
|
+
# Token is valid
|
|
138
|
+
else
|
|
139
|
+
# Token is invalid
|
|
140
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Advanced Usage
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
# Create token object
|
|
150
|
+
token_string = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
|
|
151
|
+
client_token = RobustServerSocket::ClientToken.new(token_string)
|
|
152
|
+
|
|
153
|
+
# Check validity (returns true/false)
|
|
154
|
+
if client_token.valid?
|
|
155
|
+
# Get client name
|
|
156
|
+
client_name = client_token.client
|
|
157
|
+
puts "Authorized client: #{client_name}"
|
|
158
|
+
else
|
|
159
|
+
# Token is invalid
|
|
160
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Quick validation with exceptions (recommended)
|
|
164
|
+
begin
|
|
165
|
+
service_token = RobustServerSocket::ClientToken.validate!(token_string)
|
|
166
|
+
client_name = service_token.client
|
|
167
|
+
rescue => e
|
|
168
|
+
# Handle specific errors
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Manual Rate Limiting
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
# Check current attempt count
|
|
176
|
+
attempts = RobustServerSocket::RateLimiter.current_attempts('core')
|
|
177
|
+
puts "Core service made #{attempts} requests"
|
|
178
|
+
|
|
179
|
+
# Reset counter for specific client
|
|
180
|
+
RobustServerSocket::RateLimiter.reset!('core')
|
|
181
|
+
|
|
182
|
+
# Check with exception on exceeded limit
|
|
183
|
+
begin
|
|
184
|
+
RobustServerSocket::RateLimiter.check!('core')
|
|
185
|
+
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
186
|
+
puts e.message # "Rate limit exceeded for core: 101/100 requests per 60s"
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Check without exception (returns false when exceeded)
|
|
190
|
+
if RobustServerSocket::RateLimiter.check('core')
|
|
191
|
+
# Limit not exceeded
|
|
192
|
+
else
|
|
193
|
+
# Limit exceeded
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## 🚦 Rate Limiting (Request Rate Limiting)
|
|
198
|
+
|
|
199
|
+
### How It Works
|
|
200
|
+
|
|
201
|
+
Rate Limiter protects your service from overload by limiting the number of requests from each client within a time window.
|
|
202
|
+
|
|
203
|
+
**Characteristics:**
|
|
204
|
+
- **Per-client counters**: Separate counter for each service
|
|
205
|
+
- **Sliding window**: Window resets automatically after time expires
|
|
206
|
+
- **Atomicity**: Increment and check are performed atomically (Redis LUA script)
|
|
207
|
+
- **Fail-open**: When Redis is unavailable, requests are allowed (not blocked)
|
|
208
|
+
|
|
209
|
+
### Limit Configuration
|
|
210
|
+
|
|
211
|
+
```ruby
|
|
212
|
+
RobustServerSocket.configure do |c|
|
|
213
|
+
# Enable rate limiting
|
|
214
|
+
c.rate_limit_enabled = true
|
|
215
|
+
|
|
216
|
+
# For low-traffic microservices
|
|
217
|
+
c.rate_limit_max_requests = 50
|
|
218
|
+
c.rate_limit_window_seconds = 60
|
|
219
|
+
end
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Monitoring
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
# Check current state
|
|
226
|
+
clients = ['core', 'payments', 'notifications']
|
|
227
|
+
clients.each do |client|
|
|
228
|
+
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
229
|
+
max = RobustServerSocket.configuration.rate_limit_max_requests
|
|
230
|
+
puts "#{client}: #{attempts}/#{max}"
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# In metrics (Prometheus, StatsD, etc.)
|
|
234
|
+
clients.each do |client|
|
|
235
|
+
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
236
|
+
Metrics.gauge("rate_limiter.attempts.#{client}", attempts)
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
## ❌ Error Handling
|
|
241
|
+
|
|
242
|
+
### Exception Types
|
|
243
|
+
|
|
244
|
+
| Exception | Reason | HTTP Status | Action |
|
|
245
|
+
|-----------|--------|-------------|--------|
|
|
246
|
+
| `InvalidToken` | Token cannot be decrypted or has invalid format | 401 | Check token and key correctness |
|
|
247
|
+
| `UnauthorizedClient` | Client not in whitelist | 403 | Add client to `allowed_services` |
|
|
248
|
+
| `UsedToken` | Token has already been used | 401 | Client must request new token |
|
|
249
|
+
| `StaleToken` | Token has expired | 401 | Client must request new token |
|
|
250
|
+
| `RateLimitExceeded` | Rate limit exceeded | 429 | Client should wait or retry later |
|
|
251
|
+
|
|
252
|
+
### Centralized Error Handling
|
|
253
|
+
|
|
254
|
+
```ruby
|
|
255
|
+
# In ApplicationController
|
|
256
|
+
rescue_from RobustServerSocket::ClientToken::InvalidToken,
|
|
257
|
+
RobustServerSocket::ClientToken::UsedToken,
|
|
258
|
+
RobustServerSocket::ClientToken::StaleToken,
|
|
259
|
+
with: :unauthorized_response
|
|
260
|
+
|
|
261
|
+
rescue_from RobustServerSocket::ClientToken::UnauthorizedClient,
|
|
262
|
+
with: :forbidden_response
|
|
263
|
+
|
|
264
|
+
rescue_from RobustServerSocket::RateLimiter::RateLimitExceeded,
|
|
265
|
+
with: :rate_limit_response
|
|
266
|
+
|
|
267
|
+
private
|
|
268
|
+
|
|
269
|
+
def unauthorized_response(exception)
|
|
270
|
+
render json: {
|
|
271
|
+
error: 'Authentication failed',
|
|
272
|
+
message: exception.message,
|
|
273
|
+
type: exception.class.name
|
|
274
|
+
}, status: :unauthorized
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def forbidden_response(exception)
|
|
278
|
+
render json: {
|
|
279
|
+
error: 'Access denied',
|
|
280
|
+
message: exception.message,
|
|
281
|
+
type: exception.class.name
|
|
282
|
+
}, status: :forbidden
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def rate_limit_response(exception)
|
|
286
|
+
render json: {
|
|
287
|
+
error: 'Too many requests',
|
|
288
|
+
message: exception.message,
|
|
289
|
+
type: exception.class.name,
|
|
290
|
+
retry_after: RobustServerSocket.configuration.rate_limit_window_seconds
|
|
291
|
+
}, status: :too_many_requests
|
|
292
|
+
end
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## 💡 Usage Recommendations
|
|
296
|
+
|
|
297
|
+
### 1. Key Management
|
|
298
|
+
|
|
299
|
+
**✅ DO:**
|
|
300
|
+
```ruby
|
|
301
|
+
# Store keys in environment variables
|
|
302
|
+
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
303
|
+
|
|
304
|
+
# Use secrets management (AWS Secrets Manager, Vault, etc.)
|
|
305
|
+
c.private_key = Rails.application.credentials.dig(:robust_server, :private_key)
|
|
306
|
+
|
|
307
|
+
# Generate keys correctly
|
|
308
|
+
# openssl genrsa -out private_key.pem 2048
|
|
309
|
+
# openssl rsa -in private_key.pem -pubout -out public_key.pem
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**❌ DON'T:**
|
|
313
|
+
```ruby
|
|
314
|
+
# DON'T commit keys to git
|
|
315
|
+
c.private_key = "-----BEGIN PRIVATE KEY-----\nMII..."
|
|
316
|
+
|
|
317
|
+
# DON'T use weak keys
|
|
318
|
+
# Minimum RSA-2048, RSA-4096 recommended for high security
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### 2. Redis Configuration
|
|
322
|
+
|
|
323
|
+
**✅ DO:**
|
|
324
|
+
```ruby
|
|
325
|
+
# Use separate namespace for each environment
|
|
326
|
+
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
327
|
+
|
|
328
|
+
# Configure connection pool in production
|
|
329
|
+
# In config/initializers/redis.rb
|
|
330
|
+
Redis.current = ConnectionPool.new(size: 5, timeout: 5) do
|
|
331
|
+
Redis.new(url: ENV['REDIS_URL'], password: ENV['REDIS_PASSWORD'])
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Monitor Redis status
|
|
335
|
+
# Use Redis Sentinel or Cluster for high availability
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
**❌ DON'T:**
|
|
339
|
+
```ruby
|
|
340
|
+
# DON'T use same Redis DB for all environments, use separate Redis DB
|
|
341
|
+
# DON'T ignore Redis errors (rate limiter is already fail-open, but log them)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 5. Service Whitelist
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
# Explicitly specify only necessary services
|
|
348
|
+
c.allowed_services = %w[core payments] # ✅
|
|
349
|
+
|
|
350
|
+
# DON'T use wildcards or regular expressions
|
|
351
|
+
c.allowed_services = %w[*] # ❌ DANGEROUS!
|
|
352
|
+
|
|
353
|
+
# Synchronize with client keychain
|
|
354
|
+
# Server (robust_server_socket):
|
|
355
|
+
c.allowed_services = %w[core]
|
|
356
|
+
|
|
357
|
+
# Client (robust_client_socket):
|
|
358
|
+
c.keychain = {
|
|
359
|
+
core: { # ← Must match
|
|
360
|
+
base_uri: 'https://core.example.com',
|
|
361
|
+
public_key: '-----BEGIN PUBLIC KEY-----...'
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## 🤝 Integration with RobustClientSocket
|
|
367
|
+
|
|
368
|
+
For full functionality, configure the client side:
|
|
369
|
+
|
|
370
|
+
```ruby
|
|
371
|
+
# On client (RobustClientSocket)
|
|
372
|
+
RobustClientSocket.configure do |c|
|
|
373
|
+
c.service_name = 'core' # ← Must be in server's allowed_services
|
|
374
|
+
c.keychain = {
|
|
375
|
+
payments: {
|
|
376
|
+
base_uri: 'https://payments.example.com',
|
|
377
|
+
public_key: '-----BEGIN PUBLIC KEY-----...' # Public key of payments server
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# On server (RobustServerSocket)
|
|
383
|
+
RobustServerSocket.configure do |c|
|
|
384
|
+
c.allowed_services = %w[core] # ← Matches client's service_name
|
|
385
|
+
c.private_key = '-----BEGIN PRIVATE KEY-----...' # Private pair to public_key
|
|
386
|
+
end
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## 📚 Additional Resources
|
|
390
|
+
|
|
391
|
+
- [RobustClientSocket documentation](https://github.com/tee0zed/robust_client_socket)
|
|
392
|
+
- [RSA encryption best practices](https://www.openssl.org/docs/)
|
|
393
|
+
- [Redis security guide](https://redis.io/topics/security)
|
|
394
|
+
|
|
395
|
+
## 📝 License
|
|
396
|
+
|
|
397
|
+
See [MIT-LICENSE](MIT-LICENSE) file
|
|
398
|
+
|
|
399
|
+
## 🐛 Bugs and Suggestions
|
|
400
|
+
|
|
401
|
+
Report issues through your repository's issue tracker.
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
#### Test: 1000 Requests with Token Validation
|
|
405
|
+
|
|
406
|
+
**Without RobustServerSocket (plain HTTP controller):**
|
|
407
|
+
```ruby
|
|
408
|
+
Benchmark.measure do
|
|
409
|
+
1000.times do
|
|
410
|
+
# Regular request without authorization
|
|
411
|
+
get '/api/v1/partners/719a68e4-3457-45dd-8d7f-73f1d367b87a/merchants'
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**Results (approximate):**
|
|
417
|
+
- **Real time**: ~2.5 seconds
|
|
418
|
+
- No token verification
|
|
419
|
+
- No RSA decryption
|
|
420
|
+
- No Redis checks
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
**With RobustServerSocket (full protection):**
|
|
425
|
+
```ruby
|
|
426
|
+
Benchmark.measure do
|
|
427
|
+
1000.times do
|
|
428
|
+
# Request with RobustClientSocket (RSA + tokens)
|
|
429
|
+
RobustClientSocket::CoreApi.get('/api/v1/partners/719a68e4-3457-45dd-8d7f-73f1d367b87a/merchants')
|
|
430
|
+
end
|
|
431
|
+
end
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
**Results:**
|
|
435
|
+
- **Real time**: 2.77 seconds
|
|
436
|
+
- **User CPU**: 0.23 seconds
|
|
437
|
+
- **System CPU**: 0.54 seconds
|
|
438
|
+
- **Total CPU**: 0.77 seconds
|
|
439
|
+
|
|
440
|
+
### 📊 Security Overhead Analysis
|
|
441
|
+
|
|
442
|
+
| Operation | Time | % of Request |
|
|
443
|
+
|-----------|------|-------------|
|
|
444
|
+
| **RSA Decryption** | ~0.1-0.2ms | 3-7% |
|
|
445
|
+
| **Redis Token Check** | ~0.05-0.1ms | 2-3% |
|
|
446
|
+
| **Rate Limiting** | ~0.02-0.05ms | 1% |
|
|
447
|
+
| **Whitelist Validation** | <0.01ms | <1% |
|
|
448
|
+
| **Total Overhead** | **~0.2-0.4ms** | **~10-15%** |
|
|
449
|
+
|
|
450
|
+
### 🎯 Key Findings
|
|
451
|
+
|
|
452
|
+
1. **Minimal overhead (~0.3ms per request)**
|
|
453
|
+
- RSA-2048 decryption: ~0.15ms
|
|
454
|
+
- Redis operations: ~0.08ms
|
|
455
|
+
- Rate limiting: ~0.03ms
|
|
456
|
+
|
|
457
|
+
2. **Scales linearly**
|
|
458
|
+
- 100 req/s = +30ms overhead
|
|
459
|
+
- 1000 req/s = +300ms overhead
|
|
460
|
+
- Acceptable for most applications
|
|
461
|
+
|
|
462
|
+
3. **Redis is main bottleneck**
|
|
463
|
+
- Use Redis Sentinel/Cluster
|
|
464
|
+
- Connection pooling is critical
|
|
465
|
+
- Fail-open strategy for reliability
|
|
466
|
+
|
|
467
|
+
### 💡 Performance Optimization
|
|
468
|
+
|
|
469
|
+
**1. Redis Connection Pool:**
|
|
470
|
+
|
|
471
|
+
```ruby
|
|
472
|
+
# config/initializers/redis.rb
|
|
473
|
+
require 'connection_pool'
|
|
474
|
+
|
|
475
|
+
REDIS_POOL = ConnectionPool.new(size: 25, timeout: 5) do
|
|
476
|
+
Redis.new(
|
|
477
|
+
url: ENV['REDIS_URL'],
|
|
478
|
+
password: ENV['REDIS_PASSWORD'],
|
|
479
|
+
reconnect_attempts: 3,
|
|
480
|
+
reconnect_delay: 0.5,
|
|
481
|
+
reconnect_delay_max: 5.0
|
|
482
|
+
)
|
|
483
|
+
end
|
|
484
|
+
|
|
485
|
+
# In RobustServerSocket::SecureToken::Cacher
|
|
486
|
+
def self.with_redis
|
|
487
|
+
REDIS_POOL.with do |redis|
|
|
488
|
+
yield redis
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
**2. Public Key Caching:**
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
# Keys are already cached at load!, but can be optimized
|
|
497
|
+
class RobustServerSocket::ClientToken
|
|
498
|
+
# Keys are loaded once at application startup
|
|
499
|
+
# No additional optimization needed
|
|
500
|
+
end
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**3. Rate Limiting Optimization:**
|
|
504
|
+
|
|
505
|
+
```ruby
|
|
506
|
+
RobustServerSocket.configure do |c|
|
|
507
|
+
# For high-load systems
|
|
508
|
+
c.rate_limit_enabled = true
|
|
509
|
+
c.rate_limit_max_requests = 1000 # Increase limit
|
|
510
|
+
c.rate_limit_window_seconds = 60
|
|
511
|
+
|
|
512
|
+
# For low-load systems
|
|
513
|
+
c.rate_limit_max_requests = 100
|
|
514
|
+
c.rate_limit_window_seconds = 60
|
|
515
|
+
end
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
**4. Token Expiration Optimization:**
|
|
519
|
+
|
|
520
|
+
```ruby
|
|
521
|
+
# Short lifetime = more requests for new tokens
|
|
522
|
+
c.token_expiration_time = 10.minutes # ❌ High traffic
|
|
523
|
+
|
|
524
|
+
# Optimal time for inter-service calls
|
|
525
|
+
c.token_expiration_time = 3 # ✅ 3 seconds is enough
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
### 🔬 Performance Monitoring
|
|
529
|
+
|
|
530
|
+
**Metrics to Track:**
|
|
531
|
+
|
|
532
|
+
```ruby
|
|
533
|
+
class ApiController < ApplicationController
|
|
534
|
+
around_action :track_auth_performance
|
|
535
|
+
|
|
536
|
+
private
|
|
537
|
+
|
|
538
|
+
def track_auth_performance
|
|
539
|
+
start = Time.now
|
|
540
|
+
|
|
541
|
+
begin
|
|
542
|
+
yield
|
|
543
|
+
ensure
|
|
544
|
+
duration = ((Time.now - start) * 1000).round(2)
|
|
545
|
+
|
|
546
|
+
# Total request time
|
|
547
|
+
Metrics.timing('request.duration', duration, tags: [
|
|
548
|
+
"controller:#{controller_name}",
|
|
549
|
+
"action:#{action_name}"
|
|
550
|
+
])
|
|
551
|
+
|
|
552
|
+
# Authentication attempts
|
|
553
|
+
if @current_service
|
|
554
|
+
Metrics.increment('auth.success', tags: ["service:#{@current_service.client}"])
|
|
555
|
+
else
|
|
556
|
+
Metrics.increment('auth.failure')
|
|
557
|
+
end
|
|
558
|
+
end
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Specific metrics for RobustServerSocket
|
|
563
|
+
module RobustServerSocket
|
|
564
|
+
class ClientToken
|
|
565
|
+
def self.validate_with_metrics!(token)
|
|
566
|
+
start = Time.now
|
|
567
|
+
result = validate!(token)
|
|
568
|
+
duration = ((Time.now - start) * 1000).round(2)
|
|
569
|
+
|
|
570
|
+
Metrics.timing('robust_server.validation.duration', duration)
|
|
571
|
+
result
|
|
572
|
+
rescue StandardError => e
|
|
573
|
+
Metrics.increment('robust_server.validation.error', tags: ["error:#{e.class.name}"])
|
|
574
|
+
raise
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
end
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
### 📈 Performance at Different Loads
|
|
581
|
+
|
|
582
|
+
| Req/s | Without Protection | With RobustServerSocket | Overhead | Acceptable |
|
|
583
|
+
|-------|-------------------|------------------------|----------|-----------|
|
|
584
|
+
| 10 | 100ms | 103ms | 3ms | ✅ Excellent |
|
|
585
|
+
| 100 | 1s | 1.03s | 30ms | ✅ Excellent |
|
|
586
|
+
| 500 | 5s | 5.15s | 150ms | ✅ Good |
|
|
587
|
+
| 1,000 | 10s | 10.3s | 300ms | ✅ Acceptable |
|
|
588
|
+
| 5,000 | 50s | 51.5s | 1.5s | ⚠️ Redis scaling needed |
|
|
589
|
+
| 10,000 | 100s | 103s | 3s | ⚠️ Need Redis Cluster |
|
|
590
|
+
|
|
591
|
+
**Conclusion:** Up to 1000 req/s - excellent performance. Higher loads require Redis scaling.
|
|
592
|
+
|
|
593
|
+
### 🚀 Production Recommendations
|
|
594
|
+
|
|
595
|
+
**For high-load systems (>1000 req/s):**
|
|
596
|
+
|
|
597
|
+
1. **Redis Cluster** - distributed load
|
|
598
|
+
2. **Connection Pool** - minimum 25-50 connections
|
|
599
|
+
3. **Monitor Redis** - latency, memory, connections
|
|
600
|
+
4. **Fail-over Strategy** - Redis Sentinel
|
|
601
|
+
5. **CDN for static** - reduce overall load
|
|
602
|
+
|
|
603
|
+
**For medium load (100-1000 req/s):**
|
|
604
|
+
|
|
605
|
+
1. **Standalone Redis** with persistence
|
|
606
|
+
2. **Connection Pool** - 10-25 connections
|
|
607
|
+
3. **Basic monitoring**
|
|
608
|
+
4. **Rate limiting** - spike protection
|
|
609
|
+
|
|
610
|
+
**For low load (<100 req/s):**
|
|
611
|
+
|
|
612
|
+
1. **Simple Redis** setup
|
|
613
|
+
2. **Default connection pool** (5)
|
|
614
|
+
3. **Standard configuration**
|
|
615
|
+
|
|
616
|
+
## 🤝 Integration with RobustClientSocket
|
|
617
|
+
|
|
618
|
+
For full functionality, configure the client side:
|
|
619
|
+
|
|
620
|
+
```ruby
|
|
621
|
+
# On client (RobustClientSocket)
|
|
622
|
+
RobustClientSocket.configure do |c|
|
|
623
|
+
c.service_name = 'core' # ← Must be in server's allowed_services
|
|
624
|
+
c.keychain = {
|
|
625
|
+
payments: {
|
|
626
|
+
base_uri: 'https://payments.example.com',
|
|
627
|
+
public_key: '-----BEGIN PUBLIC KEY-----...' # Public key of payments server
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# On server (RobustServerSocket)
|
|
633
|
+
RobustServerSocket.configure do |c|
|
|
634
|
+
c.allowed_services = %w[core] # ← Matches client's service_name
|
|
635
|
+
c.private_key = '-----BEGIN PRIVATE KEY-----...' # Private pair to public_key
|
|
636
|
+
end
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
## 📚 Additional Resources
|
|
640
|
+
|
|
641
|
+
- [BENCHMARK_ANALYSIS.md](BENCHMARK_ANALYSIS.md)
|
|
642
|
+
- [RobustClientSocket documentation](../robust_client_socket/README.md)
|
|
643
|
+
- [RSA encryption best practices](https://www.openssl.org/docs/)
|
|
644
|
+
- [Redis security guide](https://redis.io/topics/security)
|
|
645
|
+
|
|
646
|
+
## 📝 License
|
|
647
|
+
|
|
648
|
+
See [MIT-LICENSE](MIT-LICENSE) file
|
|
649
|
+
|
|
650
|
+
## 🐛 Bugs and Suggestions
|
|
651
|
+
|
|
652
|
+
Report issues through your repository's issue tracker.
|
data/README.md
CHANGED
|
@@ -1,59 +1,387 @@
|
|
|
1
1
|
# RobustServerSocket
|
|
2
2
|
|
|
3
|
-
Gem
|
|
3
|
+
Gem для межсервисной авторизации, используется в паре с RobustClientSocket
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
### ⚠️ Not Production Tested (yet)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- Authorized client names are stored in token and config
|
|
9
|
-
- Token is staleable
|
|
10
|
-
- Token if one-time use only
|
|
11
|
-
- Blacklist for tokens in redis
|
|
7
|
+
## 📋 Содержание
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
- [Функции безопасности](#функции-безопасности)
|
|
10
|
+
- [Установка](#установка)
|
|
11
|
+
- [Конфигурация](#конфигурация)
|
|
12
|
+
- [Использование](#использование)
|
|
13
|
+
- [Обработка ошибок](#обработка-ошибок)
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
## 🔒 Функции безопасности
|
|
16
|
+
|
|
17
|
+
RobustServerSocket реализует многоуровневую систему защиты для межсервисных коммуникаций:
|
|
18
|
+
|
|
19
|
+
### 1. Криптографическая защита
|
|
20
|
+
- **RSA-2048 шифрование**: Используется пара ключей RSA с минимальной длиной 2048 бит
|
|
21
|
+
- **Валидация ключей**: Автоматическая проверка размера ключа при конфигурации
|
|
22
|
+
- **Асимметричное шифрование**: Приватный ключ на сервере, публичный — у клиентов
|
|
23
|
+
|
|
24
|
+
### 2. Защита от повторного использования токенов
|
|
25
|
+
- **Одноразовые токены**: Каждый токен может быть использован только один раз
|
|
26
|
+
- **Blacklist в Redis**: Использованные токены автоматически добавляются в черный список
|
|
27
|
+
- **Атомарная проверка**: Race condition защищена благодаря Redis Lua скриптам
|
|
28
|
+
|
|
29
|
+
### 3. Временные ограничения
|
|
30
|
+
- **Expiration time**: Настраиваемое время жизни токена
|
|
31
|
+
- **Автоматическое истечение**: Токены автоматически становятся недействительными после истечения времени
|
|
32
|
+
- **Защита от replay attacks**: Старые токены не могут быть использованы повторно
|
|
33
|
+
|
|
34
|
+
### 4. Контроль доступа
|
|
35
|
+
- **Whitelist клиентов**: Только авторизованные сервисы могут подключаться
|
|
36
|
+
- **Идентификация по имени**: Каждый клиент должен быть явно указан в `allowed_services`
|
|
37
|
+
- **Валидация формата токена**: Строгая проверка структуры токена
|
|
38
|
+
|
|
39
|
+
### 5. Rate Limiting (опционально)
|
|
40
|
+
- **Защита от DDoS**: Ограничение количества запросов от каждого клиента
|
|
41
|
+
- **Sliding window**: Справедливое распределение запросов во времени
|
|
42
|
+
- **Fail-open стратегия**: Если Redis недоступен, запросы пропускаются (для надёжности)
|
|
43
|
+
- **Per-client лимиты**: Индивидуальные счётчики для каждого клиента
|
|
44
|
+
|
|
45
|
+
### 6. Защита от инъекций
|
|
46
|
+
- **Валидация входных данных**: Проверка типа, длины и формата токенов
|
|
47
|
+
- **Максимальная длина токена**: Ограничение 2048 символов
|
|
48
|
+
- **Проверка на пустые значения**: Отклонение пустых или некорректных токенов
|
|
49
|
+
|
|
50
|
+
## 📦 Установка
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
gem 'robust_server_socket'
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## ⚙️ Конфигурация
|
|
57
|
+
|
|
58
|
+
Создайте файл `config/initializers/robust_server_socket.rb`:
|
|
16
59
|
|
|
17
60
|
```ruby
|
|
18
61
|
RobustServerSocket.configure do |c|
|
|
19
|
-
|
|
20
|
-
c.token_expiration_time = 10.minutes # time in seconds for token expiration
|
|
21
|
-
c.allowed_services = %w(core) # list of services allowed to use this service, must be same as service name in keychain in RobustClientSocket
|
|
22
|
-
# so if we have
|
|
23
|
-
# RobustClientSocket.configure do |c|
|
|
24
|
-
# c.keychain = {
|
|
25
|
-
# core: { <<< service name
|
|
26
|
-
# base_uri: 'https://core.payrent.com',
|
|
27
|
-
# public_key: '-----BEGIN PUBLIC KEY-----[...]'
|
|
28
|
-
# },
|
|
29
|
-
# we should add 'core' to allowed_services
|
|
30
|
-
c.redis_url = 'redis://localhost:6379' # redis url for storing tokens
|
|
31
|
-
c.redis_pass = 'password' # redis password
|
|
62
|
+
# ОБЯЗАТЕЛЬНЫЕ ПАРАМЕТРЫ
|
|
32
63
|
|
|
33
|
-
#
|
|
34
|
-
c.
|
|
35
|
-
c.
|
|
36
|
-
c.rate_limit_window_seconds = 60 # time window in seconds (default: 60)
|
|
37
|
-
end
|
|
64
|
+
# Приватный ключ сервиса (RSA-2048 или выше)
|
|
65
|
+
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
66
|
+
c.token_expiration_time = 3
|
|
38
67
|
|
|
68
|
+
# Список разрешённых сервисов (whitelist)
|
|
69
|
+
# Должен совпадать с именами RobustClientSocket клиента
|
|
70
|
+
c.allowed_services = %w[core payments notifications]
|
|
71
|
+
|
|
72
|
+
# Redis для работы replay-attack protection и throttling
|
|
73
|
+
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
74
|
+
c.redis_pass = ENV['REDIS_PASSWORD']
|
|
75
|
+
|
|
76
|
+
# НЕОБЯЗАТЕЛЬНЫЕ ПАРАМЕТРЫ
|
|
77
|
+
# Включить ограничение частоты запросов (по умолчанию: false)
|
|
78
|
+
c.rate_limit_enabled = true
|
|
79
|
+
# Максимальное количество запросов в окне времени (по умолчанию: 100)
|
|
80
|
+
c.rate_limit_max_requests = 100
|
|
81
|
+
# Размер временного окна в секундах (по умолчанию: 60)
|
|
82
|
+
c.rate_limit_window_seconds = 60
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Загрузка конфигурации с валидацией
|
|
39
86
|
RobustServerSocket.load!
|
|
40
87
|
```
|
|
41
88
|
|
|
42
|
-
|
|
89
|
+
### Опции конфигурации сервиса
|
|
90
|
+
|
|
91
|
+
| Параметр | Тип | Обязательный | Default | Описание |
|
|
92
|
+
|----------|-----|--------------|---------|----------|
|
|
93
|
+
| `private_key` | String | ✅ | - | Приватный RSA ключ сервиса (RSA-2048 или выше) |
|
|
94
|
+
| `token_expiration_time` | Integer | ✅ | - | Время жизни токена в секундах |
|
|
95
|
+
| `allowed_services` | Array | ✅ | - | Список разрешённых сервисов (whitelist) |
|
|
96
|
+
| `redis_url` | String | ✅ | - | URL для подключения к Redis |
|
|
97
|
+
| `redis_pass` | String | ❌ | nil | Пароль для Redis (если требуется) |
|
|
98
|
+
| `rate_limit_enabled` | Boolean | ❌ | false | Включить ограничение частоты запросов |
|
|
99
|
+
| `rate_limit_max_requests` | Integer | ❌ | 100 | Максимальное количество запросов в окне времени |
|
|
100
|
+
| `rate_limit_window_seconds` | Integer | ❌ | 60 | Размер временного окна в секундах |
|
|
101
|
+
|
|
102
|
+
## 🚀 Использование
|
|
103
|
+
|
|
104
|
+
### Базовая авторизация
|
|
105
|
+
|
|
106
|
+
```ruby
|
|
107
|
+
# В контроллере или middleware
|
|
108
|
+
class ApiController < ApplicationController
|
|
109
|
+
before_action :authenticate_service!
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def authenticate_service!
|
|
114
|
+
# Хедер, прописанный в RobustClientSocket (SECURE-TOKEN default)
|
|
115
|
+
token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
|
|
116
|
+
|
|
117
|
+
@current_service = RobustServerSocket::ClientToken.validate!(token) # bang method (рейзит ошибки)
|
|
118
|
+
rescue RobustServerSocket::ClientToken::InvalidToken
|
|
119
|
+
render json: { error: 'Invalid token' }, status: :unauthorized
|
|
120
|
+
rescue RobustServerSocket::ClientToken::UnauthorizedClient
|
|
121
|
+
render json: { error: 'Unauthorized service' }, status: :forbidden
|
|
122
|
+
rescue RobustServerSocket::ClientToken::UsedToken
|
|
123
|
+
render json: { error: 'Token already used' }, status: :unauthorized
|
|
124
|
+
rescue RobustServerSocket::ClientToken::StaleToken
|
|
125
|
+
render json: { error: 'Token expired' }, status: :unauthorized
|
|
126
|
+
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
127
|
+
render json: { error: e.message }, status: :too_many_requests
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def authenticate_service
|
|
131
|
+
token = request.headers['SECURE-TOKEN']&.sub(/^Bearer /, '')
|
|
132
|
+
@current_service = RobustServerSocket::ClientToken.valid?(token) # не рейзит
|
|
133
|
+
|
|
134
|
+
if @current_service
|
|
135
|
+
# Токен валиден
|
|
136
|
+
else
|
|
137
|
+
# Токен невалиден
|
|
138
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Расширенное использование
|
|
145
|
+
|
|
146
|
+
```ruby
|
|
147
|
+
# Создание объекта токена
|
|
148
|
+
token_string = request.headers['Authorization']&.sub(/^Bearer /, '')
|
|
149
|
+
client_token = RobustServerSocket::ClientToken.new(token_string)
|
|
150
|
+
|
|
151
|
+
# Проверка валидности (возвращает true/false)
|
|
152
|
+
if client_token.valid?
|
|
153
|
+
# Получение имени клиента
|
|
154
|
+
client_name = client_token.client
|
|
155
|
+
puts "Authorized client: #{client_name}"
|
|
156
|
+
else
|
|
157
|
+
# Токен невалиден
|
|
158
|
+
render json: { error: 'Unauthorized' }, status: :unauthorized
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Быстрая валидация с исключениями
|
|
162
|
+
begin
|
|
163
|
+
service_token = RobustServerSocket::ClientToken.validate!(token_string)
|
|
164
|
+
client_name = service_token.client
|
|
165
|
+
rescue => e
|
|
166
|
+
# Обработка специфичных ошибок
|
|
167
|
+
end
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Rate Limiting вручную
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
# Проверка текущего количества попыток
|
|
174
|
+
attempts = RobustServerSocket::RateLimiter.current_attempts('core')
|
|
175
|
+
puts "Core service made #{attempts} requests"
|
|
176
|
+
|
|
177
|
+
# Сброс счётчика для конкретного клиента
|
|
178
|
+
RobustServerSocket::RateLimiter.reset!('core')
|
|
179
|
+
|
|
180
|
+
# Проверка с исключением при превышении
|
|
181
|
+
begin
|
|
182
|
+
RobustServerSocket::RateLimiter.check!('core')
|
|
183
|
+
rescue RobustServerSocket::RateLimiter::RateLimitExceeded => e
|
|
184
|
+
puts e.message # "Rate limit exceeded for core: 101/100 requests per 60s"
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Проверка без исключения (возвращает false при превышении)
|
|
188
|
+
if RobustServerSocket::RateLimiter.check('core')
|
|
189
|
+
# Лимит не превышен
|
|
190
|
+
else
|
|
191
|
+
# Лимит превышен
|
|
192
|
+
end
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## 🚦 Rate Limiting (Ограничение частоты запросов)
|
|
196
|
+
|
|
197
|
+
### Принцип работы
|
|
198
|
+
|
|
199
|
+
Rate Limiter защищает ваш сервис от перегрузки, ограничивая количество запросов от каждого клиента в определённом временном окне.
|
|
200
|
+
|
|
201
|
+
**Характеристики:**
|
|
202
|
+
- **Per-client counters**: Отдельный счётчик для каждого сервиса
|
|
203
|
+
- **Sliding window**: Окно сбрасывается автоматически после истечения времени
|
|
204
|
+
- **Атомарность**: Инкремент и проверка выполняются атомарно (Redis LUA script)
|
|
205
|
+
- **Fail-open**: При недоступности Redis запросы пропускаются (не блокируются)
|
|
206
|
+
|
|
207
|
+
### Мониторинг
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
# Проверка текущего состояния
|
|
211
|
+
clients = ['core', 'payments', 'notifications']
|
|
212
|
+
clients.each do |client|
|
|
213
|
+
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
214
|
+
max = RobustServerSocket.configuration.rate_limit_max_requests
|
|
215
|
+
puts "#{client}: #{attempts}/#{max}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# В метриках (Prometheus, StatsD и т.д.)
|
|
219
|
+
clients.each do |client|
|
|
220
|
+
attempts = RobustServerSocket::RateLimiter.current_attempts(client)
|
|
221
|
+
Metrics.gauge("rate_limiter.attempts.#{client}", attempts)
|
|
222
|
+
end
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## ❌ Обработка ошибок
|
|
226
|
+
|
|
227
|
+
### Типы исключений
|
|
228
|
+
|
|
229
|
+
| Исключение | Причина | HTTP статус | Действие |
|
|
230
|
+
|-----------|---------|-------------|----------|
|
|
231
|
+
| `InvalidToken` | Токен не может быть расшифрован или имеет неверный формат | 401 | Проверьте корректность токена и ключей |
|
|
232
|
+
| `UnauthorizedClient` | Клиент не в whitelist | 403 | Добавьте клиента в `allowed_services` |
|
|
233
|
+
| `UsedToken` | Токен уже был использован | 401 | Клиент должен запросить новый токен |
|
|
234
|
+
| `StaleToken` | Токен истёк | 401 | Клиент должен запросить новый токен |
|
|
235
|
+
| `RateLimitExceeded` | Превышен лимит запросов | 429 | Клиент должен подождать или ретраить позже |
|
|
236
|
+
|
|
237
|
+
### Централизованная обработка
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
# В ApplicationController
|
|
241
|
+
rescue_from RobustServerSocket::ClientToken::InvalidToken,
|
|
242
|
+
RobustServerSocket::ClientToken::UsedToken,
|
|
243
|
+
RobustServerSocket::ClientToken::StaleToken,
|
|
244
|
+
with: :unauthorized_response
|
|
245
|
+
|
|
246
|
+
rescue_from RobustServerSocket::ClientToken::UnauthorizedClient,
|
|
247
|
+
with: :forbidden_response
|
|
248
|
+
|
|
249
|
+
rescue_from RobustServerSocket::RateLimiter::RateLimitExceeded,
|
|
250
|
+
with: :rate_limit_response
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def unauthorized_response(exception)
|
|
255
|
+
render json: {
|
|
256
|
+
error: 'Authentication failed',
|
|
257
|
+
message: exception.message,
|
|
258
|
+
type: exception.class.name
|
|
259
|
+
}, status: :unauthorized
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def forbidden_response(exception)
|
|
263
|
+
render json: {
|
|
264
|
+
error: 'Access denied',
|
|
265
|
+
message: exception.message,
|
|
266
|
+
type: exception.class.name
|
|
267
|
+
}, status: :forbidden
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def rate_limit_response(exception)
|
|
271
|
+
render json: {
|
|
272
|
+
error: 'Too many requests',
|
|
273
|
+
message: exception.message,
|
|
274
|
+
type: exception.class.name,
|
|
275
|
+
retry_after: RobustServerSocket.configuration.rate_limit_window_seconds
|
|
276
|
+
}, status: :too_many_requests
|
|
277
|
+
end
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## 💡 Рекомендации по использованию
|
|
281
|
+
|
|
282
|
+
### 1. Управление ключами
|
|
283
|
+
|
|
284
|
+
**✅ DO:**
|
|
285
|
+
```ruby
|
|
286
|
+
# Храните ключи в переменных окружения
|
|
287
|
+
c.private_key = ENV['ROBUST_SERVER_PRIVATE_KEY']
|
|
288
|
+
|
|
289
|
+
# Используйте secrets management (AWS Secrets Manager, Vault, и т.д.)
|
|
290
|
+
c.private_key = Rails.application.credentials.dig(:robust_server, :private_key)
|
|
291
|
+
|
|
292
|
+
# Генерируйте ключи правильно
|
|
293
|
+
# openssl genrsa -out private_key.pem 2048
|
|
294
|
+
# openssl rsa -in private_key.pem -pubout -out public_key.pem
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**❌ DON'T:**
|
|
298
|
+
```ruby
|
|
299
|
+
# НЕ коммитьте ключи в git
|
|
300
|
+
c.private_key = "-----BEGIN PRIVATE KEY-----\nMII..."
|
|
301
|
+
|
|
302
|
+
# НЕ используйте слабые ключи
|
|
303
|
+
# Минимум RSA-2048, рекомендуется RSA-4096 для высокой безопасности
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### 2. Конфигурация Redis
|
|
43
307
|
|
|
308
|
+
**✅ DO:**
|
|
44
309
|
```ruby
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
310
|
+
# Используйте отдельный namespace для каждого окружения
|
|
311
|
+
c.redis_url = ENV.fetch('REDIS_URL', 'redis://localhost:6379/0')
|
|
312
|
+
|
|
313
|
+
# Настройте connection pool в production
|
|
314
|
+
# В config/initializers/redis.rb
|
|
315
|
+
Redis.current = ConnectionPool.new(size: 5, timeout: 5) do
|
|
316
|
+
Redis.new(url: ENV['REDIS_URL'], password: ENV['REDIS_PASSWORD'])
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
# Мониторьте состояние Redis
|
|
320
|
+
# Используйте Redis Sentinel или Cluster для высокой доступности
|
|
321
|
+
```
|
|
48
322
|
|
|
49
|
-
|
|
323
|
+
**❌ DON'T:**
|
|
324
|
+
```ruby
|
|
325
|
+
# НЕ используйте одну БД Redis для всех окружений, используйте отдельную bd redis
|
|
326
|
+
# НЕ игнорируйте ошибки Redis (rate limiter уже fail-open, но логируйте их)
|
|
50
327
|
```
|
|
51
|
-
## Errors
|
|
52
328
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
329
|
+
### 5. Whitelist сервисов
|
|
330
|
+
|
|
331
|
+
```ruby
|
|
332
|
+
# Явно указывайте только необходимые сервисы
|
|
333
|
+
c.allowed_services = %w[core payments] # ✅
|
|
334
|
+
|
|
335
|
+
# НЕ используйте wildcards или регулярные выражения
|
|
336
|
+
c.allowed_services = %w[*] # ❌ ОПАСНО!
|
|
337
|
+
|
|
338
|
+
# Синхронизируйте с keychain клиента
|
|
339
|
+
# Server (robust_server_socket):
|
|
340
|
+
c.allowed_services = %w[core]
|
|
341
|
+
|
|
342
|
+
# Client (robust_client_socket):
|
|
343
|
+
c.keychain = {
|
|
344
|
+
core: { # ← Должно совпадать
|
|
345
|
+
base_uri: 'https://core.example.com',
|
|
346
|
+
public_key: '-----BEGIN PUBLIC KEY-----...'
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## 🤝 Интеграция с RobustClientSocket
|
|
352
|
+
|
|
353
|
+
Для полноценной работы необходимо настроить клиентскую часть:
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
# На клиенте (RobustClientSocket)
|
|
357
|
+
RobustClientSocket.configure do |c|
|
|
358
|
+
c.service_name = 'core' # ← Должно быть в allowed_services сервера
|
|
359
|
+
c.keychain = {
|
|
360
|
+
payments: {
|
|
361
|
+
base_uri: 'https://payments.example.com',
|
|
362
|
+
public_key: '-----BEGIN PUBLIC KEY-----...' # Публичный ключ сервера payments
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
# На сервере (RobustServerSocket)
|
|
368
|
+
RobustServerSocket.configure do |c|
|
|
369
|
+
c.allowed_services = %w[core] # ← Соответствует service_name клиента
|
|
370
|
+
c.private_key = '-----BEGIN PRIVATE KEY-----...' # Приватная пара к public_key
|
|
371
|
+
end
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
## 📚 Дополнительные ресурсы
|
|
375
|
+
|
|
376
|
+
- [BENCHMARK_ANALYSIS.md](BENCHMARK_ANALYSIS.md)
|
|
377
|
+
- [RobustClientSocket documentation](https://github.com/tee0zed/robust_client_socket)
|
|
378
|
+
- [RSA encryption best practices](https://www.openssl.org/docs/)
|
|
379
|
+
- [Redis security guide](https://redis.io/topics/security)
|
|
380
|
+
|
|
381
|
+
## 📝 Лицензия
|
|
382
|
+
|
|
383
|
+
См. файл [MIT-LICENSE](MIT-LICENSE)
|
|
58
384
|
|
|
385
|
+
## 🐛 Баги и предложения
|
|
59
386
|
|
|
387
|
+
Сообщайте о проблемах через issue tracker вашего репозитория.
|
|
@@ -13,23 +13,7 @@ module RobustServerSocket
|
|
|
13
13
|
|
|
14
14
|
def self.validate!(secure_token)
|
|
15
15
|
new(secure_token).tap do |instance|
|
|
16
|
-
|
|
17
|
-
raise UnauthorizedClient unless instance.client
|
|
18
|
-
|
|
19
|
-
RateLimiter.check!(instance.client)
|
|
20
|
-
|
|
21
|
-
result = instance.atomic_validate_and_log_token
|
|
22
|
-
|
|
23
|
-
case result
|
|
24
|
-
when 'stale'
|
|
25
|
-
raise StaleToken
|
|
26
|
-
when 'used'
|
|
27
|
-
raise UsedToken
|
|
28
|
-
when 'ok'
|
|
29
|
-
true
|
|
30
|
-
else
|
|
31
|
-
raise InvalidToken, "Unexpected validation result: #{result}"
|
|
32
|
-
end
|
|
16
|
+
instance.validate!
|
|
33
17
|
end
|
|
34
18
|
end
|
|
35
19
|
|
|
@@ -38,6 +22,26 @@ module RobustServerSocket
|
|
|
38
22
|
@client = nil
|
|
39
23
|
end
|
|
40
24
|
|
|
25
|
+
def validate!
|
|
26
|
+
raise InvalidToken unless decrypted_token
|
|
27
|
+
raise UnauthorizedClient unless client
|
|
28
|
+
|
|
29
|
+
RateLimiter.check!(client)
|
|
30
|
+
|
|
31
|
+
result = atomic_validate_and_log_token
|
|
32
|
+
|
|
33
|
+
case result
|
|
34
|
+
when 'stale'
|
|
35
|
+
raise StaleToken
|
|
36
|
+
when 'used'
|
|
37
|
+
raise UsedToken
|
|
38
|
+
when 'ok'
|
|
39
|
+
true
|
|
40
|
+
else
|
|
41
|
+
raise InvalidToken, "Unexpected validation result: #{result}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
41
45
|
def valid?
|
|
42
46
|
!!(decrypted_token &&
|
|
43
47
|
client &&
|
data/lib/robust_server_socket.rb
CHANGED
data/lib/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: robust_server_socket
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- tee_zed
|
|
@@ -75,6 +75,7 @@ files:
|
|
|
75
75
|
- ".rspec"
|
|
76
76
|
- CODE_OF_CONDUCT.md
|
|
77
77
|
- LICENSE.txt
|
|
78
|
+
- README.en.md
|
|
78
79
|
- README.md
|
|
79
80
|
- Rakefile
|
|
80
81
|
- lib/robust_server_socket.rb
|