CallMeTechie
DE Login
Home Products Blog About Contact

TLS / Certificates: Understand & Debug

🌐 Networking & Routing · Updated 1 month ago

You open /certificates in the admin UI and see a table of your domains with status tags. One domain shows "HTTP without TLS" where you actually expected HTTPS. Or HTTPS works even though you set https_enabled=false. Or a cert for a fresh route just doesn't arrive.

This guide explains what happens under the hood, how to read the certificates page correctly, and what you can do about the typical problems.


How TLS really works in GateControl

Caddy is the cert manager

Every HTTP route is passed as a domain to Caddy. Caddy uses its built-in Automatic HTTPS and automatically fetches a Let's Encrypt certificate for every domain it serves — HTTP-01 challenge over port 80.

The relevant environment hooks:

  • GC_CADDY_EMAIL — contact email for Let's Encrypt. Without an email you get no expiry warnings from LE.
  • GC_CADDY_ACME_CA — optional custom ACME endpoint URL. Default is LE Production. For dev/test can be set to LE Staging (https://acme-staging-v02.api.letsencrypt.org/directory) or to a private CA (Step-CA, Smallstep, Vault). This is the acme_custom_ca feature.

All HTTP routes share one server block

Important implementation detail: src/services/caddyConfig.js merges all domains with HTTP routes into a single Caddy server srv0 (see code around lines 626–635). This one server listens on :443 and :80. Per-route listen configuration is discarded in the process.

The https_enabled column in the routes table still exists, but has no effect on cert issuance and listener binding. Historically it was meant as a per-route listener toggle, but was superseded by the "merge-all-into-srv0" refactor. In the code flow the field is still set, but the only place that used to honour it (listen: https_enabled ? [':443'] : [':80'] in individual server blocks) is replaced by the subsequent merge into srv0.

Practical consequence: every HTTP route with a domain automatically gets an Auto-TLS cert — regardless of what https_enabled says. If you really want to prevent HTTPS, you would have to patch the Caddy template code; we advise against that.

L4 routes — three TLS modes

L4 routes do not run through Caddy's HTTP stack, but through Caddy-L4. That's why different rules apply:

  • tls_mode=none — pure TCP/UDP proxy. No cert. For SSH, Minecraft, DNS, MQTT-plain.
  • tls_mode=passthrough — Caddy-L4 only looks at SNI, passes the TLS session 1:1 on to the backend. Cert must sit at the backend.
  • tls_mode=terminate — Caddy-L4 terminates TLS with a Let's Encrypt cert on the server side, then speaks plain TCP to the backend.

Default mode is none, because L4 is mostly used for protocols that have no TLS.

TLS tax: port 80 is mandatory

ACME HTTP-01 needs a reachable port 80. The cert refresh (automatically every ~60 days) likewise. If port 80 is blocked or sits behind a third-party firewall: cert doesn't come and expires.


Reading the certificates page

/certificates lists the current cert status for every route with a domain. The status tags:

Tag Meaning When?
Auto-TLS (green) Let's Encrypt cert is actively managed Normal case, HTTP route with domain
HTTP without TLS (amber) Legacy status, should no longer occur after v1.47.3 Historical routes with https_enabled=0 — are now treated as Auto-TLS
TLS passthrough (grey) L4 route with tls_mode=passthrough Cert lives at the backend, server does not terminate
L4 without TLS (grey) L4 route with tls_mode=none Pure TCP/UDP proxy
Caddy offline (red) Caddy process not running Supervisord lost Caddy → admin action required

The tags are computed from the interplay of route type, https_enabled/l4_tls_mode and Caddy runtime status. The code lives in src/services/caddyConfig.js + the routes service.


Troubleshooting

"My cert is not being issued"

By far the most common case. Checklist:

  1. Check DNS. The A record must point to the public IPv4 of the GateControl server.

    dig +short cloud.example.com
    # must be your server IP
    

    Or in the admin UI: in the route wizard there is a DNS check button, POST /api/v1/routes/check-dns.

  2. Port 80 reachable from outside. Test from an external machine:

    curl -I http://cloud.example.com/
    

    must at least bring up a TCP connect. If Connection refused or timeout: router firewall, server firewall or occupied by another process.

  3. Port 443 reachable from outside. Same test with https://. On "unable to verify the first certificate" that's OK — the connect is up, just the cert is not valid yet.

  4. Cloudflare proxy off. If CF sits as a proxy in front (orange cloud), HTTP-01 fails: LE sees the CF IP instead of our IP and gets the wrong response when fetching the challenge.

  5. Let's Encrypt rate limit. 50 certs per registered domain per week, 5 duplicate certs per week, 5 failed attempts per hour. If you experiment a lot, you'll hit that. Remedy: switch to LE Staging during tests:

    # in the server .env
    GC_CADDY_ACME_CA=https://acme-staging-v02.api.letsencrypt.org/directory
    

    Staging certs are not browser-trusted, but for checking the flow it's enough. Afterwards switch back to production.

  6. Inspect Caddy logs:

    docker logs gatecontrol 2>&1 | grep -i "acme\|tls.obtain\|certificate"
    

    Typical error messages:

    • authorization failed: Invalid response from … → port 80 not reachable, or challenge path not routable.
    • too many registrations for this IP → LE limit.
    • timeout waiting for response from DNS → DNS slow, often gone after propagation.

"Cert was issued, but the browser still warns"

  1. Cert for the wrong domain? Caddy fetches the cert for the domain that's in the host matcher. If you call www.example.com but the route is named example.com, Caddy only fetches the cert for example.com. Solution: either a second route for www., or add multiple domains in the route wizard.

  2. Intermediate chain incomplete?

    openssl s_client -servername cloud.example.com -connect cloud.example.com:443 -showcerts < /dev/null
    

    Look for "verify return code: 0 (ok)". With LE certs the E5/E6 intermediate should be delivered as well.

  3. Wrong cert via SNI: query the Caddy admin API:

    curl -s http://127.0.0.1:2019/pki/ca/local | jq .
    

    or via the internal admin endpoint to see what Caddy caches for which domain.

"HTTPS works even though I turned off https_enabled"

Expected behaviour since the srv0 merge refactor. The flag is currently without effect. See section "All HTTP routes share one server block" above.

If you really want HTTPS off for a specific route (for whatever reason — there's rarely a good one): create the route as L4 with tls_mode=none and choose an HTTP custom listen port. Or patch the Caddy template code.

"L4 route with TLS terminate doesn't get a cert"

Caddy-L4 handles TLS differently from Caddy-HTTP. Without an explicit SNI matcher in the L4 config Caddy doesn't know for which domain the cert applies.

Workaround: set the domain in the L4 route and maintain TLS policies manually. The GateControl admin UI currently doesn't expose a convenient switch for this — if you really need L4 terminate, the recommended path is a separate HTTP route on a custom port. For the majority of L4 use cases, none or passthrough is more appropriate anyway.

"Caddy offline" — red tag

Caddy process is not running. The supervisord watchdog should restart it automatically, but occasionally doesn't (e.g. after OOM kill or with a broken config reload cycle).

Fix:

docker exec gatecontrol supervisorctl status caddy
docker exec gatecontrol supervisorctl restart caddy

Afterwards click Caddy reload in the admin UI (Settings → Caddy). The server throws its in-memory config afresh into the Caddy admin API.

"Cert has expired and is not renewing"

Caddy renews automatically ~30 days before expiry. If that doesn't work:

  1. Port 80 must be reachable (also for renewals).
  2. Caddy data volume persistent? If /data is not mounted, all certs are gone at every container restart. Check:
    docker inspect gatecontrol | grep -A 5 Mounts
    
  3. Manual renewal kick:
    docker exec gatecontrol caddy reload --config /etc/caddy/Caddyfile
    
    (Or in the GateControl case: admin UI → Caddy reload.)

Further reading

Cookie Settings

We use cookies to improve your experience. Essential cookies are always active.

Privacy Policy
ESC
↑↓ navigate open esc close