For KC02, we securely expose Kubernetes services to the internet using Cloudflare Tunnel, powered by cloudflared. This setup provides a zero-trust model for public access, avoiding the need to open ports on our firewall or expose services directly via public IP.

Note there is no inbound connect to our home network. cloudflared makes an outbound connection to Cloudflare’s servers.

Prerequisites

  1. A Cloudflare account
  2. A domain managed by Cloudflare DNS (e.g. kyjung.com)
  3. A Kubernetes service already available locally that you want to safely expose to the internet. (e.g. http://linkding:9090)

Configuring tunnel from CLI

  1. Install cloudflared macOS:
brew install cloudflared
  1. Authenticate cloudflared
# This will log in and create a ~/.cloudflared/cert.pem file
cloudflared tunnel login

A browser window will open prompting you to log in to your Cloudflare account. After logging in, select your host name.

  1. Create Tunnel
# Create tunnel with a name and generate a tunnel credentials file
cloudflared tunnel create <tunnel_name>
  • From the output of the command, take note of the tunnel’s UUID and the path to your tunnel’s credentials file. ex. ~/.cloudflared/59c9756.json
  • This command will create a subdomain of .cfargotunnel.com
  1. Create kubernetes secret yaml file.
kubectl create secret generic tunnel-credentials --from-file=credentials.json=~/.cloudflared/59c9756.json --dry-run=client -o yaml > cloudflare-secret.yaml
  1. Encrypt secret yaml file using SOPS with age encryption. For more details, see Secrets Management.
# Add AGE_PUBLIC_KEY to environment variables
export AGE_PUBLIC_KEY=age1...
 
# Encrypt YAML manifest with age key
sops --encrypt --age $AGE_PUBLIC_KEY --encrypted-regex '^(data|stringData)$' --in-place cloudflare-secret.yaml
  1. Save to apps/<cluster>/<app>/cloudflare-secret.yaml

Configure DNS

Go to Cloudflare DNS, add a new record to the kyjung.com domain.

  • Type: CNAME
  • Name: What you want the subdomain to be. For example, if I want to deploy the Linkding app to links.kyjung.com, the name would be links
  • Target: The .cfargotunnel.com url provided by the create tunnel command. ex. ae7fc673-9039-438b-a94c-3f15e1847d86.cfargotunnel.com
  • Proxy Status: Select Proxied

Add Tunnel Manifests

Add yaml file for cloudflared

  1. Save as apps/<cluster>/<app>/cloudflare.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: 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:latest
        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:
          secretName: tunnel-credentials
 
      # Create a config.yaml file from the ConfigMap below.
      - name: config
        configMap:
          name: cloudflared
          items:
          - key: config.yaml
            path: config.yaml
---
# This ConfigMap is just a way to define the cloudflared config.yaml file in k8s.
# It's useful to define it in k8s, rather than as a stand-alone .yaml file, because
# this lets you use various k8s templating solutions (e.g. Helm charts) to
# parameterize your config, instead of just using string literals.
apiVersion: v1
kind: ConfigMap
metadata:
  name: cloudflared
data:
  config.yaml: |
    # Name of the tunnel you want to run
    
    tunnel: links
 
    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
    no-autoupdate: true
 
    ingress:
    - hostname: links.kyjung.com
      service: http://linkding:9090
 
    # This rule matches any traffic which didn't match a previous rule, and responds with HTTP 404.
    - service: http_status:404
  1. Update apps/<cluster>/<app>/kustomization.yaml file to include cloudflare-secret.yaml and cloudflare.yaml
# apps/staging/linkding/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: linkding
resources:
	- ../../base/linkding/
	- cloudflare.yaml
	- cloudflare-secret.yaml
	- linkding-container-env-secret.yaml

Resources

This guide is adapted from the cloudflare official guide to use SOPS with age encryption as described in Secrets Management.