The TLS Handshake Nobody at Home Sees: Full vs Full-Strict Behind Cloudflare

Traefik on vmi2633427 issues no certificates and holds no ACME credentials — the browser's cert and the origin's cert are two entirely different documents, and closing the gap between them is one flag in Cloudflare's dashboard.

The TLS Handshake Nobody at Home Sees: Full vs Full-Strict Behind Cloudflare

openssl s_client -connect nissaar.com:443 </dev/null 2>/dev/null | openssl x509 -noout -issuer returns issuer=C = US, O = Google Trust Services, CN = WE1. Traefik never requested that certificate, never proved a domain challenge, and never speaks to Let's Encrypt or any other ACME endpoint. It holds a self-signed certificate that has nothing to do with what a visitor's browser sees.

Full vs Full (strict) — the origin hop has its own, separate TLS handshake
Full vs Full (strict) — the origin hop has its own, separate TLS handshake

Two handshakes, two certificates — the browser only ever inspects the first one.

Two TLS connections, not one

nissaar.com is proxied through Cloudflare — orange-clouded DNS, meaning traffic terminates at Cloudflare's edge before it ever reaches vmi2633427. dig +short nissaar.com returns 104.21.39.192 and 172.67.148.88, both Cloudflare anycast addresses, not the origin's real IP. The visible TLS handshake — the one the browser's padlock is judging — ends at that edge, using Cloudflare's Universal SSL certificate, issued by Google Trust Services.

What happens next is a second, independent TLS connection: Cloudflare, acting as a client, reaches back to the origin over HTTPS. Traefik answers that connection with its own certificate — a self-signed one, generated automatically because entryPoints.websecure.http.tls is set to {} with no certResolver behind it. No ACME, no Let's Encrypt account, no .well-known/acme-challenge. Traefik's only job at the TLS layer is to have some certificate on hand so the port speaks TLS at all.

Why a self-signed cert doesn't fail the connection

Cloudflare's per-zone SSL/TLS setting controls how strict that second hop is. There are effectively two modes that matter here:

  • Full — the origin connection must be encrypted, but the certificate is not validated against any trust store. Self-signed, expired, wrong hostname: all accepted.
  • Full (strict) — the origin certificate must be valid and trusted by Cloudflare.

The zone is set to Full. That is a deliberate, working configuration — it is why Traefik can run with zero certificate management — but it has a specific, nameable consequence.

The gap: encrypted, not authenticated

Full mode means the Cloudflare-to-origin hop is encrypted against passive eavesdropping but not protected against an active man-in-the-middle sitting between Cloudflare's edge and vmi2633427. Since Cloudflare accepts any certificate at that hop, an attacker who could intercept that specific path (compromised upstream network, DNS pointed elsewhere, a rogue router) could present their own self-signed certificate and Cloudflare would not object. In practice the traffic mostly runs over a hosting provider's internal network, so the realistic exposure is narrow — but "narrow" is not "closed," and closing it costs nothing.

The fix: Origin CA, not more ACME

The right tool here is not Let's Encrypt — it's Cloudflare's Origin CA, a certificate that:

  • is free and valid for up to 15 years,
  • is trusted only by Cloudflare (never by a browser connecting directly, which is fine since the origin is never reached directly),
  • needs no ACME client, no renewal cron job, and no API token stored on the box.

Generate it in the Cloudflare dashboard (SSL/TLS → Origin Server), mount the resulting cert/key pair into the Traefik container, point tls.certificates at them in the static or dynamic config, and flip the zone from Full to Full (strict). Cloudflare will now refuse to forward traffic to an origin presenting anything else — including, usefully, an attacker's forged certificate.

The entrypoint that stays inert

Worth noting while auditing TLS on this box: the dnsovertls entrypoint on :853 is defined but unused. DNS-over-TLS cannot be proxied through Cloudflare's HTTP-only edge, and it would need its own grey-clouded (non-proxied) DNS record plus a publicly trusted certificate to work at all. No router currently binds to it. It costs nothing sitting there, but it is not a second TLS surface to worry about — it is dead configuration, not a live gap.

Series: The Self-Hosting Stack. Next: why Traefik never touches /var/run/docker.sock at all.