For bFree, I need random public subdomains like:
test123.bfree.bangie.digital
demo-client.bfree.bangie.digital
some-random-id.bfree.bangie.digital
The idea is:
*.bfree.bangie.digital -> Caddy -> bFree server
So every tunnel can get its own public HTTPS URL without me manually creating DNS records every time.
Simple idea.
Naturally, DNS and SSL immediately show up wearing steel boots.
Why I needed this
For normal domains, Caddy can usually get HTTPS certificates with the default HTTP challenge.
For wildcard domains, like:
*.bfree.bangie.digital
that is not enough.
Wildcard SSL needs DNS validation. That means Caddy has to create a temporary DNS TXT record to prove that it controls the domain.
Since my DNS is on Namecheap, Caddy needs to talk to the Namecheap API.
The default Caddy binary does not include the Namecheap DNS module, so I had to build a custom Caddy binary with
xcaddy.
What this setup does
| Part | Purpose |
|---|---|
xcaddy | Builds Caddy with extra modules |
caddy-dns/namecheap | Lets Caddy manage Namecheap DNS records |
| Namecheap API | Allows DNS-01 certificate validation |
| Wildcard DNS | Routes all *.bfree subdomains to the server |
| Caddy | Terminates HTTPS and proxies requests to the bFree backend |
The final goal is:
https://anything.bfree.bangie.digital
going through Caddy and ending up on the local bFree server.
Requirements
On a fresh Debian / Ubuntu-style VM:
sudo apt update
sudo apt install -y golang-go git curl
Check Go:
go version
I used:
go version go1.24.4 linux/amd64
Install xcaddy
go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest
export PATH="$PATH:$(go env GOPATH)/bin"
Check it:
xcaddy version
Build Caddy with Namecheap support
Create a build directory:
mkdir -p ~/build/caddy-namecheap
cd ~/build/caddy-namecheap
Build Caddy:
xcaddy build --with github.com/caddy-dns/namecheap
Check that the Namecheap DNS module exists:
./caddy list-modules | grep namecheap
Expected:
dns.providers.namecheap
If that appears, the binary has the plugin.
Good. The tiny dragon has the correct tooth.
Install the custom Caddy binary
On my fresh VM, Caddy was not installed yet, so I simply installed the custom binary:
sudo install -m 0755 ./caddy /usr/bin/caddy
Verify:
caddy version
caddy list-modules | grep namecheap
Expected:
dns.providers.namecheap
Create Caddy user and directories
Caddy should run as its own user, not as a random root process started by hand.
sudo groupadd --system caddy
sudo useradd --system \
--gid caddy \
--create-home \
--home-dir /var/lib/caddy \
--shell /usr/sbin/nologin \
--comment "Caddy web server" \
caddy
Create directories:
sudo mkdir -p /etc/caddy
sudo mkdir -p /var/lib/caddy
sudo mkdir -p /var/log/caddy
sudo chown -R caddy:caddy /var/lib/caddy /var/log/caddy
sudo chmod 755 /etc/caddy
Basic test Caddyfile
Before touching SSL, I tested plain HTTP first.
sudo nano /etc/caddy/Caddyfile
:80 {
respond "bFree Caddy is alive" 200
}
Validate:
sudo caddy validate --config /etc/caddy/Caddyfile
Run manually for a quick test:
sudo caddy run --config /etc/caddy/Caddyfile
In another terminal:
curl http://localhost
Expected:
bFree Caddy is alive
After the test, stop it with CTRL+C.
Run Caddy with systemd
sudo nano /etc/systemd/system/caddy.service
[Unit]
Description=Caddy
Documentation=https://caddyserver.com/docs/
After=network-online.target
Wants=network-online.target
[Service]
Type=notify
User=caddy
Group=caddy
ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
ProtectSystem=full
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
NoNewPrivileges=true
Restart=on-failure
[Install]
WantedBy=multi-user.target
Enable and start:
sudo systemctl daemon-reload
sudo systemctl enable caddy
sudo systemctl start caddy
sudo systemctl status caddy --no-pager
Confirm it runs as the caddy user:
ps -eo user,pid,cmd | grep '[c]addy'
Namecheap DNS records
In Namecheap, I added:
| Type | Host | Value |
|---|---|---|
| A Record | bfree | server public IPv4 |
| A Record | *.bfree | server public IPv4 |
The first record handles:
bfree.bangie.digital
The wildcard handles:
anything.bfree.bangie.digital
Wildcard DNS does not replace the base subdomain, so I created both.
Namecheap API access
In Namecheap, I enabled API access and whitelisted the public IPv4 of the VM.
On the VM, I checked the outgoing public IP with:
curl -4 https://api.ipify.org
That IP needs to be whitelisted in Namecheap.
This IP is also used as client_ip in the Caddy Namecheap DNS config.
Store Namecheap credentials
I stored the credentials in an environment file:
sudo nano /etc/caddy/.env
NAMECHEAP_API_KEY=your_namecheap_api_key
NAMECHEAP_API_USER=your_namecheap_username
NAMECHEAP_CLIENT_IP=your_server_public_ipv4
Secure it:
sudo chmod 600 /etc/caddy/.env
sudo chown root:root /etc/caddy/.env
Attach the env file to the Caddy service:
sudo systemctl edit caddy
[Service]
EnvironmentFile=/etc/caddy/.env
Reload systemd:
sudo systemctl daemon-reload
Wildcard HTTPS Caddyfile
Now replace the test Caddyfile:
sudo nano /etc/caddy/Caddyfile
bfree.bangie.digital {
tls {
dns namecheap {
api_key {env.NAMECHEAP_API_KEY}
user {env.NAMECHEAP_API_USER}
api_endpoint https://api.namecheap.com/xml.response
client_ip {env.NAMECHEAP_CLIENT_IP}
}
}
respond "bFree root is alive" 200
}
*.bfree.bangie.digital {
tls {
dns namecheap {
api_key {env.NAMECHEAP_API_KEY}
user {env.NAMECHEAP_API_USER}
api_endpoint https://api.namecheap.com/xml.response
client_ip {env.NAMECHEAP_CLIENT_IP}
}
}
reverse_proxy 127.0.0.1:7070
}
Format and validate:
sudo caddy fmt --overwrite /etc/caddy/Caddyfile
sudo caddy validate --config /etc/caddy/Caddyfile
Restart Caddy and watch logs:
sudo systemctl restart caddy
sudo journalctl -u caddy -f
Success looks like this:
certificate obtained successfully identifier="*.bfree.bangie.digital"
certificate obtained successfully identifier="bfree.bangie.digital"
That means Caddy used Namecheap DNS validation and got certificates successfully.
Test backend
Before writing the real bFree server, I used a tiny Python backend on port 7070.
sudo mkdir -p /opt/bfree-test
cd /opt/bfree-test
sudo nano server.py
from http.server import BaseHTTPRequestHandler, HTTPServer
class Handler(BaseHTTPRequestHandler):
def send_ok_headers(self):
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.end_headers()
def do_HEAD(self):
self.send_ok_headers()
def do_GET(self):
body = f"bFree dummy backend is alive\nHost: {self.headers.get('Host')}\nPath: {self.path}\n"
encoded = body.encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain")
self.send_header("Content-Length", str(len(encoded)))
self.end_headers()
self.wfile.write(encoded)
HTTPServer(("127.0.0.1", 7070), Handler).serve_forever()
Run it:
python3 server.py
Then test a random wildcard subdomain:
curl https://test123.bfree.bangie.digital
Expected:
bFree dummy backend is alive
Host: test123.bfree.bangie.digital
Path: /
I also tested another random subdomain:
curl https://test1234.bfree.bangie.digital
Same result.
That proves the important chain works:
random bFree subdomain
-> wildcard DNS
-> Caddy HTTPS
-> wildcard certificate
-> reverse proxy
-> local backend on 127.0.0.1:7070
Debug notes
If Caddy says:
module not registered: dns.providers.namecheap
then the running Caddy binary does not include the Namecheap plugin.
Check:
caddy list-modules | grep namecheap
If Namecheap says:
Parameter APIUser is missing
check the Caddyfile syntax:
user {env.NAMECHEAP_API_USER}
Also check that the service actually received the env values:
pid=$(pidof caddy)
sudo tr '\0' '\n' < /proc/$pid/environ \
| grep NAMECHEAP \
| sed 's/=.*/=<set>/'
Expected:
NAMECHEAP_API_KEY=<set>
NAMECHEAP_API_USER=<set>
NAMECHEAP_CLIENT_IP=<set>
If Namecheap complains about IP access, check the VM public IP:
curl -4 https://api.ipify.org
Then make sure that exact IP is whitelisted in Namecheap API settings.
Notes
client_ipis the public IPv4 of the VM running Caddy.- It is not
127.0.0.1. - It is not a private LAN IP.
- It must match the IP whitelisted in Namecheap.
curl -Isends aHEADrequest. If your dummy backend only implementsGET, Python will return501.- If the response contains
via: 1.0 Caddy, your request is passing through Caddy. - For real usage, keep port
443open. - Port
80is still useful for redirects and normal non-wildcard HTTP challenge cases.
What this gives bFree
This does not build the tunnel yet.
It builds the runway.
Now any temporary bFree hostname can terminate HTTPS at Caddy:
abc123.bfree.bangie.digital
quiet-wolf.bfree.bangie.digital
demo-client.bfree.bangie.digital
The next step is the real bFree server.
That server will decide:
Which subdomain belongs to which client?
Is the tunnel alive?
Where should this request be forwarded?
Should this free tunnel expire?
For now, the important part is done:
*.bfree.bangie.digital works over HTTPS.