Protecting the Caddy Web Server with Teler-WAF

A Web Application Firewall (WAF) that sits in front of and protects your web assets from online threats. In this case, Teler-WAF is a free WAF that protects Caddy web server and the sites behind it against from OWASP Top 10 Threats, known vulnerabilities, botnet, malicious crawlers and other attacks. This tutorial will explain the basics of installing the Teler module in Caddy running on Debian 13.

The tutorial starts with building Caddy with the Teler-WAF module using xCaddy. Then configuring the CaddyFile and Teler YAML config file to protect specific web assets.

Quick Comparison of Free WAF Solutions

WAF NameCaddy NativeBot DetectionCostDebian FriendlyNotable Features
Caddy WAFYesYesFreeYesRegex bot rules, IP blacklists
Teler WAFYesYesFreeYesBotns, crawlers, brute force

In this case, I went with Teler because it’s simpler to install and configure.

Install Go

Teler WAF is a Go-based middleware for Caddy. Ensure Go is installed:

sudo apt update
sudo apt install golang

Install xcaddy

Install xcaddy to GOPATH/bin (ensure GOPATH/bin is on PATH)

go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest

Create a folder and build Caddy with teler WAF

Build a Caddy binary including the teler module; the teler module’s README shows enabling CGO for the build and passing the module path with –with

mkdir teler
cd teler
CGO_ENABLED=1 xcaddy build \
  --with github.com/teler-sh/teler-caddy@latest \
  --output dist/caddy

Replace the running binary

Stop the service, replace the binary, and restart; the Caddy docs recommend overlaying the newly built binary (the same pattern used by the builder image in Docker).

sudo systemctl stop caddy
sudo cp dist/caddy /usr/bin/caddy
sudo systemctl start caddy

Verify the module is present

List compiled modules to confirm teler is included; the command-line docs and man page show list-modules for verification.

caddy list-modules --packages | grep teler

Create YAML Config File

cd /etc/caddy
sudo nano /etc/caddy/teler-waf.conf.yaml

Example minimal YAML policy fields to consider:

excludes: []  # do not disable any threat categories

whitelists:
  # allow ACME HTTP-01
  - 'request.URI startsWith "/.well-known/acme-challenge/"'  
  # allow health check
  - 'request.URI == "/health"'                               
  # allow localhost
  - 'request.IP in ["127.0.0.1","::1"]'     

  # add your API Routes
  - 'request.Method == "POST" && request.URI == "/api/users/login"'
  - 'request.Method == "POST" && request.URI == "/api/docs/search"'


customs:  # optional examples; safe to start empty
  - name: "Block common scanner UAs"
    rules:
      - dsl: 'request.Headers matches "(curl|wget|nikto|acunetix|sqlmap|nmap|masscan|zgrab)"'

log_file: "/var/log/caddy/teler-waf.log"
verbose: true
no_stderr: false
no_update_check: false
in_memory: false

Enable Teler in the Caddyfile


{
	#Add a global options block so the teler directive executes before 	
	#other handlers across the config.    
	order teler_waf first
}

(teler) {
	route {
		teler_waf {
			#pass in the config file
			load_from YAML /etc/caddy/teler-waf.conf.yaml
		}
	}
}

:80 {
	# Set this path to your site's directory.
	root * /var/www/html
	# Enable the static file server.
	file_server
	#enable file compression
	encode gzip
	#enable Teler-WAF
	import teler
}

Validate installation

Test the Caddyfile with the custom binary to catch config errors:

/usr/bin/caddy validate --config /etc/caddy/Caddyfile

Send a simple request with curl and watch logs; the teler demo shows curl being flagged as “bad crawler” in Caddy’s logs.

curl -i https://example.com/
tail -f /var/log/caddy/teler-waf.log

Further customizing Teler YAML

Regex in one line

Combine endpoints with the matches operator and alternation to allow several exact paths in a single rule while scoping to the intended HTTP method(s) for that action flow.

- 'request.Method == "POST" && request.URI matches "^/(api/users/login|api/docs/search)(/|$)"'

This pattern whitelists POSTs to /api/users/login, /api/docs/search including optional trailing slashes, all in one rule without weakening other routes

Prefix plus methods

If many endpoints share a prefix, allow mutating methods for the whole subtree with startsWith instead of listing each route, which dramatically reduces YAML churn during app changes.

- 'request.Method in ["OPTIONS","POST","PUT","PATCH","DELETE"] && request.URI startsWith "/api/"'