This is a Traefik middleware that lets a request through only if the caller's IP is on the allowlist (EDL) published by Knocknoc.
Knocknoc grants network access just-in-time: when a user authenticates, their current source IP is stored in an allowlist until their session ends, or their access is revoked. Knocknoc exposes that allowlist as an EDL (External Dynamic List). This plugin polls that EDL and only allows http requests through Traefik from Knocknoc authenticated users.
Poll the Knocknoc server directly. The plugin fetches the EDL straight from your Knocknoc server, or from a Knocknoc Agent caching the EDL.

Poll a local Knocknoc agent. An agent running alongside Traefik in-cluster caches the allowlist, and receives updates from the server. The plugin polls the agent instead, which removes the network delay associated with a round trip to the Knocknoc Server.

Either way the plugin configuration is the same. Interested in the agent-based setup? Get in touch.
Add the plugin to Traefik's static configuration:
experimental:plugins:knocknoc:moduleName: github.com/knocknoc-io/traefik-plugin-knocknocversion: v0.2.0
Declare a middleware in the dynamic configuration and attach it to a router.
http:routers:my-app:rule: Host(`app.example.com`)service: my-appmiddlewares:- knocknocmiddlewares:knocknoc:plugin:knocknoc:sourceURL: https://knocknoc.example.com/edl/<your-edl-uuid>username: apiusersecret: "{{ env `KNOCKNOC_SECRET` }}"pollInterval: 1s
labels:- "traefik.http.routers.my-app.middlewares=knocknoc"- "traefik.http.middlewares.knocknoc.plugin.knocknoc.sourceURL=https://knocknoc.example.com/edl/<your-edl-uuid>"- "traefik.http.middlewares.knocknoc.plugin.knocknoc.username=traefik"- "traefik.http.middlewares.knocknoc.plugin.knocknoc.secret=your-secret"- "traefik.http.middlewares.knocknoc.plugin.knocknoc.pollInterval=1s"
apiVersion: traefik.io/v1alpha1kind: Middlewaremetadata:name: knocknocspec:plugin:knocknoc:sourceURL: https://knocknoc.example.com/edl/<your-edl-uuid>username: traefiksecret: your-secretpollInterval: 1s
sourceURL is the only required option.
| Option | Type | Default | Description |
|---|---|---|---|
sourceURL | string | — | Required. URL of the Knocknoc EDL. Must not embed credentials in its userinfo — use username and secret. |
username | string | "" | HTTP basic auth username. When empty, no Authorization header is sent. |
secret | string | "" | HTTP basic auth password. Setting it without a username is a configuration error. |
pollInterval | duration | 2s | How long a fetched allowlist is reused before the next fetch. Minimum 1s. |
requestTimeout | duration | 5s | Timeout for a single fetch. 0 disables the timeout entirely. |
rejectStatusCode | int | 403 | Status returned to a caller that is not on the allowlist. Must be 4xx or 5xx. |
rejectBody | string | "" | Response body served to a rejected caller. When empty, a plain Forbidden is returned. |
rejectContentType | string | text/html; charset=utf-8 | Content type for rejectBody. Setting it without a rejectBody is a configuration error. |
insecureSkipVerify | bool | false | Skip TLS verification when fetching the EDL. For testing against a self-signed server only. |
ipStrategy.depth | int | 0 | Which X-Forwarded-For entry identifies the caller. 0 uses the TCP remote address. |
ipStrategy.excludedIPs | []string | [] | IPs or CIDR blocks removed from X-Forwarded-For before depth is applied. |
Durations use Go syntax (500ms, 1s, 2m). Invalid values are rejected on startup.
By default the plugin uses the TCP remote address and ignores
X-Forwarded-For. If Traefik is behind a load balancer or CDN, set depth to pick an entry
from that header, counting from the right.
Given X-Forwarded-For: 203.0.113.7, 198.51.100.4, 10.0.0.5:
| Setting | Selected IP |
|---|---|
depth: 0 (default) | the TCP remote address |
depth: 1 | 10.0.0.5 |
depth: 2 | 198.51.100.4 |
depth: 3 | 203.0.113.7 |
excludedIPs drops matching entries before depth is counted, keeping depth
consistent when a variable number of trusted proxies append themselves.
Only set
depthif every hop in front of Traefik is trusted to rewriteX-Forwarded-For. Otherwise a caller can present any IP they like.
rejectBody: |<!doctype html><html><body><h1>Access denied</h1></body></html>
Served verbatim with X-Content-Type-Options: nosniff. There is no templating.
For an API, set rejectContentType: application/json.
A rejected caller therefore gets exactly:
HTTP/1.1 403 ForbiddenContent-Type: text/html; charset=utf-8Content-Length: 4396X-Content-Type-Options: nosniff
Because you can set the deny page HTML, you can unauthenticated users to your Knocknoc login portal and have them returned to the protected URL once access is granted. This way an unauthenticated user who hits the service gets bounced through sign-in instead of hitting a dead end. See Redirecting Users.
The example below renders a branded 403 which can redirect the user to your Knocknoc login page (uncomment the script section).
http:middlewares:knocknoc:plugin:knocknoc:sourceURL: https://knocknoc.example.com/edl/<your-edl-uuid>username: traefiksecret: "{{ env `KNOCKNOC_SECRET` }}"pollInterval: 1srejectBody: |<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Forbidden</title><link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin><link href="https://fonts.googleapis.com/css2?family=Raleway:ital,wght@0,100..900;1,100..900&display=swap" rel="stylesheet"><style>body {text-align: center;background-color: #171717;color: #ffffff;}.container {margin: 60px 0;padding: 60px;}h1 {font-family: "Raleway", sans-serif;font-optical-sizing: auto;font-size: 4em;font-weight: 300;font-style: normal;}span {color: #35b59b;}</style></head><body><div class="container"><h1>Access Denied.</h1><h1>You must <span>Knoc</span> before you enter.</h1></div><svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="300" height="300" viewBox="0 0 166.57 156.56"><defs><style>.cls-1 {fill: none;}.cls-1, .cls-2 {stroke-width: 0px;}.cls-2 {fill: #35b59b;}</style></defs><g id="Layer_1-2" data-name="Layer 1"><path class="cls-2" d="M166.57,119.18c0,4-2.29,5.09-5,6.28l-103.5,31.1L0,126.1l86-17.7c.36-.16.73-.3,1.11-.41,3.26-.87,4.2-.65,8.58,1l42,17.66.14-117.91L90.75.81V.24l75.77-.24.05,119.18Z"/><line class="cls-1" x1="55.63" y1="32.13" x2="41.78" y2="18.28"/><rect class="cls-2" x="44.57" y="15.4" width="8.27" height="19.59" transform="translate(-3.55 41.82) rotate(-45)"/><rect class="cls-2" x="29.03" y="46.02" width="18.27" height="8.27"/><line class="cls-1" x1="54.19" y1="67.14" x2="40.34" y2="79.15"/><rect class="cls-2" x="38.1" y="69.01" width="18.34" height="8.27" transform="translate(-36.37 48.85) rotate(-40.93)"/></g></svg><!-- If this Javascript is uncommented, it will redirect this 403 Forbidden page to the Knocknoc user portal login, which can then redirect back to the protected website once access is granted. --><!-- <script>const baseUrl = '<<INSERT KNOCKNOC USER LOGIN PORTAL URL, eg: knocknoc.mycompany.com>>';// Milliseconds to wait before redirecting. Set to 0 for an instant redirect.const REDIRECT_DELAY_MS = 3000;setTimeout(() => {const currentUrl = document.URL;if (!currentUrl || !baseUrl) return;try {// Add http:// or https:// if it doesn't already have a schemelet normalizedBase = baseUrl.trim();if (!/^https?:\/\//i.test(normalizedBase)) {normalizedBase = 'https://' + normalizedBase;}const encodedRef = encodeURIComponent(currentUrl);const redirectUrl = normalizedBase + '?referrer=' + encodedRef;const url = new URL(redirectUrl);window.location.href = url.toString();} catch (err) {console.error('Invalid redirect URL:', err);}}, REDIRECT_DELAY_MS);</script> --></body></html>
pollInterval. A user whose session just ended
may still be admitted for up to that long.pollInterval, only when
traffic arrives.make # lint, test, and run the tests under the Yaegi interpreter