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 suppressAfterAttempts globally 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-8588 from customization/nusz
  • Ticket: SLANUSZ-25 / FKITDEV-8588
  • Tech Stack: Node.js, Jest, nconf-based config (config/docker.json)

Risk Assessment

Validator Summary

ValidatorRatingDetails
SecurityLOW-MEDIUMCleanup 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.
ConfigLOW-MEDIUMJSON 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.
TestsLOWExisting tests unaffected (no overrides in their config). 6 new test cases cover override behavior, fallback, and cooldown.

Additional Risk Table

Risk AreaRatingMitigation
Backward compatibility: missing overrides keyNONEFalls back to global defaults; no existing config breaks
Global filter (no tag): WebServer.js:35NONE_getConfigForTag(undefined) returns global defaults
Existing test breakageNONEExisting tests don’t use overrides; behavior unchanged without config
Config schema rejectionLOWnconf doesn’t validate schemas; unknown keys pass through

File Map

ActionFileResponsibility
Modifyserver/service/IpFilterService.jsAdd override resolution logic
Modifyconfig/docker.jsonAdd overrides to security.ipFilter
Modifyconfig/dev.jsonAdd overrides to security.ipFilter (dev parity)
Modifytest/tests/unit/services/ip-filter-service.test.jsAdd tests for override behavior
Modifydocs/config/schemas/security.schema.jsonAdd 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-8588

Task 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:

  1. Per-tag override is used — 6 screenshot attempts do NOT throttle (limit 100), but 6 email attempts DO throttle (limit 5)
  2. Fallback to global — tag without override uses global suppressAfterAttempts
  3. Missing overrides key — no overrides in config at all, global behavior unchanged
  4. Override cooldown — throttled tag uses per-tag coolDownPeriodMs (10s), released after override cooldown
  5. Global cooldown for non-overridden tag — tag without override still uses global 300s cooldown
  6. Cleanup interval tag-awareness — cleanup uses monitoredIP.tag to resolve correct throttle period

Key Code Changes in IpFilterService

  1. Constructor reads overrides from config (defaults to {})
  2. New _getConfigForTag(tag) method — resolves override values with global fallback
  3. New helper methods: _getThrottlePeriodMs(tag), _getSuppressAfterAttempts(tag), _getCoolDownPeriodMs(tag)
  4. New _attemptsFilterForTag(attempts, tag) — uses per-tag throttle period
  5. monitorIp stores tag on monitor object for cleanup interval access
  6. monitorIp uses _getCoolDownPeriodMs(tag) for cooldown
  7. shouldThrottle uses per-tag helpers
  8. Cleanup interval uses monitoredIP.tag to resolve correct throttle period
  9. Original attemptsFilter kept 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:

  1. Remove overrides key from config — IpFilterService falls back to global defaults automatically
  2. 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 suppressAfterAttempts globally to 50+ in config/docker.json. This weakens login protection globally but unblocks screenshot capture. The proper fix (this plan) separates the thresholds per tag.

  • 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