Mahbubur Riad
Back to blog
Hosting & Server 6 min read

Automating Certificate Management for Self-Hosted Cloudflare Tunnel Alternatives with ACME.sh: A Step-by-Step Guide

Jun 16, 2026 · Mahbubur Riad

Learn how to automate TLS cert issuance and renewal for self-hosted tunnel alternatives like Caddy, Nginx Proxy Manager, and WireGuard with ACME.sh and DNS-01 validation.

On this page

Automating Certificate Management for Self-Hosted Cloudflare Tunnel Alternatives with ACME.sh: A Step-by-Step Guide

If you’ve outgrown Cloudflare Tunnel (formerly Argo Tunnel) — or prefer full control over your edge infrastructure — you’re likely building a self-hosted tunnel alternative. Tools like Caddy, Nginx Proxy Manager, or WireGuard (with HTTP(S) tunneling) are popular choices.

But here’s the hard truth: managing TLS certificates manually for these setups quickly becomes a nightmare. Expiry alerts, DNS propagation delays, and renewal failures are not theoretical — they’re daily operational hazards.

Enter ACME.sh — a pure POSIX shell implementation of the ACME protocol. It’s lightweight, dependency-light, and designed for automation. In this guide, we’ll walk through how to use ACME.sh with DNS-01 validation to automatically obtain and renew TLS certificates for your self-hosted tunnel infrastructure — no root access required, and no browser popups.

Note: DNS-01 is the only reliable option for headless or remote VPS/bare-metal setups where HTTP-01 challenges (port 80) may be blocked or inaccessible. It’s also essential if your tunnel frontends don’t expose a public HTTP endpoint (e.g., WireGuard + TLS termination elsewhere).

Why ACME.sh Over Certbot?

Before we dive in, a quick reality check:

Feature ACME.sh Certbot
Language Pure shell (bash/dash/sh) Python
Dependencies Minimal (curl, openssl) Python, pip, virtualenv
DNS API Support 90+ providers (via --dns flags) Limited (via plugins, often outdated)
Renewal Scripting Built-in hooks (--renew-hook) Possible, but less flexible
Container Friendly Runs in Alpine, BusyBox, etc. Heavy for minimal containers
Learning Curve Slightly steeper at first Easier initial setup

For infrastructure-as-code, CI/CD, or headless servers — ACME.sh wins. It’s also the only option if you’re using a DNS provider without official Certbot plugins (looking at you, Cloudflare, but also Namecheap, Porkbun, Cloudns, etc.).

Step 1: Install ACME.sh

Assuming you’re on a modern Linux server (Ubuntu, Debian, Rocky, etc.):

Bash
# Install via official installer (safe, no sudo needed)
curl -fsSL https://get.acme.sh | sh -s -- --home ~/acme.sh

This installs ACME.sh to ~/acme.sh and creates a cron job for automatic renewal.

Pro tip: Add source ~/.bashrc or source ~/.zshrc if you don’t want to log out/in to get acme.sh in your PATH.

Step 2: Set Up DNS API Credentials

DNS-01 validation requires programmatic access to your DNS provider. Let’s use Cloudflare as an example — but the process is nearly identical for others.

For Cloudflare:

  1. Generate a Global API Key (not API Token — it has insufficient scope for this).
  2. Export it:
Bash
export CF_Key="your_global_api_key_here"
export CF_Email="[email protected]"

⚠️ Never hardcode secrets in scripts. Use environment variables or a .env file with strict chmod 600.

For Other Providers

ACME.sh supports 90+ DNS providers out-of-the-box. Run:

Bash
acme.sh --list-providers

Examples:

  • --dns dns_aws
  • --dns dns_cloudns
  • --dns dns_gd
  • --dns dns_route53

Check the full list here.

Step 3: Issue a Certificate (With DNS-01)

Let’s say your tunnel domain is tunnel.example.com.

Bash
acme.sh --issue --dns dns_cf \
  -d tunnel.example.com \
  --key-length 2048 \
  --ca-bundle /etc/ssl/certs/ca-certificates.crt

--ca-bundle ensures compatibility with older clients (e.g., older Android, legacy corporate proxies).

On first run, ACME.sh will:

  1. Generate a CSR.
  2. Create a DNS TXT record _acme-challenge.tunnel.example.com.
  3. Wait for propagation (it polls until visible).
  4. Validate and fetch the cert.

Once complete, certificates are stored under ~/.acme.sh/tunnel.example.com/.

Key Files:

  • tunnel.example.com.key → Private key (PEM)
  • tunnel.example.com.cer → Certificate (PEM, includes chain)
  • fullchain.cer → Full chain (cert + intermediate) — use this for most servers

Step 4: Auto-Renewal (The Real Win)

ACME.sh automatically installs a cron job that checks for renewals daily. But you can also trigger renewals manually with:

Bash
acme.sh --renew -d tunnel.example.com

Custom Hooks for Integration

This is where automation shines. Let’s integrate with your tunnel tools.

For Caddy (v2+)

Caddy supports ACME natively — but if you want manual control (e.g., to avoid Caddy writing certs to disk), use a --deploy-hook:

Bash
acme.sh --issue --dns dns_cf -d tunnel.example.com \
  --deploy-hook caddy

Or manually deploy:

Bash
acme.sh --install-cert -d tunnel.example.com \
  --cert-file /etc/caddy/certs/tunnel.example.com.crt \
  --key-file /etc/caddy/certs/tunnel.example.com.key \
  --fullchain-file /etc/caddy/certs/tunnel.example.com.fullchain.crt \
  --reloadcmd "sudo systemctl reload caddy"

--reloadcmd runs after cert install. Adjust sudo and service names per your setup.

For Nginx Proxy Manager (NPM)

NPM expects certs in /data/nginx/cert/. You can use a deploy hook script:

Bash
# Create hook script
cat > ~/deploy-npm.sh << 'EOF'
#!/bin/sh
cp "$1" /data/nginx/cert/public.crt
cp "$2" /data/nginx/cert/private.key
cp "$3" /data/nginx/cert/ca.crt
# Restart NPM container
docker restart npm_app_1  # or your container name
EOF
chmod +x ~/deploy-npm.sh

Then issue with:

Bash
acme.sh --issue --dns dns_cf -d tunnel.example.com \
  --deploy-hook "bash ~/deploy-npm.sh"

For WireGuard + TLS Termination (e.g., via stunnel, HAProxy, or nginx)

WireGuard itself doesn’t do TLS — you’ll likely have a reverse proxy in front. Example with stunnel:

Bash
# Create a deploy hook
cat > ~/deploy-stunnel.sh << 'EOF'
#!/bin/sh
cat "$1" > /etc/stunnel/tunnel.example.com.crt
cat "$2" >> /etc/stunnel/tunnel.example.com.crt
cp "$4" /etc/stunnel/tunnel.example.com.key  # fullchain + privkey
systemctl reload stunnel@tunnel
EOF
chmod +x ~/deploy-stunnel.sh

Then:

Bash
acme.sh --issue --dns dns_cf -d tunnel.example.com \
  --deploy-hook "bash ~/deploy-stunnel.sh"

Always test hooks in dry-run mode first (--debug) to avoid breaking production.

Step 5: Security Hardening

  • Rotate keys: Use --key-length 4096 (if your clients support it — modern browsers do).
  • Pin renewal: Ensure your cron runs as a non-root user with least privilege.
  • Audit logs: Log acme.sh --issue output to a dedicated file.
  • Limit permissions: Don’t run ACME.sh as root unless absolutely necessary.

Common Pitfalls (And How to Avoid Them)

1. DNS Propagation Delays

ACME.sh auto-waits for DNS propagation — but if your provider is slow (looking at GoDaddy), add:

Bash
--dnssleep 60  # wait 60 seconds after DNS update

2. Rate Limits

Let’s Encrypt limits to:

  • 50 certs per domain per week
  • 300 new orders per 3 hours

Use --staging for testing:

Bash
acme.sh --issue --dns dns_cf -d tunnel.example.com --staging

3. TXT Record Conflicts

If you already have _acme-challenge records (e.g., from a previous cert), ACME.sh will overwrite them. This is intentional — but if you run multiple tools, coordinate.

Tip: Use a subdomain like certs.tunnel.example.com for DNS-01 challenges to isolate them.

4. Timezone & System Clock

If your server time is off by >15 minutes, Let’s Encrypt will reject the challenge. Run:

Bash
timedatectl status
sudo timedatectl set-ntp true

Integration Comparison: Which Tool Fits Best?

Tool Cert Integration Method Best With ACME.sh? Notes
Caddy Built-in ACME or manual reload ✅ Yes --deploy-hook caddy works out-of-the-box
Nginx Proxy Manager Manual file copy + restart ✅ Yes Use --deploy-hook script
Traefik ACME plugin or file-based ⚠️ Tricky Prefer Traefik’s native ACME unless headless
WireGuard + stunnel/HAPROXY Manual cert reload ✅ Yes Use custom --deploy-hook script
Apache a2enmod ssl, reload ✅ Yes --reloadcmd "systemctl reload apache2"

FAQ

1. Can I use ACME.sh without root access?

Yes. ACME.sh runs as any user. Just ensure the target service (Nginx, Caddy, etc.) can read the certs — either by changing ownership (chown) or using a group (e.g., www-data).

2. Does ACME.sh support wildcard certs?

Yes — but only with DNS-01. Example:

Bash
acme.sh --issue --dns dns_cf -d "*.example.com" -d example.com

Note: You must include both *.example.com and example.com if you want the bare domain.

3. How long do certificates last?

Let’s Encrypt issues 90-day certs. ACME.sh renews at ~60 days. You can check expiry with:

Bash
openssl x509 -in ~/.acme.sh/tunnel.example.com/tunnel.example.com.cer -noout -dates

4. What if my DNS provider isn’t in the ACME.sh list?

You can write a custom DNS API script. ACME.sh provides a template. Most providers expose REST APIs — so it’s usually 50–100 lines of shell.

5. Should I use DNS-01 or HTTP-01?

  • DNS-01: Best for headless, remote, or non-HTTP services (like WireGuard).
  • HTTP-01: Faster, but only works if port 80 is open and reachable.

If you’re building a tunnel, DNS-01 is the only sane choice.

Wrapping Up

Self-hosting your tunnel infrastructure gives you control, performance, and cost savings — but only if you automate the boring parts. TLS certificate management is where many projects stall. ACME.sh, combined with DNS-01 validation, turns this from a weekly chore into a set-and-forget process.

Start small: issue one cert manually. Then automate renewal. Then add hooks for your stack. Soon, your certs will renew themselves — while you sleep.

If you run into issues, check the ACME.sh GitHub issues — most edge cases are already documented there.

And if you found this useful, drop by mahbuburriad.com — I share more hands-on sysadmin deep dives like this, no fluff.

Related

Related posts