
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:
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:
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
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 | kubectl port-forward | |
|---|---|---|
| Reaches | the whole API + any service via URL | one pod/service port |
| How you address it | API paths on :8001 | plain localhost:PORT |
| Best for | poking the API, quick service peeks | opening one app in a browser or tool |
| Speaks the app's protocol? | wrapped in an API path | yes, raw and transparent |
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.
kind create cluster
ClusterIP only
proxy + port-forward
delete cluster
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:
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:
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
/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.
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
- Kubernetes docs: use kubectl proxy to access the API
- Kubernetes docs: use port-forward to access an application
- kind: quick start (the lab cluster)
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.
Related reading
- Topic 8: Kubeconfig and API groups (the credentials both tools borrow)
- Topic 9: Authorization and RBAC (who's allowed to port-forward)
- Topic 11: Kubelet security (another API exposed on a port)
- Browse the whole Kubernetes Journey