How to Lock Down Private Services for Better Security
Cloudflare’s biggest strength is how dead simple it makes DNS management and automatic SSL certificates. Anyone who’s been surprised by an expired Let’s Encrypt cert on a Saturday evening knows the pain of constant renewals. Cloudflare takes that busywork off your plate entirely and issues valid certificates for all managed domains automatically. So naturally, the idea of routing internal, private services through Cloudflare to get that same convenience is pretty tempting.
In my infrastructure, almost every private service has its own subdomain: NAS, Vault, AdGuard, Grafana, Portainer. Everything runs through Cloudflare. I covered how I built that with Docker Stacks and Cloudflare Tunnels in part 2 of my homelab series. It saves me from certificate management and gives me secure access from anywhere. But how do you make sure these services don’t suddenly become wide open to the entire internet? That’s where Cloudflare Access steps in as the bouncer at the door.
The Path to Cloudflare: No Open Ports, No Panic
Most of my services run in Docker containers on a home server. To connect them securely to Cloudflare, we use Cloudflare Tunnels. The concept is elegant: traffic flows from the inside out to Cloudflare. You don’t need to open any ports on your router. Fewer open ports mean less attack surface and significantly better sleep at night.
All traffic flows through this tunnel, and access control is handled comfortably in the Cloudflare Access Dashboard.
You create a tunnel in the Cloudflare Zero Trust Dashboard under Networks > Tunnels > Create a tunnel. That’s where you configure the cloudflared service. Once done, you get a token to use in your Docker setup with the cloudflare/cloudflared:latest image.
When setting up the container, the network configuration matters. You essentially have two options:
- Host network: The path of least resistance. The container runs on the host network and has direct access to all local services. Super easy to set up, but carries the theoretical risk that a compromised tunnel could reach your entire system.
- Dedicated Docker network: The more secure, slightly nerdier approach. You create a dedicated Docker network and only put the relevant private services and the
cloudflaredcontainer in it. This isolates the tunnel and ensures it only talks to the services that should be exposed.
Configuring Tunnel Routes: Who Goes Where?
Once the tunnel is up, you need to define routes. This is where you map which public subdomain points to which internal service. nas.example.com routes to your NAS IP, while grafana.example.com lands at the corresponding container. Cloudflare conveniently creates the matching DNS records automatically.
If you’re pointing to another Docker container running on the same network, you just need the container’s hostname and port. Since Docker has built-in internal DNS resolution, the target containers don’t even need their ports mapped to the host.

Watch out, common pitfall: When routing to a NAS or certain web frontends that strictly enforce HTTPS, you’ll often hit a 502 Bad Gateway error. This happens because of failed TLS handshakes between Cloudflare and your internal service. The fix is simple: enable No TLS Verify in the tunnel settings under Additional application settings > TLS. Cloudflare then ignores the failed handshake from the self-signed internal certificate and the connection works.

Pro tip: Reverse Proxy Inception
In my setup, I only have a single service configured in the tunnel for my websites: http://traefik:80. Traefik acts as my reverse proxy. Cloudflare passes all requests through to Traefik, and Traefik handles the internal routing based on subdomains. This way I don’t have to touch the Cloudflare tunnel for every new container. You can find my Traefik configuration for inspiration in my GitHub repo.
Cloudflare Access Policies: The VIP Section
I used to handle access control with Security Rules in the domain dashboard, but Zero Trust is way more comfortable.
The tunnel is up, the services are reachable. Now we need to lock them down again.
We create a new Application under Access > Applications. Let’s call it simply “Home”. As the Application URL, I configured *.dieck-labs.de since I run a separate domain for my home infrastructure. The wildcard entry protects all subdomains at once.
Now we define the policies (the rules for our bouncer):
- Remote access (Allow): So I can reach my services on the go, I set up an Allow rule. I pick “Emails” as the selector and enter my personal email address. Family members who need access to the Jellyfin server go here too. When someone visits the page, they get a login screen, enter their email, receive a one-time code, and they’re in.
- Convenient access from home (Bypass): At home, you don’t want to deal with PIN codes every time. For this we need a Bypass rule. You could theoretically enter IP ranges or your public IP, but with dynamic IPs (without constant DynDNS updates) that gets annoying fast. The far more elegant solution: select Gateway as the selector. Combined with the Cloudflare WARP client (the 1.1.1.1 app) on your devices, Cloudflare automatically recognizes you as authorized. Access from your home network (or with an active WARP client) works completely seamlessly, no PIN entry required.

Securing Device Enrollment
Lastly, you should make sure that not just any device can join your organization. Under Settings > WARP Client > Device enrollment permissions (or depending on the dashboard version, under Team & Resources > Devices) you need to define a rule:
- Click Manage or Add a rule.
- Rule Name: “My Devices” (or similar).
- Selector: Emails.
- Value: Your email address(es).

I also recommend enabling “Device authentication identity” in the Teams and Apps settings.


With that, the setup is complete: your services are shielded by Cloudflare, only accessible to authorized users from outside, and at home you can keep accessing your homelab comfortably thanks to elegant bypass rules, no constant logins required.