click the screen · press Enter
← back to blog
Kubernetes Journey · Topic 12

kubectl proxy and port-forward Aren't Bypasses, They're Shortcuts

kubectl proxy and port-forward explained - Kubernetes Journey

Say you've got a service running inside your cluster, a little web app, and it's set to ClusterIP, which means it's only reachable from inside the cluster. Nothing outside can see it. Now you want to poke at it from your laptop to check it's alive. Do you rush off and expose it to the whole internet? No. There are two clean, authenticated ways in, and by the end of this you'll have run both.

Those two tools are kubectl proxy and kubectl port-forward. Both lean on your kubeconfig so you never type a password or juggle certificates, and both keep the service private to your machine. I'll explain what each one does, then we'll spin up a tiny lab and actually use them.

Why kubectl gets in and raw curl doesn't

Quick foundation, because it explains everything that follows. When you run kubectl, you're not typing credentials, yet it works. That's because kubeconfig already holds your identity (a client certificate or token), and kubectl presents it automatically. Watch what happens when you skip that and hit the API server directly with curl:

raw curl to the API, no credentials
$ curl -k https://192.168.1.10:6443/ { "kind": "Status", "status": "Failure", "message": "forbidden: User \"system:anonymous\" cannot get path \"/\"", "reason": "Forbidden", "code": 403 }
No creds means you're system:anonymous, and the API server says no. That 403 is authentication doing its job.

So the whole trick of both tools is: they borrow your kubeconfig identity so you don't have to attach certs by hand. The API is still locked down, you're just walking through with a valid pass. Neither tool is a bypass. They're authenticated shortcuts.

kubectl proxy: an authenticated door to the API

kubectl proxy is a small web proxy that runs on your machine and forwards everything to the API server, adding your credentials on the way. You start it, it listens on 127.0.0.1:8001, and from then on any plain HTTP request to that address gets authenticated and passed through. No --cert, no --key, nothing.

kubectl proxy
# Starting to serve on 127.0.0.1:8001

Now the same request that got a 403 a moment ago just works, because the proxy is signing it with your identity:

through the proxy, fully authenticated
$ curl http://localhost:8001/ { "paths": [ "/api", "/api/v1", "/apis", "/healthz", "/metrics", "/openapi/v2" ] }
Same 6443 API, but now you're through. Note it binds to localhost only, so nobody else on the network can use your proxy.

The neat part: the proxy can also reach services inside the cluster, using a special API path. That's how you get to a ClusterIP service that has no external address. The URL shape is /api/v1/namespaces/<namespace>/services/<service>/proxy/:

curl http://localhost:8001/api/v1/namespaces/default/services/nginx/proxy/
# ...the service's own HTTP response comes back
Don't confuse this with the other proxy kubectl proxy is a helper on your laptop for reaching the API. kube-proxy is a cluster component that wires up pod and service networking on every node. Same word, totally different jobs. I mixed these up more than once early on.

kubectl port-forward: a direct tunnel to one thing

kubectl port-forward is a straight pipe from a port on your laptop to a port on one specific pod or service. You pick a local port, point it at a target and its port, and kubectl holds the tunnel open for as long as the command runs. It's the quickest way to open an internal-only app in your browser as if it were running locally.

# forward local 28080 to port 80 on the nginx service
kubectl port-forward service/nginx 28080:80
# Forwarding from 127.0.0.1:28080 -> 80

Leave that running, open a second terminal, and hit your local port:

curl http://localhost:28080/
# <h1>Welcome to nginx!</h1>

You can forward to a pod/, a service/, or a deployment/. The tunnel lives only as long as the command runs, and only on your machine. Close it and the door shuts.

Which one should you reach for?

They overlap, but they're not the same tool. Here's how I decide:

kubectl proxy vs kubectl port-forward
 kubectl proxykubectl port-forward
Reachesthe whole API + any service via URLone pod/service port
How you address itAPI paths on :8001plain localhost:PORT
Best forpoking the API, quick service peeksopening one app in a browser or tool
Speaks the app's protocol?wrapped in an API pathyes, raw and transparent
Rough rule: proxy when you want the API, port-forward when you want one service to feel local.

Try it yourself: a 5-minute lab

Enough theory. Let's stand up a throwaway cluster, deploy nginx as a ClusterIP (no external access), and reach it both ways. You only need Docker plus kind (Kubernetes in Docker), or minikube if you prefer. Everything here runs locally and tears down in one command.

01
Cluster

kind create cluster

02
Deploy nginx

ClusterIP only

03
Reach it two ways

proxy + port-forward

04
Tear down

delete cluster

The whole lab, start to finish. Nothing is ever exposed outside your machine.

Step 1. Make a cluster. One command gives you a working control plane in Docker:

kind create cluster --name proxy-lab

Step 2. Deploy nginx and give it a ClusterIP service. This runs the web server and creates an internal-only address for it:

kubectl create deployment nginx --image=nginx
kubectl expose deployment nginx --port=80 --name=nginx   # type ClusterIP by default

Check it's internal, with no external IP, which is the whole point:

a ClusterIP service, unreachable from outside
$ kubectl get svc nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) nginx ClusterIP 10.96.121.14 <none> 80/TCP # EXTERNAL-IP is none. from your laptop, this is invisible.
CLUSTER-IP is internal, EXTERNAL-IP is none. That's exactly the situation both tools solve.

Step 3a. Reach it with the proxy. Start the proxy in one terminal, then curl the service through its API path in another:

# terminal 1
kubectl proxy

# terminal 2
curl -s http://localhost:8001/api/v1/namespaces/default/services/nginx/proxy/ | grep -i welcome
# <title>Welcome to nginx!</title>

Step 3b. Reach it with port-forward. Stop the proxy, then map a local port straight to the service:

port-forward, then curl the local port
$ kubectl port-forward service/nginx 28080:80 & Forwarding from 127.0.0.1:28080 -> 80 $ curl -s http://localhost:28080/ | grep -i welcome <title>Welcome to nginx!</title> # the internal-only service is now answering on localhost:28080
Verify step: if you see the nginx welcome title, the private service is reaching your laptop. It worked.

Step 4. Tear it down. Kill the port-forward (kill %1 or Ctrl-C) and delete the cluster so nothing lingers:

kind delete cluster --name proxy-lab
Gotcha I hit Two things. First, the proxy service URL needs the trailing slash after /proxy/, or nginx redirects and you get an empty reply. Second, if 28080 is already busy on your machine the forward fails instantly, so just pick another local port. The right-hand number (80) must match the service's port.

The security angle

Here's why this matters beyond convenience. Both tools are authenticated and bind to localhost by default, which is the safe, sensible behaviour. The risk shows up when people override that. If you run kubectl proxy --address=0.0.0.0 --accept-hosts='.*', you've just published an unauthenticated door to your cluster's API on the network, because anything hitting your proxy now inherits your credentials. I've seen that exact flag combo in "just for testing" scripts, and it's a genuine open goal.

From the attacker's side, port-forward is a favourite after a foothold. Once you have a valid kubeconfig or a pod you can run kubectl from, port-forward is how you quietly reach an internal admin panel or database that was never meant to leave the cluster. So the same thing that helps you debug also helps someone move. The defence is RBAC: the API server still checks whether your identity is allowed to do the port-forward or hit that service, so tight roles limit the blast radius.

Defender checklist Never bind kubectl proxy to 0.0.0.0. Keep both tools on localhost. Lean on RBAC so a low-priv identity can't port-forward to sensitive services (the verbs are pods/portforward and services/proxy). And watch the audit log for proxy and port-forward calls from identities that shouldn't be making them.

The 403 that made it click

These two are the tools I now reach for constantly, and the thing that finally made them click was that 403. Once I saw that a bare curl is anonymous and that both tools just lend it my kubeconfig, the rest stopped feeling like magic. proxy is my Swiss-army way into the API, port-forward is my "make this one service feel local" button. I'm still not 100% on how port-forward behaves with services that have multiple pods behind them (I think it just picks one, but I want to test that properly), so if you know the exact behaviour, set me straight. Next I want to get into ingress and the proper, permanent ways to expose a service, now that I've done the temporary ones.

If this helped, come say hi on LinkedIn or the contact page, and tell me what to break down next. More in the Kubernetes Journey.

Further reading

FAQ

What is the difference between kubectl proxy and kubectl port-forward?

kubectl proxy opens an authenticated gateway to the whole API server, so you reach anything through API URLs. kubectl port-forward maps one local port straight to one pod or service port, giving you a plain tunnel to a single app. Proxy is for the API, port-forward is for a service.

How do I access a ClusterIP service from my laptop?

A ClusterIP service is internal only. Use kubectl port-forward service/name localPort:servicePort, then browse localhost on that port. Or reach it through kubectl proxy at /api/v1/namespaces/NS/services/NAME/proxy/. Neither exposes the service to the outside world.

Why does curl to the Kubernetes API on port 6443 return 403?

A raw curl sends no credentials, so the API server treats you as system:anonymous and forbids the request. kubectl and kubectl proxy read your kubeconfig and present its client certificate or token, which is why they get through and a bare curl does not.