* feat(settings): expose the API rate limiter in the Security tab The general per-IP limiter had six settings in app_settings and a backend route to write them, and no screen. Installs ran on the code fallback — 300 requests per 15 minutes per IP — with no way to see it, which is how issue 1287 played out: a 546-photo gallery exhausted the budget for one viewer and the operator learned about the setting from a grep of the backend log. Security tab: a card with the six settings, the validation ranges the route enforces, a one-line explanation per field, and a note that the unit is the client IP — an office or household behind one NAT shares a budget, and behind a proxy TRUST_PROXY has to cover the proxy or every visitor shares its address. The tab's Save button saves the limiter through its own route. The limiter values are checked against the route's ranges before anything is written and the limiter is written first, so a rejected value cannot leave the password/session settings half-saved behind a failure toast. Backend, three things the screen needed: - The settings read fills the six keys with the code defaults when they have no row, so the form shows the budget in force rather than an empty field; the defaults live in one exported constant the limiter itself reads. - The write route upserts instead of updating: on a fresh install, which has no rows, the old UPDATE matched nothing and the route answered 200 while changing nothing. - The live limiter instances move into rateLimitService and the write route rebuilds them. express-rate-limit fixes windowMs when an instance is built — max and skip re-read the settings per request, the window does not — so a saved window used to apply only after a restart. The gates in server.js resolve the instance per request through the service's getters. A rebuild starts fresh counters, which on a settings change is acceptable. The limiters get explicit MemoryStores and a rebuild shuts the superseded ones down, because a store keeps a cleanup interval alive for as long as it exists and dropping the reference alone would leak one timer per save. Tests: the read surfaces defaults and honours the key filter; the write creates rows on a fresh database, the limiter sees the values immediately and hands the gates a fresh instance; existing rows are updated not duplicated; out-of-range values are rejected. The tab renders the values, edits through the hook state, carries the ranges, saves with the tab's button, and the pre-write validation accepts the bounds and rejects outside them and cleared fields. Relates to issue 1337 * docs(security): point the rate limiter doc at the Security tab and the upsert --------- Co-authored-by: Paul Nothaft <paul@MacStudio-von-Paul.local>
5.5 KiB
Security Logging Documentation
Overview
This document describes the comprehensive security logging implemented in the PicPeak application to track authentication failures, rate limiting, and suspicious activities.
Log Files
1. security.log
- Location:
logs/security.log - Contains: All security-related events (authentication, rate limiting, suspicious activity)
- Max Size: 20MB with rotation (keeps 10 files)
- Format: JSON with timestamp
2. error.log
- Location:
logs/error.log - Contains: All error-level logs including auth failures
- Max Size: 10MB with rotation (keeps 5 files)
3. combined.log
- Location:
logs/combined.log - Contains: All logs (info, warn, error)
- Max Size: 50MB with rotation (keeps 10 files)
Security Events Logged
Rate Limiting
When rate limits are exceeded, the following is logged:
{
"timestamp": "2024-01-18 14:23:45.123",
"level": "warn",
"message": "Rate limit exceeded",
"security": true,
"ip": "192.168.1.1",
"path": "/api/admin/login",
"method": "POST",
"authenticated": false,
"userAgent": "Mozilla/5.0...",
"referer": "https://app.example.com",
"origin": "https://app.example.com",
"headers": {
"x-forwarded-for": "192.168.1.1",
"x-real-ip": "192.168.1.1"
},
"requestUrl": "/api/admin/login",
"rateLimitInfo": {
"limit": 5,
"current": 6,
"remaining": 0,
"resetTime": "2024-01-18T14:38:45.123Z"
}
}
Authentication Failures
Admin Login Failures
- Tracked in
login_attemptstable - Logged with: IP address, username, user agent, timestamp
- Account lockout after 5 failures in 15 minutes
Gallery Password Failures
- Tracked in
access_logstable with action='login_fail' - Logged with: event_id, IP address, user agent
- Gallery lockout after 5 failures in 15 minutes
JWT Validation Failures
{
"timestamp": "2024-01-18 14:23:45.123",
"level": "warn",
"message": "JWT validation failed",
"ip": "192.168.1.1",
"path": "/api/admin/events",
"method": "GET",
"userAgent": "Mozilla/5.0...",
"error": "TokenExpiredError",
"message": "jwt expired"
}
Suspicious Activity
- Multiple IPs attempting login for same account
- Token usage from different IP than issued
- Token usage after password change
- Revoked token usage attempts
Configuration Settings
The general limiter reads these keys from app_settings (cached for 60 seconds). They are edited in the admin panel under Settings → Security (the API rate limiter card), which saves through PUT /api/admin/settings/security/rate-limit (all six fields required). The route upserts, so a fresh install needs no rows first, and the live limiters are rebuilt on save, so a new window applies without a restart. The defaults below are what applies when a key has no row; the settings read fills them in, so the form shows the budget in force.
| Setting | Default | Range | Description |
|---|---|---|---|
| rate_limit_enabled | true | - | Enable/disable rate limiting |
| rate_limit_window_minutes | 15 | 1-60 | Time window for rate limit |
| rate_limit_max_requests | 300 | 10-10000 | Per-IP budget for /api/ requests that are not exempt |
| rate_limit_auth_max_requests | 5 | 1-100 | Per-IP budget of failed admin-login / gallery-verify attempts (own bucket) |
| rate_limit_skip_authenticated | true | - | Admin sessions are exempt; since v3.127.0-beta.0 a verified gallery viewer's image requests (thumbnail, preview, hero, photo) are exempt too |
| rate_limit_public_endpoints_only | false | - | Only rate limit /api/public/* and /api/gallery/* |
The limiter keys on the client IP as Express reports it, so behind a proxy TRUST_PROXY has to cover that proxy or every visitor shares one budget. Several people behind one NAT (an office, a household, carrier NAT) share a budget by design; before v3.127.0-beta.0 a large gallery could exhaust it for a single viewer, which surfaced as blank tiles with no error (issue 1287).
Database Tables
login_attempts
- id
- username
- ip_address
- user_agent
- success (boolean)
- created_at
access_logs
- id
- event_id
- ip_address
- user_agent
- action ('view', 'download', 'login_success', 'login_fail')
- photo_id (nullable)
- created_at
Environment Variables
LOG_LEVEL: Set logging level (default: 'info')LOG_TO_CONSOLE: Enable console logging in production (default: false)
Monitoring Recommendations
-
Set up alerts for:
- Rate limit exceeded events (possible DDoS)
- Multiple failed login attempts from same IP
- Account lockout events
- JWT validation failures spike
-
Regular review:
- Check security.log for patterns
- Review login_attempts table for brute force attempts
- Monitor access_logs for suspicious gallery access patterns
-
Log analysis tools:
- Use log aggregation tools (ELK stack, Splunk)
- Set up dashboards for security metrics
- Configure alerts for threshold breaches
Production Deployment Notes
- Ensure logs directory has proper permissions
- Set up log rotation outside of application if needed
- Consider shipping logs to centralized logging service
- Monitor disk space for log files
- Set
LOG_TO_CONSOLE=truefor container deployments
Security Best Practices
- Never log sensitive data (passwords, tokens)
- Use generic error messages to prevent user enumeration
- Clean up old login attempts regularly (7 days retention)
- Monitor for unusual patterns in real-time
- Keep rate limit settings appropriate for your usage