Skip to content

fix(enforce): round a positive remaining TTL up — never 0, which the helper reads as permanent (#615) - #620

Merged
evertramos merged 4 commits into
devfrom
fix/issue-615-subsecond-ttl
Sep 15, 2026
Merged

evertramos merged 4 commits into
devfrom
fix/issue-615-subsecond-ttl

Conversation

@evertramos

Copy link
Copy Markdown
Owner

What & why

Closes #615 (Phase 2 of #605, invariant A2/A5).

NftablesEnforcer sent TTLSeconds: int64(t.TTL.Seconds()) from Ban, the Sync add pass and the covered-add retry. A ban with less than one second remaining when a reconcile ran truncated to 0, and 0 means permanent to the helper (element without timeout, cache deadline zero). The helper's own migration and rollback paths already rounded up; the client did not.

Acceptance criteria restated:

  • Harness: a store row with 600 ms left, real enforcer client + real helper + scripted kernel → after Reconcile the element is never permanent and is gone two seconds later (TestBanLifecycle_SubSecondTTLNeverBecomesPermanent; fails on dev before the fix, commit 1).

Changes

  • internal/enforce/nftables.go: ttlSeconds() — 0 stays 0 (permanent), positive rounds up and clamps at 1 s; used by Ban, Sync add and the covered retry.
  • internal/enforce/ttl_test.go: table test on the request the mock helper records (600 ms → 1, 90.2 s → 91, 1 h → 3600, 0 → 0) and the Sync path.
  • docs/internal/INVARIANTS.md: A2/A5 updated.

Feed elements (feeds.go) keep their own whole-minute TTLs; unchanged.

Tests

  • Unit tests added/updated
  • New parser/rule? fixture — N/A
  • Parser change? fuzz — N/A
  • make lint test green locally (-race): go test -race ./... ok; gofmt -l . empty; go vet ./... ok; golangci-lint run ./... (v2 image) 0 issues; ip-hygiene gate clean.

Blast radius (per docs/internal/INVARIANTS.md)

  • Invariant(s): A2 (a ban lasts exactly its TTL in every layer), A5 (reconcile never creates drift).
  • Other readers/writers re-verified: store ActiveBans remaining-TTL computation unchanged (store_test, e2e A scenarios); helper add deadline = nowFn + TTLSeconds unchanged (enforcerd tests; e2e StrikeTTLReaperLadder asserts kernel expiry == ladder TTL for whole-second TTLs); deferred retry (gatedban.go:storedBanTarget) feeds Ban → covered by TestBanLifecycle_GateRefusalIsDeferredNotLost (remaining ≈ 4m50s → 290 s); edge enforcers consume no TTL (unchanged); the reconcile's repair accounting unchanged (sync_repair_test). Rounding up by < 1 s lengthens a ban by at most 999 ms — inside the reaper's one-minute tolerance and never past the store's own view as read by the helper's next list.
  • Harness scenario that failed before the fix: internal/e2e.TestBanLifecycle_SubSecondTTLNeverBecomesPermanent.

Security review (per docs/internal/SECURITY-REVIEW.md)

  • §1 Input handling: N/A.
  • §2 Decision engine (lock-out / false-ban): OK — only the wire encoding of an already-decided TTL changes; a gated/allowlisted target never reaches this code. No permanent element can now be created by a timing accident, which removes a lock-out vector against an address that was banned for minutes.
  • §3 Privilege separation / enforcer: OK — same verb, same helper; the helper already clamps in its own paths.
  • §4 Secrets: N/A. §5 AI: N/A. §6 Control surfaces: N/A. §7 Plugins: N/A. §8 Edge: N/A. §9 Dependencies: math from stdlib.
  • §10 Logging / audit / fail-safe: OK — unchanged. §10 self-review walked (secs < 1 clamp is reachable only for 0 < ttl < 1s, where Ceil already yields 1 — kept as an explicit guard against a future change to the rounding).

Self-assessment: no step toward lock-out, injection, privilege escalation, secret exfiltration or evasion.

Checklist

  • Follows AGENTS.md Hard Rules
  • No hardening systemd directive removed
  • Docs updated (INVARIANTS.md)
  • New dependency justified (none)

🤖 Generated with Claude Code

https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k

evertramos and others added 2 commits September 11, 2026 17:58
…es a permanent kernel element

Refs #615

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k
…ver 0, which the helper reads as permanent (#615)

Ban, Sync and the covered-add retry sent int64(TTL.Seconds()); a ban
with under a second left at reconcile time truncated to 0, and 0 means
"no timeout" to the helper, so the kernel got a permanent element for an
IP whose ban was over. ttlSeconds() keeps 0 for permanent, ceils
anything positive and clamps at 1 s.

Closes #615

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k
Copilot AI lite review requested due to automatic review settings September 11, 2026 21:01
@strix-security

Copy link
Copy Markdown

Strix is installed on this repository, but we couldn't run this PR security review because this workspace's trial has ended. Add a card to resume code reviews here.

So far, Strix has reviewed 49 pull requests, surfaced 8 security issues (1 critical/high) and blocked 3 risky merges across this workspace.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@amazon-q-developer amazon-q-developer Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fix correctly addresses issue #615 by ensuring sub-second TTLs never truncate to 0, which the helper interprets as permanent. The implementation properly rounds up positive TTLs to at least 1 second using math.Ceil, includes comprehensive test coverage at both unit and e2e levels, and updates the relevant invariant documentation. The defensive check at line 152 is documented as protection against future changes rather than a logic error.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

… becoming permanent (#615 hardening)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KCu1p3UrUGSzWVwhNuJ47k
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