Today, I’ll guide you through creating a Cloudflare Tunnel to proxy connections to a local server from the public internet without needing a Static IP or to open up your local network’s firewall. This is a great way to expose development services and small production environments or provide a public access point to a home network without managing Dynamic DNS, network security configurations, or changing IP addresses.
Cloudflare Tunnel leverages Cloudflare’s Edge Servers to establish a reverse proxy from your server to theirs. Reverse Proxies operate by having a service within your control making an outbound request to a public server to develop a secure connection. When the public server receives a request for your private server from the internet, it can forward it to your environment without you needing to open any point of ingress in your home network or firewall.
Prerequisites
- A Kubernetes Cluster with active Deployments and Services (I’m using Microk8s)
kubectl
CLI installed on the computer that will be deploying instructions- Cloudflare Domain with DNS Records
- Cloudflare Zero-Trust Account
Step 0: Create a Kubernetes Deployment you want accessible
For this example, I’ll use a public container hosting the game 2048. I encourage you to replace it with your own, as you don’t want to run containers on private networks you don’t trust.
Our example application has the following sections:
Namespace
: A virtual environment in which our demo app will live. Great for keeping things organized and easy to clean.Deployment
: managing the container that handles the requests,Service
: tells Kubernetes how to route requests to it.
kind: Namespace
apiVersion: v1
metadata:
name: workbench
labels:
name: workbench
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: game-2048
namespace: workbench
spec:
replicas: 1
selector:
matchLabels:
app: game-2048
template:
metadata:
labels:
app: game-2048
spec:
containers:
- name: backend
image: alexwhen/docker-2048
ports:
- name: http
containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: game-2048
namespace: workbench
spec:
ports:
- name: http
port: 80
targetPort: 80
selector:
app: game-2048
You can deploy the above configurations to Kuberentes by running the following command:
kubectl apply -f ./workbench.yaml
Step 1: Install cloudflared
locally
We must use cloudflared
locally to generate a configuration file to ensure the Kubernetes service can authenticate with our Cloudflare account.
I use homebrew to manage all of the development tools I use and highly recommend it. However, if you don’t, Cloudflare provides alternative installation options.
brew install cloudflared
Step 2: Create a Cloudflare Tunnel
Once cloudflared
is installed, we must log into our Cloudflare account to create our tunnel.
Run this command to log in, which will open a browser window for you to sign in with your usual credentials:
cloudflared tunnel login
Once authenticated, you’ll run the following command to create a new Cloudflare Tunnel and generate the JSON file with the necessary credentials in the next step.
cloudflared tunnel create YOUR_TUNNEL_NAME
Step 3: Create credentials secret
When you create your Cloudflare Tunnel, it will generate a credentials file that will allow the cloudflared
deployment to connect to Cloudflare’s public servers.
The credentials that cloudflared
generates will have the following naming convention: <Tunnel-UUID>.json
where Tunnel_UUID
is the unique ID of the Cloudflare Tunnel created from the command in the previous step.
Use the following command to add a secret to your Kubernetes cluster, making sure to update the file location and the Tunnel_UUID.
kubectl create secret generic tunnel-credentials --from-file=credentials.json=./TUNNEL_UUID.json
Step 4: Create cloudflared
ConfigMap and Deployment
Now that we have the credentials uploaded to the Kubernetes Cluster, we can add our cloudflared
service and have it provide a secure connection using our Cloudflare Tunnel.
Our cloudflared
application, in the YAML below, has the following sections:
Namespace
: A virtual environment specifically forcloudflared
to keep things organized and allow us to use the same solution for future applications we want to use with tunnels.Deployment
: Manages the container that will runcloudflared
.ConfigMap
: gives thecloudflared
instance configuration instructions. This Kubernetes resource replaces having to manually write aconfig.yaml
file and will properly track its state.
Secret to give authentication to the deployment.
kind: Namespace
apiVersion: v1
metadata:
name: cloudflared
labels:
name: cloudflared
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflared
spec:
selector:
matchLabels:
app: cloudflared
replicas: 2 # You could also consider elastic scaling for this deployment
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:2022.3.0
args:
- tunnel
# Points cloudflared to the config file, which configures what
# cloudflared will actually do. This file is created by a ConfigMap
# below.
- --config
- /etc/cloudflared/config/config.yaml
- run
livenessProbe:
httpGet:
# Cloudflared has a /ready endpoint which returns 200 if and only if
# it has an active connection to the edge.
path: /ready
port: 2000
failureThreshold: 1
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: config
mountPath: /etc/cloudflared/config
readOnly: true
# Each tunnel has an associated "credentials file" which authorizes machines
# to run the tunnel. cloudflared will read this file from its local filesystem,
# and it'll be stored in a k8s secret.
- name: creds
mountPath: /etc/cloudflared/creds
readOnly: true
volumes:
- name: creds
secret:
# By default, the credentials file will be created under ~/.cloudflared/<tunnel ID>.json
# when you run `cloudflared tunnel create`. You can move it into a secret by using:
# ```sh
# kubectl create secret generic tunnel-credentials \
# --from-file=credentials.json=/Users/yourusername/.cloudflared/<tunnel ID>.json
# ```
secretName: {}tunnel-credentials
# Create a config.yaml file from the ConfigMap below.
- name: config
configMap:
name: cloudflared
items:
- key: config.yaml
path: config.yaml
---
kind: ConfigMap
metadata:
name: cloudflared
namespace: cloudflared
data:
config.yaml: |
# Name of the tunnel you want to run
tunnel: homelab
credentials-file: /etc/cloudflared/creds/credentials.json
# Serves the metrics server under /metrics and the readiness server under /ready
metrics: 0.0.0.0:2000
# Autoupdates applied in a k8s pod will be lost when the pod is removed or restarted, so
# autoupdate doesn't make sense in Kubernetes. However, outside of Kubernetes, we strongly
# recommend using autoupdate.
no-autoupdate: true
# The `ingress` block tells cloudflared which local service to route incoming
# requests to. For more about ingress rules, see
# https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/ingress
#
# Remember, these rules route traffic from cloudflared to a local service. To route traffic
# from the internet to cloudflared, run `cloudflared tunnel route dns <tunnel> <hostname>`.
# E.g. `cloudflared tunnel route dns example-tunnel tunnel.example.com`.
ingress:
# The first rule proxies traffic to the httpbin sample Service defined in app.yaml
- hostname: 2048.example.biz
service: http://game-2048.workbench:80
# This rule sends traffic to the built-in hello-world HTTP server. This can help debug connectivity
# issues. If hello.example.com resolves and tunnel.example.com does not, then the problem is
# in the connection from cloudflared to your local service, not from the internet to cloudflared.
# - hostname: hello.example.com
# service: hello_world
# # This rule matches any traffic which didn't match a previous rule, and responds with HTTP 404.
- service: http_status:404
While this YAML example contains a lot of the initiation that Cloudflare created, we are changing the way they state the ingress.service
value to be compatible with Services within different namespaces than where cloudflared
runs. If your application and cloudflared
run in the same namespace, you only need to use the service name for the application.
You can deploy the above configurations to Kuberentes by running the following command:
kubectl apply -f ./cloudflared.yaml
Step 5: Connect your domain to your new tunnel
This last step will be completed within Cloudflare’s web app. Go to your Dashboard for the account with the domain you want to use, then navigate to DNS > Records.
Add a new CNAME Record for the domain/sub-domain you want to use, ensuring that Proxy Status is enabled and the target is TUNNEL_UUID.cfargotunnel.com
(replacing TUNNEL_UUID
with the UUID from Step 3). If you forgot or prefer to access your Tunnel UUID easily, it is visible within your Cloudflare Zero Trust dashboard under Networks > Tunnels.
Once you save the new record, Cloudflare’s public servers will route traffic from that domain to your private cluster within minutes.
Congrats
You should now be able to access the Deployment on your private Kubernetes Cluster by the domain specified in Step 5! Anytime you want to add a new service, all you need to do is edit and redeploy the ConfigMap for cloudflared
by adding the Service of the new app to the ingress
section at the bottom.
I hope you enjoy your new server capabilities!