Setting up Zero-Trust Access to a Home Kubernetes Cluster

June 13, 2024

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 for cloudflared 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 run cloudflared.
  • ConfigMap: gives the cloudflared instance configuration instructions. This Kubernetes resource replaces having to manually write a config.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

Untitled

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!

Resources