Model or dataset
JasonLovesDoggo/caddy-defender avatar
JasonLovesDoggo/caddy-defender

Caddy Defender: the default range list is three cloud providers and three AI services

Caddy module to block or manipulate requests originating from AIs or cloud services trying to train on your websites

583 stars25 forksGoMIT

At a glance

What is it?
A Caddy middleware that matches the client IP against embedded CIDR ranges and answers with one of seven responders, from a 403 to a dropped connection to a deliberately slow stream. The part worth reading before you install it is the default: aws, azurepubliccloud and gcloud are in it, so an unconfigured block list covers three of the largest clouds on the internet, while eighteen other keys ship disabled.
Who is it for?
caddy-defender is a small, well-shaped piece of middleware with one decision that will surprise people: the out-of-the-box range list is not a list of AI scrapers but a list that includes three cloud providers wholesale, and a request arriving through AWS will match it.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 2 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The defaults are three whole clouds and three AI services

The `ranges` option defaults to a fixed list, and it reads like this:

code
aws azurepubliccloud deepseek gcloud githubcopilot openai

Three of those six are entire cloud providers rather than AI services. A request from an AWS address, from Google Cloud or from the Azure public cloud matches the default before you configure anything, which is a much broader rule than the project's description of blocking AI training traffic implies.

The embedded table is far wider than the default. Alongside those keys it ships aliyun, vpn, oci, mistral, vultr, cloudflare, digitalocean, linode and datadog, plus per-region AWS keys for us-east-1, us-west-1 and eu-west-1. Every key points at a fetcher file under `ranges/fetchers/`, which is how the ranges are produced.

The ranges are compiled into the binary, and no update mechanism is described anywhere on the page. A provider that publishes new addresses is only covered when a new build is released.

Eight example directories for seven documented responders

The examples directory holds eight entries: block, custom, drop, garbage, ratelimit, redirect, tarpit and whitelist. The configuration section documents seven responders, so `whitelist` is an example with no responder of that name in the documented syntax.

The syntax itself is small enough to read in one breath:

caddyfile
defender <responder> {
    message <custom message>
    ranges <ip_ranges...>
    url <url>
}

Three of the seven responders need something else. `custom` requires `message`, `redirect` requires `url`, and `ratelimit` does not act on its own at all: it marks requests for rate limiting and needs Caddy-Ratelimit installed alongside. So of the seven names, one is a marker for another module, one is an alias for a Caddy built-in status, and the rest do the work here.

Four responders, four amounts of unpleasant

The ladder runs from polite to deliberate. `block` returns 403 Forbidden. `drop` closes the connection. `redirect` answers 308 Permanent Redirect to a URL you supply. `custom` returns a message you write.

Then there are the two responders whose purpose is to make a scraper's life difficult rather than to refuse it. `garbage` returns garbage data, described as polluting AI training. `tarpit` streams data at a slow but configurable rate, described as stalling bots and polluting AI training.

Both of those are the least specified part of the page. There is no content type, no payload size and no example output for `garbage`, and no default rate, ceiling or timeout for `tarpit`, only the word configurable with nothing to configure against. A responder that returns data at a slow rate is also the one most likely to hold connections and sockets open, which is an operational cost the documentation does not mention.

The homepage is http:// and the newest tag is four months behind

The repository's declared homepage is `http://defender.jsn.cam/`, plain HTTP, while the documentation the README sends you to for installation, configuration and the getting started guide is served over HTTPS from a GitHub Pages address. The two are different hosts serving the same project.

The release line is also uneven. v0.9.0 is dated 2025-06-29, v0.10.0 on 2025-12-30 and carries a codename in its title, Gaimon, and v0.10.1 on 2026-05-14. The default branch `main` was last pushed on 2026-09-30, four months after the newest tag, with the license at MIT.

Nothing about that gap is wrong on its own. It does mean the Docker image published as `latest` and the newest tag can be different builds, and there is no tag-to-commit statement on the page to check them against.

Built on caddy:builder, shipped on caddy:latest

The image is a two-stage build, and neither stage is pinned:

code
FROM caddy:builder AS builder

COPY . /defender

RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    xcaddy build \
    --with pkg.jsn.cam/caddy-defender=/defender

FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

The compile happens with xcaddy against the module path, with cache mounts for the module cache and the build cache, and the resulting single binary is copied over a fresh `caddy:latest`. Meanwhile go.mod pins the Caddy library itself at v2.11.4. So the Caddy that compiles the plugin and the Caddy that runs it are chosen independently, by two floating tags.

The run command the page gives is equally unpinned, and it mounts your configuration and opens two ports:

bash
docker run -d \
  --name caddy \
  -v /path/to/Caddyfile:/etc/caddy/Caddyfile \
  -p 80:80 -p 443:443 \
  ghcr.io/jasonlovesdoggo/caddy-defender:latest

A vanity module path, a BART table and a cache library

The Go module is `pkg.jsn.cam/caddy-defender` while the repository is `JasonLovesDoggo/caddy-defender`, so the import path and the project name are different things, which is a deliberate vanity setup rather than a mistake. The language floor is Go 1.25.10.

Five direct dependencies do the visible work: Caddy v2.11.4 for the module framework, `github.com/gaissmai/bart` at v0.30.0 for the routing table that IP lookups run against, `testify` for the tests, `github.com/viccon/sturdyc` at v1.1.5, and `zap` for logging. A balanced routing table is the structure that makes longest-prefix IP matching cheap, and a separate caching library next to it suggests the same lookups repeat across requests.

The source layout is flat at the top with the interesting parts in directories: `config.go`, `middleware.go` and `plugin.go` at the root, `matchers/`, `ranges/` and `responders/` beneath them, and `config_test.go` and `middleware_test.go` beside the two files they cover. Also at the root are `.golangci.yaml`, `mkdocs.yml`, `benchmarks/`, `cache/`, `development/`, a `FEATURED` entry and an `.idea/` directory, which is JetBrains project configuration checked into a public repository.

Every hard question is delegated to a documentation site

What the README does carry is the responder list, the shape of the directive, the default range keys, the embedded range table and the Docker path. Everything else is a pointer.

Installation says to see the online documentation for other methods. Configuration refers to the configuration page on the website. The quick start is a link to a getting started guide, and examples live in `docs/examples.md`. The one matcher question the page raises, the row for custom ranges in the embedded table, is annotated with a link to Caddy's own documentation for the remote IP matcher rather than explained.

So the behaviour that decides whether this plugin is correct for you, what happens when a request falls in several ranges, how a whitelist interacts with the six defaults, and whether the directive can appear more than once in a Caddyfile, is documented somewhere other than here. That is a reasonable division for a small project, but it means the README alone cannot tell you whether the default rule is safe for your own traffic.

Editorial conclusion

caddy-defender is a small, well-shaped piece of middleware with one decision that will surprise people: the out-of-the-box range list is not a list of AI scrapers but a list that includes three cloud providers wholesale, and a request arriving through AWS will match it. It suits an operator who wants an immediate answer for known AI ranges and intends to narrow the list on day one, and it does not suit anyone who installs it expecting AI-only blocking, or anyone who needs the garbage and tarpit responders specified, since neither has a documented payload, rate or duration. Before enabling it in production, read the default ranges line, decide which keys you actually want, check whether your own traffic comes from a listed cloud, and pick the mildest responder that solves the problem.

Frequently asked questions

What does caddy-defender block with no configuration?

The ranges option defaults to aws, azurepubliccloud, deepseek, gcloud, githubcopilot and openai. Three of those are whole cloud providers, so any request arriving from AWS, Google Cloud or Azure public cloud matches before you add anything. The binary also embeds aliyun, vpn, oci, mistral, vultr, cloudflare, digitalocean, linode, datadog and per-region AWS keys, none of which are enabled by default.

Which responders does caddy-defender provide?

block returns 403 Forbidden, drop closes the connection, redirect returns 308 to a url you supply, custom returns a message you write, ratelimit marks requests and needs Caddy-Ratelimit installed too, garbage returns data intended to pollute AI training, and tarpit streams data at a slow configurable rate.

How do I install caddy-defender with Docker?

Pull ghcr.io/jasonlovesdoggo/caddy-defender:latest, then run the container detached with your Caddyfile mounted at /etc/caddy/Caddyfile and ports 80 and 443 published. The image is built with xcaddy from caddy:builder and the binary is copied into caddy:latest, so neither stage is pinned to a version.

How do I add my own IP ranges to caddy-defender?

With the ranges sub-directive inside the defender block, which takes CIDR ranges or predefined range keys, and which defaults to the six keys listed above. Each predefined key maps to a fetcher file under ranges/fetchers/, and the ranges themselves are embedded in the compiled binary.

Official sources

  1. JasonLovesDoggo/caddy-defender on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/jasonlovesdoggo-caddy-defender.svg)](https://hysenlabs.com/projects/jasonlovesdoggo-caddy-defender)