DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

Expose Your Services to the Internet Without Opening Ports: Cloudflare

Last month, I needed to expose a new monitoring service I installed on my own VPS to the internet. I could have opened port 443 the classic way and routed Nginx, but that always gives me an uneasy feeling, as if I'm exposing my internal network to the outside world. Especially while working on an internal platform for a bank, I experienced firsthand many times that even the smallest vulnerability carries massive risks. This is exactly where the Cloudflare Tunnel solution came to my rescue, completely eliminating the risk of opening ports.

Cloudflare Tunnel is a Zero Trust solution that creates an encrypted outbound tunnel from your server to the Cloudflare network, allowing you to securely expose your services to the internet without opening any inbound ports. This simplifies your firewall configurations on your servers, leverages Cloudflare's vast infrastructure against DDoS attacks, and significantly narrows your potential vulnerability surface. When we needed to expose the operator screens of a production ERP to the outside, this approach provided us with serious security and management convenience.

What is Cloudflare Tunnel and Why is it Important?

Cloudflare Tunnel, as the name suggests, is a service that creates a secure and encrypted "tunnel" between your server and Cloudflare's global network. Thanks to this tunnel, you can publish any service running on your internal network (web server, SSH, RDP, database, etc.) through Cloudflare's proxy capabilities without exposing it directly to the internet.

For me, the biggest advantage was not having to open public HTTP/S ports like 80 or 443 in my server's firewall. In traditional approaches, when we want to expose a web service to the outside, we open the relevant ports, and these ports become entry points for potential attackers. Cloudflare Tunnel reverses this paradigm by establishing an outbound connection from the server to Cloudflare, and all traffic flows through this tunnel. This way, none of my server's external-facing ports remain open, which largely eliminates attack vectors like brute-force and port scanning. I preferred this method especially when exposing the backend of my own side project, and having Cloudflare handle more than 99% of the daily incoming attack traffic lifted a huge burden off my shoulders.

What are the Risks of the Traditional Port Forwarding Approach?

Traditionally, when we expose a service to the internet, we open the relevant ports (for example, 80/443 for HTTP/S, 22 for SSH) in our server's firewall. Although this process seems simple, it brings along many security risks, and I have encountered these risks multiple times. Seeing brute-force attacks hit the SSH port just 7 minutes after spinning up a server made me realize how quickly any open port can become a target.

At the top of these risks is the increased attack surface. Since an open port is accessible from the outside, it can be scanned by potential attackers and used to search for vulnerabilities. If there is a known security vulnerability in the service, it can be quickly exploited. Additionally, DDoS attacks targeting open ports can consume your server's resources, leading to service interruptions. Another risk is configuration errors. Misconfiguring firewall rules, granting broader access permissions than expected, or leaving unnecessary ports open are easy mistakes to make, and these mistakes usually lead to major problems before they are noticed. For example, forgetting a test port that was opened just once in a production ERP and detecting the data leaked through this port cost us 3 days. Cloudflare Tunnel eliminates such risks and offers a more secure infrastructure.

⚠️ Remember

Open ports are a server's doors to the outside world. The fewer doors you open, the fewer entry attempts you will face. Regularly reviewing your firewall rules and opening only the ports that are truly needed is of critical importance.

How Cloudflare Tunnel Works: The Foundation of Zero Trust Networking

Unlike traditional network architecture, the working logic of Cloudflare Tunnel is that it initiates the connection from the server to Cloudflare. This is based on the "outbound connection" principle instead of "inbound connection" and is one of the cornerstones of Zero Trust architecture. A small daemon (service) called cloudflared running on your server constantly establishes and maintains an encrypted TCP connection to the Cloudflare network.

When users want to access your site or service, their requests go directly to the Cloudflare network. Cloudflare forwards this request through the tunnel to the cloudflared daemon on your server. cloudflared receives this request, routes it to the relevant service on the local network, gets the response, and sends it back to Cloudflare through the tunnel. Cloudflare then delivers this response to the end user. Thanks to this flow, your server has no listening ports facing the internet; all communication takes place over Cloudflare's secure infrastructure. This model aligns perfectly with the principle of not letting external traffic directly into the internal network, which I frequently see in remote access (VPN/ZTNA) architectures. In a client project, we used a similar architecture while providing employees with access to internal applications via ZTNA.

Diagram

ℹ️ Zero Trust Emphasis

Zero Trust architecture is based on the principle of "never trust, always verify." Cloudflare Tunnel applies this principle by routing all communication through Cloudflare's security layer and closing the server's external-facing ports, instead of routing incoming traffic directly to the server.

Cloudflare Tunnel Installation: Step-by-Step Guide

Installing Cloudflare Tunnel consists of a few simple steps and usually takes me 15-20 minutes. Here are the steps I applied on my own VPS and in a production ERP:

1. Cloudflare Account and Domain Setup

First, you must have an active Cloudflare account and a domain pointed to Cloudflare. Your domain's DNS records must be managed through Cloudflare. If you don't have a domain yet, you need to get one and add it to Cloudflare. I usually enable DNSSEC as well, as it provides an extra layer of security.

2. Installing the cloudflared Daemon

You need to install the cloudflared tool on your server. This tool is the client software that will connect the tunnel to the Cloudflare network. For Linux, I generally follow these steps:

# First, let's install the necessary packages
sudo apt update && sudo apt install -y curl lsb-release

# Let's add the Cloudflare repository
curl -fsSL https://pkg.cloudflare.com/cloudflare-repo.gpg | sudo gpg --dearmor -o /usr/share/keyrings/cloudflare-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/cloudflare-archive-keyring.gpg] https://pkg.cloudflare.com/cloudflared $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/cloudflared.list > /dev/null

# Let's install cloudflared
sudo apt update && sudo apt install cloudflared
Enter fullscreen mode Exit fullscreen mode

After installation, we can check if it is installed correctly using the cloudflared --version command.

3. cloudflared Authorization

After installing cloudflared, you need to associate it with your Cloudflare account. This is an authorization process done via a browser:

cloudflared tunnel login
Enter fullscreen mode Exit fullscreen mode

When you run this command, it will give you a URL. Open this URL in your browser, log in to your Cloudflare account, and select which domain you want to authorize your tunnel for. After successful authorization, a certificate file will be created at ~/.cloudflared/cert.pem. This certificate allows cloudflared to connect securely to the Cloudflare network.

4. Creating a New Tunnel

Now let's create a tunnel. I usually name the tunnel in a way that describes the service, such as monitoring-app or erp-frontend.

cloudflared tunnel create my-first-tunnel
Enter fullscreen mode Exit fullscreen mode

This command will return a Tunnel ID and create a credentials file at ~/.cloudflared/<Tunnel-ID>.json. This JSON file is used to authenticate the tunnel and must be kept absolutely secure. Note down this Tunnel ID, as we will use it in the configuration file.

5. Creating the Tunnel Configuration File

To specify which incoming requests the tunnel will route to which local services, we need to create a YAML configuration file. I usually place this file at /etc/cloudflared/config.yaml.

# /etc/cloudflared/config.yaml
tunnel: <Tunnel-ID>  # The Tunnel ID you got from the create command
credentials-file: /root/.cloudflared/<Tunnel-ID>.json # Path to the credentials file

ingress:
  - hostname: myapp.example.com
    service: http://localhost:8080
  - hostname: ssh.example.com
    service: ssh://localhost:22
  - service: http_status:404 # Return 404 for all requests that do not match the hostnames above
Enter fullscreen mode Exit fullscreen mode

In the example above, all requests coming to myapp.example.com will be routed to port 8080 on the server, and requests coming to ssh.example.com will be routed to port 22. The http_status:404 is a catch-all rule to reject all requests coming outside of the defined hostnames. This forms the basis of security: route only what is allowed, block the rest.

6. Creating DNS Records

In your Cloudflare panel, you need to create a CNAME DNS record for each hostname you will route to your tunnel. For example, for myapp.example.com:

  • Type: CNAME
  • Name: myapp
  • Target: <Tunnel-ID>.cfargotunnel.com
  • Proxy Status: Proxied (the cloud icon must be orange)

This record ensures that requests coming to myapp.example.com are routed to the Cloudflare network and makes it possible to reach your service through the tunnel.

7. Running the Tunnel as a Service

Running cloudflared as a systemd service ensures that it starts automatically when the server reboots and remains constantly active.

sudo systemctl enable cloudflared tunnel run my-first-tunnel
sudo systemctl start cloudflared tunnel run my-first-tunnel
sudo systemctl status cloudflared tunnel run my-first-tunnel
Enter fullscreen mode Exit fullscreen mode

Verify that the service is running using the systemctl status command. If there is a problem, you can examine the logs with the journalctl -u cloudflared command and troubleshoot. Last month, while spinning up a cloudflared service, it got OOM-killed because I misconfigured the cgroup memory.high limit; by examining the logs, I realized I needed to allocate more memory to cloudflared.

💡 Debugging Tips

If your tunnel doesn't work, first try running the cloudflared tunnel run my-first-tunnel command directly in the terminal to see errors live. Usually, a typo in the configuration file or a mistake in the DNS record causes the issues.

Advanced Configurations and Use Cases

Cloudflare Tunnel can do much more than just publishing simple HTTP/S services. I have used it in different scenarios, such as for the backend of my self-developed Android spam application, or for internal database access in a client project.

Secure Access to Internal Services

With Cloudflare Tunnel, you can securely expose not only web services but also internal services like SSH, RDP, and databases (PostgreSQL, MySQL, etc.) to the outside. For example, when a development team needs to access a remote PostgreSQL database, we can use Cloudflare Tunnel instead of opening ports directly.

# /etc/cloudflared/config.yaml (For PostgreSQL)
tunnel: <Tunnel-ID>
credentials-file: /root/.cloudflared/<Tunnel-ID>.json

ingress:
  - hostname: pgadmin.example.com
    service: http://localhost:5050 # PgAdmin panel
  - hostname: postgres.example.com
    service: tcp://localhost:5432 # Direct PostgreSQL connection
    originRequest:
      noTLSVerify: true # Local PostgreSQL usually doesn't use TLS, be careful
  - service: http_status:404
Enter fullscreen mode Exit fullscreen mode

This way, we can establish a direct TCP connection to the PgAdmin panel via pgadmin.example.com and to the PostgreSQL database via postgres.example.com. However, settings like noTLSVerify can carry security risks, so they should be used carefully, and TLS should be enabled on local services if possible.

Adding an Authentication Layer with Cloudflare Access

Cloudflare Access allows you to control access to services you publish through the tunnel on an identity basis. This is a fundamental part of Zero Trust architecture. For example, I can ensure that only people with specific email addresses can access pgadmin.example.com.

  1. Creating an Application in the Cloudflare Panel: Go to the "Access" tab in the Cloudflare Dashboard and create a new application under "Applications".
  2. Specifying Hostname: Specify the hostname that the application will protect (such as pgadmin.example.com).
  3. Defining Policies: Create rules that determine who can access (for example, "Email is @example.com").
  4. Identity Provider Integration: Integrate with an Identity Provider like Google Workspace, Okta, or Azure AD to verify user identities.

This way, when users try to access pgadmin.example.com, they are first subjected to identity verification by Cloudflare Access. If successful, they are routed to the service through the tunnel. This method provides an extra layer of security, especially when exposing our internal tools to the outside, and eliminates the need for a VPN.

Load Balancing and High Availability

If you are running the same service on multiple servers, you can perform load balancing with Cloudflare Tunnel. A separate cloudflared daemon is run on each server and connects to Cloudflare with the same tunnel ID. Cloudflare can automatically balance incoming traffic among these tunnels.

# config.yaml on Server A
tunnel: <Tunnel-ID>
credentials-file: /root/.cloudflared/<Tunnel-ID>.json
ingress:
  - hostname: myapp.example.com
    service: http://localhost:8080
  - service: http_status:404

# config.yaml on Server B (with the same Tunnel ID)
tunnel: <Tunnel-ID>
credentials-file: /root/.cloudflared/<Tunnel-ID>.json
ingress:
  - hostname: myapp.example.com
    service: http://localhost:8080
  - service: http_status:404
Enter fullscreen mode Exit fullscreen mode

Cloudflare can balance the load between these two tunnels using round-robin or more advanced algorithms (along with health checks). This ensures that the service continues to run even if one server goes down, which is an important step for High Availability. We used this kind of architecture in real-time dashboard designs in a production ERP, so that the failure of a single server did not stop the entire operation.

Benefits and Trade-offs I Experienced with Cloudflare Tunnel

I have been actively using Cloudflare Tunnel in various projects for years, and the benefits it brings to me are quite tangible. However, as with any technology, there are some trade-offs here as well.

Benefits:

  1. Enhanced Security: The biggest benefit. Since I can completely close the external-facing ports of my servers, my attack surface is minimized. I can directly use Cloudflare features like DDoS protection and WAF (Web Application Firewall). In a production ERP, having external SQL injection attempts automatically blocked by Cloudflare WAF eliminated a significant workload for us.
  2. Simple Configuration: I don't have to deal with port forwardings, NAT settings, or complex firewall rules. I get the job done with a single cloudflared service and a simple YAML file.
  3. Zero Trust Integration: Integration with Cloudflare Access allows me to manage access to my internal services on an identity basis. This reduces or completely eliminates the need for a VPN.
  4. Performance and Reliability: Thanks to Cloudflare's global CDN network, users connect to the service from the nearest Cloudflare Edge location, which reduces latency. Additionally, it provides high availability and automatic failover thanks to Cloudflare's vast infrastructure.

Trade-offs:

  1. Vendor Lock-in: You become dependent on the Cloudflare ecosystem. If an outage occurs in Cloudflare services, the services you publish through the tunnel may also be affected. However, since Cloudflare's uptime rates are generally very high, I can tolerate this risk.
  2. Extra Latency: Since traffic goes from your server to Cloudflare and then to the user, a very small extra latency can theoretically occur. However, this latency is usually negligible and is balanced by Cloudflare's CDN advantages. In tests I conducted on my own side project, I observed an average increase of 10-20ms compared to direct access, which did not affect the user experience.
  3. Training Cost: Since it is a new tool, other people in your team may need to learn how cloudflared works and how to manage it.

Overall, in terms of security and management convenience, the benefits offered by Cloudflare Tunnel more than make up for these trade-offs for me. Especially when I want to focus on the core part of my work instead of dealing with security vulnerabilities, Cloudflare Tunnel becomes an indispensable tool.

Conclusion

Exposing your services to the internet without opening ports is a principle that forms the basis of modern security approaches, and Cloudflare Tunnel allows you to put this principle into practice in a convenient way. By minimizing the risks brought by traditional port forwarding methods, you significantly strengthen the security posture of your servers. I actively use this technology in my own projects and client work, ensuring I do not compromise on security while reducing operational complexity.

I hope this guide has helped you understand what Cloudflare Tunnel is, how it works, and how to set it up. I am sure this tool will provide you with great convenience on your journey to building a secure infrastructure. In my next post, I will talk about an interesting cgroup limit error I experienced in systemd units and how I resolved it.

Top comments (0)