Kubernetes Networking 101: Service, Ingress, Gateway API, and NetworkPolicy

In Kubernetes, networking is best understood as several layers rather than a single β€œnetwork object.” Pods are constantly changing: they are recreated, receive new IP addresses, move between Nodes, and disappear when a Deployment is updated. For that reason, access cannot be built directly around Pods.

In this model, each object has its own role:

  • Service provides a stable access point to a group of Pods: a persistent DNS name and an internal address.
  • Ingress describes HTTP/HTTPS routes from outside to a Service: for example, which host or path should be routed to which backend.
  • Gateway API addresses a similar ingress traffic challenge, but in a more formal way: with a separation of roles between the platform team and application teams.
  • NetworkPolicy does not publish an application or select a route. It answers a different question: which connections are allowed between Pods, namespaces, and external addresses.

The key is not to confuse addressing, routing, and access control. A Service makes an application addressable, but it does not make it secure. Ingress and the Gateway API help route incoming requests, but they do not provide network isolation by themselves. NetworkPolicy restricts connections, but it does not replace Service, Ingress, or Gateway.

In practice, the chain usually looks like this: an external client reaches a load balancer or ingress controller, then the request passes through Ingress or the Gateway API, reaches a Service, and the Service forwards it to the appropriate Pods. For internal traffic, the path is shorter: one Pod connects to the DNS name of a Service, and the Service routes the request to the required group of Pods.

That is why Kubernetes networking is best designed in this order: first determine what needs to communicate with what, then create a Service as a stable target, choose Ingress or the Gateway API for incoming HTTP/HTTPS traffic, and only then close off unnecessary connections with NetworkPolicy.

The Principle of Separating Network Responsibilities in Kubernetes

In Kubernetes, network access is not built around individual Pods. A Pod can be recreated, receive a new IP address, move to another Node, or disappear during a Deployment update. If an application connects to specific Pod IP addresses, that design will quickly break.

For this reason, Kubernetes uses multiple networking layers. A Service hides the volatility of Pods and provides a stable internal access point. Ingress and the Gateway API define rules for inbound traffic. NetworkPolicy specifies which connections are permitted in the first place.

These objects often appear together in the same architecture, but they solve different problems. Problems begin when a Service is expected to provide protection, Ingress is expected to provide network isolation, and NetworkPolicy is expected to handle routing. In practice, they work together: one layer is responsible for the address, another for the inbound route, and a third for permissions.

It is therefore logical to first examine the path of a request in Kubernetes, and then look separately at Service, Ingress, the Gateway API, and NetworkPolicy.

How a request flows through Kubernetes

In Kubernetes, it is important to distinguish between an object that defines rules and the component that actually handles traffic. Service, Ingress, and Gateway API are not β€œone big pipe”; they are different layers in the same chain.

For an incoming HTTP/HTTPS request, the path usually looks like this:

external client

β†’ cloud Load Balancer

β†’ Ingress Controller or Gateway Controller

β†’ Ingress / Gateway + HTTPRoute

β†’ Service

β†’ Pod

A Service sits closer to the Pods. It hides their volatility and provides a stable internal target: a DNS name or ClusterIP. A client should not need to know the IP address of a specific Pod inside the cluster β€” it connects to the Service, and Kubernetes directs traffic to the current backend Pods.

Ingress and Gateway API sit closer to the external entry point. They describe which request should be routed to which Service: for example, app.example.com/api goes to api-service, while app.example.com/web goes to web-service. However, these objects do not accept traffic as running processes on their own. Ingress is handled by an Ingress Controller, and Gateway API resources are handled by a Gateway Controller.

For internal traffic, the chain is shorter:

Pod A β†’ Service DNS / ClusterIP β†’ Pod B

For example, a frontend connects to backend.app.svc.cluster.local, and the Service routes the request to one of the backend Pods. If a backend Pod is recreated and receives a new IP address, the client does not need to change anything: the Service remains a stable access point.

NetworkPolicy operates in a different plane. It does not choose the route or expose the application externally. Its job is to check whether a connection between participants in the chain is allowed. In other words, a Service can make a backend addressable, and Ingress or Gateway can route an external request to it, but NetworkPolicy determines who is actually allowed to connect.

This makes Kubernetes networking as a whole easier to understand: Service is responsible for a stable target, Ingress and Gateway API are responsible for the incoming route, and NetworkPolicy is responsible for whether the connection is allowed. Next, we can examine Service separately as the basic addressing layer for Pods.

Service: a stable access point for Pods

A Service is the basic addressing layer in Kubernetes. It is needed because Pods are short-lived: they are recreated, change IP addresses, move between Nodes, and are updated as part of a Deployment. A client should not connect to a specific Pod IP, because that address may quickly cease to exist.

Instead, the application connects to a Service. For example, a frontend can use the stable name backend.default.svc.cluster.local instead of the IP address of a backend Pod. Kubernetes maintains the link between the Service and the current set of Pods.

In simplified form, the chain looks like this:

labels on Pods

β†’ selector in the Service

β†’ EndpointSlice with up-to-date addresses

β†’ Service DNS name or ClusterIP

β†’ backend Pods

A Service selects Pods through a selector. For example, if backend Pods have the label app: backend, a Service with the same selector will route traffic to those Pods. When Pods are scaled, updated, or recreated, Kubernetes updates the EndpointSlice, while the client continues to use the same DNS name or ClusterIP.

The Service type determines where and how this stable access point will be available. It is important not to confuse this with other tasks: a Service does not define HTTP routes, manage TLS, or act as a security policy. The main options are:

Service typeWhere it is availableWhen to use it
ClusterIPInside the clusterInternal calls between applications
NodePortThrough a port on each NodeIntermediate access, often behind an external load balancer
LoadBalancerThrough an external provider load balancerDirect external exposure of a single service
ExternalNameAs a DNS aliasAccessing an external name through a Kubernetes Service

ClusterIP is the primary option for internal services. It does not expose the application externally, but it provides a stable target inside the cluster.

NodePort opens a port on each Node. Today, it is often used as an intermediate layer behind an external load balancer or in specific scenarios, but it does not provide domain- or path-based routing.

LoadBalancer creates an external load balancer through integration with a cloud provider. This is convenient for directly exposing a single service, but it does not replace Ingress or the Gateway API when you need to route multiple applications by host/path.

ExternalName works as a DNS alias for an external name. It does not create a ClusterIP and does not balance traffic to Pods.

The key takeaway: a Service makes Pods addressable through a stable name and address. But it does not solve every networking task. For inbound HTTP/HTTPS traffic, you need Ingress or the Gateway API, and to restrict connections, you need NetworkPolicy.

Ingress: HTTP/HTTPS routing to a Service

Once a Service is in place, the next question is how an external HTTP/HTTPS request should reach the appropriate application inside the cluster. In Kubernetes, Ingress is often used for this.

Ingress defines routing rules based on hostname and path. For example, a single domain can route to different internal Services:

app.example.com/api β†’ api-service

app.example.com/web β†’ web-service

In this scenario, an external client connects to app.example.com. The request reaches the front-end load balancer and is then forwarded to the Ingress Controller. The controller reads the Ingress rules and selects which Service to route the request to: /api to api-service, and /web to web-service.

It is important to distinguish between Ingress as an object and the Ingress Controller as a component. Ingress itself is a set of rules in the Kubernetes API. Actual traffic is received and processed by the controller, such as NGINX Ingress Controller, Traefik, HAProxy, cloud-controller, or another selected option.

Ingress is useful as a single entry point for multiple internal Services. Instead of creating a separate LoadBalancer for each application, you can use a shared ingress layer and define routing rules declaratively.

The main responsibilities of Ingress are:

  • HTTP/HTTPS routing by host and path;
  • TLS termination;
  • Forwarding the request to a Service;
  • Basic ingress rules for web/API applications.

However, Ingress does not replace a Service and is not a network isolation mechanism. It routes incoming requests, but it does not define which Pods are allowed to communicate with each other. NetworkPolicy is required for that.

Ingress also has a practical limitation: many advanced capabilities depend on the specific controller and its annotations. When there are only a few routes, this is usually not a problem. But in multi-team clusters, infrastructure settings, application-level rules, and controller-specific annotations can start to get mixed together. In such cases, it is worth considering Gateway API.

Gateway API: a more manageable model for inbound traffic

Gateway API addresses a similar problem: it describes inbound L4/L7 traffic to applications in Kubernetes. However, it does so in a more formal way, with a clear separation of roles. It should not be confused with an API gateway product used for authorization, rate limiting, and API business logic. In Kubernetes, Gateway API is a set of resources that describe entry points, routes, and their relationship with a controller.

The basic model is built around three objects:

ObjectResponsibility
GatewayClassSpecifies the implementation class and the associated Gateway Controller
GatewayDescribes the infrastructure entry point: listeners, protocols, and domains
HTTPRouteDefines application-level routing rules to a Service

The main difference from Ingress is a clearer separation of responsibilities. The platform team can manage the GatewayClass and shared Gateway, while application teams can create their own HTTPRoute resources in permitted namespaces.

For example, the platform creates a shared HTTPS entry point for *.example.com, and the application team defines a route:

app.example.com/api β†’ api-service

In this model, the application team does not manage the entire ingress infrastructure, but it can safely add its own routes within the permitted rules. This is especially useful when a cluster has many teams, shared domains, multiple namespaces, and delegation requirements.

Gateway API helps reduce reliance on informal annotations and is better suited to scenarios that require shared entry points, separation of infrastructure and application responsibilities, stricter rules for attaching routes, and an extensible traffic model.

However, Gateway API does not need to replace Ingress in every cluster. For a simple HTTP/HTTPS entry point, Ingress is often sufficient: one controller, a few host/path rules, and a straightforward way to publish a Service. Gateway API becomes more useful when inbound traffic needs to be managed not just as something β€œforwarded to a service,” but as part of the platform.

After covering Ingress and Gateway API, we can compare all three objectsβ€”Service, Ingress, and Gateway APIβ€”by purpose, so that addressing, inbound routing, and network permissions are no longer conflated.

Service, Ingress, and Gateway API: a task-based comparison

They operate at different points in the network path. Service provides a stable target inside the cluster, Ingress defines basic HTTP/HTTPS routing, and Gateway API helps manage inbound traffic more formally and divide responsibilities between teams.

The easiest way to compare them is by task:

ObjectPrimary purposeTypical scenarioWhat it does not do
ServiceStable addressing for a group of Podsfrontend β†’ backend ServiceDoes not route HTTP by host/path and does not set access permissions
IngressHTTP/HTTPS routing to a Serviceapp.example.com/api β†’ api-serviceDoes not replace Service and is not a network policy
Gateway APIA more role-oriented model for inbound trafficA shared Gateway and separate HTTPRoutes for teamsNot required for every simple route and does not restrict Pod-to-Pod traffic

Service is almost always needed as a stable internal target. Ingress is used for standard HTTP/HTTPS exposure. Gateway API is appropriate when inbound traffic needs to be managed more formally: through shared entry points, route delegation, and separation of infrastructure and application responsibilities.

NetworkPolicy does not belong in this table as an alternative because it solves a different problem. It does not expose an application or select a route. It answers the question of which connections are allowed on top of the addressing and routing that already exist.

NetworkPolicy: network permissions, not routing

After Service, Ingress, and Gateway API, one final layer remains: network permissions. NetworkPolicy does not answer the question β€œwhere should this request be routed?” It answers β€œis this connection allowed?”

Service makes Pods addressable. Ingress and Gateway API route incoming HTTP/HTTPS requests to the appropriate Service. NetworkPolicy restricts who can connect to selected Pods and where those Pods can initiate connections themselves.

The easiest way to think about NetworkPolicy is as access rules:

  1. First, the policy selects the Pods that need to be protected;
  2. Then it defines which sources they are allowed to accept inbound connections from;
  3. Separately, it defines which destinations they are allowed to initiate outbound connections to;
  4. Anything not covered by the permissions is blocked for the selected Pods.

Standard NetworkPolicy does not have a familiar deny rule. Blocking works differently: if a Pod is selected by a policy but the required connection is not allowed, the connection does not go through. If a Pod is not selected by any NetworkPolicy at all, it usually remains non-isolated.

The basic principles are:

  • PodSelector selects the Pods to which the policy applies;
  • Ingress restricts inbound connections to the selected Pods;
  • Egress restricts outbound connections from the selected Pods;
  • NamespaceSelector is used when the source or destination is in another namespace;
  • the actual behavior of NetworkPolicy depends on the CNI plugin.

If the network plugin does not support NetworkPolicy, the manifests may be created successfully in the Kubernetes API, but the expected isolation will not be enforced.

In practice, NetworkPolicy is easier to understand through three simple scenarios: block everything by default, allow frontend to connect to backend, and restrict communication between namespaces.

Scenario 1. Deny everything by default

Default deny is the starting policy: β€œnothing is allowed until we explicitly allow it.” It selects all Pods in a namespace and blocks both inbound and outbound traffic for them unless separate allow rules are defined.

This is useful when a team wants to move from an open network to a controlled model. First, the namespace is locked down; then only the required exceptions are added: access to DNS, the backend, the database, the broker, monitoring, or an external API.

The most common mistake after enabling default deny is forgetting about DNS. A Pod may be running and the Service may exist, but the application will no longer be able to resolve names such as backend.app.svc.cluster.local unless outbound requests to CoreDNS are allowed.

The purpose of default deny, therefore, is not to β€œbreak everything,” but to change the approach: instead of looking for what to block, explicitly list what to allow. This approach is sometimes called a β€œdefault-closed firewall.”

Scenario 2. Allow the frontend to access the backend

Suppose the app namespace contains a frontend and a backend. The frontend must be able to access the backend on port 8080, while other Pods must not.

Here, the NetworkPolicy selects the backend Pods and specifies that inbound traffic to them is allowed only from Pods with the app: frontend label and only on the required port. All other traffic to the backend is not allowed.

The Service continues to work as usual. The name backend.app.svc.cluster.local can resolve successfully for everyone. However, name resolution does not imply permission to connect. An unauthorized Pod will see the address, but the connection will be blocked by the policy.

The difference is easier to remember this way:

  • Service answers: β€œwhere is the backend?”;
  • NetworkPolicy answers: β€œis this Pod allowed to connect to the backend?”

If the frontend is in another namespace, you need to allow not only the Pod label but also the namespace the traffic comes from. This is done using namespaceSelector.

Scenario 3. Restrict traffic between namespaces

In a multi-team cluster, Pods within the same namespace often need to communicate with each other, but should not connect directly to another team’s namespace.

Here, the policy selects all Pods in the namespace and allows them to receive and send traffic only within that same namespace. For example, team-a Pods can communicate with each other, but they do not automatically get access to Pods in team-b.

An important detail: a podSelector without a namespaceSelector applies only within the namespace where the policy is created. Therefore, the rule β€œallow Pods with podSelector: {}” means β€œallow Pods in this namespace,” not across the entire cluster.

For real isolation, this model must be applied in every namespace where it is needed. And you will almost always need to add exceptions: DNS, the ingress-controller, the Gateway Controller, monitoring, a service mesh, or external managed services.

These scenarios provide the basic logic: first select the Pods to protect, then explicitly allow the required connections, and separately verify system dependencies. If something still does not work, the issue is usually in labels, namespaces, policyTypes, DNS, or NetworkPolicy support by the CNI.

What to check when debugging NetworkPolicy

NetworkPolicy issues often look the same: β€œthe service is not responding.” But the policy may not be the cause. First, make sure the route itself works: the Pod exists, the Service selects the right Pods, the DNS name resolves, the port is open, and the Ingress or Gateway actually routes the request to the right Service.

After that, you can check the NetworkPolicy. It is easier to follow the chain:

  1. What the policy protects. Check whether podSelector selects exactly the Pods that need to be isolated. A common mistake is that the labels in the policy do not match the labels of the actual Pods.
  2. Who is the traffic source. If access should be allowed only from the frontend, you need to check the labels of the frontend Pods. If the source is in another namespace, a podSelector alone is not enough: a namespaceSelector will be required.
  3. Which type of traffic is restricted. Ingress controls incoming connections to the selected Pods, while Egress controls outgoing connections from them. If you omit the required policyTypes, the behavior may differ from expectations.
  4. Check whether DNS is broken. After default deny egress is applied, Pods often lose access to CoreDNS. As a result, the application cannot reach the Service by name, even though the Service itself and the backend are working.
  5. Whether the CNI supports NetworkPolicy. The Kubernetes API can accept a NetworkPolicy manifest, but the actual isolation is enforced by the network plugin. If the CNI does not support NetworkPolicy, the rules will not have the expected effect.
  6. Whether system exceptions are needed. It is often necessary to explicitly allow access from the ingress-controller or Gateway Controller to the application Pods, as well as egress to monitoring, an external database, a broker, object storage, or an API.

NetworkPolicy is best designed once the route is understood. First, describe the request path: external client β†’ Ingress/Gateway β†’ Service β†’ Pod, or Pod β†’ Service β†’ Pod. Then, for each segment, allow only the connections that are actually required.

This makes NetworkPolicy not a random set of deny rules, but the final layer of the network model: the address is already in place, the route is clear, and access now needs to be restricted.

Conclusion

In Kubernetes, networking should be broken down by layer. A Service provides a stable internal access point to a changing set of Pods. Ingress and the Gateway API define inbound HTTP/HTTPS routing and require a controller to apply those rules. NetworkPolicy does not expose an application or select a route; it restricts which connections are allowed.

The practical sequence is straightforward: first determine which components need to communicate with each other, then use a Service as a stable target, choose Ingress or the Gateway API for inbound traffic, and block unnecessary connections with NetworkPolicy. Exceptions should be checked separately: DNS, system controllers, monitoring, and external dependencies. This approach reduces the risk of common mistakes, such as treating a Service as a security boundary, expecting isolation from Ingress, or creating a separate LoadBalancer for every HTTP route.

FAQ

How is a Service different from an Ingress?

A Service provides a stable address and DNS name for a group of Pods. An Ingress defines HTTP/HTTPS rules for incoming traffic based on host and path and routes requests to a Service through an Ingress Controller.

When should you choose Gateway API over Ingress?

Gateway API is appropriate when you need to separate responsibilities between platform and application teams, manage shared entry points, and define more expressive routes. For simple HTTP/HTTPS scenarios, Ingress is often still sufficient.

Is a Service a security boundary?

No. A Service makes Pods addressable, but on its own it does not restrict who can connect to them. NetworkPolicy is used for network permissions, along with additional security mechanisms if needed.

Why don’t Ingress or the Gateway API work without a controller?

Ingress, Gateway, and HTTPRoute are declarative Kubernetes objects. They store rules but do not process traffic themselves. The actual work is done by a deployed Ingress Controller or Gateway Controller.

What happens to traffic if NetworkPolicies are not configured?

Traffic is typically allowed as long as the Pods are not isolated by policies. NetworkPolicy uses an allow-list model: once isolation is applied, permitted ingress and egress connections must be explicitly defined.

Why can default-deny egress break DNS?

If all outbound traffic from Pods is blocked, they may lose access to kube-dns or CoreDNS. As a result, Service names and external domains will no longer resolve, so DNS traffic must be allowed by a separate rule.

Sources


1. Kubernetes Documentation β€” Service


2. Kubernetes Documentation β€” Ingress


3. Gateway API Documentation β€” Introduction


4. Kubernetes Documentation β€” Network Policies

Comment

Subscribe to our newsletter to get articles and news