How I Secure Remote Access to My Homelab
How I use Tailscale, Cloudflare Tunnel, and Cloudflare Access for different kinds of remote access without opening inbound ports.
Remote access used to mean making a service reachable and trusting its login page to handle the rest.
I did not like that model for my homelab.
Some services only need to be available to my own devices. Others are easier to use through a normal web address without connecting to a VPN first. A few applications have their own authentication, but I still do not want their login pages exposed as the first and only security boundary.
I needed more than one remote-access method because the services do not all have the same requirements.
My current approach uses Tailscale for private network access and Cloudflare Tunnel with Cloudflare Access for selected browser-based services. Neither one makes the homelab automatically secure. They solve different access problems, and the policy still depends on how I configure each service.
What I was trying to protect
The first requirement was simple: I did not want to open inbound ports on my router for every application I needed to reach remotely.
Opening a port is not automatically insecure, but it creates another public entry point that I have to patch, monitor, and understand. That did not make sense for services intended only for me or a small list of approved users.
I also wanted to reduce how much trust I placed in each application’s login page. If a public hostname leads directly to an application, anyone who discovers that hostname can usually reach the login screen. The application then becomes responsible for handling every unauthenticated request correctly.
My goals were:
- Keep private services off the public internet
- Avoid inbound router port forwarding
- Authenticate users before selected requests reach an application
- Grant access per service instead of granting access to everything
- Keep the application’s own authentication enabled
- Use a normal browser for the few services that should not require VPN access
That last requirement is why I did not use one tool for everything.
Tailscale for private access
Tailscale is my default for services that should behave like private network resources.
A device joins my tailnet, authenticates, and receives access according to the rules I configure. From there, I can reach internal systems without publishing each one through a public hostname.
The path is roughly:
Approved device → Tailscale → private homelab service
This works well for administration and services that only I need. The service does not need a public DNS record, and someone cannot reach it merely by finding a subdomain.
The tradeoff is that the client device needs Tailscale. That is reasonable for my laptop, phone, and other managed devices. It is less convenient when I want to give someone browser access without asking them to join my private network first.
That is where Cloudflare fits.
What Cloudflare Tunnel changes
Cloudflare Tunnel lets a connector inside my network establish an outbound connection to Cloudflare. Because the connector initiates that connection, I do not need to forward a public router port directly to the application.
The basic path is:
Browser → Cloudflare → outbound tunnel → internal application
That solved the connectivity problem, but it did not solve the entire access problem.
Without another policy in front of the tunnel, someone who found one of my subdomains could still reach the application’s login page. I had removed the inbound port, but the application was still publicly reachable through Cloudflare.
This was the distinction I had missed at first:
- Cloudflare Tunnel creates a route to the service.
- Cloudflare Access decides who may use that route.
A tunnel is not an authentication policy.
Adding an identity check before the application
I added Cloudflare Access in front of the services published through Cloudflare Tunnel.
When I visit one of those subdomains, Cloudflare evaluates the Access policy before forwarding the request through the tunnel. For my current setup, authentication uses a one-time PIN sent to an approved email address.
The request flow is:
User requests service.example.com
↓
Cloudflare evaluates the Access policy
↓
User authenticates with an approved identity
↓
Cloudflare permits the request
↓
Request continues through Cloudflare Tunnel
↓
Application receives the request
↓
Application applies its own authentication and authorization
An identity that is not allowed by the policy does not get forwarded to the application.
I created a separate Access application and policy for each service instead of using one rule for the entire homelab. That lets me approve someone for one application without giving that person access to everything behind the tunnel.
This is where least privilege became practical for me. Access is not only about deciding whether a person is trusted. It is also about limiting what that approved person can reach.
Why I keep the application login enabled
Cloudflare Access adds an authentication layer, but I do not treat it as a replacement for the application’s own authentication.
The two layers answer different questions:
- Cloudflare Access asks whether this identity may reach the application.
- The application decides what the authenticated user may do inside it.
If I removed the application’s login, anyone who passed the outer policy could potentially inherit the same application permissions. Keeping both layers gives me a smaller public surface without collapsing identity and application authorization into one control.
It also protects against mistakes elsewhere in the design. If I later change a DNS record, tunnel route, or Access policy incorrectly, the application still has its own login rather than assuming every request is already trusted.
How I decide which method to use
I now start by asking who needs the service and what kind of access they need.
| Requirement | My starting point |
|---|---|
| Only my managed devices need access | Tailscale |
| I need administrative or network-level access | Tailscale |
| A user needs browser access without joining my tailnet | Cloudflare Tunnel with Access |
| A service should be public for everyone | Public DNS and the application’s normal public architecture |
| A service should never have a public hostname | Tailscale or internal-only access |
This is not a universal product comparison. It is the decision process that fits my homelab.
Tailscale gives an approved device private network access. Cloudflare Access evaluates identity at a public application boundary. Using Cloudflare for a service does not mean I should also expose its entire host or network. Using Tailscale does not mean every connected device should automatically reach every internal system.
The scope still matters.
How I verify the boundary
A configuration page saying a policy is enabled is not enough. I need to test what an actual request can do.
For each service published through Cloudflare, the checks should include:
- An unauthenticated browser is stopped by Cloudflare Access.
- An unapproved identity is denied.
- An approved identity can complete authentication.
- The application still requires its own login when appropriate.
- The service is not reachable through an unintended public IP or router port.
- A different Access application does not grant access accidentally.
- Internal access still behaves the way I expect.
This verification matters because remote access crosses several layers. DNS can point at the wrong place. A tunnel route can select the wrong service. An Access policy can be too broad. The application can also have its own authentication or proxy assumptions.
Testing the complete path is the only way to know which boundary is actually enforcing access.
What this setup does not solve
Cloudflare Access and Tailscale reduce exposure, but neither one removes the need to secure the applications and systems behind them.
Cloudflare Access does not:
- Patch a vulnerable application
- Prevent an approved account from being compromised
- Replace authorization inside the application
- Protect an origin that is exposed through another route
- Make every other homelab service private automatically
Tailscale does not:
- Make every connected device trustworthy forever
- Replace access-control rules between systems
- Protect a service from a compromised device that is already authorized
- Remove the need to update and monitor internal services
The one-time PIN method also depends on the security of the approved email account. If that account is compromised, the attacker may be able to pass the Access policy. A stronger identity provider or additional authentication factor may be appropriate for more sensitive applications.
These limitations do not make the tools ineffective. They define the boundaries I should not expect them to cover.
What I learned
The most useful lesson was that connectivity and authorization are separate problems.
Cloudflare Tunnel gave me a path to an internal service without an inbound router port. Cloudflare Access added an identity decision before a request could use that path. The application still controls what a user can do after the request arrives.
Tailscale solves a different problem. It lets approved devices reach private resources without turning each resource into a public web application.
I had originally treated remote access as a yes-or-no decision: either a service was reachable or it was not. I now think about it in layers:
Can the device or user reach the entry point?
↓
Has the identity been verified?
↓
Is that identity allowed to reach this service?
↓
What may the user do inside the application?
That does not make my homelab “zero trust.” It gives me a practical way to verify access, reduce public exposure, and avoid giving every approved user the same reach.
The goal is not to put Cloudflare in front of everything or force every service through Tailscale. The goal is to choose the narrowest access path that still lets the service do its job.