FKITDEV-8588: IpFilterService Per-Tag Throttle Overrides
Implementation plan for adding per-tag throttle override support to IpFilterService, fixing SLANUSZ-25 where all NUSZ operators behind a single NAT IP get blocked after 5 combined screenshot attempts.
For Agents
This is the proper fix (Fix #3 from Recommended Fixes). A quick fix (bumping
suppressAfterAttemptsglobally or whitelisting the NAT IP) is available but weakens security for all endpoints. This plan keeps global rate limiting intact while relaxing thresholds only for the screenshot tag.
Goal
Extend IpFilterService to read an optional overrides map from config, keyed by tag name. Methods that use global thresholds (monitorIp, shouldThrottle, attemptsFilter, cleanup interval) resolve per-tag values first, falling back to globals. No changes needed to callers (screenshot.js, routes.js, WebServer.js) — they already pass tags.
Architecture
security.ipFilter config
+-- suppressAfterAttempts: 5 (global default)
+-- throttlePeriodMs: 10000 (global default)
+-- coolDownPeriodMs: 600000 (global default)
+-- overrides:
+-- videochat-operator-screenshot:
+-- suppressAfterAttempts: 100
+-- throttlePeriodMs: 60000
+-- coolDownPeriodMs: 60000
Resolution logic: _getConfigForTag(tag) checks this.overrides[tag] first, falls back to global values for any missing keys. When tag is undefined (e.g., WebServer.js global filter), always returns global defaults.
Branch and Ticket
- Branch:
feature/FKITDEV-8588fromcustomization/nusz - Ticket: SLANUSZ-25 / FKITDEV-8588
- Tech Stack: Node.js, Jest, nconf-based config (
config/docker.json)
Risk Assessment
Validator Summary
| Validator | Rating | Details |
|---|---|---|
| Security | LOW-MEDIUM | Cleanup interval needs tag-awareness — addressed in plan. Raised screenshot limit (100/60s) is acceptable because screenshots require authenticated operator session and the endpoint is POST-only with CSRF. |
| Config | LOW-MEDIUM | JSON schema needs update — added to plan (docs/config/schemas/security.schema.json). nconf does not validate schemas at runtime, so missing schema update is non-breaking but should be kept in sync. |
| Tests | LOW | Existing tests unaffected (no overrides in their config). 6 new test cases cover override behavior, fallback, and cooldown. |
Additional Risk Table
| Risk Area | Rating | Mitigation |
|---|---|---|
Backward compatibility: missing overrides key | NONE | Falls back to global defaults; no existing config breaks |
Global filter (no tag): WebServer.js:35 | NONE | _getConfigForTag(undefined) returns global defaults |
| Existing test breakage | NONE | Existing tests don’t use overrides; behavior unchanged without config |
| Config schema rejection | LOW | nconf doesn’t validate schemas; unknown keys pass through |
File Map
| Action | File | Responsibility |
|---|---|---|
| Modify | server/service/IpFilterService.js | Add override resolution logic |
| Modify | config/docker.json | Add overrides to security.ipFilter |
| Modify | config/dev.json | Add overrides to security.ipFilter (dev parity) |
| Modify | test/tests/unit/services/ip-filter-service.test.js | Add tests for override behavior |
| Modify | docs/config/schemas/security.schema.json | Add overrides property to ipFilter schema |
Not modified: server/web/api/operator/screenshot.js, server/web/routes.js, server/web/WebServer.js — these already pass tags and need no changes.
Implementation Tasks
Task 1: Create Feature Branch
git checkout customization/nusz
git checkout -b feature/FKITDEV-8588Task 2: Add Override Resolution to IpFilterService
This is the core change. TDD approach: write failing tests first, then implement.
New Tests (6 cases)
Added to test/tests/unit/services/ip-filter-service.test.js:
- Per-tag override is used — 6 screenshot attempts do NOT throttle (limit 100), but 6 email attempts DO throttle (limit 5)
- Fallback to global — tag without override uses global
suppressAfterAttempts - Missing overrides key — no
overridesin config at all, global behavior unchanged - Override cooldown — throttled tag uses per-tag
coolDownPeriodMs(10s), released after override cooldown - Global cooldown for non-overridden tag — tag without override still uses global 300s cooldown
- Cleanup interval tag-awareness — cleanup uses
monitoredIP.tagto resolve correct throttle period
Key Code Changes in IpFilterService
- Constructor reads
overridesfrom config (defaults to{}) - New
_getConfigForTag(tag)method — resolves override values with global fallback - New helper methods:
_getThrottlePeriodMs(tag),_getSuppressAfterAttempts(tag),_getCoolDownPeriodMs(tag) - New
_attemptsFilterForTag(attempts, tag)— uses per-tag throttle period monitorIpstorestagon monitor object for cleanup interval accessmonitorIpuses_getCoolDownPeriodMs(tag)for cooldownshouldThrottleuses per-tag helpers- Cleanup interval uses
monitoredIP.tagto resolve correct throttle period - Original
attemptsFilterkept for backward compatibility
Task 3: Add Config Override for NUSZ Screenshot
Add to both config/docker.json and config/dev.json:
"ipFilter": {
"uncheckedIps": "",
"suppressAfterAttempts": 5,
"throttlePeriodMs": 10000,
"coolDownPeriodMs": 600000,
"overrides": {
"videochat-operator-screenshot": {
"suppressAfterAttempts": 100,
"throttlePeriodMs": 60000,
"coolDownPeriodMs": 60000
}
}
}Config Value Rationale
suppressAfterAttempts: 100— allows 100 screenshots per minute from same IP (NUSZ has ~20 operators behind NAT)throttlePeriodMs: 60000— 1-minute sliding window (vs 10s global)coolDownPeriodMs: 60000— 1-minute cooldown (vs 10-minute global); screenshots are authenticated so lower risk
Task 4: Manual Verification
- Run full test suite, confirm no regressions
- Verify no-tag requests still use global behavior
- Verify screenshot-tagged requests use override thresholds
Rollback Plan
If issues arise in production:
- Remove
overrideskey from config —IpFilterServicefalls back to global defaults automatically - No code rollback needed; the code change is backward-compatible
Quick Fix (Interim)
Quick Fix vs Proper Fix
If the NUSZ deployment needs immediate relief before this plan is implemented, bump
suppressAfterAttemptsglobally to 50+ inconfig/docker.json. This weakens login protection globally but unblocks screenshot capture. The proper fix (this plan) separates the thresholds per tag.
Related
- SLANUSZ-25 — Root cause investigation (this plan implements Fix #3)
- FaceKom — Platform overview
- vuer_oss — Backend architecture (IpFilterService in Services Layer)
- security-audit — Rate limiting findings; this change refines per-endpoint thresholds