CaddySmith is a small Python script that reads a Cobalt Strike or Sliver C2 profile and forges a Caddy web server config out of it. The generated Caddyfile turns a regular Linux box into a redirector: legitimate beacon traffic gets reverse-proxied to your team server, and anything else (scanners, search bots, blue team probes, random curl) gets bounced to a decoy URL.
I wrote this because Caddy is a much friendlier web server to stand up quickly than Apache, single static binary, automatic Let's Encrypt certs, no a2enmod dance.
Authorized engagements only. This is offensive security tooling. Only run it against environments you have written permission to test.
CaddySmith auto-detects the format from the file contents:
- Cobalt Strike — the classic text format with
set uri "/foo"directives. Each HTTP-GET / HTTP-POST URI becomes its own exact-path route with the profile's client headers enforced. - Sliver — the JSON implant config exported from Sliver. Sliver doesn't have fixed URIs; it generates them at beacon-build time from path / file / extension lists. CaddySmith handles this by matching the top-level path prefixes as globs (e.g.
path /api* /static* /resources*) and doing a substring match on the Chrome build number from the UA (which survives Sliver's per-platform UA rewrites).
You can also force a specific parser with --profile-type cobaltstrike or --profile-type sliver.
Given a profile file, the script pulls out:
- The User-Agent string
- URIs (CS) or path prefixes (Sliver)
- Client-side headers (CS only — Sliver doesn't dictate specific headers)
- The Host header (used as the redirector's domain name if you don't pass
--server-name) - Whether staging is enabled (CS only —
set host_stage)
Then it builds a Caddyfile that:
- (Optional) Returns 403 to plain HTTP traffic
- Blocks ~15 known-bad user agents (curl, nmap, sqlmap, Googlebot, etc.)
- Blocks direct-IP access and any HTTP method that isn't GET/POST
- Reverse-proxies only the profile's URIs (or prefixes for Sliver), gated on the matching User-Agent
- Blocks common scanner probe paths (
.env,/wp-admin,.php, etc.) - Redirects everything else to your decoy URL
- Python 3.7+ (uses only the standard library, no
pip installneeded) - Caddy 2.x on the redirector itself (install guide)
python3 caddysmith.py my.profile \
--backend https://teamserver.internal:443 \
--decoy https://www.example.com/ \
--server-name redirector.example.com \
--email you@example.com \
--forbid-http \
-o redirector.caddyThis writes the generated config to redirector.caddy and prints a summary of what it did to stderr.
To deploy it on the redirector, drop the file in /etc/caddy/ and import it from your main Caddyfile:
# /etc/caddy/Caddyfile
import /etc/caddy/redirector.caddyIf you passed --email (recommended), the generated snippet already contains the global options block, so the main Caddyfile only needs the import line. If you didn't, add the email manually to a { } block above the import.
Then validate and reload:
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddyCaddy will auto-provision a Let's Encrypt cert for the domain in --server-name as long as the A record points at the redirector.
| Flag | Default | What it does |
|---|---|---|
profile |
(required) | Path to the .profile file |
--backend |
https://teamserver.local:443 |
Where to proxy matched traffic |
--decoy |
https://www.example.com/ |
Where unmatched traffic redirects |
--server-name |
c2.example.com |
Your redirector's domain name |
--policy |
strict |
strict, lax, or none |
--profile-type |
auto |
Force cobaltstrike or sliver (default: auto-detect) |
--extra-uri PATH |
— | Extra URI to proxy (with UA check). Can repeat. |
--lax-uri PATH |
— | Extra URI to proxy (no checks). Can repeat. |
--allow-ua STRING |
— | Extra UA allowed on --extra-uri routes. Can repeat. |
--forbid-http |
off | Return 403 on plain HTTP |
--email EMAIL |
— | Email for Let's Encrypt registration and renewal notices |
-o, --output FILE |
stdout | Write the config to a file |
- strict (default) — checks User-Agent, all client headers, direct-IP, HTTP method, and probe paths
- lax — only blocks bad user agents, no per-route header matching
- none — proxies any request to a profile URI, no filtering
Strict is what you usually want. Lax is useful when you're debugging why a real beacon isn't connecting.
Say you have a profile that mimics an Amazon endpoint and you want to deploy it on redirector.0xtb.sh:
python3 caddysmith.py amazon.profile \
--backend https://10.1.1.10:443 \
--decoy https://www.amazon.com/ \
--server-name redirector.0xtb.sh \
--forbid-http \
--policy strict \
-o /etc/caddy/redirector.caddyThe summary shows you exactly which routes got built, e.g.:
Routes built: 2
- [profile-get] /broadcast
- [profile-post] /1/events/com.amazon.csm.csa.prod
If you regenerate and see no routes, the script probably failed to parse your profile — check the warnings on stderr.
For a Sliver implant config (JSON):
python3 caddysmith.py sliver-implant.json \
--backend https://10.1.1.10:443 \
--decoy https://docs.godotengine.org/ \
--server-name gdscript.rocks \
--email you@example.com \
--forbid-http \
--policy strict \
-o /etc/caddy/redirector.caddyThe summary will tell you that Sliver was detected and show the prefix route:
Profile type: sliver
Routes built: 1
- [sliver] /api /public /resources /services /static (prefix)
Because Sliver generates URIs randomly from path × file × extension combinations, the generated path matcher uses prefix globs (path /api* /public* /resources* /services* /static*) rather than exact paths. The User-Agent matcher uses a substring of the Chrome build number (e.g. 3921.146), which Sliver preserves across its per-platform UA rewrites.
# Plain HTTP should be 403'd (if you used --forbid-http)
curl -I http://redirector.0xtb.sh/
# Bare hostname should redirect to the decoy
curl -kI https://redirector.0xtb.sh/
# Bad UA should also redirect
curl -kI -A "curl/8.4.0" https://redirector.0xtb.sh/broadcast
# A request with the right UA + path should proxy through (200)
# You need to also send all the profile's client headers in strict mode.- Staging rules aren't generated (Cobalt Strike). If your CS profile has
set host_stage "true"(or doesn't set it at all), the script will warn you and skip stager URIs. Addset host_stage "false";to your profile, or pass the stager URIs explicitly via--extra-uri. - Sliver prefix matching is wider than CS exact matching. With Sliver, the proxy route claims any URL starting with
/api,/static, etc. Scanner probes that happen to use those prefixes (e.g./api/.env) will be sent to the team server rather than blocked locally — but Sliver's own HTTP transport authenticates via implant ID, so unauthorized requests get rejected at the C2 layer. The UA gating still keeps most scanners out. - One backend per run. Every route proxies to the same
--backend. If you need multiple team servers, run the script multiple times and merge by hand. - Profile URI parsing is line-based (Cobalt Strike).
set uri "/path1 /path2";works (multiple paths on one line), but unusual formatting might trip up the parser. Check the route list in the summary to confirm. - HTTPS backend cert verification is off by default. Team servers usually have self-signed certs, so the generated config includes
tls_insecure_skip_verify. If your backend has a real cert, delete that line from the generated file.
The Apache-based Malleable-Redirector was the starting point for what this script generates, same three-track URI model (profile URIs / extra URIs / lax URIs), same policy modes, same general layout. CaddySmith just translates the output to Caddy syntax instead of Apache .htaccess.
MIT