Exposing Services in Kubernetes
There are different ways to expose services in Kubernetes so both internal endpoints and external endpoints can access them.[2] This Kubernetes configuration is pretty critical as the administrator could give access to attackers to services they shouldn't be able to access.
Automatic Enumeration
Before starting enumerating the ways K8s offers to expose services to the public, know that if you can list namespaces, services and ingresses, you can find everything exposed to the public with:
kubectl get namespace -o custom-columns='NAME:.metadata.name' | grep -v NAME | while IFS='' read -r ns; do
echo "Namespace: $ns"
kubectl get service -n "$ns"
kubectl get ingress -n "$ns"
echo "=============================================="
echo ""
echo ""
done | grep -v "ClusterIP"
# Remove the last '| grep -v "ClusterIP"' to see also type ClusterIP
ClusterIP
A ClusterIP service is the default Kubernetes service. It gives you a service inside your cluster that other apps inside your cluster can access. There is no external access.[2]
However, this can be accessed using the Kubernetes Proxy:[13][1]
kubectl proxy --port=8080
Now, you can navigate through the Kubernetes API to access services using this scheme:[13][1]
http://localhost:8080/api/v1/proxy/namespaces/<NAMESPACE>/services/<SERVICE-NAME>:<PORT-NAME>/
For example you could use the following URL:[13][1]
http://localhost:8080/api/v1/proxy/namespaces/default/services/my-internal-service:http/
to access this service:
apiVersion: v1
kind: Service
metadata:
name: my-internal-service
spec:
selector:
app: my-app
type: ClusterIP
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
This method requires you to run kubectl as an authenticated user.[13][1]
List all ClusterIPs:
kubectl get services --all-namespaces -o=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CLUSTER-IP:.spec.clusterIP,PORT(S):.spec.ports[*].port,TARGETPORT(S):.spec.ports[*].targetPort,SELECTOR:.spec.selector' | grep ClusterIP
NodePort
When NodePort is utilised, a designated port is made available on all Nodes (representing the Virtual Machines). Traffic directed to this specific port is then systematically routed to the service. Typically, this method is not recommended due to its drawbacks.[2][1]
List all NodePorts:
kubectl get services --all-namespaces -o=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CLUSTER-IP:.spec.clusterIP,PORT(S):.spec.ports[*].port,NODEPORT(S):.spec.ports[*].nodePort,TARGETPORT(S):.spec.ports[*].targetPort,SELECTOR:.spec.selector' | grep NodePort
An example of NodePort specification:[2][1]
apiVersion: v1
kind: Service
metadata:
name: my-nodeport-service
spec:
selector:
app: my-app
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30036
protocol: TCP
If you don't specify the nodePort in the yaml (it's the port that will be opened) a port in the range 30000โ32767 will be used.[2]
When reviewing NodePort or LoadBalancer Services, also inspect traffic-policy fields because they change which nodes and backends are useful from a given source:[5][6][7]
kubectl get services --all-namespaces \
-o custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,ETP:.spec.externalTrafficPolicy,ITP:.spec.internalTrafficPolicy,AFFINITY:.spec.sessionAffinity,DIST:.spec.trafficDistribution,NODEPORTS:.spec.ports[*].nodePort'
- NodePorts are normally exposed on node addresses, but kube-proxy can restrict the address ranges with
--nodeport-addressesornodePortAddressesin its configuration. Check the active kube-proxy or CNI service-proxy replacement configuration before assuming the NodePort is reachable on every node IP.[2][3] externalTrafficPolicy: Localpreserves the original client source IP for NodePort/LoadBalancer traffic and avoids forwarding to endpoints on other nodes. A node without a local ready endpoint may drop the traffic even if the Service has endpoints elsewhere.[6]externalTrafficPolicy: Clusteris the default and can forward through any node, but backend logs may see node IPs instead of the real external client IP.[6]internalTrafficPolicy: Locallimits in-cluster Service traffic to endpoints local to the source node. This is locality routing, not an authorization boundary.[5]sessionAffinity: ClientIPcan make repeated tests from one client hit the same backend, hiding other ready endpoints during manual checks.[2]trafficDistributionand EndpointSlice topology hints can prefer same-zone or same-node endpoints on newer clusters; treat them as routing preferences rather than hard security policy.[2][7]
LoadBalancer
Exposes the Service externally using a cloud provider's load balancer. On GKE, this will spin up a Network Load Balancer that will give you a single IP address that will forward all traffic to your service. In AWS it will launch a Load Balancer.[2][18]
You have to pay for a LoadBalancer per exposed service, which can be expensive.[1]
List all LoadBalancers:
kubectl get services --all-namespaces -o=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,TYPE:.spec.type,CLUSTER-IP:.spec.clusterIP,EXTERNAL-IP:.status.loadBalancer.ingress[*],PORT(S):.spec.ports[*].port,NODEPORT(S):.spec.ports[*].nodePort,TARGETPORT(S):.spec.ports[*].targetPort,SELECTOR:.spec.selector' | grep LoadBalancer
External IPs
[!TIP] External IPs are exposed by services of type Load Balancers and they are generally used when an external Cloud Provider Load Balancer is being used.[2][4]
For finding them, check for load balancers with values in the
EXTERNAL-IPfield.[2][4]
Traffic that ingresses into the cluster with the external IP (as destination IP), on the Service port, will be routed to one of the Service endpoints. externalIPs are not managed by Kubernetes and are the responsibility of the cluster administrator.[2][4]
externalIPs is a sensitive route-control field because a user who can set it might claim traffic for an IP address the Service owner should not control if the surrounding network routes that IP to the cluster. Kubernetes announced the deprecation and planned removal of Service externalIPs in v1.36, so prefer controller-owned exposure mechanisms such as LoadBalancer integrations or Gateway API where possible, and restrict/admit this field carefully while it still exists.[4]
In the Service spec, externalIPs can be specified along with any of the ServiceTypes. In the example below, "my-service" can be accessed by clients on "80.11.12.10:80" (externalIP:port)[2]
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: MyApp
ports:
- name: http
protocol: TCP
port: 80
targetPort: 9376
externalIPs:
- 80.11.12.10
ExternalName
From the docs: Services of type ExternalName map a Service to a DNS name, not to a typical selector such as my-service or cassandra. You specify these Services with the spec.externalName parameter.[2]
This Service definition, for example, maps the my-service Service in the prod namespace to my.database.example.com:[2]
apiVersion: v1
kind: Service
metadata:
name: my-service
namespace: prod
spec:
type: ExternalName
externalName: my.database.example.com
When looking up the host my-service.prod.svc.cluster.local, the cluster DNS Service returns a CNAME record with the value my.database.example.com. Accessing my-service works in the same way as other Services but with the crucial difference that redirection happens at the DNS level rather than via proxying or forwarding.[2]
Security review note: if an Ingress controller, Gateway implementation, service mesh, or application accepts an ExternalName Service as a backend, the controller may resolve and reach the external name from its own network position. That can expose internal-only services through public routing infrastructure when users can create both the route object and the ExternalName Service. Review the specific controller implementation and version, ExternalName support flags or allowlists, route status, and the exact target domain before treating this as safe. For example, Skipper patched a Kubernetes ExternalName SSRF issue in v0.24.0 by disabling ExternalName backends by default and documenting an allowlist option.[2][12]
List all ExternalNames:
kubectl get services --all-namespaces | grep ExternalName
EndpointSlices
EndpointSlices show the concrete backend addresses and ports that a Service currently routes to. They are especially useful when a Service has no selector, when labels do not explain the traffic path, or when only some backends are ready.[8]
List EndpointSlices associated with Services:
kubectl get endpointslices --all-namespaces
kubectl get endpointslice -n <namespace> -l kubernetes.io/service-name=<service-name> -o yaml
kubectl get endpointslice -n <namespace> -l kubernetes.io/service-name=<service-name> \
-o custom-columns='NAME:.metadata.name,ADDR:.endpoints[*].addresses,READY:.endpoints[*].conditions.ready,PORTS:.ports[*].port'
When reviewing exposure, compare the Service selector with the EndpointSlice targetRef, endpoint addresses, readiness conditions, and ports. A selectorless Service can be paired with manually managed EndpointSlices and route traffic to non-Pod or unexpected destinations.[2][8]
Ingress
Unlike all the above examples, Ingress is NOT a type of service. Instead, it sits in front of multiple services and act as a โsmart routerโ or entrypoint into your cluster.[2][1]
You can do a lot of different things with an Ingress, and there are many types of Ingress controllers that have different capabilities.[1]
The default GKE ingress controller will spin up a HTTP(S) Load Balancer for you. This will let you do both path based and subdomain based routing to backend services. For example, you can send everything on foo.yourdomain.com to the foo service, and everything under the yourdomain.com/bar/ path to the bar service.[19]
The YAML for a Ingress object on GKE with a L7 HTTP Load Balancer might look like this:[19][1]
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
defaultBackend:
service:
name: other
port:
number: 8080
rules:
- host: foo.mydomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: foo
port:
number: 8080
- host: mydomain.com
http:
paths:
- path: /bar
pathType: Prefix
backend:
service:
name: bar
port:
number: 8080
List all the ingresses:
kubectl get ingresses --all-namespaces -o=custom-columns='NAMESPACE:.metadata.namespace,NAME:.metadata.name,RULES:spec.rules[*],STATUS:status'
Although in this case it's better to get the info of each one by one to read it better:
kubectl get ingresses --all-namespaces -o=yaml
Gateway API
Gateway API is the newer Kubernetes API for exposing Services. It separates infrastructure-owned Gateway objects from application-owned Route objects such as HTTPRoute. This is useful for delegation, but it also means exposure can be split across namespaces.[14]
List Gateway API exposure objects:
kubectl get gatewayclasses
kubectl get gateways --all-namespaces
kubectl get httproutes --all-namespaces
kubectl get grpcroutes,tlsroutes,tcproutes,udproutes --all-namespaces
kubectl get referencegrants --all-namespaces
kubectl get backendtlspolicies --all-namespaces
kubectl get gateway -n <namespace> <gateway-name> -o yaml
kubectl get httproute -n <namespace> <route-name> -o yaml
Check Gateway listeners, allowed route namespaces, Route parentRefs, hostnames or SNI matches, filters, backend references, and status conditions such as Accepted, ResolvedRefs, and Programmed. A Route that is accepted by a shared Gateway can expose a backend even when no legacy Ingress object exists.[14][15]
Do not check only HTTPRoute. GRPCRoute, TLSRoute, TCPRoute, and UDPRoute can expose non-HTTP services such as admin ports, brokers, databases, service-mesh gateways, or pass-through TLS backends. Also review ReferenceGrant objects for cross-namespace backend or certificate references and BackendTLSPolicy for the TLS identity the Gateway uses when connecting to backend Services.[10][15][16][17] Backend TLS policy is not proof of public reachability by itself, but it is useful evidence when a programmed Gateway route reaches a ready Service with weak, shared, or wrong backend identity validation.
References
- [1] Kubernetes NodePort vs LoadBalancer vs Ingress: When should I use what?
- [2] Service
- [3] kube-proxy
- [4] Kubernetes v1.36: Deprecation and removal of Service ExternalIPs
- [5] Service Internal Traffic Policy
- [6] Using Source IP
- [7] Topology Aware Routing
- [8] EndpointSlices
- [9] Gateway API
- [10] Gateway API 1.4: New Features
- [11] BackendTLSPolicy
- [12] dataclient/kubernetes ExternalName SSRF Leading to Internal Service Exposure
- [13] kubectl proxy
- [14] API Overview
- [15] Implementer's Guide
- [16] ReferenceGrant
- [17] BackendTLSPolicy
- [18] Backend service-based regional external passthrough Network Load Balancer overview
- [19] External Application Load Balancer overview
[!TIP] Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Browse the full HackTricks Training catalog.Support HackTricks
- Check the subscription plans!
- Join the ๐ฌ Discord group or the telegram group or follow us on Twitter ๐ฆ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.


