Make exposure an explicit, reviewed choice by rejecting Service types that publish workloads, such as LoadBalancer and NodePort, unless a namespace is approved for them. Add the DenyServiceExternalIPs plugin where you can, and audit what is already exposed. One extra line in a manifest should not open a service to the internet.
What can expose a Service
The Kubernetes documentation lists the Service types. ClusterIP is the default, internal to the cluster. NodePort exposes the Service on each node's IP at a static port. LoadBalancer exposes it externally using an external load balancer, which on cloud providers provisions one for the Service. ExternalName maps the Service to a DNS name. Ingress and Gateway resources are another route to the outside, handled separately.
Each of those can turn a private workload into a public one with a small change.
The externalIPs field
The admission controller reference describes DenyServiceExternalIPs: it rejects all net new use of the Service externalIPs field, because the feature is powerful, allows network traffic interception, and is not well controlled by policy. Existing uses are not affected, and people may remove values. It is a validating plugin that is not on by default, so check whether your cluster enables it.
Step 1: write a policy for Service types
A ValidatingAdmissionPolicy can match Services on create and update and reject the types you want to gate.
validations:
- expression: "object.spec.type != 'LoadBalancer' && object.spec.type != 'NodePort'"
message: "Public Service types need an approved namespace"
Bind it with Warn and Audit first, as the documentation's validation actions allow. Use a namespaceSelector on a separate binding, or exemption labels governed by RBAC, so approved namespaces such as an edge or ingress namespace can still create what they need.
Step 2: decide the approved path
| Need | Approved route |
|---|---|
| Public web app | Shared ingress or gateway in an edge namespace |
| Internal service for other teams | ClusterIP, optionally with an internal load balancer annotation per your provider |
| Admin or debug access | Port forward or a bastion, not a public Service |
| Temporary exposure | Time limited exception with an owner and an expiry |
Describe this in a short page developers can find. A block without a path forward just creates workarounds.
Step 3: cover Ingress too
Services are not the only way out. Add a policy or a review requirement for Ingress and Gateway objects, such as requiring a hostname from an approved list and TLS configuration. Which fields to check depends on your ingress implementation, so read its documentation.
Step 4: audit what is already public
Admission only sees new changes, so run a sweep of current Services and Ingresses.
- List every Service of type LoadBalancer or NodePort and every Ingress.
- For each, record owner, purpose and whether it is meant to be public.
- Close the ones nobody claims, and add the rest to the approved list.
- Repeat on a schedule, and alert on new ones.
Do not forget the network layer
Cloud load balancers and node firewalls also matter. A NodePort may be blocked by firewall rules in one environment and open in another. Treat the admission rule as one layer and check your cloud settings as well.
Where Safeguard fits
Safeguard's asset discovery helps you build an inventory of what you run, and its policies and gates enforce rules on changes before release. Cluster exposure itself is enforced by the Kubernetes API and your network configuration.
Quick check
Does blocking LoadBalancer Services stop all exposure?
No. NodePort, externalIPs, Ingress and Gateway objects are other routes. Cover each one.
Will DenyServiceExternalIPs remove existing externalIPs?
No. The documentation says existing uses are not affected. It rejects new usage.
Sources
- Kubernetes Services
- Kubernetes admission controllers reference
- Kubernetes Validating Admission Policy
Last reviewed: 19 October 2026.