As my application nears production readiness, one of the key considerations is securely accessing internal Kubernetes services—such as databases and message brokers—from my local development machine. Initially, I configured TCP forwarding for these services on my ingress controller:

tcp:
  "4222": nats/nats-cluster:4222
  "5432": pgo/astring-ha:5432
  "6379": redis/redis:6379
  "9042": scylla/scylla-client:9042

While each service requires authentication, I’m still not fully comfortable exposing them directly to the public internet. Ideally, only HTTP endpoints (like my backend services or monitoring tools) should be publicly accessible, protected via basic auth or other mechanisms. For internal services like PostgreSQL, NATS, Redis, and Scylla, I’d prefer a more secure approach.

Why Use WireGuard?

WireGuard is a lightweight, fast, and modern VPN solution that provides secure connections (tunnels) between devices or networks. Compared to alternatives like OpenVPN, WireGuard is often easier to configure and requires small resources. By using WireGuard, I can:

  • Hide Internal Services: Keep them off the public internet.
  • Establish Secure Tunnels: Encrypt traffic between my laptop and the Kubernetes cluster.
  • Maintain Ease of Access: Treat internal services as if they were on my local network.

Deploying WireGuard on Kubernetes

I leveraged Helm to deploy a WireGuard server inside my Kubernetes cluster. Below is an overview of the process and configuration.

Step 1: Helm Configuration

First, I created a values.yaml file to specify basic settings for the WireGuard chart:

service:
  enabled: true
  type: ClusterIP

wireguard:
  clients:
    - AllowedIPs: 10.34.0.2/32
      PublicKey: <client_public_key>
  • service.enabled: true and type: ClusterIP: Creates a cluster-internal service for the WireGuard pod.
  • wireguard.clients: Defines client configurations, mapping each client’s public key and allowed IP range.

Step 2: Install the WireGuard Helm Chart

Add the Helm repository:

helm repo add wireguard https://bryopsida.github.io/wireguard-chart
helm repo update

Then install WireGuard into a dedicated namespace:

helm install wireguard wireguard/wireguard \
  --namespace wireguard \
  --create-namespace \
  -f values.yaml

After a successful installation, you can verify the pod’s status:

kubectl get pods -n wireguard

Step 3: Expose the WireGuard UDP Port

To allow external traffic to reach the WireGuard server, you must expose UDP port 51820. In my ingress controller’s configuration, I enabled a UDP forward:

udp:
  "51820": wireguard/wireguard-wireguard:51820

Note: If you’re using a different load-balancing solution or a cloud environment, make sure UDP port 51820 is open in your firewall or security groups.

Generating Client Keys

On your local machine, install the WireGuard tools (e.g., wireguard-tools). Generate a key pair:

wg genkey | tee privatekey | wg pubkey > publickey
  • privatekey: Your client’s private key (keep it secret).
  • publickey: Your client’s public key (used in the values.yaml file).

If you want multiple clients (e.g., your laptop, another VM, etc.), generate keys for each one.

Retrieving Server Public Key

The server’s public key is stored in a Kubernetes secret. You can retrieve it by:

kubectl get secret -n wireguard

Look for a secret that contains WireGuard configuration or keys. The server’s public key is typically base64-encoded. Decode it like so:

kubectl get secret wireguard-wireguard -n wireguard -o yaml | grep publicKey
# or
kubectl describe secret wireguard-wireguard -n wireguard

Once decoded, you’ll have the server’s WireGuard public key.

Client-Side Configuration

Create a WireGuard configuration file on your client machine (e.g., wg0.conf):

[Interface]
PrivateKey = <privateKey>     # Your client private key
Address = 172.32.32.2/32      # The IP assigned to this client within the tunnel
DNS = 10.43.0.10, 8.8.8.8     # (Optional) DNS servers

[Peer]
PublicKey = <server_public_key>      # The server's public key
AllowedIPs = 10.0.0.0/16, 10.43.0.0/16, 172.32.32.0/24
Endpoint = <public_ip>:51820
PersistentKeepalive = 25

Explanation:

  • PrivateKey: Your generated client private key.
  • Address: The WireGuard interface IP. Must match what was specified in the Helm config (e.g., 10.34.0.2/32).
  • DNS: Optional, configures DNS servers for resolving cluster hostnames.
  • PublicKey: The server’s public key.
  • AllowedIPs: Specifies which IP ranges should route through the VPN tunnel. This often includes pod networks (e.g., 10.0.0.0/16, 10.43.0.0/16) and your WireGuard subnet.
  • Endpoint: The public IP or domain name of your cluster plus :51820.
  • PersistentKeepalive: Sends periodic keepalive packets, preventing NAT timeouts.

Connecting to the Kubernetes Cluster

Once the client configuration is ready, bring up the WireGuard tunnel:

wg-quick up wg0

(Where wg0 is the filename of your WireGuard config in /etc/wireguard/ or the path to your config file.)

Now, any requests to the internal Kubernetes networks will route through the WireGuard tunnel, ensuring that your internal services (e.g., NATS, PostgreSQL, Redis, Scylla) are not publicly exposed, yet remain accessible to you from anywhere via the VPN.

Conclusion

By deploying WireGuard in Kubernetes and configuring a UDP pass-through on your ingress or load balancer, you can securely tunnel into your cluster’s internal services without publicly exposing them. This approach:

  • Enhances Security: Limits direct public exposure of critical databases and message brokers.
  • Simplicity: WireGuard is lightweight and straightforward to set up compared to some other VPN solutions.
  • Scalability: Easily manage multiple clients by adding their public keys to the Helm chart or dynamically updating the Kubernetes config.

As my application moves closer to production, having this secure connection to internal services greatly reduces risk and streamlines my development workflow. If you need a fast, modern, and secure VPN, WireGuard is definitely worth exploring further.