Challenge ·Medium

Access a Private Kubernetes API Server Through an SSH SOCKS Proxy

The production Kubernetes cluster lives inside a private VPC, and its API server has no public endpoint. Turn an SSH connection to the bastion host into a SOCKS proxy and teach kubectl to use it, so the cluster becomes manageable from your workstation.

You're a platform engineer at Acme. The production Kubernetes cluster runs inside a private VPC, and, as is common for hardened setups, its API server has no public endpoint: the control plane node (cplane-01) listens on 172.16.0.2:6443, an address that only machines inside the VPC can reach.

You have the cluster's admin credentials in ~/.kube/config on your workstation, but the workstation has no route into the VPC, so every kubectl command just hangs and eventually times out:

kubectl get nodes --request-timeout=5s
E0901 16:19:02.264814    1566 memcache.go:265] "Unhandled Error" err="couldn't get current server API group list: Get \"https://172.16.0.2:6443/api?timeout=5s\": net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)"
...
Unable to connect to the server: net/http: request canceled while waiting for connection (Client.Timeout exceeded while awaiting headers)

There is a public-facing bastion (jump host) (bastion, 203.0.113.20) that you can SSH into, and it is connected to the VPC - so it can reach the API server. You could forward a single local port to 172.16.0.2:6443 and point the kubeconfig at localhost instead, but that means rewriting the server address, which might make its certificate not valid.

The Kubernetes API server listens on a private network address, and the workstation has no route to it - both curl and kubectl time out.

Kubernetes has a cleaner answer to this, documented in the official Use a SOCKS5 Proxy to Access the Kubernetes API task: run a SOCKS proxy over SSH and tell kubectl to send its API traffic through it. kubectl keeps connecting to the real API server address, so certificates keep working.

Your task consists of two steps:

First, set up an SSH dynamic local port forwarding session through the bastion so that a SOCKS proxy on the workstation's localhost:1080 can reach the API server inside the VPC:

Then, make kubectl use that proxy persistently, by configuring the cluster entry in ~/.kube/config, so that a plain kubectl get nodes works from any new terminal on the workstation, without extra environment variables:

Hint 1: How dynamic local port forwarding works

A regular local tunnel (ssh -L local_port:host:port bastion) pins the forwarding to a single destination. Dynamic local forwarding does not specify a destination: with ssh -D and only a single local port, OpenSSH turns your ssh client into a SOCKS proxy. Each connection made through that proxy is sent over your SSH link and connected to whatever address the SOCKS client asks for - resolved and reached from the bastion's side of the network.

SSH dynamic local port forwarding (SOCKS proxy) visualized.

Run man ssh and read the -D section, or walk through the example in the SSH Tunnels tutorial. If you have never used a SOCKS proxy over SSH before, solve Reach a Whole VPC Through an SSH SOCKS Proxy first.

Hint 2: Checking the proxy without kubectl

curl speaks SOCKS, too. Before touching the kubeconfig, you can point curl at the proxy (see the --socks5-hostname option) and request https://172.16.0.2:6443/version. An HTTP 401 Unauthorized response is a good sign here - it means you reached the API server and it is asking for credentials.

Hint 3: How kubectl can be told to use a proxy

kubectl honors the usual HTTPS_PROXY and https_proxy environment variables, and it also understands SOCKS URLs in it (socks5://...). That is a quick way to test the tunnel, but it only affects the shell where the variable is exported, and it applies to every cluster you talk to from that shell.

The persistent, per-cluster way is the proxy-url field of a cluster entry in the kubeconfig file. You can add it by editing ~/.kube/config directly or with kubectl config set-cluster (see its --help output). Both approaches are described in the official task.