Visitar URL original
fix(security): honour the failed-login time window by MaxFreedomPollard · Pull Request #180 · jhd3197/ServerKit · GitHub
Skip to content

fix(security): honour the failed-login time window - #180

Merged
jhd3197 merged 1 commit into
jhd3197:devfrom
MaxFreedomPollard:fix/failed-logins-time-window
Oct 24, 2026
Merged

jhd3197 merged 1 commit into
jhd3197:devfrom
MaxFreedomPollard:fix/failed-logins-time-window

Conversation

@MaxFreedomPollard

Copy link
Copy Markdown
Contributor

Description

The Security → Events tab asks for the last 24 hours of failed logins (api.getFailedLogins(24) in frontend/src/components/security/EventsTab.jsx:38) and shows the result under "Failed login attempts (24h)". The backend ignores the window and counts every failure still in the auth log.

Reproduction. Put five failures in /var/log/auth.log: three from 2 to 5 days ago and two from the last two hours. GET /api/v1/security/failed-logins?hours=24 returns failed_attempts: 5 and alert_triggered: true (the default threshold is 5), and sends the alert "High number of failed login attempts: 5 in the last 24 hours". The correct result is 2, with no alert. With the usual weekly rotation of auth.log and secure, the card can count up to a week of SSH attempts, and on an internet-facing server that keeps the alert firing on old attempts.

Cause. SecurityService.check_failed_logins (backend/app/services/security_service.py:231, as of dev at 0d1f594) computes cutoff_time from since_hours at line 245 and never reads it again. Every line matching Failed password or authentication failure is appended (line 250), whatever its date. The hours query parameter (backend/app/api/security.py:88) and the "in the last {since_hours} hours" alert text (line 260) both describe a window the code never applies. The function has not changed since it was added.

Fix. Each matching line is dated from its timestamp, and lines older than the cutoff are skipped:

  • RFC 3339 stamps (2026-10-22T09:15:02.123456+02:00, rsyslog's high-precision format and the default on recent Ubuntu releases) are parsed with datetime.fromisoformat and converted to local time.
  • Traditional BSD stamps (Oct 2 09:15:02, still written on Debian, the RHEL family and by syslog-ng) are local time with no year. The parser uses the current year and steps back one year if that would put the line more than a day in the future, which is what happens to December lines read in January. Month names are matched against a fixed English table, the way bandwidth_service.py and request_metrics_service.py already handle nginx's $time_local, so the process locale does not matter.
  • A line whose stamp matches neither format is still counted. A host with a custom rsyslog template keeps today's behaviour instead of showing zero failures.

The hardcoded log paths move to a class attribute, SecurityService.AUTH_LOGS, so the tests can point at a temp file. Out of scope: under UsePAM yes, one failed SSH password usually logs both a pam_unix authentication failure line and an sshd Failed password line. The check counts both, as it did before this change.

Related Issues

None.

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • 🚀 New feature (non-breaking change which adds functionality)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🎨 UI/UX improvement

How Has This Been Tested?

backend/tests/test_failed_logins_window.py adds six tests. Each writes a temp auth log, points AUTH_LOGS at it, isolates the security config and captures alerts instead of sending them.

  • Failures on both sides of the cutoff, once with BSD stamps and once with RFC 3339 stamps: only the recent ones are counted, and no alert fires.
  • The window follows the hours value passed in (24, 72 and 168).
  • The alert reports the count inside the window ("6 in the last 24 hours", not 26).
  • A Dec 31 stamp read on 1 January belongs to the previous year.
  • A line with an unrecognised stamp is still counted.

I ran the tests against dev with only the AUTH_LOGS hoist applied, which does not change behaviour. They fail on the bug: assert 5 == 2 (BSD and RFC 3339), assert 3 == 1, assert 26 == 6. The year-rollover test errors because the helper does not exist yet. The unrecognised-stamp test passes on both sides by design. With the fix, python -m pytest tests/test_failed_logins_window.py -q gives 6 passed. The tests use the real clock, so they also pass under TZ=UTC, Asia/Kolkata, America/New_York, Pacific/Kiritimati and Pacific/Pago_Pago, and under Python 3.13.

Other checks:

  • Every test file that touches SecurityService (test_security_suite_extraction, test_file_integrity, test_ssh_keys_roundtrip, test_fail2ban_jail_service) passed. So did the source-scanning ratchets, test_route_authz_static, test_route_authz_sweep, test_authz_structural_sweep, test_api_surface_inventory and test_env_var_doc_guard: 314 passed, 2 skipped in total.
  • ruff check --select F821,F823 app tests is clean.
  • python tests/check_test_count.py reports collected=5884 baseline=5279.
  • Bandit 1.9.3 reports the same five low-severity findings on security_service.py as on dev, and none in the new code.

All runs used Python 3.11.17 on macOS. I did not run the complete suite end to end; the files listed above are every test that reaches SecurityService, plus the repository-wide guards. No route was added or removed.

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added a concise user-facing note under Unreleased in CHANGELOG.md, or explained why this change needs no release note
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes

Shell scripts only (install.sh, uninstall.sh, serverkit, scripts/**)

Not applicable: no shell scripts are touched.

🤖 Generated with Claude Code

check_failed_logins computed a cutoff from since_hours and never used
it, so the Security -> Events "Failed login attempts (24h)" card and
its alert counted every failure still in /var/log/auth.log or
/var/log/secure: up to a week of SSH attempts under the usual weekly
log rotation.

Each matching line is now dated from its stamp, either RFC 3339
(rsyslog's high-precision format) or the traditional BSD syslog stamp
(no year, so a December line read in January is placed in the previous
year), and lines older than the cutoff are skipped. A line whose stamp
is not recognised is still counted rather than hidden. The log paths
move to SecurityService.AUTH_LOGS so the tests can point at a temp
file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jhd3197
jhd3197 merged commit 3de85c2 into jhd3197:dev Oct 24, 2026
17 checks passed
jhd3197 added a commit that referenced this pull request Oct 24, 2026
* fix(security): honour the failed-login time window (#180)

check_failed_logins computed a cutoff from since_hours and never used
it, so the Security -> Events "Failed login attempts (24h)" card and
its alert counted every failure still in /var/log/auth.log or
/var/log/secure: up to a week of SSH attempts under the usual weekly
log rotation.

Each matching line is now dated from its stamp, either RFC 3339
(rsyslog's high-precision format) or the traditional BSD syslog stamp
(no year, so a December line read in January is placed in the previous
year), and lines older than the cutoff are skipped. A line whose stamp
is not recognised is still counted rather than hidden. The log paths
move to SecurityService.AUTH_LOGS so the tests can point at a temp
file.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* refactor: separate backend startup phases and guard ordering

* refactor: route file operations through capability-checked adapters

* chore: bump version to 1.11.25 [skip ci]

* refactor: split File Manager state and views with regression guards

* test: enforce core boundaries and document extension rules

* test: cover File Manager tree races and listing failures

* fix: preserve File Manager deep links on initial mount

* refactor: keep File Manager download operations in the action hook

* fix: start agent maintenance only in the guarded runtime phase

* test: capture nginx rendering contract before extraction

* refactor: separate nginx configuration rendering from host operations

* test: enforce pure nginx configuration boundaries

* fix: require provisioned runtime before advertising previews

* feat: expose value-free effective environment provenance through app access boundary

* feat: add effective environment inspector with browser regression checks

* feat: provision isolated generic previews with retryable resource cleanup

* feat: queue preview automation and preserve cleanup ownership through app deletion

* feat: add pinned preview deployment controls and queued lifecycle feedback

* fix: preserve container port semantics for previews of slot deployments

* fix: preserve search focus and ignore stale list responses

* feat: run approved assistant deployments through persistent recipe operations

* test: run search and assistant deployment regressions in CI

* fix: refresh preview API and controller boundary inventories

* docs: refresh controller migration count after preview extraction

* fix: bind file editor previews to their storage target

* chore: bump version to 1.11.26 [skip ci]

---------

Co-authored-by: Max Freedom Pollard <272618364+MaxFreedomPollard@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants