Skip to content

Add shared jail feature#258

Open
xavernitsch wants to merge 4 commits intotomMoulard:mainfrom
xavernitsch:main
Open

Add shared jail feature#258
xavernitsch wants to merge 4 commits intotomMoulard:mainfrom
xavernitsch:main

Conversation

@xavernitsch
Copy link
Copy Markdown

@xavernitsch xavernitsch commented Mar 5, 2026

With the help of AI I was able to implement the shared jail feature #256

I hope you and some others like it

Summary by CodeRabbit

  • New Features

    • Added an option to share a jail across multiple routers so bans propagate across associated instances/subdomains.
  • Documentation

    • Added a "Shared Jail" subsection with YAML example and behavior notes.
  • Tests

    • Added tests validating shared-jail behavior, including successive-request scenarios across handlers.
  • Chores

    • Improved logging around creation and reuse of jail instances.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 5, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds optional shared-jail support to the fail2ban middleware: a package-level shared jail registry with mutex, a new Config.SharedJail flag, New(name string, ...) updated to use shared or per-instance jails, README docs updated, and tests added to verify shared vs. per-instance behavior.

Changes

Cohort / File(s) Summary
Documentation
README.md
Added "Shared Jail" section and YAML example documenting sharedJail: true and that bans propagate across subdomains when enabled.
Core Implementation
fail2ban.go
Added globalJails map and globalMu mutex; added Config.SharedJail bool; updated New to accept name string; added getOrCreateSharedJail using SHA-256 keyed by name+config and bounded registry eviction; introduced crypto/sha256 and sync imports.
Tests
fail2ban_test.go
Added const testRemoteAddr = "10.0.0.0", imported crypto/sha256, and added TestFail2Ban_SuccessiveRequests_SharedJail to assert ban propagation between two handler instances sharing a computed jail key; test cleans relevant globalJails entry.
Manifest
go.mod
Module manifest lines updated (small manifest change noted).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Client as Client
    participant H1 as Handler A
    participant Global as globalJails (map + globalMu)
    participant Jail as Fail2Ban Jail
    participant H2 as Handler B

    Client->>H1: HTTP request
    H1->>Global: lock globalMu / lookup or create jail (key: name+config hash)
    Global-->>H1: return Jail
    H1->>Jail: check ban / record failure
    Jail-->>H1: banned? / allow or block
    H1->>Client: response

    Note over Client,H2: Subsequent request via another middleware instance
    Client->>H2: HTTP request
    H2->>Global: lock globalMu / lookup jail
    Global-->>H2: return same Jail (if SharedJail)
    H2->>Jail: check ban
    Jail-->>H2: banned? / allow or block
    H2->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I found a shared little key,
A map, a lock, and one small tree,
Two routers hop into the same jail,
Bans echo lightly down the trail,
Hooray — one mutex, one shared spree.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add shared jail feature' directly and clearly summarizes the main change: introducing a new shared jail feature as documented in README, implemented in fail2ban.go, and tested in fail2ban_test.go.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.11.4)

Error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions
The command is terminated due to an error: can't load config: unsupported version of the configuration: "" See https://golangci-lint.run/docs/product/migration-guide for migration instructions


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fail2ban_test.go`:
- Around line 519-537: Two table-driven test cases share the same "name" value
which makes test failures ambiguous; update the second test case's name field to
a unique, descriptive string (e.g., differentiate by "shared jail" vs "separate
jail" or include an index) so subtest names are distinct. Locate the duplicate
"name" fields in the table of test cases (entries that include cfg: &Config{...}
and rules.Rules) and rename the second occurrence to a different string, keeping
the rest of the test data (cfg, handlerStatus, expectStatus, expectStatusSecond)
unchanged.
- Around line 554-565: The subtest must use a unique jail key per run and
fail-fast on setup errors: replace the fixed string "fail2ban_test" passed to
New(...) with a deterministic per-subtest key (e.g., include test.name or
t.Name()) to avoid shared-jail state leakage, and check the returned error from
both calls to New (handler, err := New(...)) and (handler2, err := New(...)) and
call t.Fatalf (or t.Fatal) if err != nil so the test stops immediately on setup
failure.

In `@fail2ban.go`:
- Around line 148-157: The current SharedJail logic reuses an existing f2b from
globalJails keyed only by `name`, which silently ignores new
`rules`/`allowNetIPs`; modify the logic in the block that references
`SharedJail`, `jailKey`, `globalMu`, `globalJails`, and `fail2ban.New` to detect
config drift or include the config in the key: either compute a stable
fingerprint (hash) of `rules` and `allowNetIPs` and use `name + ":" +
fingerprint` as the `jailKey`, or load the existing `f2b` and compare its stored
config to the incoming `rules`/`allowNetIPs` and return an error if they differ;
ensure access is still protected by `globalMu` and that creation still calls
`fail2ban.New(rules, allowNetIPs)` when no matching keyed jail exists.

In `@README.md`:
- Around line 55-56: The markdown heading "### Shared Jail" is not followed by a
blank line which triggers markdownlint rule MD022; fix it by inserting a single
empty line immediately after the "### Shared Jail" heading in README.md so the
heading is separated from the following paragraph content and the lint rule
passes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 931bb141-7ec2-44ba-a0b0-a1ac529e51ce

📥 Commits

Reviewing files that changed from the base of the PR and between de2a436 and 07bc3b3.

📒 Files selected for processing (3)
  • README.md
  • fail2ban.go
  • fail2ban_test.go

Comment thread fail2ban_test.go Outdated
Comment thread fail2ban_test.go Outdated
Comment thread fail2ban.go Outdated
Comment thread README.md
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fail2ban.go`:
- Around line 152-153: The json.Marshal error is ignored when creating jailKey
(keyBytes, _ := json.Marshal(config)); instead capture the error and handle it:
call json.Marshal into (keyBytes, err), and if err != nil either return the
error up from the function or log it and fail fast so you don't compute md5.Sum
on nil bytes; then proceed to compute jailKey := fmt.Sprintf("%s-%x", name,
md5.Sum(keyBytes)) only when keyBytes is valid. Ensure you reference the
existing names json.Marshal, keyBytes, err, jailKey, md5.Sum, name, and config
when implementing the fix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: db6e6350-b6b9-4e20-951b-af811c0788b8

📥 Commits

Reviewing files that changed from the base of the PR and between 07bc3b3 and 4a4b223.

📒 Files selected for processing (3)
  • README.md
  • fail2ban.go
  • fail2ban_test.go

Comment thread fail2ban.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fail2ban_test.go`:
- Around line 560-562: The cleanup is deleting the wrong key: tests currently
call delete(globalJails, jailKey) but shared jails are stored under a
"name-hash" key. While holding globalMu, replace the deletion to remove the
actual shared-jail key (e.g., compute and delete fmt.Sprintf("%s-%s", name,
hash) or call the existing helper used to build the shared key) instead of
jailKey so globalJails is truly cleaned up; ensure you use the same name and
hash values that were used to create the shared jail.

In `@fail2ban.go`:
- Around line 150-163: The shared-jail map (globalJails) grows unbounded because
every distinct config hash creates a new entry in the block inside the
config.SharedJail branch; change this to use a bounded-retention strategy: when
inserting a new jailKey in the else branch (where fail2ban.New is called and
globalJails[jailKey] = f2b) maintain eviction metadata (e.g., lastUsed timestamp
or an LRU list) and evict old/stale entries when the map exceeds a configured
max size or when entries age out (implement eviction under globalMu to preserve
concurrency safety); ensure accesses (lookup, update of lastUsed, insert, and
eviction) are all protected by globalMu and consider a background cleanup
goroutine to remove expired entries if using TTLs so stale jails are reclaimed.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6121fed7-e7ce-43c6-90ce-c9dc88875b90

📥 Commits

Reviewing files that changed from the base of the PR and between 4a4b223 and 6e1923b.

📒 Files selected for processing (2)
  • fail2ban.go
  • fail2ban_test.go

Comment thread fail2ban_test.go
Comment thread fail2ban.go Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@fail2ban_test.go`:
- Around line 559-563: The cleanup deletes the wrong key because the test builds
jailKey as t.Name()-hash(config) but New(name) calls getOrCreateSharedJail which
stores a key as name-hash(config) again, so the stored map key is
name-hash(config)-hash(config) (i.e. double-hashed); fix by making the test use
the same key-generation logic as getOrCreateSharedJail: either call
New(t.Name()) (so stored key matches computed jailKey) or compute the full
stored key for deletion by calling the same formatting and sha256.Sum256 on the
config used by New(); update references to jailKey, New(), getOrCreateSharedJail
and globalJails accordingly so the delete removes the actual map entry.

In `@README.md`:
- Around line 57-64: The README incorrectly says routers "using the same
middleware name" share a jail; update the docs to state that sharedJail requires
both the same middleware name and identical middleware configuration because
fail2ban constructs the key as jailKey := fmt.Sprintf("%s-%x", name,
sha256.Sum256([]byte(fmt.Sprintf("%v", config)))) in fail2ban.go, so only
routers with the same name and an identical config hash will share the jail;
mention the use of sha256.Sum256 and the config hashing so readers know
configuration must match exactly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b4a9c65a-dd38-4485-a4c3-ad4611e166d9

📥 Commits

Reviewing files that changed from the base of the PR and between 5993ff4 and 8f797c9.

📒 Files selected for processing (3)
  • README.md
  • fail2ban.go
  • fail2ban_test.go

Comment thread fail2ban_test.go
Comment on lines +559 to +563
jailKey := fmt.Sprintf("%s-%x", t.Name(), sha256.Sum256([]byte(fmt.Sprintf("%v", test.cfg))))

globalMu.Lock()
delete(globalJails, jailKey)
globalMu.Unlock()
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Test cleanup is ineffective - deletes wrong key from globalJails.

The test computes jailKey as t.Name()-<configHash> and tries to delete it from globalJails. However, when New() is called with jailKey as the name parameter, getOrCreateSharedJail computes the actual stored key as:

// fail2ban.go line 36
jailKey := fmt.Sprintf("%s-%x", name, sha256.Sum256([]byte(fmt.Sprintf("%v", config))))

This means the actual key in globalJails is jailKey-<anotherHash>, not jailKey. The delete operation is a no-op.

🛠️ Proposed fix

Either pass t.Name() directly to New() and compute the full shared key for cleanup:

-			jailKey := fmt.Sprintf("%s-%x", t.Name(), sha256.Sum256([]byte(fmt.Sprintf("%v", test.cfg))))
+			middlewareName := t.Name()
+			sharedKey := fmt.Sprintf("%s-%x", middlewareName, sha256.Sum256([]byte(fmt.Sprintf("%v", test.cfg))))

 			globalMu.Lock()
-			delete(globalJails, jailKey)
+			delete(globalJails, sharedKey)
 			globalMu.Unlock()
@@
-			handler, err := New(t.Context(), next, test.cfg, jailKey)
+			handler, err := New(t.Context(), next, test.cfg, middlewareName)
 			require.NoError(t, err)
-			handler2, err := New(t.Context(), next, test.cfg, jailKey)
+			handler2, err := New(t.Context(), next, test.cfg, middlewareName)
 			require.NoError(t, err)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@fail2ban_test.go` around lines 559 - 563, The cleanup deletes the wrong key
because the test builds jailKey as t.Name()-hash(config) but New(name) calls
getOrCreateSharedJail which stores a key as name-hash(config) again, so the
stored map key is name-hash(config)-hash(config) (i.e. double-hashed); fix by
making the test use the same key-generation logic as getOrCreateSharedJail:
either call New(t.Name()) (so stored key matches computed jailKey) or compute
the full stored key for deletion by calling the same formatting and
sha256.Sum256 on the config used by New(); update references to jailKey, New(),
getOrCreateSharedJail and globalJails accordingly so the delete removes the
actual map entry.

Comment thread README.md
Comment on lines +57 to +64
By default, each middleware instance has its own jail. To share a jail across multiple routers using the same middleware name, set `sharedJail` to `true`:

```yml
testData:
sharedJail: true
```

When enabled, all routers using the same named middleware will share the same jail, allowing bans to propagate across subdomains.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Documentation understates the shared jail matching criteria.

The documentation states that routers "using the same middleware name" will share a jail. However, the implementation in fail2ban.go (line 36) uses both the middleware name and a hash of the entire config as the key:

jailKey := fmt.Sprintf("%s-%x", name, sha256.Sum256([]byte(fmt.Sprintf("%v", config))))

This means routers must have both the same name and identical configuration to share a jail. Consider updating the documentation to clarify this behavior.

📝 Suggested documentation fix
-By default, each middleware instance has its own jail. To share a jail across multiple routers using the same middleware name, set `sharedJail` to `true`:
+By default, each middleware instance has its own jail. To share a jail across multiple routers using the same middleware name and identical configuration, set `sharedJail` to `true`:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 57 - 64, The README incorrectly says routers "using
the same middleware name" share a jail; update the docs to state that sharedJail
requires both the same middleware name and identical middleware configuration
because fail2ban constructs the key as jailKey := fmt.Sprintf("%s-%x", name,
sha256.Sum256([]byte(fmt.Sprintf("%v", config)))) in fail2ban.go, so only
routers with the same name and an identical config hash will share the jail;
mention the use of sha256.Sum256 and the config hashing so readers know
configuration must match exactly.

Copy link
Copy Markdown
Owner

@tomMoulard tomMoulard left a comment

Choose a reason for hiding this comment

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

Hello @xavernitsch,

Thanks for your contribution!

I am a bit concerned that your solution might cover a small section of it's potential.
For me, the best way to implement this feature would be to implement a mechanism to store the jail on a backend (local, redis, ...), and be able to load the jail content from those backend.

For your use case, it would mean that you setup all your middlewares to use the same backend (e.g., a local file), therefore all middlewares use the same jail.

For other use cases, it would allow sharing jails, loading jails from other providers (e.g., CrowdSec) (#39), or even get jail update notifications (#50, #174, #238).

@xavernitsch
Copy link
Copy Markdown
Author

xavernitsch commented Mar 28, 2026

Hi @tomMoulard,

Thank you for the feedback! I completely see your point. Moving towards a pluggable storage backend (Redis, File, etc.) makes total sense for horizontal scaling and integration with tools like CrowdSec.

For my use case (single Traefik instance, low traffic), a full Redis setup would be overkill, but I agree that the architecture should support it from the start.

How about we compromise on an extensible configuration structure? I could refactor the PR to introduce a "Storage Provider" abstraction. The configuration could look like this:

# Proposed config structure
sharedJail:
  provider: "inMemory" # Default for now
  key: "my-shared-jail"

This way:

  1. Now: I implement the inMemory provider, which allows sharing the jail across routers within the same Traefik process (solving my issue).

  2. Future: It creates the necessary boilerplate for anyone to add redis, file, or crowdsec providers without breaking the API.

What do you think about this approach? If you agree, I'll refactor the implementation to follow this provider pattern.

@tomMoulard
Copy link
Copy Markdown
Owner

Sure, I would rather go with this kind of configuration:

jail:
  - type: "inMemory" # only implement this jail type in this PR
    configuration: 
      path: "./jail.csv" # Could be optional with a good default

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants