Repository: cilium/cilium
Pull request #11092 (2020-04-22 05:05:48)

Add policy support for named ports
Add support for container port names in policy ports. With this change, in addition to using a L4 port number (like "80") in the network policy (K8s Network Policy or Cilium Network Policy), the port number can be expressed as a name specified in any one of the POD specs in the cluster. If the port name is not specified in any container spec, the traffic specified in the network policy rule can't be allowed, but will be allowed as soon as a container with a matching port name is added.

Port names are stored from a POD spec on the node in which the POD is deployed, from where then name/port mappings are distributed to other nodes via CEP CRDs.

```release-note
Support for named k8s container ports is added to both K8s Network Policies and Cilium Network Policies.
```


jrajahalme: test-me-please
coveralls: 
[![Coverage Status](https://coveralls.io/builds/30248486/badge)](https://coveralls.io/builds/30248486)

Coverage increased (+0.0003%) to 44.696% when pulling **2864669965067bc29dd4d1027defbf31e76618ba on pr/jrajahalme/named-ports** into **fff6d6c6b02d0d2aaa2d45edb1d370ce70f8cba1 on master**.

jrajahalme: test-me-please
jrajahalme: test-me-please
jrajahalme: test-me-please
jrajahalme: test-me-please
jrajahalme: Fixed unit test breakage, Ginkgo passed except for one unrelated test that depended on external connectivity to 1.1.1.1, most likely a flake.
tgraf: ```
The CiliumNetworkPolicy "l3-l4-policy-named-port" is invalid: []: Invalid value: map[string]interface {}{"apiVersion":"cilium.io/v2", "description":"L3-L4 policy with named l4 port", "kind":"CiliumNetworkPolicy", "metadata":map[string]interface {}{"name":"l3-l4-policy-named-port", "namespace":"202004230524k8spolicytestbasictestchecksallkindofkubernetespoli", "creationTimestamp":"2020-04-23T05:25:03Z", "annotations":map[string]interface {}{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"cilium.io/v2\",\"description\":\"L3-L4 policy with named l4 port\",\"kind\":\"CiliumNetworkPolicy\",\"metadata\":{\"annotations\":{},\"name\":\"l3-l4-policy-named-port\",\"namespace\":\"202004230524k8spolicytestbasictestchecksallkindofkubernetespoli\"},\"spec\":{\"endpointSelector\":{\"matchLabels\":{\"id\":\"app1\"}},\"ingress\":[{\"fromEndpoints\":[{\"matchLabels\":{\"id\":\"app2\"}}],\"toPorts\":[{\"ports\":[{\"port\":\"http-80\",\"protocol\":\"TCP\"}]}]}]}}\n"}, "generation":1, "uid":"c913034c-8522-11ea-8bee-08002711e3c4"}, "spec":map[string]interface {}{"endpointSelector":map[string]interface {}{"matchLabels":map[string]interface {}{"id":"app1"}}, "ingress":[]interface {}{map[string]interface {}{"fromEndpoints":[]interface {}{map[string]interface {}{"matchLabels":map[string]interface {}{"id":"app2"}}}, "toPorts":[]interface {}{map[string]interface {}{"ports":[]interface {}{map[string]interface {}{"port":"http-80", "protocol":"TCP"}}}}}}}}: validation failure list:
	 spec.ingress.toPorts.ports.port in body should match '^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4})$'
```

We need to update the schema validation for CiliumNetworkPolicy
jrajahalme: > ```
> The CiliumNetworkPolicy "l3-l4-policy-named-port" is invalid: []: Invalid value: map[string]interface {}{"apiVersion":"cilium.io/v2", "description":"L3-L4 policy with named l4 port", "kind":"CiliumNetworkPolicy", "metadata":map[string]interface {}{"name":"l3-l4-policy-named-port", "namespace":"202004230524k8spolicytestbasictestchecksallkindofkubernetespoli", "creationTimestamp":"2020-04-23T05:25:03Z", "annotations":map[string]interface {}{"kubectl.kubernetes.io/last-applied-configuration":"{\"apiVersion\":\"cilium.io/v2\",\"description\":\"L3-L4 policy with named l4 port\",\"kind\":\"CiliumNetworkPolicy\",\"metadata\":{\"annotations\":{},\"name\":\"l3-l4-policy-named-port\",\"namespace\":\"202004230524k8spolicytestbasictestchecksallkindofkubernetespoli\"},\"spec\":{\"endpointSelector\":{\"matchLabels\":{\"id\":\"app1\"}},\"ingress\":[{\"fromEndpoints\":[{\"matchLabels\":{\"id\":\"app2\"}}],\"toPorts\":[{\"ports\":[{\"port\":\"http-80\",\"protocol\":\"TCP\"}]}]}]}}\n"}, "generation":1, "uid":"c913034c-8522-11ea-8bee-08002711e3c4"}, "spec":map[string]interface {}{"endpointSelector":map[string]interface {}{"matchLabels":map[string]interface {}{"id":"app1"}}, "ingress":[]interface {}{map[string]interface {}{"fromEndpoints":[]interface {}{map[string]interface {}{"matchLabels":map[string]interface {}{"id":"app2"}}}, "toPorts":[]interface {}{map[string]interface {}{"ports":[]interface {}{map[string]interface {}{"port":"http-80", "protocol":"TCP"}}}}}}}}: validation failure list:
> 	 spec.ingress.toPorts.ports.port in body should match '^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[0-9]{1,4})$'
> ```
> 
> 
> We need to update the schema validation for CiliumNetworkPolicy

@tgraf Already did that, which tells me that this message must be coming from an old build of Cilium?
See https://github.com/cilium/cilium/pull/11092/files#diff-da7eac65a6ed1ef87f8fec512cb90529L1166
jrajahalme: Added CRD debugging.
jrajahalme: test-missed-k8s
jrajahalme: k8s 1.14 & 1.15 failing due to https://github.com/cilium/cilium/pull/11072.
jrajahalme: k8s 1.13  passed without CNP CRD flake.
jrajahalme: test-me-please
jrajahalme: Fixed CRD version check, retesting...
jrajahalme: test-me-please
jrajahalme: test-missed-k8s
jrajahalme: Couple of fixes merged, to there is a chance this will pass now, retesting.
jrajahalme: test-me-please
jrajahalme: test-missed-k8s
jrajahalme: Changed test policies with named ports to use the same name as non-named-port policies so that they replace each other as expected when applied.
jrajahalme: test-me-please
jrajahalme: test-missed-k8s
jrajahalme: Issued #11182 for the GKE CI failure to "select nodes", retesting.
jrajahalme: test-gke
jrajahalme: @aanm Rebase is needed so I'm setting up for one more round for clean-ups. Now would be a good time for comments, if you have any now that I have addressed your requested changes :-)

jrajahalme: Rebased, testing again.
jrajahalme: never-tell-me-the-odds
jrajahalme: VirtualBox fail with kernel-4.19 CI, retesting
jrajahalme: test-with-kernel
jrajahalme: never-tell-me-the-odds

Patch:
diff --git a/Documentation/Dockerfile b/Documentation/Dockerfile
index a86818ec3a..b817d6d5d7 100644
--- a/Documentation/Dockerfile
+++ b/Documentation/Dockerfile
@@ -1,4 +1,4 @@
-FROM docker.io/library/python:3.7.5-alpine3.10
+FROM docker.io/library/python:3.7.7-alpine3.11
 
 LABEL maintainer="maintainer@cilium.io"
 
diff --git a/Documentation/requirements.txt b/Documentation/requirements.txt
index 1d051f83ee..553868ea3b 100644
--- a/Documentation/requirements.txt
+++ b/Documentation/requirements.txt
@@ -22,9 +22,9 @@ sphinxcontrib-openapi==0.3.2
 sphinxcontrib-websupport==1.1.0
 typing==3.6.6
 urllib3==1.24.2
-sphinx-tabs==1.1.7
+sphinx-tabs==1.1.13
 recommonmark==0.4.0
-sphinxcontrib-spelling==4.2.0
+sphinxcontrib-spelling==4.2.1
 sphinx-version-warning==1.1.2
 semver==2.9.0
 yamllint==1.22.0
diff --git a/api/v1/models/endpoint_status.go b/api/v1/models/endpoint_status.go
index 212821111e..05121e490f 100644
--- a/api/v1/models/endpoint_status.go
+++ b/api/v1/models/endpoint_status.go
@@ -34,6 +34,9 @@ type EndpointStatus struct {
 	// Most recent status log. See endpoint/{id}/log for the complete log.
 	Log EndpointStatusLog `json:"log,omitempty"`
 
+	// List of named ports that can be used in Network Policy
+	NamedPorts NamedPorts `json:"namedPorts,omitempty"`
+
 	// Networking properties of the endpoint
 	Networking *EndpointNetworking `json:"networking,omitempty"`
 
@@ -76,6 +79,10 @@ func (m *EndpointStatus) Validate(formats strfmt.Registry) error {
 		res = append(res, err)
 	}
 
+	if err := m.validateNamedPorts(formats); err != nil {
+		res = append(res, err)
+	}
+
 	if err := m.validateNetworking(formats); err != nil {
 		res = append(res, err)
 	}
@@ -202,6 +209,22 @@ func (m *EndpointStatus) validateLog(formats strfmt.Registry) error {
 	return nil
 }
 
+func (m *EndpointStatus) validateNamedPorts(formats strfmt.Registry) error {
+
+	if swag.IsZero(m.NamedPorts) { // not required
+		return nil
+	}
+
+	if err := m.NamedPorts.Validate(formats); err != nil {
+		if ve, ok := err.(*errors.Validation); ok {
+			return ve.ValidateName("namedPorts")
+		}
+		return err
+	}
+
+	return nil
+}
+
 func (m *EndpointStatus) validateNetworking(formats strfmt.Registry) error {
 
 	if swag.IsZero(m.Networking) { // not required
diff --git a/api/v1/models/named_ports.go b/api/v1/models/named_ports.go
new file mode 100644
index 0000000000..aa75014a31
--- /dev/null
+++ b/api/v1/models/named_ports.go
@@ -0,0 +1,47 @@
+// Code generated by go-swagger; DO NOT EDIT.
+
+package models
+
+// This file was generated by the swagger tool.
+// Editing this file might prove futile when you re-run the swagger generate command
+
+import (
+	"strconv"
+
+	strfmt "github.com/go-openapi/strfmt"
+
+	"github.com/go-openapi/errors"
+	"github.com/go-openapi/swag"
+)
+
+// NamedPorts List of named Layer 4 port and protocol pairs which will be used in Network
+// Policy specs.
+//
+// swagger:model NamedPorts
+type NamedPorts []*Port
+
+// Validate validates this named ports
+func (m NamedPorts) Validate(formats strfmt.Registry) error {
+	var res []error
+
+	for i := 0; i < len(m); i++ {
+		if swag.IsZero(m[i]) { // not required
+			continue
+		}
+
+		if m[i] != nil {
+			if err := m[i].Validate(formats); err != nil {
+				if ve, ok := err.(*errors.Validation); ok {
+					return ve.ValidateName(strconv.Itoa(i))
+				}
+				return err
+			}
+		}
+
+	}
+
+	if len(res) > 0 {
+		return errors.CompositeValidationError(res...)
+	}
+	return nil
+}
diff --git a/api/v1/models/port.go b/api/v1/models/port.go
index ac1ee97553..d164493cda 100644
--- a/api/v1/models/port.go
+++ b/api/v1/models/port.go
@@ -19,6 +19,9 @@ import (
 // swagger:model Port
 type Port struct {
 
+	// Optional layer 4 port name
+	Name string `json:"name,omitempty"`
+
 	// Layer 4 port number
 	Port uint16 `json:"port,omitempty"`
 
diff --git a/api/v1/openapi.yaml b/api/v1/openapi.yaml
index e33cd68540..15e6ed8b12 100644
--- a/api/v1/openapi.yaml
+++ b/api/v1/openapi.yaml
@@ -1121,6 +1121,9 @@ definitions:
       health:
         description: Summary overall endpoint & subcomponent health
         "$ref": "#/definitions/EndpointHealth"
+      namedPorts:
+        description: List of named ports that can be used in Network Policy
+        "$ref": "#/definitions/NamedPorts"
   EndpointState:
     description: State of endpoint
     type: string
@@ -2024,6 +2027,9 @@ definitions:
         description: Layer 4 port number
         type: integer
         format: uint16
+      name:
+        description: Optional layer 4 port name
+        type: string
   SelectorCache:
     description: cache of which identities match selectors in the policy repository
     type: array
@@ -2451,3 +2457,10 @@ definitions:
       name:
         description: Name assigned to the IP (e.g. Kubernetes pod name)
         type: string
+  NamedPorts:
+    description: |
+      List of named Layer 4 port and protocol pairs which will be used in Network
+      Policy specs.
+    type: array
+    items:
+      "$ref": "#/definitions/Port"
diff --git a/api/v1/server/embedded_spec.go b/api/v1/server/embedded_spec.go
index 185c237992..4a686f5ac0 100644
--- a/api/v1/server/embedded_spec.go
+++ b/api/v1/server/embedded_spec.go
@@ -2151,6 +2151,10 @@ func init() {
           "description": "Most recent status log. See endpoint/{id}/log for the complete log.",
           "$ref": "#/definitions/EndpointStatusLog"
         },
+        "namedPorts": {
+          "description": "List of named ports that can be used in Network Policy",
+          "$ref": "#/definitions/NamedPorts"
+        },
         "networking": {
           "description": "Networking properties of the endpoint",
           "$ref": "#/definitions/EndpointNetworking"
@@ -2742,6 +2746,13 @@ func init() {
         }
       }
     },
+    "NamedPorts": {
+      "description": "List of named Layer 4 port and protocol pairs which will be used in Network\nPolicy specs.\n",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Port"
+      }
+    },
     "NodeAddressing": {
       "description": "Addressing information of a node for all address families",
       "type": "object",
@@ -2849,6 +2860,10 @@ func init() {
       "description": "Layer 4 port / protocol pair",
       "type": "object",
       "properties": {
+        "name": {
+          "description": "Optional layer 4 port name",
+          "type": "string"
+        },
         "port": {
           "description": "Layer 4 port number",
           "type": "integer",
@@ -5700,6 +5715,10 @@ func init() {
           "description": "Most recent status log. See endpoint/{id}/log for the complete log.",
           "$ref": "#/definitions/EndpointStatusLog"
         },
+        "namedPorts": {
+          "description": "List of named ports that can be used in Network Policy",
+          "$ref": "#/definitions/NamedPorts"
+        },
         "networking": {
           "description": "Networking properties of the endpoint",
           "$ref": "#/definitions/EndpointNetworking"
@@ -6291,6 +6310,13 @@ func init() {
         }
       }
     },
+    "NamedPorts": {
+      "description": "List of named Layer 4 port and protocol pairs which will be used in Network\nPolicy specs.\n",
+      "type": "array",
+      "items": {
+        "$ref": "#/definitions/Port"
+      }
+    },
     "NodeAddressing": {
       "description": "Addressing information of a node for all address families",
       "type": "object",
@@ -6398,6 +6424,10 @@ func init() {
       "description": "Layer 4 port / protocol pair",
       "type": "object",
       "properties": {
+        "name": {
+          "description": "Optional layer 4 port name",
+          "type": "string"
+        },
         "port": {
           "description": "Layer 4 port number",
           "type": "integer",
diff --git a/cilium/cmd/policy_trace.go b/cilium/cmd/policy_trace.go
index ec19c44e13..50aaaf8dca 100644
--- a/cilium/cmd/policy_trace.go
+++ b/cilium/cmd/policy_trace.go
@@ -26,6 +26,7 @@ import (
 	"github.com/cilium/cilium/pkg/api"
 	"github.com/cilium/cilium/pkg/command"
 	endpointid "github.com/cilium/cilium/pkg/endpoint/id"
+	"github.com/cilium/cilium/pkg/iana"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/k8s"
 	clientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned"
@@ -273,7 +274,7 @@ func getSecIDFromK8s(podName string) (string, error) {
 }
 
 // parseL4PortsSlice parses a given `slice` of strings. Each string should be in
-// the form of `<port>[/<protocol>]`, where the `<port>` is an integer and
+// the form of `<port>[/<protocol>]`, where the `<port>` is an integer or a port name and
 // `<protocol>` is an optional layer 4 protocol `tcp` or `udp`. In case
 // `protocol` is not present, or is set to `any`, the parsed port will be set to
 // `models.PortProtocolAny`.
@@ -295,13 +296,19 @@ func parseL4PortsSlice(slice []string) ([]*models.Port, error) {
 		default:
 			return nil, fmt.Errorf("invalid format %q. Should be <port>[/<protocol>]", v)
 		}
+		port := 0
 		portStr := vSplit[0]
-		port, err := strconv.Atoi(portStr)
-		if err != nil {
-			return nil, fmt.Errorf("invalid port %q: %s", portStr, err)
+		if !iana.IsSvcName(portStr) {
+			var err error
+			port, err = strconv.Atoi(portStr)
+			if err != nil {
+				return nil, fmt.Errorf("invalid port %q: %s", portStr, err)
+			}
+			portStr = ""
 		}
 		l4 := &models.Port{
 			Port:     uint16(port),
+			Name:     portStr,
 			Protocol: protoStr,
 		}
 		rules = append(rules, l4)
diff --git a/daemon/cmd/endpoint.go b/daemon/cmd/endpoint.go
index 3c65da0240..b31212e314 100644
--- a/daemon/cmd/endpoint.go
+++ b/daemon/cmd/endpoint.go
@@ -154,24 +154,24 @@ func NewPutEndpointIDHandler(d *Daemon) PutEndpointIDHandler {
 // endpoint metadata. It implements endpoint.MetadataResolverCB.
 // The returned pod is deepcopied which means the its fields can be written
 // into.
-func (d *Daemon) fetchK8sLabelsAndAnnotations(nsName, podName string) (*types.Pod, labels.Labels, labels.Labels, map[string]string, error) {
+func (d *Daemon) fetchK8sLabelsAndAnnotations(nsName, podName string) (*types.Pod, []types.ContainerPort, labels.Labels, labels.Labels, map[string]string, error) {
 	p, err := d.k8sWatcher.GetCachedPod(nsName, podName)
 	if err != nil {
-		return nil, nil, nil, nil, err
+		return nil, nil, nil, nil, nil, err
 	}
 	ns, err := d.k8sWatcher.GetCachedNamespace(nsName)
 	if err != nil {
-		return nil, nil, nil, nil, err
+		return nil, nil, nil, nil, nil, err
 	}
 
-	lbls, annotations, err := k8s.GetPodMetadata(ns, p)
+	containerPorts, lbls, annotations, err := k8s.GetPodMetadata(ns, p)
 	if err != nil {
-		return nil, nil, nil, nil, err
+		return nil, nil, nil, nil, nil, err
 	}
 
 	k8sLbls := labels.Map2Labels(lbls, labels.LabelSourceK8s)
 	identityLabels, infoLabels := labelsfilter.Filter(k8sLbls)
-	return p, identityLabels, infoLabels, annotations, nil
+	return p, containerPorts, identityLabels, infoLabels, annotations, nil
 }
 
 func invalidDataError(ep *endpoint.Endpoint, err error) (*endpoint.Endpoint, int, error) {
@@ -265,11 +265,14 @@ func (d *Daemon) createEndpoint(ctx context.Context, epTemplate *models.Endpoint
 	}
 
 	if ep.K8sNamespaceAndPodNameIsSet() && k8s.IsEnabled() {
-		pod, identityLabels, info, _, err := d.fetchK8sLabelsAndAnnotations(ep.K8sNamespace, ep.K8sPodName)
+		pod, cp, identityLabels, info, _, err := d.fetchK8sLabelsAndAnnotations(ep.K8sNamespace, ep.K8sPodName)
 		if err != nil {
 			ep.Logger("api").WithError(err).Warning("Unable to fetch kubernetes labels")
 		} else {
 			ep.SetPod(pod)
+			if err := ep.SetNamedPorts(cp); err != nil {
+				return invalidDataError(ep, fmt.Errorf("Invalid ContainerPorts %v: %s", cp, err))
+			}
 			addLabels.MergeLabels(identityLabels)
 			infoLabels.MergeLabels(info)
 		}
diff --git a/daemon/cmd/ipcache.go b/daemon/cmd/ipcache.go
index 2f1b9f26c5..5858708a11 100644
--- a/daemon/cmd/ipcache.go
+++ b/daemon/cmd/ipcache.go
@@ -86,6 +86,8 @@ func (ipc *ipCacheDumpListener) OnIPIdentityCacheChange(modType ipcache.CacheMod
 			Source:    string(source.Kubernetes),
 			Namespace: k8sMeta.Namespace,
 			Name:      k8sMeta.PodName,
+			// TODO (jrajahalme): Consider if named ports should be
+			//                    made visible in the model.
 		}
 	}
 
diff --git a/daemon/cmd/state.go b/daemon/cmd/state.go
index 951350da90..fa133a1572 100644
--- a/daemon/cmd/state.go
+++ b/daemon/cmd/state.go
@@ -41,7 +41,7 @@ type endpointRestoreState struct {
 	toClean  []*endpoint.Endpoint
 }
 
-// validateEndpoint attempts to determine that the endpoint is valid, ie it
+// validateEndpoint attempts to determine that the restored endpoint is valid, ie it
 // still exists in k8s, its datapath devices are present, and Cilium is
 // responsible for its workload, etc.
 //
diff --git a/install/kubernetes/cilium/charts/hubble-relay/Chart.yaml b/install/kubernetes/cilium/charts/hubble-relay/Chart.yaml
new file mode 100644
index 0000000000..660b9b602e
--- /dev/null
+++ b/install/kubernetes/cilium/charts/hubble-relay/Chart.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+name: hubble-relay
+version: 1.7.90
+appVersion: 1.7.90
+tillerVersion: ">=2.7.2"
+description: Helm chart for running Hubble Relay
+sources:
+  - https://github.com/cilium/cilium
+engine: gotpl
diff --git a/install/kubernetes/cilium/charts/hubble-relay/templates/NOTES.txt b/install/kubernetes/cilium/charts/hubble-relay/templates/NOTES.txt
new file mode 100644
index 0000000000..bdf511f4d7
--- /dev/null
+++ b/install/kubernetes/cilium/charts/hubble-relay/templates/NOTES.txt
@@ -0,0 +1,5 @@
+You have successfully installed Cilium {{ .Chart.Name }}
+
+Your release version is {{ .Chart.Version }}
+
+For any further help, visit https://docs.cilium.io/en/v{{ substr 0 3 .Chart.Version }}/gettinghelp
diff --git a/install/kubernetes/cilium/charts/hubble-relay/templates/deployment.yaml b/install/kubernetes/cilium/charts/hubble-relay/templates/deployment.yaml
new file mode 100644
index 0000000000..592c28d50d
--- /dev/null
+++ b/install/kubernetes/cilium/charts/hubble-relay/templates/deployment.yaml
@@ -0,0 +1,75 @@
+{{- if .Values.global.hubble.enabled }}
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ .Chart.Name }}
+  namespace: {{ .Release.Namespace }}
+  labels:
+    k8s-app: {{ .Chart.Name }}
+spec:
+  replicas: {{ .Values.numReplicas }}
+  selector:
+    matchLabels:
+      k8s-app: {{ .Chart.Name }}
+  template:
+    metadata:
+      labels:
+        k8s-app: {{ .Chart.Name }}
+    spec:
+      affinity:
+        podAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+                - key: "k8s-app"
+                  operator: In
+                  values:
+                    - cilium
+            topologyKey: "kubernetes.io/hostname"
+      containers:
+        - name: hubble-relay
+{{- if contains "/" .Values.image }}
+          image: "{{ .Values.image }}"
+{{- else }}
+          image: "{{ .Values.global.registry }}/{{ .Values.image }}:{{ .Values.global.tag }}"
+{{- end }}
+          imagePullPolicy: {{ .Values.global.pullPolicy }}
+          command:
+            - "hubble-relay"
+          args:
+            - "serve"
+            - "--peer-service=unix://{{ .Values.global.hubble.socketPath }}"
+            - "--listen-address={{ .Values.listenHost }}:{{ .Values.listenPort }}"
+{{- if .Values.dialTimeout }}
+            - "--dial-timeout={{ .Values.dialTimeout }}"
+{{- end }}
+{{- if .Values.retryTimeout }}
+            - "--retry-timeout={{ .Values.retryTimeout }}"
+{{- end }}
+          ports:
+            - name: grpc
+              containerPort: {{ .Values.listenPort }}
+          readinessProbe:
+            tcpSocket:
+              port: grpc
+          livenessProbe:
+            tcpSocket:
+              port: grpc
+{{- if .Values.resources }}
+          resources:
+{{- toYaml .Values.resources | trim | nindent 10 }}
+{{- end }}
+          volumeMounts:
+          - mountPath: {{ dir .Values.global.hubble.socketPath }}
+            name: hubble-sock-dir
+            readOnly: true
+      restartPolicy: Always
+      terminationGracePeriodSeconds: 0
+      tolerations:
+      - operator: Exists
+      volumes:
+      - hostPath:
+          path: {{ dir .Values.global.hubble.socketPath }}
+          type: Directory
+        name: hubble-sock-dir
+{{- end }}
diff --git a/install/kubernetes/cilium/charts/hubble-relay/templates/service.yaml b/install/kubernetes/cilium/charts/hubble-relay/templates/service.yaml
new file mode 100644
index 0000000000..16b2e792db
--- /dev/null
+++ b/install/kubernetes/cilium/charts/hubble-relay/templates/service.yaml
@@ -0,0 +1,17 @@
+{{- if .Values.global.hubble.enabled }}
+kind: Service
+apiVersion: v1
+metadata:
+  name: {{ .Chart.Name }}
+  namespace: {{ .Release.Namespace }}
+  labels:
+    k8s-app: {{ .Chart.Name }}
+spec:
+  type: ClusterIP
+  selector:
+    k8s-app: {{ .Chart.Name }}
+  ports:
+  - protocol: TCP
+    port: {{ .Values.servicePort }}
+    targetPort: {{ .Values.listenPort }}
+{{- end }}
diff --git a/install/kubernetes/cilium/charts/hubble-relay/values.yaml b/install/kubernetes/cilium/charts/hubble-relay/values.yaml
new file mode 100644
index 0000000000..0d380c8291
--- /dev/null
+++ b/install/kubernetes/cilium/charts/hubble-relay/values.yaml
@@ -0,0 +1,16 @@
+image: hubble-relay
+
+# Specifies the resources for the hubble-relay pods
+resources: {}
+# Number of replicas run for the hubble-relay deployment.
+numReplicas: 1
+# Host to listen to. Specify an empty string to bind to all the interfaces.
+listenHost: ""
+# Port to listen to.
+listenPort: "4245"
+# Dial timeout to connect to the local hubble instance to receive peer information (e.g. "30s").
+dialTimeout: ~
+# Backoff duration to retry connecting to the local hubble instance in case of failure (e.g. "30s").
+retryTimeout: ~
+# Port to use for the k8s service backed by hubble-relay pods.
+servicePort: 80
diff --git a/install/kubernetes/cilium/requirements.yaml b/install/kubernetes/cilium/requirements.yaml
index 81edb21806..bd6dea3d63 100644
--- a/install/kubernetes/cilium/requirements.yaml
+++ b/install/kubernetes/cilium/requirements.yaml
@@ -8,6 +8,9 @@ dependencies:
   - name: hubble-cli
     version: 1.7.90
     condition: global.hubble.cli.enabled
+  - name: hubble-relay
+    version: 1.7.90
+    condition: global.hubble.relay.enabled
   - name: hubble-ui
     version: 1.7.90
     condition: global.hubble.ui.enabled
diff --git a/install/kubernetes/cilium/values.yaml b/install/kubernetes/cilium/values.yaml
index ee48e0b0a0..656af1125b 100644
--- a/install/kubernetes/cilium/values.yaml
+++ b/install/kubernetes/cilium/values.yaml
@@ -475,6 +475,9 @@ global:
     # Configures the hubble-cli subchart
     cli:
       enabled: false
+    # Configures the hubble-relay subchart
+    relay:
+      enabled: false
 
   # CI specific options: DO NOT USE IN PRODUCTION.
   ci:
diff --git a/pkg/endpoint/api.go b/pkg/endpoint/api.go
index bac1df8246..6add3492c6 100644
--- a/pkg/endpoint/api.go
+++ b/pkg/endpoint/api.go
@@ -37,6 +37,7 @@ import (
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/policy/trafficdirection"
+	"github.com/cilium/cilium/pkg/u8proto"
 )
 
 // GetLabelsModel returns the labels of the endpoint in their representation
@@ -227,6 +228,7 @@ func (e *Endpoint) GetModelRLocked() *models.Endpoint {
 			Controllers: controllerMdl,
 			State:       currentState, // TODO: Validate
 			Health:      e.getHealthModel(),
+			NamedPorts:  e.getNamedPortsModel(),
 		},
 	}
 
@@ -298,6 +300,31 @@ func (e *Endpoint) GetHealthModel() *models.EndpointHealth {
 	return e.getHealthModel()
 }
 
+// getNamedPortsModel returns the endpoint's NamedPorts object.
+//
+// Must be called with e.Mutex locked.
+func (e *Endpoint) getNamedPortsModel() (np models.NamedPorts) {
+	k8sPorts := e.k8sPorts
+	np = make(models.NamedPorts, 0, len(k8sPorts))
+	for name, value := range k8sPorts {
+		np = append(np, &models.Port{
+			Name:     name,
+			Port:     value.Port,
+			Protocol: u8proto.U8proto(value.Proto).String(),
+		})
+	}
+	return np
+}
+
+// GetNamedPortsModel returns the endpoint's NamedPorts object.
+func (e *Endpoint) GetNamedPortsModel() models.NamedPorts {
+	if err := e.rlockAlive(); err != nil {
+		return nil
+	}
+	defer e.runlock()
+	return e.getNamedPortsModel()
+}
+
 // GetModel returns the API model of endpoint e.
 func (e *Endpoint) GetModel() *models.Endpoint {
 	if e == nil {
diff --git a/pkg/endpoint/bpf.go b/pkg/endpoint/bpf.go
index 7f5ae7fa7e..22106071d8 100644
--- a/pkg/endpoint/bpf.go
+++ b/pkg/endpoint/bpf.go
@@ -145,11 +145,11 @@ func (e *Endpoint) writeHeaderfile(prefix string) error {
 }
 
 // addNewRedirectsFromDesiredPolicy must be called while holding the endpoint lock for
-// writing. On success, returns nil; otherwise, returns an error  indicating the
+// writing. On success, returns nil; otherwise, returns an error indicating the
 // problem that occurred while adding an l7 redirect for the specified policy.
 // Must be called with endpoint.Mutex held.
 func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirects map[string]bool, proxyWaitGroup *completion.WaitGroup) (error, revert.FinalizeFunc, revert.RevertFunc) {
-	if option.Config.DryMode {
+	if option.Config.DryMode || e.isProxyDisabled() {
 		return nil, nil, nil
 	}
 
@@ -172,14 +172,23 @@ func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirec
 	for _, l4 := range m {
 		if l4.IsRedirect() {
 			var redirectPort uint16
-			var err error
 			// Only create a redirect if the proxy is NOT running in a sidecar
 			// container. If running in a sidecar container, just allow traffic
 			// to the port at L4 by setting the proxy port to 0.
 			if !e.hasSidecarProxy || l4.L7Parser != policy.ParserTypeHTTP {
 				var finalizeFunc revert.FinalizeFunc
 				var revertFunc revert.RevertFunc
-				redirectPort, err, finalizeFunc, revertFunc = e.updateProxyRedirect(l4, proxyWaitGroup)
+
+				proxyID, err := e.ProxyID(e.desiredPolicy.NamedPortsMap, l4)
+				if err != nil {
+					// skip redirects for which a proxyID cannot be created.
+					// Right now this happens only due to named port mapping not existing and
+					// we'll be called again when the mapping is available.
+					log.WithError(err).WithField(logfields.EndpointID, e.ID).Warning("Skipping adding redirect")
+					continue
+				}
+
+				redirectPort, err, finalizeFunc, revertFunc = e.proxy.CreateOrUpdateRedirect(l4, proxyID, e, proxyWaitGroup)
 				if err != nil {
 					revertStack.Revert() // Ignore errors while reverting. This is best-effort.
 					return err, nil, nil
@@ -187,7 +196,6 @@ func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirec
 				finalizeList.Append(finalizeFunc)
 				revertStack.Push(revertFunc)
 
-				proxyID := e.ProxyID(l4)
 				if e.realizedRedirects == nil {
 					e.realizedRedirects = make(map[string]uint16)
 				}
@@ -219,11 +227,15 @@ func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirec
 				direction = trafficdirection.Egress
 			}
 
-			keysFromFilter := l4.ToMapState(direction)
+			keysFromFilter := l4.ToMapState(e.desiredPolicy.NamedPortsMap, direction)
 
 			for keyFromFilter, entry := range keysFromFilter {
 				if oldEntry, ok := e.desiredPolicy.PolicyMapState[keyFromFilter]; ok {
-					updatedDesiredMapState[keyFromFilter] = oldEntry
+					// Keep the original old entry for revert if there are duplicate keys,
+					// which are possible due to named ports resolving to the same number.
+					if _, isDup := updatedDesiredMapState[keyFromFilter]; !isDup {
+						updatedDesiredMapState[keyFromFilter] = oldEntry
+					}
 				} else {
 					insertedDesiredMapState[keyFromFilter] = struct{}{}
 				}
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index 67aa5b7c4d..efe31ecfc6 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -216,6 +216,10 @@ type Endpoint struct {
 	// pod
 	pod *types.Pod
 
+	// k8sPorts contains container ports associated in the pod.
+	// It is used to enforce k8s network policies with port names.
+	k8sPorts policy.NamedPortsMap
+
 	// policyRevision is the policy revision this endpoint is currently on
 	// to modify this field please use endpoint.setPolicyRevision instead
 	policyRevision uint64
@@ -1077,6 +1081,32 @@ func (e *Endpoint) SetK8sNamespace(name string) {
 	e.unlock()
 }
 
+// SetNamedPorts sets the k8s container ports specified by kubernetes.
+// Note that once put in place, the new k8sPorts is never changed,
+// so that the map can be used concurrently without keeping locks.
+// Reading the 'e.k8sPorts' member (the "map pointer") *itself* requires the endpoint lock!
+// Can't really error out as that might break backwards compatibility.
+func (e *Endpoint) SetNamedPorts(containerPorts []types.ContainerPort) error {
+	k8sPorts := make(policy.NamedPortsMap, len(containerPorts))
+	for _, cp := range containerPorts {
+		if cp.Name == "" {
+			continue // silently skip unnamed ports
+		}
+		err := k8sPorts.AddPort(cp.Name, int(cp.ContainerPort), cp.Protocol)
+		if err != nil {
+			e.getLogger().WithError(err).Warning("Adding named port failed")
+			continue
+		}
+	}
+	if len(k8sPorts) == 0 {
+		k8sPorts = nil // nil map with no storage
+	}
+	e.mutex.Lock()
+	e.k8sPorts = k8sPorts
+	e.mutex.Unlock()
+	return nil
+}
+
 // K8sNamespaceAndPodNameIsSet returns true if the pod name is set
 func (e *Endpoint) K8sNamespaceAndPodNameIsSet() bool {
 	e.unconditionalLock()
@@ -1479,7 +1509,7 @@ func APICanModify(e *Endpoint) error {
 
 // MetadataResolverCB provides an implementation for resolving the endpoint
 // metadata for an endpoint such as the associated labels and annotations.
-type MetadataResolverCB func(ns, podName string) (pod *types.Pod, identityLabels labels.Labels, infoLabels labels.Labels, annotations map[string]string, err error)
+type MetadataResolverCB func(ns, podName string) (pod *types.Pod, _ []types.ContainerPort, identityLabels labels.Labels, infoLabels labels.Labels, annotations map[string]string, err error)
 
 // RunMetadataResolver starts a controller associated with the received
 // endpoint which will periodically attempt to resolve the metadata for the
@@ -1501,14 +1531,15 @@ func (e *Endpoint) RunMetadataResolver(resolveMetadata MetadataResolverCB) {
 		controller.ControllerParams{
 			DoFunc: func(ctx context.Context) error {
 				ns, podName := e.GetK8sNamespace(), e.GetK8sPodName()
-				pod, identityLabels, info, _, err := resolveMetadata(ns, podName)
+				pod, cp, identityLabels, info, _, err := resolveMetadata(ns, podName)
 				if err != nil {
 					e.Logger(controllerName).WithError(err).Warning("Unable to fetch kubernetes labels")
 					return err
 				}
 				e.SetPod(pod)
+				e.SetNamedPorts(cp)
 				e.UpdateVisibilityPolicy(func(_, _ string) (proxyVisibility string, err error) {
-					_, _, _, annotations, err := resolveMetadata(ns, podName)
+					_, _, _, _, annotations, err := resolveMetadata(ns, podName)
 					if err != nil {
 						return "", err
 					}
diff --git a/pkg/endpoint/endpoint_status.go b/pkg/endpoint/endpoint_status.go
index 9bc281ebe8..70bbb6808b 100644
--- a/pkg/endpoint/endpoint_status.go
+++ b/pkg/endpoint/endpoint_status.go
@@ -327,6 +327,7 @@ func (e *Endpoint) GetCiliumEndpointStatus(conf EndpointStatusConfiguration) *ci
 		Networking:          getEndpointNetworking(modelStatus),
 		State:               compressEndpointState(modelStatus.State),
 		Encryption:          cilium_v2.EncryptionSpec{Key: int(node.GetIPsecKeyIdentity())},
+		NamedPorts:          modelStatus.NamedPorts,
 	}
 
 	if conf.EndpointStatusIsEnabled(option.EndpointStatusControllers) {
diff --git a/pkg/endpoint/policy.go b/pkg/endpoint/policy.go
index f38c818caa..c3b522736c 100644
--- a/pkg/endpoint/policy.go
+++ b/pkg/endpoint/policy.go
@@ -45,19 +45,23 @@ import (
 )
 
 // ProxyID returns a unique string to identify a proxy mapping.
-func (e *Endpoint) ProxyID(l4 *policy.L4Filter) string {
-	return policy.ProxyIDFromFilter(e.ID, l4)
+func (e *Endpoint) ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error) {
+	return policy.ProxyIDFromFilter(e.ID, npMap, l4)
 }
 
 // lookupRedirectPort returns the redirect L4 proxy port for the given L4
 // policy map key, in host byte order. Returns 0 if not found or the
 // filter doesn't require a redirect.
 // Must be called with Endpoint.Mutex held.
-func (e *Endpoint) LookupRedirectPort(l4Filter *policy.L4Filter) uint16 {
+func (e *Endpoint) LookupRedirectPortLocked(npMap policy.NamedPortsMap, l4Filter *policy.L4Filter) uint16 {
 	if !l4Filter.IsRedirect() {
 		return 0
 	}
-	proxyID := e.ProxyID(l4Filter)
+	proxyID, err := e.ProxyID(npMap, l4Filter)
+	if err != nil {
+		e.getLogger().WithError(err).Warn("ProxyID failed")
+		return 0
+	}
 	return e.realizedRedirects[proxyID]
 }
 
@@ -84,7 +88,7 @@ func (e *Endpoint) updateNetworkPolicy(proxyWaitGroup *completion.WaitGroup) (re
 	}
 
 	// Publish the updated policy to L7 proxies.
-	return e.proxy.UpdateNetworkPolicy(e, e.desiredPolicy.L4Policy, e.desiredPolicy.IngressPolicyEnabled, e.desiredPolicy.EgressPolicyEnabled, proxyWaitGroup)
+	return e.proxy.UpdateNetworkPolicy(e, e.desiredPolicy.L4Policy, e.desiredPolicy.NamedPortsMap, e.desiredPolicy.IngressPolicyEnabled, e.desiredPolicy.EgressPolicyEnabled, proxyWaitGroup)
 }
 
 func (e *Endpoint) useCurrentNetworkPolicy(proxyWaitGroup *completion.WaitGroup) {
@@ -177,7 +181,8 @@ func (e *Endpoint) regeneratePolicy() (retErr error) {
 		e.getLogger().WithError(err).Warning("Failed to update policy")
 		return err
 	}
-	calculatedPolicy := e.selectorPolicy.Consume(e)
+	calculatedPolicy := e.selectorPolicy.Consume(e, ipcache.IPIdentityCache.GetNamedPorts())
+
 	stats.policyCalculation.End(true)
 
 	// This marks the e.desiredPolicy different from the previously realized policy
@@ -495,8 +500,8 @@ func (e *Endpoint) RegenerateIfAlive(regenMetadata *regeneration.ExternalRegener
 }
 
 // Regenerate forces the regeneration of endpoint programs & policy
-// Should only be called with e.state == StateWaitingToRegenerate or with
-// e.state == StateWaitingForIdentity
+// Should only be called with e.state at StateWaitingToRegenerate,
+// StateWaitingForIdentity, or StateRestoring
 func (e *Endpoint) Regenerate(regenMetadata *regeneration.ExternalRegenerationMetadata) <-chan bool {
 	done := make(chan bool, 1)
 
@@ -675,12 +680,13 @@ func (e *Endpoint) runIPIdentitySync(endpointIP addressing.CiliumIP) {
 				metadata := e.FormatGlobalEndpointID()
 				k8sNamespace := e.K8sNamespace
 				k8sPodName := e.K8sPodName
+				namedPorts := e.k8sPorts
 
 				// Release lock as we do not want to have long-lasting key-value
 				// store operations resulting in lock being held for a long time.
 				e.runlock()
 
-				if err := ipcache.UpsertIPToKVStore(ctx, IP, hostIP, ID, key, metadata, k8sNamespace, k8sPodName); err != nil {
+				if err := ipcache.UpsertIPToKVStore(ctx, IP, hostIP, ID, key, metadata, k8sNamespace, k8sPodName, namedPorts); err != nil {
 					return fmt.Errorf("unable to add endpoint IP mapping '%s'->'%d': %s", IP.String(), ID, err)
 				}
 				return nil
diff --git a/pkg/endpoint/proxy.go b/pkg/endpoint/proxy.go
index a77f8b64a4..636f700b3c 100644
--- a/pkg/endpoint/proxy.go
+++ b/pkg/endpoint/proxy.go
@@ -15,7 +15,6 @@
 package endpoint
 
 import (
-	"fmt"
 	"reflect"
 
 	"github.com/cilium/cilium/pkg/completion"
@@ -28,7 +27,7 @@ import (
 type EndpointProxy interface {
 	CreateOrUpdateRedirect(l4 policy.ProxyPolicy, id string, localEndpoint logger.EndpointUpdater, wg *completion.WaitGroup) (proxyPort uint16, err error, finalizeFunc revert.FinalizeFunc, revertFunc revert.RevertFunc)
 	RemoveRedirect(id string, wg *completion.WaitGroup) (error, revert.FinalizeFunc, revert.RevertFunc)
-	UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error)
+	UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error)
 	UseCurrentNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, wg *completion.WaitGroup)
 	RemoveNetworkPolicy(ep logger.EndpointInfoSource)
 }
@@ -40,15 +39,6 @@ func (e *Endpoint) SetProxy(p EndpointProxy) {
 	e.proxy = p
 }
 
-// updateProxyRedirect updates the redirect rules in the proxy for a particular
-// endpoint using the provided L4 filter. Returns the allocated proxy port
-func (e *Endpoint) updateProxyRedirect(l4 *policy.L4Filter, proxyWaitGroup *completion.WaitGroup) (proxyPort uint16, err error, finalizeFunc revert.FinalizeFunc, revertFunc revert.RevertFunc) {
-	if e.isProxyDisabled() {
-		return 0, fmt.Errorf("can't redirect, proxy disabled"), nil, nil
-	}
-	return e.proxy.CreateOrUpdateRedirect(l4, e.ProxyID(l4), e, proxyWaitGroup)
-}
-
 func (e *Endpoint) removeNetworkPolicy() {
 	if e.isProxyDisabled() {
 		return
@@ -74,7 +64,7 @@ func (f *FakeEndpointProxy) RemoveRedirect(id string, wg *completion.WaitGroup)
 }
 
 // UpdateNetworkPolicy does nothing.
-func (f *FakeEndpointProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
+func (f *FakeEndpointProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
 	return nil, nil
 }
 
diff --git a/pkg/endpoint/regeneration/owner.go b/pkg/endpoint/regeneration/owner.go
index 8eaef9b645..f870f42eef 100644
--- a/pkg/endpoint/regeneration/owner.go
+++ b/pkg/endpoint/regeneration/owner.go
@@ -58,7 +58,7 @@ type EndpointInfoSource interface {
 	ConntrackNameLocked() string
 	GetIngressPolicyEnabledLocked() bool
 	GetEgressPolicyEnabledLocked() bool
-	ProxyID(l4 *policy.L4Filter) string
+	ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error)
 	GetProxyInfoByFields() (uint64, string, string, []string, string, uint64, error)
 }
 
diff --git a/pkg/envoy/server.go b/pkg/envoy/server.go
index 27d5e6c340..c904d4a63e 100644
--- a/pkg/envoy/server.go
+++ b/pkg/envoy/server.go
@@ -30,6 +30,7 @@ import (
 	"github.com/cilium/cilium/pkg/envoy/xds"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/lock"
+	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/policy/api"
@@ -878,7 +879,7 @@ func getWildcardNetworkPolicyRule(selectors policy.L7DataMap) *cilium.PortNetwor
 	}
 }
 
-func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool) []*cilium.PortNetworkPolicy {
+func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, npMap policy.NamedPortsMap, policyEnforced bool) []*cilium.PortNetworkPolicy {
 	if !policyEnforced {
 		// Return an allow-all policy.
 		return allowAllPortNetworkPolicy
@@ -889,7 +890,8 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool)
 	}
 
 	PerPortPolicies := make([]*cilium.PortNetworkPolicy, 0, len(l4Policy))
-
+	// map to locate entries already on the same port
+	pnps := make(map[string]*cilium.PortNetworkPolicy, len(l4Policy))
 	for _, l4 := range l4Policy {
 		var protocol envoy_api_v2_core.SocketAddress_Protocol
 		switch l4.Protocol {
@@ -899,19 +901,26 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool)
 			protocol = envoy_api_v2_core.SocketAddress_UDP
 		}
 
-		pnp := &cilium.PortNetworkPolicy{
-			Port:     uint32(l4.Port),
-			Protocol: protocol,
-			Rules:    make([]*cilium.PortNetworkPolicyRule, 0, len(l4.L7RulesPerSelector)),
+		port := uint16(l4.Port)
+		if port == 0 && l4.PortName != "" {
+			var err error
+			port, err = npMap.GetNamedPort(l4.PortName, uint8(l4.U8Proto))
+			if err != nil {
+				log.WithError(err).WithField(logfields.PortName, l4.PortName).Debug("getDirectionNetworkPolicy: Skipping named port")
+				continue
+			}
 		}
+
+		rules := make([]*cilium.PortNetworkPolicyRule, 0, len(l4.L7RulesPerSelector))
 		allowAll := false
+
 		// Assume none of the rules have side-effects so that rule evaluation can
 		// be stopped as soon as the first allowing rule is found. 'canShortCircuit'
 		// is set to 'false' below if any rules with side effects are encountered,
 		// causing all the applicable rules to be evaluated instead.
 		canShortCircuit := true
 
-		if l4.Port == 0 {
+		if port == 0 {
 			// L3-only rule, must generate L7 allow-all in case there are other
 			// port-specific rules. Otherwise traffic from allowed remotes could be dropped.
 			rule := getWildcardNetworkPolicyRule(l4.L7RulesPerSelector)
@@ -921,7 +930,7 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool)
 					// the other rules.
 					allowAll = true
 				}
-				pnp.Rules = append(pnp.Rules, rule)
+				rules = append(rules, rule)
 			}
 		} else {
 			for sel, l7 := range l4.L7RulesPerSelector {
@@ -935,27 +944,39 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool)
 						// the other rules.
 						allowAll = true
 					}
-					pnp.Rules = append(pnp.Rules, rule)
+					rules = append(rules, rule)
 				}
 			}
 		}
 		// Short-circuit rules if a rule allows all and all other rules can be short-circuited
 		if allowAll && canShortCircuit {
 			log.Debug("Short circuiting HTTP rules due to rule allowing all and no other rules needing attention")
-			pnp.Rules = nil
+			rules = nil
 		}
 
 		// No rule for this port matches any remote identity.
 		// This means that no traffic was explicitly allowed for this port.
 		// In this case, just don't generate any PortNetworkPolicy for this
 		// port.
-		if !allowAll && len(pnp.Rules) == 0 {
+		if !allowAll && len(rules) == 0 {
 			continue
 		}
 
+		// Add rules to a new or existing entry for this port
+		key := fmt.Sprintf("%d/%s", port, l4.Protocol)
+		pnp, exists := pnps[key]
+		if !exists {
+			pnp = &cilium.PortNetworkPolicy{
+				Port:     uint32(port),
+				Protocol: protocol,
+				Rules:    rules,
+			}
+			pnps[key] = pnp
+			PerPortPolicies = append(PerPortPolicies, pnp)
+		} else {
+			pnp.Rules = append(pnp.Rules, rules...)
+		}
 		SortPortNetworkPolicyRules(pnp.Rules)
-
-		PerPortPolicies = append(PerPortPolicies, pnp)
 	}
 
 	if len(PerPortPolicies) == 0 {
@@ -969,7 +990,7 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool)
 
 // getNetworkPolicy converts a network policy into a cilium.NetworkPolicy.
 func getNetworkPolicy(name string, id identity.NumericIdentity, conntrackName string, policy *policy.L4Policy,
-	ingressPolicyEnforced, egressPolicyEnforced bool) *cilium.NetworkPolicy {
+	npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool) *cilium.NetworkPolicy {
 	p := &cilium.NetworkPolicy{
 		Name:             name,
 		Policy:           uint64(id),
@@ -978,8 +999,8 @@ func getNetworkPolicy(name string, id identity.NumericIdentity, conntrackName st
 
 	// If no policy, deny all traffic. Otherwise, convert the policies for ingress and egress.
 	if policy != nil {
-		p.IngressPerPortPolicies = getDirectionNetworkPolicy(policy.Ingress, ingressPolicyEnforced)
-		p.EgressPerPortPolicies = getDirectionNetworkPolicy(policy.Egress, egressPolicyEnforced)
+		p.IngressPerPortPolicies = getDirectionNetworkPolicy(policy.Ingress, npMap, ingressPolicyEnforced)
+		p.EgressPerPortPolicies = getDirectionNetworkPolicy(policy.Egress, npMap, egressPolicyEnforced)
 	}
 
 	return p
@@ -1018,7 +1039,7 @@ func getNodeIDs(ep logger.EndpointUpdater, policy *policy.L4Policy) []string {
 // to L7 proxies.
 // When the proxy acknowledges the network policy update, it will result in
 // a subsequent call to the endpoint's OnProxyPolicyUpdate() function.
-func (s *XDSServer) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy,
+func (s *XDSServer) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap,
 	ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
 	s.mutex.Lock()
 	defer s.mutex.Unlock()
@@ -1033,7 +1054,7 @@ func (s *XDSServer) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *polic
 		if ip == "" {
 			continue
 		}
-		networkPolicy := getNetworkPolicy(ip, ep.GetIdentity(), ep.ConntrackNameLocked(), policy,
+		networkPolicy := getNetworkPolicy(ip, ep.GetIdentity(), ep.ConntrackNameLocked(), policy, npMap,
 			ingressPolicyEnforced, egressPolicyEnforced)
 		err := networkPolicy.Validate()
 		if err != nil {
diff --git a/pkg/iana/svcname.go b/pkg/iana/svcname.go
new file mode 100644
index 0000000000..976975dfa4
--- /dev/null
+++ b/pkg/iana/svcname.go
@@ -0,0 +1,31 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package iana
+
+import (
+	"regexp"
+)
+
+// IANA Service Name consists of alphanumeric characters of which at
+// least one is not a number, as well as non-consecutive dashes ('-')
+// except for in the beginning or the end.
+// Note: Character case must be ignored when comparing service names.
+var isSvcName = regexp.MustCompile(`^([a-zA-Z0-9]-?)*[a-zA-Z](-?[a-zA-Z0-9])*$`).MatchString
+
+// IsSvcName returns true if the string conforms to IANA Service Name specification
+// (RFC 6335 Section 5.1. Service Name Syntax)
+func IsSvcName(name string) bool {
+	return len(name) > 0 && len(name) <= 15 && isSvcName(name)
+}
diff --git a/pkg/identity/identity.go b/pkg/identity/identity.go
index dfebf0f7e9..0d8a540a44 100644
--- a/pkg/identity/identity.go
+++ b/pkg/identity/identity.go
@@ -67,6 +67,18 @@ type IPIdentityPair struct {
 	Metadata     string          `json:"Metadata"`
 	K8sNamespace string          `json:"K8sNamespace,omitempty"`
 	K8sPodName   string          `json:"K8sPodName,omitempty"`
+	NamedPorts   []NamedPort     `json:"NamedPorts,omitempty"`
+}
+
+// NamedPort is a mapping from a port name to a port number and protocol.
+//
+// WARNING - STABLE API
+// This structure is written as JSON to the key-value store. Do NOT modify this
+// structure in ways which are not JSON forward compatible.
+type NamedPort struct {
+	Name     string `json:"Name"`
+	Port     uint16 `json:"Port"`
+	Protocol string `json:"Protocol"`
 }
 
 // Sanitize takes a partially initialized Identity (for example, deserialized
diff --git a/pkg/ipcache/fake/ipcache.go b/pkg/ipcache/fake/ipcache.go
index 23ff3dd86b..d2c4105643 100644
--- a/pkg/ipcache/fake/ipcache.go
+++ b/pkg/ipcache/fake/ipcache.go
@@ -45,11 +45,12 @@ func NewIPCache(events bool) *IPCache {
 	}
 }
 
-func (i *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *ipcache.K8sMetadata, newIdentity ipcache.Identity) bool {
+func (i *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *ipcache.K8sMetadata, newIdentity ipcache.Identity) (bool, bool) {
 	i.Events <- NodeEvent{EventUpsert, net.ParseIP(ip)}
-	return true
+	return true, false
 }
 
-func (i *IPCache) Delete(IP string, source source.Source) {
+func (i *IPCache) Delete(IP string, source source.Source) bool {
 	i.Events <- NodeEvent{EventDelete, net.ParseIP(IP)}
+	return false
 }
diff --git a/pkg/ipcache/ipcache.go b/pkg/ipcache/ipcache.go
index 2f7bb22632..842babf491 100644
--- a/pkg/ipcache/ipcache.go
+++ b/pkg/ipcache/ipcache.go
@@ -22,6 +22,7 @@ import (
 	"github.com/cilium/cilium/pkg/lock"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/source"
 	"github.com/sirupsen/logrus"
 )
@@ -54,6 +55,8 @@ type K8sMetadata struct {
 	Namespace string
 	// PodName is the Kubernetes pod name behind the IP
 	PodName string
+	// NamedPorts is the set of named ports for the pod
+	NamedPorts policy.NamedPortsMap
 }
 
 // IPCache is a collection of mappings:
@@ -73,6 +76,8 @@ type IPCache struct {
 	v6PrefixLengths map[int]int
 
 	listeners []IPIdentityMappingListener
+
+	namedPorts policy.NamedPortsMap
 }
 
 // NewIPCache returns a new IPCache with the mappings of endpoint IP to security
@@ -86,6 +91,7 @@ func NewIPCache() *IPCache {
 		ipToK8sMetadata:   map[string]K8sMetadata{},
 		v4PrefixLengths:   map[int]int{},
 		v6PrefixLengths:   map[int]int{},
+		namedPorts:        nil,
 	}
 }
 
@@ -157,6 +163,38 @@ func (ipc *IPCache) GetK8sMetadata(ip string) *K8sMetadata {
 	return nil
 }
 
+func (ipc *IPCache) updateNamedPorts() (namedPortsChanged bool) {
+	// Collect new named Ports
+	npm := make(policy.NamedPortsMap, len(ipc.namedPorts))
+	for _, km := range ipc.ipToK8sMetadata {
+		for name, value := range km.NamedPorts {
+			if oldValue, done := npm[name]; done {
+				// Do not process duplicate values
+				if oldValue != value {
+					log.Warning("Confilicting named port definition")
+				}
+				continue
+			}
+			if oldValue, ok := ipc.namedPorts[name]; !ok || oldValue != value {
+				namedPortsChanged = true
+			}
+			npm[name] = value
+		}
+	}
+	if len(npm) != len(ipc.namedPorts) {
+		namedPortsChanged = true
+	}
+	if namedPortsChanged {
+		// swap the new map in
+		if len(npm) == 0 {
+			ipc.namedPorts = nil
+		} else {
+			ipc.namedPorts = npm
+		}
+	}
+	return namedPortsChanged
+}
+
 // Upsert adds / updates the provided IP (endpoint or CIDR prefix) and identity
 // into the IPCache.
 //
@@ -166,7 +204,12 @@ func (ipc *IPCache) GetK8sMetadata(ip string) *K8sMetadata {
 // ownership. hostIP is the location of the given IP. It is optional (may be
 // nil) and is propagated to the listeners. k8sMeta contains Kubernetes-specific
 // metadata such as pod namespace and pod name belonging to the IP (may be nil).
-func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8sMetadata, newIdentity Identity) bool {
+func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8sMetadata, newIdentity Identity) (updated bool, namedPortsChanged bool) {
+	var newNamedPorts policy.NamedPortsMap
+	if k8sMeta != nil {
+		newNamedPorts = k8sMeta.NamedPorts
+	}
+
 	scopedLog := log
 	if option.Config.Debug {
 		scopedLog = log.WithFields(logrus.Fields{
@@ -174,6 +217,13 @@ func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8s
 			logfields.Identity: newIdentity,
 			logfields.Key:      hostKey,
 		})
+		if k8sMeta != nil {
+			scopedLog = scopedLog.WithFields(logrus.Fields{
+				logfields.K8sPodName:   k8sMeta.PodName,
+				logfields.K8sNamespace: k8sMeta.Namespace,
+				logfields.NamedPorts:   k8sMeta.NamedPorts,
+			})
+		}
 	}
 
 	ipc.mutex.Lock()
@@ -185,18 +235,19 @@ func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8s
 
 	oldHostIP, oldHostKey := ipc.getHostIPCache(ip)
 	oldK8sMeta := ipc.ipToK8sMetadata[ip]
+	metaEqual := oldK8sMeta.Equal(k8sMeta)
 
 	cachedIdentity, found := ipc.ipToIdentityCache[ip]
 	if found {
 		if !source.AllowOverwrite(cachedIdentity.Source, newIdentity.Source) {
-			return false
+			return false, false
 		}
 
 		// Skip update if IP is already mapped to the given identity
 		// and the host IP hasn't changed.
 		if cachedIdentity == newIdentity && oldHostIP.Equal(hostIP) &&
-			hostKey == oldHostKey && oldK8sMeta.Equal(k8sMeta) {
-			return true
+			hostKey == oldHostKey && metaEqual {
+			return true, false
 		}
 
 		oldIdentity = &cachedIdentity.ID
@@ -242,7 +293,7 @@ func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8s
 			logfields.Identity: newIdentity,
 			logfields.Key:      hostKey,
 		}).Error("Attempt to upsert invalid IP into ipcache layer")
-		return false
+		return false, false
 	}
 
 	scopedLog.Debug("Upserting IP into ipcache layer")
@@ -267,10 +318,34 @@ func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8s
 		ipc.ipToHostIPCache[ip] = IPKeyPair{IP: hostIP, Key: hostKey}
 	}
 
-	if k8sMeta == nil {
-		delete(ipc.ipToK8sMetadata, ip)
-	} else {
-		ipc.ipToK8sMetadata[ip] = *k8sMeta
+	if !metaEqual {
+		if k8sMeta == nil {
+			delete(ipc.ipToK8sMetadata, ip)
+		} else {
+			ipc.ipToK8sMetadata[ip] = *k8sMeta
+		}
+
+		// Update named ports, first check for deleted values
+		for k := range oldK8sMeta.NamedPorts {
+			if _, ok := newNamedPorts[k]; !ok {
+				namedPortsChanged = true
+				break
+			}
+		}
+		if !namedPortsChanged {
+			// Check for added new or changed entries
+			for k, v := range newNamedPorts {
+				if v2, ok := oldK8sMeta.NamedPorts[k]; !ok || v2 != v {
+					namedPortsChanged = true
+					break
+				}
+			}
+		}
+		if namedPortsChanged {
+			// It is possible that some other POD defines same values, check if
+			// anything changes over all the PODs.
+			namedPortsChanged = ipc.updateNamedPorts()
+		}
 	}
 
 	if callbackListeners {
@@ -279,7 +354,7 @@ func (ipc *IPCache) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *K8s
 		}
 	}
 
-	return true
+	return true, namedPortsChanged
 }
 
 // DumpToListenerLocked dumps the entire contents of the IPCache by triggering
@@ -299,7 +374,7 @@ func (ipc *IPCache) DumpToListenerLocked(listener IPIdentityMappingListener) {
 
 // deleteLocked removes the provided IP-to-security-identity mapping
 // from ipc with the assumption that the IPCache's mutex is held.
-func (ipc *IPCache) deleteLocked(ip string, source source.Source) {
+func (ipc *IPCache) deleteLocked(ip string, source source.Source) (namedPortsChanged bool) {
 	scopedLog := log.WithFields(logrus.Fields{
 		logfields.IPAddr: ip,
 	})
@@ -307,13 +382,13 @@ func (ipc *IPCache) deleteLocked(ip string, source source.Source) {
 	cachedIdentity, found := ipc.ipToIdentityCache[ip]
 	if !found {
 		scopedLog.Debug("Attempt to remove non-existing IP from ipcache layer")
-		return
+		return false
 	}
 
 	if cachedIdentity.Source != source {
 		scopedLog.WithField("source", cachedIdentity.Source).
 			Debugf("Skipping delete of identity from source %s", source)
-		return
+		return false
 	}
 
 	var cidr *net.IPNet
@@ -356,7 +431,7 @@ func (ipc *IPCache) deleteLocked(ip string, source source.Source) {
 		}
 	} else {
 		scopedLog.Error("Attempt to delete invalid IP from ipcache layer")
-		return
+		return false
 	}
 
 	scopedLog.Debug("Deleting IP from ipcache layer")
@@ -369,19 +444,37 @@ func (ipc *IPCache) deleteLocked(ip string, source source.Source) {
 	delete(ipc.ipToHostIPCache, ip)
 	delete(ipc.ipToK8sMetadata, ip)
 
+	// Update named ports
+	namedPortsChanged = false
+	if oldK8sMeta != nil && len(oldK8sMeta.NamedPorts) > 0 {
+		namedPortsChanged = ipc.updateNamedPorts()
+	}
+
 	if callbackListeners {
 		for _, listener := range ipc.listeners {
 			listener.OnIPIdentityCacheChange(cacheModification, *cidr, oldHostIP, newHostIP,
 				oldIdentity, newIdentity.ID, encryptKey, oldK8sMeta)
 		}
 	}
+
+	return namedPortsChanged
+}
+
+// GetNamedPorts returns a copy of the named ports map. May return nil.
+func (ipc *IPCache) GetNamedPorts() (npm policy.NamedPortsMap) {
+	ipc.mutex.Lock()
+	// Caller can keep using the map after the lock is released, as the map is never changed
+	// once published.
+	npm = ipc.namedPorts
+	ipc.mutex.Unlock()
+	return npm
 }
 
 // Delete removes the provided IP-to-security-identity mapping from the IPCache.
-func (ipc *IPCache) Delete(IP string, source source.Source) {
+func (ipc *IPCache) Delete(IP string, source source.Source) (namedPortsChanged bool) {
 	ipc.mutex.Lock()
 	defer ipc.mutex.Unlock()
-	ipc.deleteLocked(IP, source)
+	return ipc.deleteLocked(IP, source)
 }
 
 // LookupByIP returns the corresponding security identity that endpoint IP maps
@@ -455,7 +548,14 @@ func (m *K8sMetadata) Equal(o *K8sMetadata) bool {
 	} else if m == nil || o == nil {
 		return false
 	}
-
+	if len(m.NamedPorts) != len(o.NamedPorts) {
+		return false
+	}
+	for k, v := range m.NamedPorts {
+		if v2, ok := o.NamedPorts[k]; !ok || v != v2 {
+			return false
+		}
+	}
 	return m.Namespace == o.Namespace && m.PodName == o.PodName
 }
 
diff --git a/pkg/ipcache/kvstore.go b/pkg/ipcache/kvstore.go
index 9453812cad..d132426f3d 100644
--- a/pkg/ipcache/kvstore.go
+++ b/pkg/ipcache/kvstore.go
@@ -20,6 +20,7 @@ import (
 	"fmt"
 	"net"
 	"path"
+	"sort"
 	"strings"
 	"sync"
 	"time"
@@ -29,7 +30,9 @@ import (
 	"github.com/cilium/cilium/pkg/lock"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/source"
+	"github.com/cilium/cilium/pkg/u8proto"
 
 	"github.com/sirupsen/logrus"
 )
@@ -105,7 +108,20 @@ func newKVReferenceCounter(s store) *kvReferenceCounter {
 // UpsertIPToKVStore updates / inserts the provided IP->Identity mapping into the
 // kvstore, which will subsequently trigger an event in NewIPIdentityWatcher().
 func UpsertIPToKVStore(ctx context.Context, IP, hostIP net.IP, ID identity.NumericIdentity, key uint8,
-	metadata, k8sNamespace, k8sPodName string) error {
+	metadata, k8sNamespace, k8sPodName string, npm policy.NamedPortsMap) error {
+	// Sort named ports into a slice
+	namedPorts := make([]identity.NamedPort, 0, len(npm))
+	for name, value := range npm {
+		namedPorts = append(namedPorts, identity.NamedPort{
+			Name:     name,
+			Port:     value.Port,
+			Protocol: u8proto.U8proto(value.Proto).String(),
+		})
+	}
+	sort.Slice(namedPorts, func(i, j int) bool {
+		return namedPorts[i].Name < namedPorts[j].Name
+	})
+
 	ipKey := path.Join(IPIdentitiesPath, AddressSpace, IP.String())
 	ipIDPair := identity.IPIdentityPair{
 		IP:           IP,
@@ -115,6 +131,7 @@ func UpsertIPToKVStore(ctx context.Context, IP, hostIP net.IP, ID identity.Numer
 		Key:          key,
 		K8sNamespace: k8sNamespace,
 		K8sPodName:   k8sPodName,
+		NamedPorts:   namedPorts,
 	}
 
 	marshaledIPIDPair, err := json.Marshal(ipIDPair)
@@ -270,10 +287,18 @@ restart:
 					continue
 				}
 				var k8sMeta *K8sMetadata
-				if ipIDPair.K8sNamespace != "" || ipIDPair.K8sPodName != "" {
+				if ipIDPair.K8sNamespace != "" || ipIDPair.K8sPodName != "" || len(ipIDPair.NamedPorts) > 0 {
 					k8sMeta = &K8sMetadata{
-						Namespace: ipIDPair.K8sNamespace,
-						PodName:   ipIDPair.K8sPodName,
+						Namespace:  ipIDPair.K8sNamespace,
+						PodName:    ipIDPair.K8sPodName,
+						NamedPorts: make(policy.NamedPortsMap, len(ipIDPair.NamedPorts)),
+					}
+					for _, np := range ipIDPair.NamedPorts {
+						err = k8sMeta.NamedPorts.AddPort(np.Name, int(np.Port), np.Protocol)
+						if err != nil {
+							log.WithFields(logrus.Fields{"kvstore-event": event.Typ.String(), "key": event.Key}).
+								WithError(err).Error("Parsing named port failed")
+						}
 					}
 				}
 
diff --git a/pkg/k8s/apis/cilium.io/v2/client/register.go b/pkg/k8s/apis/cilium.io/v2/client/register.go
index ffa3e571f0..437d7fe243 100644
--- a/pkg/k8s/apis/cilium.io/v2/client/register.go
+++ b/pkg/k8s/apis/cilium.io/v2/client/register.go
@@ -54,7 +54,7 @@ const (
 
 	// CustomResourceDefinitionSchemaVersion is semver-conformant version of CRD schema
 	// Used to determine if CRD needs to be updated in cluster
-	CustomResourceDefinitionSchemaVersion = "1.17"
+	CustomResourceDefinitionSchemaVersion = "1.18"
 
 	// CustomResourceDefinitionSchemaVersionKey is key to label which holds the CRD schema version
 	CustomResourceDefinitionSchemaVersionKey = "io.cilium.k8s.crd.schema.version"
@@ -1132,13 +1132,16 @@ var (
 		},
 		Properties: map[string]apiextensionsv1beta1.JSONSchemaProps{
 			"port": {
-				Description: "Port is an L4 port number. For now the string will be strictly " +
-					"parsed as a single uint16. In the future, this field may support ranges " +
+				Description: "Port is an L4 port number or a port name. For now the string will be strictly " +
+					"parsed as a single uint16 or a valid port name, which consists of at most 15 alphanumeric " +
+					"characters of which at least one is not a number, as well as non-consecutive dashes " +
+					"('-') except for in the beginning or the end. In the future, this field may support ranges " +
 					"in the form \"1024-2048",
 				Type: "string",
-				// uint16 string regex
+				// uint16 or port name string regex
 				Pattern: `^(6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|` +
-					`[1-5][0-9]{4}|[0-9]{1,4})$`,
+					`[1-5][0-9]{4}|[0-9]{1,4}|` +
+					`([a-zA-Z0-9]-?)*[a-zA-Z](-?[a-zA-Z0-9])*)$`,
 			},
 			"protocol": {
 				Description: `Protocol is the L4 protocol. If omitted or empty, any protocol ` +
diff --git a/pkg/k8s/apis/cilium.io/v2/types.go b/pkg/k8s/apis/cilium.io/v2/types.go
index a7122a36e5..98cd18e9cd 100644
--- a/pkg/k8s/apis/cilium.io/v2/types.go
+++ b/pkg/k8s/apis/cilium.io/v2/types.go
@@ -387,6 +387,8 @@ type EndpointStatus struct {
 	// - disconnecting
 	// - disconnected
 	State string `json:"state,omitempty"`
+
+	NamedPorts models.NamedPorts `json:"named-ports,omitempty"`
 }
 
 // EndpointStatusLogEntries is the maximum number of log entries in EndpointStatus.Log
diff --git a/pkg/k8s/factory_functions.go b/pkg/k8s/factory_functions.go
index 84d3ace604..2f7bf17792 100644
--- a/pkg/k8s/factory_functions.go
+++ b/pkg/k8s/factory_functions.go
@@ -572,6 +572,35 @@ func getPodIPs(podStats v1.PodStatus) []string {
 	return ips
 }
 
+func k8sContainerToTypesContainer(k8sContainers []v1.Container) []types.PodContainer {
+	var containers []types.PodContainer
+	for _, c := range k8sContainers {
+		var vmps []string
+		var ports []types.ContainerPort
+		for _, cvm := range c.VolumeMounts {
+			vmps = append(vmps, cvm.MountPath)
+		}
+		for _, port := range c.Ports {
+			cp := types.ContainerPort{
+				Protocol:      string(port.Protocol),
+				ContainerPort: port.ContainerPort,
+				HostPort:      port.HostPort,
+				HostIP:        port.HostIP,
+				Name:          port.Name,
+			}
+			ports = append(ports, cp)
+		}
+		pc := types.PodContainer{
+			Name:              c.Name,
+			Image:             c.Image,
+			VolumeMountsPaths: vmps,
+			ContainerPorts:    ports,
+		}
+		containers = append(containers, pc)
+	}
+	return containers
+}
+
 // ConvertToPod converts a *v1.Pod into a
 // *types.Pod or a cache.DeletedFinalStateUnknown into
 // a cache.DeletedFinalStateUnknown with a *types.Pod in its Obj.
@@ -582,30 +611,8 @@ func getPodIPs(podStats v1.PodStatus) []string {
 func ConvertToPod(obj interface{}) interface{} {
 	switch concreteObj := obj.(type) {
 	case *v1.Pod:
-		var containers []types.PodContainer
-		for _, c := range concreteObj.Spec.Containers {
-			var vmps []string
-			var ports []types.ContainerPort
-			for _, cvm := range c.VolumeMounts {
-				vmps = append(vmps, cvm.MountPath)
-			}
-			for _, port := range c.Ports {
-				cp := types.ContainerPort{
-					Protocol:      string(port.Protocol),
-					ContainerPort: port.ContainerPort,
-					HostPort:      port.HostPort,
-					HostIP:        port.HostIP,
-				}
-				ports = append(ports, cp)
-			}
-			pc := types.PodContainer{
-				Name:              c.Name,
-				Image:             c.Image,
-				VolumeMountsPaths: vmps,
-				HostPorts:         ports,
-			}
-			containers = append(containers, pc)
-		}
+		containers := k8sContainerToTypesContainer(concreteObj.Spec.Containers)
+		containers = append(containers, k8sContainerToTypesContainer(concreteObj.Spec.InitContainers)...)
 		p := &types.Pod{
 			TypeMeta:               concreteObj.TypeMeta,
 			ObjectMeta:             concreteObj.ObjectMeta,
@@ -622,30 +629,8 @@ func ConvertToPod(obj interface{}) interface{} {
 		if !ok {
 			return obj
 		}
-		var containers []types.PodContainer
-		for _, c := range pod.Spec.Containers {
-			var vmps []string
-			var ports []types.ContainerPort
-			for _, cvm := range c.VolumeMounts {
-				vmps = append(vmps, cvm.MountPath)
-			}
-			for _, port := range c.Ports {
-				cp := types.ContainerPort{
-					Protocol:      string(port.Protocol),
-					ContainerPort: port.ContainerPort,
-					HostPort:      port.HostPort,
-					HostIP:        port.HostIP,
-				}
-				ports = append(ports, cp)
-			}
-			pc := types.PodContainer{
-				Name:              c.Name,
-				Image:             c.Image,
-				VolumeMountsPaths: vmps,
-				HostPorts:         ports,
-			}
-			containers = append(containers, pc)
-		}
+		containers := k8sContainerToTypesContainer(pod.Spec.Containers)
+		containers = append(containers, k8sContainerToTypesContainer(pod.Spec.InitContainers)...)
 		dfsu := cache.DeletedFinalStateUnknown{
 			Key: concreteObj.Key,
 			Obj: &types.Pod{
@@ -793,6 +778,7 @@ func ConvertToCiliumEndpoint(obj interface{}) interface{} {
 			Encryption: &concreteObj.Status.Encryption,
 			Identity:   concreteObj.Status.Identity,
 			Networking: concreteObj.Status.Networking,
+			NamedPorts: concreteObj.Status.NamedPorts,
 		}
 		*concreteObj = cilium_v2.CiliumEndpoint{}
 		return p
@@ -809,6 +795,7 @@ func ConvertToCiliumEndpoint(obj interface{}) interface{} {
 				Encryption: &ciliumEndpoint.Status.Encryption,
 				Identity:   ciliumEndpoint.Status.Identity,
 				Networking: ciliumEndpoint.Status.Networking,
+				NamedPorts: ciliumEndpoint.Status.NamedPorts,
 			},
 		}
 		*ciliumEndpoint = cilium_v2.CiliumEndpoint{}
diff --git a/pkg/k8s/labels.go b/pkg/k8s/labels.go
index 6e9e7ea83b..98f90a9de9 100644
--- a/pkg/k8s/labels.go
+++ b/pkg/k8s/labels.go
@@ -95,7 +95,7 @@ func isInjectedWithIstioSidecarProxy(scopedLog *logrus.Entry, pod *types.Pod) bo
 
 // GetPodMetadata returns the labels and annotations of the pod with the given
 // namespace / name.
-func GetPodMetadata(k8sNs *types.Namespace, pod *types.Pod) (lbls map[string]string, retAnno map[string]string, retErr error) {
+func GetPodMetadata(k8sNs *types.Namespace, pod *types.Pod) (containerPorts []types.ContainerPort, lbls map[string]string, retAnno map[string]string, retErr error) {
 	namespace := pod.Namespace
 	scopedLog := log.WithFields(logrus.Fields{
 		logfields.K8sNamespace: namespace,
@@ -130,5 +130,11 @@ func GetPodMetadata(k8sNs *types.Namespace, pod *types.Pod) (lbls map[string]str
 
 	k8sLabels[k8sConst.PolicyLabelCluster] = option.Config.ClusterName
 
-	return k8sLabels, annotations, nil
+	for _, containers := range pod.SpecContainers {
+		for _, cp := range containers.ContainerPorts {
+			containerPorts = append(containerPorts, cp)
+		}
+	}
+
+	return containerPorts, k8sLabels, annotations, nil
 }
diff --git a/pkg/k8s/types/types.go b/pkg/k8s/types/types.go
index 2e71d38960..059d8487e6 100644
--- a/pkg/k8s/types/types.go
+++ b/pkg/k8s/types/types.go
@@ -15,6 +15,7 @@
 package types
 
 import (
+	"github.com/cilium/cilium/api/v1/models"
 	"github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
 
 	"k8s.io/api/core/v1"
@@ -49,6 +50,7 @@ type SlimCNP struct {
 }
 
 type ContainerPort struct {
+	Name          string
 	Protocol      string
 	ContainerPort int32
 	HostPort      int32
@@ -59,7 +61,7 @@ type PodContainer struct {
 	Name              string
 	Image             string
 	VolumeMountsPaths []string
-	HostPorts         []ContainerPort
+	ContainerPorts    []ContainerPort
 }
 
 // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
@@ -117,4 +119,5 @@ type CiliumEndpoint struct {
 	Identity   *v2.EndpointIdentity
 	Networking *v2.EndpointNetworking
 	Encryption *v2.EncryptionSpec
+	NamedPorts models.NamedPorts
 }
diff --git a/pkg/k8s/types/zz_generated.deepcopy.go b/pkg/k8s/types/zz_generated.deepcopy.go
index 694c459868..f5ed3a22af 100644
--- a/pkg/k8s/types/zz_generated.deepcopy.go
+++ b/pkg/k8s/types/zz_generated.deepcopy.go
@@ -314,8 +314,8 @@ func (in *PodContainer) DeepCopyInto(out *PodContainer) {
 		*out = make([]string, len(*in))
 		copy(*out, *in)
 	}
-	if in.HostPorts != nil {
-		in, out := &in.HostPorts, &out.HostPorts
+	if in.ContainerPorts != nil {
+		in, out := &in.ContainerPorts, &out.ContainerPorts
 		*out = make([]ContainerPort, len(*in))
 		copy(*out, *in)
 	}
diff --git a/pkg/k8s/watchers/cilium_endpoint.go b/pkg/k8s/watchers/cilium_endpoint.go
index 3279741946..91e92c8007 100644
--- a/pkg/k8s/watchers/cilium_endpoint.go
+++ b/pkg/k8s/watchers/cilium_endpoint.go
@@ -27,7 +27,9 @@ import (
 	"github.com/cilium/cilium/pkg/k8s/types"
 	"github.com/cilium/cilium/pkg/kvstore"
 	"github.com/cilium/cilium/pkg/node"
+	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/source"
+	"github.com/cilium/cilium/pkg/u8proto"
 
 	v1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/fields"
@@ -50,7 +52,7 @@ func (k *K8sWatcher) ciliumEndpointsInit(ciliumNPClient *k8s.K8sCiliumClient, as
 					defer func() { k.K8sEventReceived(metricCiliumEndpoint, metricCreate, valid, equal) }()
 					if ciliumEndpoint, ok := obj.(*types.CiliumEndpoint); ok {
 						valid = true
-						endpointUpdated(ciliumEndpoint)
+						k.endpointUpdated(ciliumEndpoint)
 						k.K8sEventProcessed(metricCiliumEndpoint, metricCreate, true)
 					}
 				},
@@ -65,7 +67,7 @@ func (k *K8sWatcher) ciliumEndpointsInit(ciliumNPClient *k8s.K8sCiliumClient, as
 							// 	equal = true
 							// 	return
 							// }
-							endpointUpdated(newCE)
+							k.endpointUpdated(newCE)
 							k.K8sEventProcessed(metricCiliumEndpoint, metricUpdate, true)
 						}
 					}
@@ -88,7 +90,7 @@ func (k *K8sWatcher) ciliumEndpointsInit(ciliumNPClient *k8s.K8sCiliumClient, as
 						}
 					}
 					valid = true
-					endpointDeleted(ciliumEndpoint)
+					k.endpointDeleted(ciliumEndpoint)
 				},
 			},
 			k8s.ConvertToCiliumEndpoint,
@@ -121,7 +123,7 @@ func (k *K8sWatcher) ciliumEndpointsInit(ciliumNPClient *k8s.K8sCiliumClient, as
 	}
 }
 
-func endpointUpdated(endpoint *types.CiliumEndpoint) {
+func (k *K8sWatcher) endpointUpdated(endpoint *types.CiliumEndpoint) {
 	// default to the standard key
 	encryptionKey := node.GetIPsecKeyIdentity()
 
@@ -149,34 +151,65 @@ func endpointUpdated(endpoint *types.CiliumEndpoint) {
 		}
 
 		k8sMeta := &ipcache.K8sMetadata{
-			Namespace: endpoint.Namespace,
-			PodName:   endpoint.Name,
+			Namespace:  endpoint.Namespace,
+			PodName:    endpoint.Name,
+			NamedPorts: make(policy.NamedPortsMap, len(endpoint.NamedPorts)),
+		}
+		for _, port := range endpoint.NamedPorts {
+			p, err := u8proto.ParseProtocol(port.Protocol)
+			if err != nil {
+				continue
+			}
+			k8sMeta.NamedPorts[port.Name] = policy.NamedPort{
+				Proto: uint8(p),
+				Port:  uint16(port.Port),
+			}
 		}
 
+		namedPortsChanged := false
 		for _, pair := range endpoint.Networking.Addressing {
 			if pair.IPV4 != "" {
-				ipcache.IPIdentityCache.Upsert(pair.IPV4, nodeIP, encryptionKey, k8sMeta,
+				_, portsChanged := ipcache.IPIdentityCache.Upsert(pair.IPV4, nodeIP, encryptionKey, k8sMeta,
 					ipcache.Identity{ID: id, Source: source.CustomResource})
+				if portsChanged {
+					namedPortsChanged = true
+				}
 			}
 
 			if pair.IPV6 != "" {
-				ipcache.IPIdentityCache.Upsert(pair.IPV6, nodeIP, encryptionKey, k8sMeta,
+				_, portsChanged := ipcache.IPIdentityCache.Upsert(pair.IPV6, nodeIP, encryptionKey, k8sMeta,
 					ipcache.Identity{ID: id, Source: source.CustomResource})
+				if portsChanged {
+					namedPortsChanged = true
+				}
 			}
 		}
+		if namedPortsChanged {
+			k.policyManager.TriggerPolicyUpdates(true, "Named ports added or updated")
+		}
 	}
 }
 
-func endpointDeleted(endpoint *types.CiliumEndpoint) {
+func (k *K8sWatcher) endpointDeleted(endpoint *types.CiliumEndpoint) {
 	if endpoint.Networking != nil {
+		namedPortsChanged := false
 		for _, pair := range endpoint.Networking.Addressing {
 			if pair.IPV4 != "" {
-				ipcache.IPIdentityCache.Delete(pair.IPV4, source.CustomResource)
+				portsChanged := ipcache.IPIdentityCache.Delete(pair.IPV4, source.CustomResource)
+				if portsChanged {
+					namedPortsChanged = true
+				}
 			}
 
 			if pair.IPV6 != "" {
-				ipcache.IPIdentityCache.Delete(pair.IPV6, source.CustomResource)
+				portsChanged := ipcache.IPIdentityCache.Delete(pair.IPV6, source.CustomResource)
+				if portsChanged {
+					namedPortsChanged = true
+				}
 			}
 		}
+		if namedPortsChanged {
+			k.policyManager.TriggerPolicyUpdates(true, "Named ports deleted")
+		}
 	}
 }
diff --git a/pkg/k8s/watchers/pod.go b/pkg/k8s/watchers/pod.go
index 59f7e5bfbd..155cc390e0 100644
--- a/pkg/k8s/watchers/pod.go
+++ b/pkg/k8s/watchers/pod.go
@@ -40,7 +40,9 @@ import (
 	"github.com/cilium/cilium/pkg/node"
 	nodeTypes "github.com/cilium/cilium/pkg/node/types"
 	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/source"
+	"github.com/cilium/cilium/pkg/u8proto"
 
 	"github.com/sirupsen/logrus"
 	v1 "k8s.io/api/core/v1"
@@ -312,7 +314,7 @@ func (k *K8sWatcher) deleteK8sPodV1(pod *types.Pod) error {
 func genServiceMappings(pod *types.Pod) []loadbalancer.SVC {
 	var svcs []loadbalancer.SVC
 	for _, c := range pod.SpecContainers {
-		for _, p := range c.HostPorts {
+		for _, p := range c.ContainerPorts {
 			if p.HostPort == 0 {
 				continue
 			}
@@ -458,28 +460,51 @@ func (k *K8sWatcher) updatePodHostData(pod *types.Pod) (bool, error) {
 		PodName:   pod.Name,
 	}
 
-	var (
-		errs    []string
-		skipped bool
-	)
+	// Store Named ports, if any.
+	for _, container := range pod.SpecContainers {
+		for _, port := range container.ContainerPorts {
+			if port.Name == "" {
+				continue
+			}
+			p, err := u8proto.ParseProtocol(port.Protocol)
+			if err != nil {
+				return true, fmt.Errorf("ContainerPort: invalid protocol: %s", port.Protocol)
+			}
+			if port.ContainerPort < 1 || port.ContainerPort > 65535 {
+				return true, fmt.Errorf("ContainerPort: invalid port: %d", port.ContainerPort)
+			}
+			if k8sMeta.NamedPorts == nil {
+				k8sMeta.NamedPorts = make(policy.NamedPortsMap)
+			}
+			k8sMeta.NamedPorts[port.Name] = policy.NamedPort{
+				Proto: uint8(p),
+				Port:  uint16(port.ContainerPort),
+			}
+		}
+	}
+
+	var errs []string
 	for _, podIP := range pod.StatusPodIPs {
 		// Initial mapping of podIP <-> hostIP <-> identity. The mapping is
 		// later updated once the allocator has determined the real identity.
 		// If the endpoint remains unmanaged, the identity remains untouched.
-		selfOwned := ipcache.IPIdentityCache.Upsert(podIP, hostIP, hostKey, k8sMeta, ipcache.Identity{
+		selfOwned, namedPortsChanged := ipcache.IPIdentityCache.Upsert(podIP, hostIP, hostKey, k8sMeta, ipcache.Identity{
 			ID:     identity.ReservedIdentityUnmanaged,
 			Source: source.Kubernetes,
 		})
+		// This happens at most once due to k8sMeta being the same for all podIPs in this loop
+		if namedPortsChanged {
+			k.policyManager.TriggerPolicyUpdates(true, "Named ports added or updated")
+		}
 		if !selfOwned {
-			skipped = true
 			errs = append(errs, fmt.Sprintf("ipcache entry for podIP %s owned by kvstore or agent", podIP))
 		}
 	}
 	if len(errs) != 0 {
-		return skipped, errors.New(strings.Join(errs, ", "))
+		return true, errors.New(strings.Join(errs, ", "))
 	}
 
-	return skipped, nil
+	return false, nil
 }
 
 func (k *K8sWatcher) deletePodHostData(pod *types.Pod) (bool, error) {
diff --git a/pkg/logging/logfields/logfields.go b/pkg/logging/logfields/logfields.go
index d266fa86c8..00fb0077f2 100644
--- a/pkg/logging/logfields/logfields.go
+++ b/pkg/logging/logfields/logfields.go
@@ -147,6 +147,12 @@ const (
 	// Port is a L4 port
 	Port = "port"
 
+	// PortName is a k8s ContainerPort Name
+	PortName = "portName"
+
+	// NamedPorts is a set of named ports
+	NamedPorts = "namedPorts"
+
 	// Family is the L3 protocol family
 	Family = "family"
 
diff --git a/pkg/node/manager/manager.go b/pkg/node/manager/manager.go
index 68432824f4..48ae726fcf 100644
--- a/pkg/node/manager/manager.go
+++ b/pkg/node/manager/manager.go
@@ -50,8 +50,8 @@ type nodeEntry struct {
 
 // IPCache is the set of interactions the node manager performs with the ipcache
 type IPCache interface {
-	Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *ipcache.K8sMetadata, newIdentity ipcache.Identity) bool
-	Delete(IP string, source source.Source)
+	Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *ipcache.K8sMetadata, newIdentity ipcache.Identity) (bool, bool)
+	Delete(IP string, source source.Source) bool
 }
 
 // Configuration is the set of configuration options the node manager depends
@@ -347,7 +347,7 @@ func (m *Manager) NodeUpdated(n nodeTypes.Node) {
 			continue
 		}
 
-		isOwning := m.ipcache.Upsert(address.IP.String(), tunnelIP, n.EncryptionKey, nil, ipcache.Identity{
+		isOwning, _ := m.ipcache.Upsert(address.IP.String(), tunnelIP, n.EncryptionKey, nil, ipcache.Identity{
 			ID:     remoteHostIdentity,
 			Source: n.Source,
 		})
@@ -365,7 +365,7 @@ func (m *Manager) NodeUpdated(n nodeTypes.Node) {
 		if address == nil {
 			continue
 		}
-		isOwning := m.ipcache.Upsert(address.String(), nodeIP, n.EncryptionKey, nil, ipcache.Identity{
+		isOwning, _ := m.ipcache.Upsert(address.String(), nodeIP, n.EncryptionKey, nil, ipcache.Identity{
 			ID:     identity.ReservedIdentityHealth,
 			Source: n.Source,
 		})
diff --git a/pkg/policy/api/l4.go b/pkg/policy/api/l4.go
index 25dff9103f..0887d97276 100644
--- a/pkg/policy/api/l4.go
+++ b/pkg/policy/api/l4.go
@@ -1,4 +1,4 @@
-// Copyright 2016-2019 Authors of Cilium
+// Copyright 2016-2020 Authors of Cilium
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -32,6 +32,9 @@ type PortProtocol struct {
 	// Port is an L4 port number. For now the string will be strictly
 	// parsed as a single uint16. In the future, this field may support
 	// ranges in the form "1024-2048
+	// Port can also be a port name, which must contain at least one [a-z],
+	// and may also contain [0-9] and '-' anywhere exept adjacent to another
+	// '-' or in the beginning or the end.
 	Port string `json:"port"`
 
 	// Protocol is the L4 protocol. If omitted or empty, any protocol
@@ -39,12 +42,15 @@ type PortProtocol struct {
 	//
 	// Matching on ICMP is not supported.
 	//
+	// Named port specified for a container may narrow this down, but may not
+	// contradict this.
 	// +optional
 	Protocol L4Proto `json:"protocol,omitempty"`
 }
 
 // Covers returns true if the ports and protocol specified in the received
 // PortProtocol are equal to or a superset of the ports and protocol in 'other'.
+// Named ports only cover other named ports with exactly the same name.
 func (p PortProtocol) Covers(other PortProtocol) bool {
 	if p.Port != other.Port {
 		return false
diff --git a/pkg/policy/api/rule_validation.go b/pkg/policy/api/rule_validation.go
index 2beb86a538..bc6352c0f4 100644
--- a/pkg/policy/api/rule_validation.go
+++ b/pkg/policy/api/rule_validation.go
@@ -21,6 +21,7 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/cilium/cilium/pkg/iana"
 	"github.com/cilium/cilium/pkg/labels"
 	"github.com/cilium/cilium/pkg/option"
 )
@@ -409,15 +410,22 @@ func (pp *PortProtocol) sanitize() error {
 		return fmt.Errorf("Port must be specified")
 	}
 
-	p, err := strconv.ParseUint(pp.Port, 0, 16)
-	if err != nil {
-		return fmt.Errorf("Unable to parse port: %s", err)
-	}
-
-	if p == 0 {
-		return fmt.Errorf("Port cannot be 0")
+	// Port names are formatted as IANA Service Names.  This means that
+	// some legal numeric literals are no longer considered numbers, e.g,
+	// 0x10 is now considered a name rather than number 16.
+	if iana.IsSvcName(pp.Port) {
+		pp.Port = strings.ToLower(pp.Port) // Normalize for case insensitive comparison
+	} else {
+		p, err := strconv.ParseUint(pp.Port, 0, 16)
+		if err != nil {
+			return fmt.Errorf("Unable to parse port: %s", err)
+		}
+		if p == 0 {
+			return fmt.Errorf("Port cannot be 0")
+		}
 	}
 
+	var err error
 	pp.Protocol, err = ParseL4Proto(string(pp.Protocol))
 	if err != nil {
 		return err
diff --git a/pkg/policy/distillery.go b/pkg/policy/distillery.go
index 35b18dac11..66eb768f45 100644
--- a/pkg/policy/distillery.go
+++ b/pkg/policy/distillery.go
@@ -29,8 +29,8 @@ import (
 // to compute datapath-level policy configuration.
 type SelectorPolicy interface {
 	// Consume returns the policy in terms of connectivity to peer
-	// Identities. The callee MUST NOT modify the returned pointer.
-	Consume(owner PolicyOwner) *EndpointPolicy
+	// Identities.
+	Consume(owner PolicyOwner, npMap NamedPortsMap) *EndpointPolicy
 }
 
 // PolicyCache represents a cache of resolved policies for identities.
@@ -211,9 +211,9 @@ func (cip *cachedSelectorPolicy) setPolicy(policy *selectorPolicy) {
 //
 // This denotes that a particular endpoint is 'consuming' the policy from the
 // selector policy cache.
-func (cip *cachedSelectorPolicy) Consume(owner PolicyOwner) *EndpointPolicy {
+func (cip *cachedSelectorPolicy) Consume(owner PolicyOwner, npMap NamedPortsMap) *EndpointPolicy {
 	// TODO: This currently computes the EndpointPolicy from SelectorPolicy
 	// on-demand, however in future the cip is intended to cache the
 	// EndpointPolicy for this Identity and emit datapath deltas instead.
-	return cip.getPolicy().DistillPolicy(owner)
+	return cip.getPolicy().DistillPolicy(owner, npMap)
 }
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index b9a919389d..1e612ff0e8 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -21,14 +21,17 @@ import (
 	"reflect"
 	"sort"
 	"strconv"
+	"strings"
 	"sync/atomic"
 	"unsafe"
 
 	"github.com/cilium/cilium/api/v1/models"
+	"github.com/cilium/cilium/pkg/iana"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/labels"
 	"github.com/cilium/cilium/pkg/lock"
 	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy/api"
 	"github.com/cilium/cilium/pkg/policy/trafficdirection"
 	"github.com/cilium/cilium/pkg/u8proto"
@@ -180,7 +183,8 @@ const (
 type L4Filter struct {
 	// Port is the destination port to allow. Port 0 indicates that all traffic
 	// is allowed at L4.
-	Port int `json:"port"`
+	Port     int    `json:"port"`
+	PortName string `json:"port-name,omitempty"`
 	// Protocol is the L4 protocol to allow or NONE
 	Protocol api.L4Proto `json:"protocol"`
 	// U8Proto is the Protocol in numeric format, or 0 for NONE
@@ -240,6 +244,71 @@ func (l4 *L4Filter) GetPort() uint16 {
 	return uint16(l4.Port)
 }
 
+// NamedPort represents a mapping from a port name to the port number and protocol
+type NamedPort struct {
+	Port  uint16 // non-0
+	Proto uint8  // 0 for any
+}
+
+type NamedPortsMap map[string]NamedPort
+
+func ValidatePortName(name string) (string, error) {
+	if !iana.IsSvcName(name) { // Port names are formatted as IANA Service Names
+		return "", fmt.Errorf("Invalid port name \"%s\", not using as a named port", name)
+	}
+	return strings.ToLower(name), nil // Normalize for case-insensitive comparison
+}
+
+func ParsePortProtocol(port int, protocol string) (np NamedPort, err error) {
+	var u8p u8proto.U8proto
+	if protocol == "" {
+		u8p = u8proto.TCP // K8s ContainerPort protocol defaults to TCP
+	} else {
+		var err error
+		u8p, err = u8proto.ParseProtocol(protocol)
+		if err != nil {
+			return np, fmt.Errorf("Invalid protocol \"%s\": %s", protocol, err)
+		}
+	}
+	if port < 1 || port > 65535 {
+		return np, fmt.Errorf("Port number %d out of 16-bit range", port)
+	}
+	return NamedPort{
+		Proto: uint8(u8p),
+		Port:  uint16(port),
+	}, nil
+}
+
+func (npm NamedPortsMap) AddPort(name string, port int, protocol string) error {
+	name, err := ValidatePortName(name)
+	if err != nil {
+		return err
+	}
+	np, err := ParsePortProtocol(port, protocol)
+	if err != nil {
+		return err
+	}
+	npm[name] = np
+	return nil
+}
+
+func (npm NamedPortsMap) GetNamedPort(name string, proto uint8) (port uint16, err error) {
+	if npm == nil {
+		return 0, fmt.Errorf("nil map")
+	}
+	np, ok := npm[name]
+	if !ok {
+		return 0, fmt.Errorf("named port %s not found in %v", name, npm)
+	}
+	if np.Proto != 0 && proto != np.Proto {
+		return 0, fmt.Errorf("incompatible proto")
+	}
+	if np.Port == 0 {
+		return 0, fmt.Errorf("named port has zero value")
+	}
+	return np.Port, nil
+}
+
 // ToMapState converts filter into a MapState with two possible values:
 // - Entry with ProxyPort = 0: No proxy redirection is needed for this key
 // - Entry with any other port #: Proxy redirection is required for this key,
@@ -248,11 +317,32 @@ func (l4 *L4Filter) GetPort() uint16 {
 // Note: It is possible for two selectors to select the same security ID.
 // To give priority for L7 redirection (e.g., for visibility purposes), we use
 // RedirectPreferredInsert() instead of directly inserting the value to the map.
-func (l4 *L4Filter) ToMapState(direction trafficdirection.TrafficDirection) MapState {
+func (l4 *L4Filter) ToMapState(npMap NamedPortsMap, direction trafficdirection.TrafficDirection) MapState {
 	port := uint16(l4.Port)
 	proto := uint8(l4.U8Proto)
 
+	logger := log
+	if option.Config.Debug {
+		logger = log.WithFields(logrus.Fields{
+			logfields.Port:             port,
+			logfields.PortName:         l4.PortName,
+			logfields.Protocol:         proto,
+			logfields.TrafficDirection: direction,
+		})
+	}
+
 	keysToAdd := MapState{}
+
+	// resolve named port
+	if port == 0 && l4.PortName != "" {
+		var err error
+		port, err = npMap.GetNamedPort(l4.PortName, proto)
+		if err != nil {
+			logger.Debugf("ToMapState: Skipping named port: %s", err)
+			return keysToAdd
+		}
+	}
+
 	keyToAdd := Key{
 		Identity:         0,    // Set in the loop below (if not wildcard)
 		DestPort:         port, // NOTE: Port is in host byte-order!
@@ -282,13 +372,7 @@ func (l4 *L4Filter) ToMapState(direction trafficdirection.TrafficDirection) MapS
 		//
 		// have wildcard?        this is a L3L4 key?  not the "no" case?
 		if l4.wildcard != nil && cs != l4.wildcard && !(l7 != nil && wildcardL7Policy == nil) {
-			log.WithFields(logrus.Fields{
-				logfields.EndpointSelector: cs,
-				logfields.PolicyID:         cs.GetSelections(),
-				logfields.Port:             port,
-				logfields.Protocol:         proto,
-				logfields.TrafficDirection: direction,
-			}).Debug("ToMapState: Skipping L3/L4 key due to existing L4-only key")
+			logger.WithField(logfields.EndpointSelector, cs).Debug("ToMapState: Skipping L3/L4 key due to existing L4-only key")
 			continue
 		}
 
@@ -299,27 +383,21 @@ func (l4 *L4Filter) ToMapState(direction trafficdirection.TrafficDirection) MapS
 
 			if port == 0 {
 				// Allow-all
-				log.WithFields(logrus.Fields{
-					logfields.TrafficDirection: direction,
-				}).Debug("ToMapState: allow all")
+				logger.WithField(logfields.EndpointSelector, cs).Debug("ToMapState: allow all")
 			} else {
 				// L4 allow
-				log.WithFields(logrus.Fields{
-					logfields.Port:             port,
-					logfields.Protocol:         proto,
-					logfields.TrafficDirection: direction,
-				}).Debug("ToMapState: L4 allow all")
+				logger.WithField(logfields.EndpointSelector, cs).Debug("ToMapState: L4 allow all")
 			}
 			continue
 		}
 
 		identities := cs.GetSelections()
-		log.WithFields(logrus.Fields{
-			logfields.TrafficDirection: direction,
-			logfields.EndpointSelector: cs,
-			logfields.PolicyID:         identities,
-		}).Debug("ToMapState: Allowed remote IDs")
-
+		if option.Config.Debug {
+			logger.WithFields(logrus.Fields{
+				logfields.EndpointSelector: cs,
+				logfields.PolicyID:         identities,
+			}).Debug("ToMapState: Allowed remote IDs")
+		}
 		for _, id := range identities {
 			keyToAdd.Identity = id.Uint32()
 			keysToAdd.RedirectPreferredInsert(keyToAdd, entry)
@@ -360,8 +438,7 @@ func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, selections
 		if l4.Ingress {
 			direction = trafficdirection.Ingress
 		}
-		l4Policy.AccumulateMapChanges(added, deleted, uint16(l4.Port), uint8(l4.U8Proto), direction,
-			l4.L7RulesPerSelector[selector] != nil, l4.DerivedFromRules)
+		l4Policy.AccumulateMapChanges(added, deleted, l4, direction, l4.L7RulesPerSelector[selector] != nil)
 	}
 }
 
@@ -465,13 +542,21 @@ func createL4Filter(policyCtx PolicyContext, peerEndpoints api.EndpointSelectorS
 	protocol api.L4Proto, ruleLabels labels.LabelArray, ingress bool, fqdns api.FQDNSelectorSlice) (*L4Filter, error) {
 	selectorCache := policyCtx.GetSelectorCache()
 
-	// already validated via PortRule.Validate()
-	p, _ := strconv.ParseUint(port.Port, 0, 16)
-	// already validated via L4Proto.Validate()
+	portName := ""
+	p := uint64(0)
+	if iana.IsSvcName(port.Port) {
+		portName = port.Port
+	} else {
+		// already validated via PortRule.Validate()
+		p, _ = strconv.ParseUint(port.Port, 0, 16)
+	}
+
+	// already validated via L4Proto.Validate(), never "ANY"
 	u8p, _ := u8proto.ParseProtocol(string(protocol))
 
 	l4 := &L4Filter{
-		Port:               int(p),
+		Port:               int(p),   // 0 for L3-only rules and named ports
+		PortName:           portName, // non-"" for named ports
 		Protocol:           protocol,
 		U8Proto:            u8p,
 		L7RulesPerSelector: make(L7DataMap),
@@ -728,15 +813,19 @@ func (l4 L4PolicyMap) containsAllL3L4(labels labels.LabelArray, ports []*models.
 	}
 
 	for _, l4Ctx := range ports {
+		portStr := l4Ctx.Name
+		if !iana.IsSvcName(portStr) {
+			portStr = fmt.Sprintf("%d", l4Ctx.Port)
+		}
 		lwrProtocol := l4Ctx.Protocol
 		switch lwrProtocol {
 		case "", models.PortProtocolANY:
-			tcpPort := fmt.Sprintf("%d/TCP", l4Ctx.Port)
+			tcpPort := fmt.Sprintf("%s/TCP", portStr)
 			tcpFilter, tcpmatch := l4[tcpPort]
 			if tcpmatch {
 				tcpmatch = tcpFilter.matchesLabels(labels)
 			}
-			udpPort := fmt.Sprintf("%d/UDP", l4Ctx.Port)
+			udpPort := fmt.Sprintf("%s/UDP", portStr)
 			udpFilter, udpmatch := l4[udpPort]
 			if udpmatch {
 				udpmatch = udpFilter.matchesLabels(labels)
@@ -745,7 +834,7 @@ func (l4 L4PolicyMap) containsAllL3L4(labels labels.LabelArray, ports []*models.
 				return api.Denied
 			}
 		default:
-			port := fmt.Sprintf("%d/%s", l4Ctx.Port, lwrProtocol)
+			port := fmt.Sprintf("%s/%s", portStr, lwrProtocol)
 			filter, match := l4[port]
 			if !match || !filter.matchesLabels(labels) {
 				return api.Denied
@@ -804,11 +893,33 @@ func (l4 *L4Policy) insertUser(user *EndpointPolicy) {
 //
 // The caller is responsible for making sure the same identity is not
 // present in both 'adds' and 'deletes'.
-func (l4 *L4Policy) AccumulateMapChanges(adds, deletes []identity.NumericIdentity,
-	port uint16, proto uint8, direction trafficdirection.TrafficDirection,
-	redirect bool, derivedFrom labels.LabelArrayList) {
+func (l4 *L4Policy) AccumulateMapChanges(adds, deletes []identity.NumericIdentity, l4Filter *L4Filter,
+	direction trafficdirection.TrafficDirection, redirect bool) {
+	port := uint16(l4Filter.Port)
+	proto := uint8(l4Filter.U8Proto)
+	derivedFrom := l4Filter.DerivedFromRules
+
 	l4.mutex.RLock()
 	for epPolicy := range l4.users {
+		// resolve named port
+		if port == 0 && l4Filter.PortName != "" {
+			var err error
+			port, err = epPolicy.NamedPortsMap.GetNamedPort(l4Filter.PortName, proto)
+			if err != nil {
+				if option.Config.Debug {
+					logger := log.WithFields(logrus.Fields{
+						logfields.Port:             port,
+						logfields.PortName:         l4Filter.PortName,
+						logfields.Protocol:         proto,
+						logfields.TrafficDirection: direction,
+						logfields.EndpointID:       epPolicy.PolicyOwner.GetID(),
+					})
+					logger.WithError(err).Debug("AccumulateMapChanges: Skipping named port")
+				}
+				continue
+			}
+		}
+
 		epPolicy.policyMapChanges.AccumulateMapChanges(adds, deletes, port, proto, direction, redirect, derivedFrom)
 	}
 	l4.mutex.RUnlock()
diff --git a/pkg/policy/policy.go b/pkg/policy/policy.go
index 31c7f105a3..89e2e21648 100644
--- a/pkg/policy/policy.go
+++ b/pkg/policy/policy.go
@@ -90,7 +90,11 @@ func (s *SearchContext) String() string {
 		to = append(to, toLabel.String())
 	}
 	for _, dport := range s.DPorts {
-		dports = append(dports, fmt.Sprintf("%d/%s", dport.Port, dport.Protocol))
+		if dport.Name != "" {
+			dports = append(dports, fmt.Sprintf("%s/%s", dport.Name, dport.Protocol))
+		} else {
+			dports = append(dports, fmt.Sprintf("%d/%s", dport.Port, dport.Protocol))
+		}
 	}
 	ret := fmt.Sprintf("From: [%s]", strings.Join(from, ", "))
 	ret += fmt.Sprintf(" => To: [%s]", strings.Join(to, ", "))
diff --git a/pkg/policy/proxyid.go b/pkg/policy/proxyid.go
index 2334b86a5b..8f4ec8ee99 100644
--- a/pkg/policy/proxyid.go
+++ b/pkg/policy/proxyid.go
@@ -38,8 +38,15 @@ func ProxyIDFromKey(endpointID uint16, key Key) string {
 }
 
 // ProxyIDFromFilter returns a unique string to identify a proxy mapping.
-func ProxyIDFromFilter(endpointID uint16, l4 *L4Filter) string {
-	return ProxyID(endpointID, l4.Ingress, string(l4.Protocol), uint16(l4.Port))
+func ProxyIDFromFilter(endpointID uint16, npMap NamedPortsMap, l4 *L4Filter) (id string, err error) {
+	port := uint16(l4.Port)
+	if port == 0 && l4.PortName != "" {
+		port, err = npMap.GetNamedPort(l4.PortName, uint8(l4.U8Proto))
+		if err != nil {
+			return "", err
+		}
+	}
+	return ProxyID(endpointID, l4.Ingress, string(l4.Protocol), port), nil
 }
 
 // ParseProxyID parses a proxy ID returned by ProxyID and returns its components.
diff --git a/pkg/policy/resolve.go b/pkg/policy/resolve.go
index ce6e5b768e..19b5f9cfab 100644
--- a/pkg/policy/resolve.go
+++ b/pkg/policy/resolve.go
@@ -57,6 +57,10 @@ type EndpointPolicy struct {
 	// referring to a shared selectorPolicy!
 	*selectorPolicy
 
+	// NamedPortsMap maps PortNames in L4Filters to port numbers. This mapping
+	// is endpoint specific. Can be nil!
+	NamedPortsMap NamedPortsMap
+
 	// PolicyMapState contains the state of this policy as it relates to the
 	// datapath. In the future, this will be factored out of this object to
 	// decouple the policy as it relates to the datapath vs. its userspace
@@ -75,7 +79,8 @@ type EndpointPolicy struct {
 
 // PolicyOwner is anything which consumes a EndpointPolicy.
 type PolicyOwner interface {
-	LookupRedirectPort(l4 *L4Filter) uint16
+	GetID() uint64
+	LookupRedirectPortLocked(npMap NamedPortsMap, l4 *L4Filter) uint16
 }
 
 // newSelectorPolicy returns an empty selectorPolicy stub.
@@ -108,9 +113,11 @@ func (p *selectorPolicy) Detach() {
 // SelectorCache. These can subsequently be plumbed into the datapath.
 //
 // Must be performed while holding the Repository lock.
-func (p *selectorPolicy) DistillPolicy(policyOwner PolicyOwner) *EndpointPolicy {
+// PolicyOwner (aka Endpoint) is also locked during this call.
+func (p *selectorPolicy) DistillPolicy(policyOwner PolicyOwner, npMap NamedPortsMap) *EndpointPolicy {
 	calculatedPolicy := &EndpointPolicy{
 		selectorPolicy: p,
+		NamedPortsMap:  npMap,
 		PolicyMapState: make(MapState),
 		PolicyOwner:    policyOwner,
 	}
@@ -154,11 +161,18 @@ func (p *EndpointPolicy) computeDesiredL4PolicyMapEntries() {
 
 func (p *EndpointPolicy) computeDirectionL4PolicyMapEntries(l4PolicyMap L4PolicyMap, direction trafficdirection.TrafficDirection) {
 	for _, filter := range l4PolicyMap {
-		keysFromFilter := filter.ToMapState(direction)
+		lookupDone := false
+		proxyport := uint16(0)
+		keysFromFilter := filter.ToMapState(p.NamedPortsMap, direction)
 		for keyFromFilter, entry := range keysFromFilter {
 			// Fix up the proxy port for entries that need proxy redirection
 			if entry.IsRedirectEntry() {
-				entry.ProxyPort = p.PolicyOwner.LookupRedirectPort(filter)
+				if !lookupDone {
+					// only lookup once for each filter
+					proxyport = p.PolicyOwner.LookupRedirectPortLocked(p.NamedPortsMap, filter)
+					lookupDone = true
+				}
+				entry.ProxyPort = proxyport
 				// If the currently allocated proxy port is 0, this is a new
 				// redirect, for which no port has been allocated yet. Ignore
 				// it for now. This will be configured by
diff --git a/pkg/policy/rule.go b/pkg/policy/rule.go
index b8a5796fe7..49c9f529bb 100644
--- a/pkg/policy/rule.go
+++ b/pkg/policy/rule.go
@@ -297,9 +297,13 @@ func rulePortsCoverSearchContext(ports []api.PortProtocol, ctx *SearchContext) b
 	for _, p := range ports {
 		for _, dp := range ctx.DPorts {
 			tracePort := api.PortProtocol{
-				Port:     fmt.Sprintf("%d", dp.Port),
 				Protocol: api.L4Proto(dp.Protocol),
 			}
+			if dp.Name != "" {
+				tracePort.Port = dp.Name
+			} else {
+				tracePort.Port = fmt.Sprintf("%d", dp.Port)
+			}
 			if p.Covers(tracePort) {
 				return true
 			}
diff --git a/pkg/proxy/logger/epinfo.go b/pkg/proxy/logger/epinfo.go
index cc2e01158f..d7e52dc13a 100644
--- a/pkg/proxy/logger/epinfo.go
+++ b/pkg/proxy/logger/epinfo.go
@@ -41,7 +41,7 @@ type EndpointInfoSource interface {
 	ConntrackNameLocked() string
 	GetIngressPolicyEnabledLocked() bool
 	GetEgressPolicyEnabledLocked() bool
-	ProxyID(l4 *policy.L4Filter) string
+	ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error)
 	GetProxyInfoByFields() (uint64, string, string, []string, string, uint64, error)
 }
 
diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go
index 47a240a3a0..3707932ad2 100644
--- a/pkg/proxy/proxy.go
+++ b/pkg/proxy/proxy.go
@@ -643,8 +643,8 @@ func (p *Proxy) UpdateRedirectMetrics() {
 }
 
 // UpdateNetworkPolicy must update the redirect configuration of an endpoint in the proxy
-func (p *Proxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
-	return p.XDSServer.UpdateNetworkPolicy(ep, policy, ingressPolicyEnforced, egressPolicyEnforced, wg)
+func (p *Proxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
+	return p.XDSServer.UpdateNetworkPolicy(ep, policy, npMap, ingressPolicyEnforced, egressPolicyEnforced, wg)
 }
 
 // UseCurrentNetworkPolicy inserts a Completion to the WaitGroup if the current network policy has not yet been acked


Test patch:
diff --git a/pkg/endpoint/endpoint_test.go b/pkg/endpoint/endpoint_test.go
index 53777838f8..72990171aa 100644
--- a/pkg/endpoint/endpoint_test.go
+++ b/pkg/endpoint/endpoint_test.go
@@ -444,7 +444,8 @@ func (s *EndpointSuite) TestWaitForPolicyRevision(c *C) {
 func (s *EndpointSuite) TestProxyID(c *C) {
 	e := &Endpoint{ID: 123, policyRevision: 0}
 
-	id := e.ProxyID(&policy.L4Filter{Port: 8080, Protocol: api.ProtoTCP, Ingress: true})
+	id, err := e.ProxyID(nil, &policy.L4Filter{Port: 8080, Protocol: api.ProtoTCP, Ingress: true})
+	c.Assert(err, IsNil)
 	endpointID, ingress, protocol, port, err := policy.ParseProxyID(id)
 	c.Assert(endpointID, Equals, uint16(123))
 	c.Assert(ingress, Equals, true)
diff --git a/pkg/endpoint/redirect_test.go b/pkg/endpoint/redirect_test.go
index 9cf19e64b5..9fde0f947e 100644
--- a/pkg/endpoint/redirect_test.go
+++ b/pkg/endpoint/redirect_test.go
@@ -63,7 +63,7 @@ func (r *RedirectSuiteProxy) RemoveRedirect(id string, wg *completion.WaitGroup)
 }
 
 // UpdateNetworkPolicy does nothing.
-func (r *RedirectSuiteProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
+func (r *RedirectSuiteProxy) UpdateNetworkPolicy(ep logger.EndpointUpdater, policy *policy.L4Policy, npMap policy.NamedPortsMap, ingressPolicyEnforced, egressPolicyEnforced bool, wg *completion.WaitGroup) (error, func() error) {
 	return nil, nil
 }
 
diff --git a/pkg/envoy/server_test.go b/pkg/envoy/server_test.go
index c6e7871083..1459bf15d6 100644
--- a/pkg/envoy/server_test.go
+++ b/pkg/envoy/server_test.go
@@ -490,28 +490,28 @@ func (s *ServerSuite) TestGetPortNetworkPolicyRule(c *C) {
 
 func (s *ServerSuite) TestGetDirectionNetworkPolicy(c *C) {
 	// L4+L7
-	obtained := getDirectionNetworkPolicy(L4PolicyMap1, true)
+	obtained := getDirectionNetworkPolicy(L4PolicyMap1, nil, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies1)
 
 	// L4+L7 with header mods
-	obtained = getDirectionNetworkPolicy(L4PolicyMap1HeaderMatch, true)
+	obtained = getDirectionNetworkPolicy(L4PolicyMap1HeaderMatch, nil, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies1HeaderMatch)
 
 	// L4+L7
-	obtained = getDirectionNetworkPolicy(L4PolicyMap2, true)
+	obtained = getDirectionNetworkPolicy(L4PolicyMap2, nil, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies2)
 
 	// L4-only
-	obtained = getDirectionNetworkPolicy(L4PolicyMap4, true)
+	obtained = getDirectionNetworkPolicy(L4PolicyMap4, nil, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies6)
 
 	// L4-only
-	obtained = getDirectionNetworkPolicy(L4PolicyMap5, true)
+	obtained = getDirectionNetworkPolicy(L4PolicyMap5, nil, true)
 	c.Assert(obtained, checker.Equals, ExpectedPerPortPolicies7)
 }
 
 func (s *ServerSuite) TestGetNetworkPolicy(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy1, true, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy1, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -522,7 +522,7 @@ func (s *ServerSuite) TestGetNetworkPolicy(c *C) {
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyWildcard(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2, true, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -533,7 +533,7 @@ func (s *ServerSuite) TestGetNetworkPolicyWildcard(c *C) {
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyDeny(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy1RequiresV2, true, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy1RequiresV2, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -544,7 +544,7 @@ func (s *ServerSuite) TestGetNetworkPolicyDeny(c *C) {
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyWildcardDeny(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2RequiresV2, true, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2RequiresV2, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -555,7 +555,7 @@ func (s *ServerSuite) TestGetNetworkPolicyWildcardDeny(c *C) {
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyNil(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", nil, true, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", nil, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -566,7 +566,7 @@ func (s *ServerSuite) TestGetNetworkPolicyNil(c *C) {
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyIngressNotEnforced(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2, false, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2, nil, false, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -577,7 +577,7 @@ func (s *ServerSuite) TestGetNetworkPolicyIngressNotEnforced(c *C) {
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyEgressNotEnforced(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2RequiresV2, true, false)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4Policy2RequiresV2, nil, true, false)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
@@ -634,7 +634,7 @@ var ExpectedPerPortPoliciesL7 = []*cilium.PortNetworkPolicy{
 }
 
 func (s *ServerSuite) TestGetNetworkPolicyL7(c *C) {
-	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4PolicyL7, true, true)
+	obtained := getNetworkPolicy(IPv4Addr, Identity, "", L4PolicyL7, nil, true, true)
 	expected := &cilium.NetworkPolicy{
 		Name:                   IPv4Addr,
 		Policy:                 uint64(Identity),
diff --git a/pkg/iana/svcname_test.go b/pkg/iana/svcname_test.go
new file mode 100644
index 0000000000..76d4aa5703
--- /dev/null
+++ b/pkg/iana/svcname_test.go
@@ -0,0 +1,42 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build !privileged_tests
+
+package iana
+
+import (
+	. "gopkg.in/check.v1"
+)
+
+type IANATestSuite struct{}
+
+var _ = Suite(&IANATestSuite{})
+
+func (s *IANATestSuite) TestIsSvcName(c *C) {
+	c.Assert(IsSvcName(""), Equals, false)                 // Too short
+	c.Assert(IsSvcName("1234567890abcdef"), Equals, false) // Too long
+	c.Assert(IsSvcName("1"), Equals, false)                // Missing letter
+	c.Assert(IsSvcName("1a"), Equals, true)
+	c.Assert(IsSvcName("Z"), Equals, true)
+	c.Assert(IsSvcName("a9"), Equals, true)
+	c.Assert(IsSvcName("a-9"), Equals, true)
+	c.Assert(IsSvcName("a--9"), Equals, false) // Two consecutive dashes
+	c.Assert(IsSvcName("-a9"), Equals, false)  // Begins with a dash
+	c.Assert(IsSvcName("a9-"), Equals, false)  // Ends with a dash
+	c.Assert(IsSvcName("a-b9-1"), Equals, true)
+	c.Assert(IsSvcName("1-a-9"), Equals, true)
+	c.Assert(IsSvcName("a-b-c-d-e-f"), Equals, true)
+	c.Assert(IsSvcName("1-2-3-4"), Equals, false) // No letter(s)
+}
diff --git a/pkg/ipcache/ipcache_test.go b/pkg/ipcache/ipcache_test.go
index b354731732..8767ccb863 100644
--- a/pkg/ipcache/ipcache_test.go
+++ b/pkg/ipcache/ipcache_test.go
@@ -17,13 +17,16 @@
 package ipcache
 
 import (
+	"fmt"
 	"net"
 	"reflect"
 	"testing"
 
 	"github.com/cilium/cilium/pkg/checker"
 	identityPkg "github.com/cilium/cilium/pkg/identity"
+	"github.com/cilium/cilium/pkg/policy"
 	"github.com/cilium/cilium/pkg/source"
+	"github.com/cilium/cilium/pkg/u8proto"
 
 	. "gopkg.in/check.v1"
 )
@@ -63,7 +66,7 @@ func (s *IPCacheTestSuite) TestIPCache(c *C) {
 	c.Assert(cachedIdentity.Source, Equals, source.KVStore)
 
 	// kubernetes source cannot update kvstore source
-	updated := IPIdentityCache.Upsert(endpointIP, nil, 0, nil, Identity{
+	updated, _ := IPIdentityCache.Upsert(endpointIP, nil, 0, nil, Identity{
 		ID:     identity,
 		Source: source.Kubernetes,
 	})
@@ -258,3 +261,265 @@ func (s *IPCacheTestSuite) TestKeyToIPNet(c *C) {
 	c.Assert(err, Not(IsNil))
 	c.Assert(isHost, Equals, false)
 }
+
+func (s *IPCacheTestSuite) TestIPCacheNamedPorts(c *C) {
+	endpointIP := "10.0.0.15"
+	identity := (identityPkg.NumericIdentity(68))
+
+	// Assure sane state at start.
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 0)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 0)
+
+	// Deletion of key that doesn't exist doesn't cause panic.
+	namedPortsChanged := IPIdentityCache.Delete(endpointIP, source.KVStore)
+	c.Assert(namedPortsChanged, Equals, false)
+
+	meta := K8sMetadata{
+		Namespace: "default",
+		PodName:   "app1",
+		NamedPorts: policy.NamedPortsMap{
+			"http": policy.NamedPort{Port: 80, Proto: uint8(u8proto.TCP)},
+			"dns":  policy.NamedPort{Port: 53},
+		},
+	}
+
+	updated, namedPortsChanged := IPIdentityCache.Upsert(endpointIP, nil, 0, &meta, Identity{
+		ID:     identity,
+		Source: source.Kubernetes,
+	})
+	c.Assert(updated, Equals, true)
+
+	// Assure both caches are updated..
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 0)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 1)
+
+	cachedIdentity, exists := IPIdentityCache.LookupByIP(endpointIP)
+	c.Assert(exists, Equals, true)
+	c.Assert(cachedIdentity.ID, Equals, identity)
+	c.Assert(cachedIdentity.Source, Equals, source.Kubernetes)
+
+	// Named ports have been updated
+	c.Assert(namedPortsChanged, Equals, true)
+	npm := IPIdentityCache.GetNamedPorts()
+	c.Assert(npm, NotNil)
+	c.Assert(len(npm), Equals, 2)
+	c.Assert(npm["http"].Port, Equals, uint16(80))
+	c.Assert(npm["http"].Proto, Equals, uint8(6)) // TCP
+	c.Assert(npm["dns"].Port, Equals, uint16(53))
+	c.Assert(npm["dns"].Proto, Equals, uint8(0)) // ANY
+
+	// No duplicates.
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 0)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 1)
+
+	cachedIdentity, exists = IPIdentityCache.LookupByIP(endpointIP)
+	c.Assert(exists, Equals, true)
+	c.Assert(cachedIdentity.ID, Equals, identity)
+	c.Assert(cachedIdentity.Source, Equals, source.Kubernetes)
+
+	// 2nd identity
+	endpointIP2 := "10.0.0.16"
+	identity2 := (identityPkg.NumericIdentity(70))
+
+	meta2 := K8sMetadata{
+		Namespace: "testing",
+		PodName:   "app2",
+		NamedPorts: policy.NamedPortsMap{
+			"https": policy.NamedPort{Port: 443, Proto: uint8(u8proto.TCP)},
+			"dns":   policy.NamedPort{Port: 53},
+		},
+	}
+
+	updated, namedPortsChanged = IPIdentityCache.Upsert(endpointIP2, nil, 0, &meta2, Identity{
+		ID:     identity2,
+		Source: source.Kubernetes,
+	})
+	c.Assert(updated, Equals, true)
+
+	// Assure both caches are updated..
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 2)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 2)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 0)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 2)
+
+	cachedIdentity, exists = IPIdentityCache.LookupByIP(endpointIP2)
+	c.Assert(exists, Equals, true)
+	c.Assert(cachedIdentity.ID, Equals, identity2)
+	c.Assert(cachedIdentity.Source, Equals, source.Kubernetes)
+
+	// Named ports have been updated
+	c.Assert(namedPortsChanged, Equals, true)
+	npm = IPIdentityCache.GetNamedPorts()
+	c.Assert(npm, NotNil)
+	c.Assert(len(npm), Equals, 3)
+	c.Assert(npm["http"].Port, Equals, uint16(80))
+	c.Assert(npm["http"].Proto, Equals, uint8(6)) // TCP
+	c.Assert(npm["dns"].Port, Equals, uint16(53))
+	c.Assert(npm["dns"].Proto, Equals, uint8(0)) // ANY
+	c.Assert(npm["https"].Port, Equals, uint16(443))
+	c.Assert(npm["https"].Proto, Equals, uint8(6)) // TCP
+
+	namedPortsChanged = IPIdentityCache.Delete(endpointIP, source.Kubernetes)
+	c.Assert(namedPortsChanged, Equals, true)
+	npm = IPIdentityCache.GetNamedPorts()
+	c.Assert(npm, NotNil)
+	c.Assert(len(npm), Equals, 2)
+	c.Assert(npm["dns"].Port, Equals, uint16(53))
+	c.Assert(npm["dns"].Proto, Equals, uint8(0)) // ANY
+	c.Assert(npm["https"].Port, Equals, uint16(443))
+	c.Assert(npm["https"].Proto, Equals, uint8(6)) // TCP
+
+	// Assure deletion occurs across all mappings.
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 0)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 1)
+
+	_, exists = IPIdentityCache.LookupByIP(endpointIP)
+
+	c.Assert(exists, Equals, false)
+
+	hostIP := net.ParseIP("192.168.1.10")
+	k8sMeta := &K8sMetadata{
+		Namespace: "default",
+		PodName:   "podname",
+	}
+
+	updated, namedPortsChanged = IPIdentityCache.Upsert(endpointIP, hostIP, 0, k8sMeta, Identity{
+		ID:     identity,
+		Source: source.KVStore,
+	})
+	c.Assert(updated, Equals, true)
+	c.Assert(namedPortsChanged, Equals, false)
+
+	// Assure upsert occurs across all mappings.
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 2)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 2)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 2)
+
+	cachedHostIP, _ := IPIdentityCache.getHostIPCache(endpointIP)
+	c.Assert(cachedHostIP, checker.DeepEquals, hostIP)
+	c.Assert(IPIdentityCache.GetK8sMetadata(endpointIP), checker.DeepEquals, k8sMeta)
+
+	newIdentity := identityPkg.NumericIdentity(69)
+	updated, namedPortsChanged = IPIdentityCache.Upsert(endpointIP, hostIP, 0, k8sMeta, Identity{
+		ID:     newIdentity,
+		Source: source.KVStore,
+	})
+	c.Assert(updated, Equals, true)
+	c.Assert(namedPortsChanged, Equals, false)
+
+	// Assure upsert occurs across all mappings.
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 2)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 2)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 2)
+
+	// Ensure that update of cache with new identity doesn't keep old identity-to-ip
+	// mapping around.
+	_, exists = IPIdentityCache.LookupByIdentity(identity)
+	c.Assert(exists, Equals, false)
+
+	cachedIPSet, exists := IPIdentityCache.LookupByIdentity(newIdentity)
+	c.Assert(exists, Equals, true)
+	for cachedIP := range cachedIPSet {
+		c.Assert(cachedIP, Equals, endpointIP)
+	}
+
+	namedPortsChanged = IPIdentityCache.Delete(endpointIP, source.KVStore)
+	// Deleted identity did not have any named ports, so no change
+	c.Assert(namedPortsChanged, Equals, false)
+
+	// Assure deletion occurs across both mappings.
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.ipToHostIPCache), Equals, 0)
+	c.Assert(len(IPIdentityCache.ipToK8sMetadata), Equals, 1)
+
+	// Test mapping of multiple IPs to same identity.
+	endpointIPs := []string{"192.168.0.1", "20.3.75.3", "27.2.2.2", "127.0.0.1", "127.0.0.1"}
+	identities := []identityPkg.NumericIdentity{5, 67, 29, 29, 29}
+	k8sMeta.NamedPorts = policy.NamedPortsMap{
+		"http2": policy.NamedPort{Port: 8080, Proto: uint8(u8proto.TCP)},
+	}
+
+	for index := range endpointIPs {
+		k8sMeta.PodName = fmt.Sprintf("pod-%d", int(identities[index]))
+		updated, namedPortsChanged = IPIdentityCache.Upsert(endpointIPs[index], nil, 0, k8sMeta, Identity{
+			ID:     identities[index],
+			Source: source.KVStore,
+		})
+		c.Assert(updated, Equals, true)
+		npm = IPIdentityCache.GetNamedPorts()
+		c.Assert(npm, NotNil)
+		c.Assert(npm["http2"].Port, Equals, uint16(8080))
+		// only the first changes named ports, as they are all the same
+		c.Assert(namedPortsChanged, Equals, index == 0)
+		cachedIdentity, _ := IPIdentityCache.LookupByIP(endpointIPs[index])
+		c.Assert(cachedIdentity.ID, Equals, identities[index])
+	}
+
+	expectedIPList := map[string]struct{}{
+		"27.2.2.2":  {},
+		"127.0.0.1": {},
+	}
+
+	cachedEndpointIPs, _ := IPIdentityCache.LookupByIdentity(29)
+	c.Assert(reflect.DeepEqual(cachedEndpointIPs, expectedIPList), Equals, true)
+
+	namedPortsChanged = IPIdentityCache.Delete("27.2.2.2", source.KVStore)
+	c.Assert(namedPortsChanged, Equals, false)
+
+	expectedIPList = map[string]struct{}{
+		"127.0.0.1": {},
+	}
+
+	cachedEndpointIPs, exists = IPIdentityCache.LookupByIdentity(29)
+	c.Assert(exists, Equals, true)
+	c.Assert(reflect.DeepEqual(cachedEndpointIPs, expectedIPList), Equals, true)
+
+	cachedIdentity, exists = IPIdentityCache.LookupByIP("127.0.0.1")
+	c.Assert(exists, Equals, true)
+	c.Assert(cachedIdentity.ID, Equals, identityPkg.NumericIdentity(29))
+
+	cachedIdentity, exists = IPIdentityCache.LookupByPrefix("127.0.0.1/32")
+	c.Assert(exists, Equals, true)
+	c.Assert(cachedIdentity.ID, Equals, identityPkg.NumericIdentity(29))
+
+	IPIdentityCache.Delete("127.0.0.1", source.KVStore)
+
+	_, exists = IPIdentityCache.LookupByIdentity(29)
+	c.Assert(exists, Equals, false)
+
+	_, exists = IPIdentityCache.LookupByPrefix("127.0.0.1/32")
+	c.Assert(exists, Equals, false)
+
+	// Clean up.
+	for index := range endpointIPs {
+		namedPortsChanged = IPIdentityCache.Delete(endpointIPs[index], source.KVStore)
+		npm = IPIdentityCache.GetNamedPorts()
+		c.Assert(npm, NotNil)
+		log.Infof("Named ports after Delete %d: %v", index, npm)
+		// 2nd delete removes named port mapping, as remaining IPs have an already deleted ID (29)
+		c.Assert(namedPortsChanged, Equals, index == 1)
+
+		_, exists = IPIdentityCache.LookupByIP(endpointIPs[index])
+		c.Assert(exists, Equals, false)
+
+		_, exists = IPIdentityCache.LookupByIdentity(identities[index])
+		c.Assert(exists, Equals, false)
+	}
+
+	c.Assert(len(IPIdentityCache.ipToIdentityCache), Equals, 1)
+	c.Assert(len(IPIdentityCache.identityToIPCache), Equals, 1)
+
+	namedPortsChanged = IPIdentityCache.Delete(endpointIP2, source.Kubernetes)
+	c.Assert(namedPortsChanged, Equals, true)
+	npm = IPIdentityCache.GetNamedPorts()
+	c.Assert(npm, IsNil)
+}
diff --git a/pkg/k8s/network_policy_test.go b/pkg/k8s/network_policy_test.go
index 3ebf3f3186..136c085a02 100644
--- a/pkg/k8s/network_policy_test.go
+++ b/pkg/k8s/network_policy_test.go
@@ -651,7 +651,31 @@ func (s *K8sSuite) TestParseNetworkPolicyIngressL4AllowAll(c *C) {
 	c.Assert(repo.AllowsIngressRLocked(&ctxAToC90), Equals, api.Denied)
 }
 
+func (s *K8sSuite) TestParseNetworkPolicyNamedPort(c *C) {
+	netPolicy := &networkingv1.NetworkPolicy{
+		Spec: networkingv1.NetworkPolicySpec{
+			Ingress: []networkingv1.NetworkPolicyIngressRule{
+				{
+					Ports: []networkingv1.NetworkPolicyPort{
+						{
+							Port: &intstr.IntOrString{
+								Type:   intstr.String,
+								StrVal: "port-80",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	rules, err := ParseNetworkPolicy(netPolicy)
+	c.Assert(err, IsNil)
+	c.Assert(len(rules), Equals, 1)
+}
+
 func (s *K8sSuite) TestParseNetworkPolicyUnknownProto(c *C) {
+	unknownProtocol := v1.Protocol("unknown")
 	netPolicy := &networkingv1.NetworkPolicy{
 		Spec: networkingv1.NetworkPolicySpec{
 			Ingress: []networkingv1.NetworkPolicyIngressRule{
@@ -660,8 +684,9 @@ func (s *K8sSuite) TestParseNetworkPolicyUnknownProto(c *C) {
 						{
 							Port: &intstr.IntOrString{
 								Type:   intstr.String,
-								StrVal: "unknown",
+								StrVal: "port-80",
 							},
+							Protocol: &unknownProtocol,
 						},
 					},
 				},
diff --git a/pkg/node/manager/manager_test.go b/pkg/node/manager/manager_test.go
index 04033016e5..2906b17a5f 100644
--- a/pkg/node/manager/manager_test.go
+++ b/pkg/node/manager/manager_test.go
@@ -70,13 +70,14 @@ func newIPcacheMock() *ipcacheMock {
 	}
 }
 
-func (i *ipcacheMock) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *ipcache.K8sMetadata, newIdentity ipcache.Identity) bool {
+func (i *ipcacheMock) Upsert(ip string, hostIP net.IP, hostKey uint8, k8sMeta *ipcache.K8sMetadata, newIdentity ipcache.Identity) (bool, bool) {
 	i.events <- nodeEvent{"upsert", net.ParseIP(ip)}
-	return true
+	return true, false
 }
 
-func (i *ipcacheMock) Delete(IP string, source source.Source) {
+func (i *ipcacheMock) Delete(IP string, source source.Source) bool {
 	i.events <- nodeEvent{"delete", net.ParseIP(IP)}
+	return false
 }
 
 type signalNodeHandler struct {
diff --git a/pkg/policy/api/ingress_test.go b/pkg/policy/api/ingress_test.go
index dfa57c154f..bbb0f1a658 100644
--- a/pkg/policy/api/ingress_test.go
+++ b/pkg/policy/api/ingress_test.go
@@ -155,6 +155,30 @@ func (s *PolicyAPITestSuite) TestIsLabelBasedIngress(c *C) {
 				}
 			},
 		},
+		{
+			name: "rule-with-named-port",
+			setupArgs: func() args {
+				return args{
+					&IngressRule{
+						ToPorts: []PortRule{
+							{
+								Ports: []PortProtocol{
+									{
+										Port:     "port-80",
+										Protocol: ProtoTCP,
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			setupWanted: func() wanted {
+				return wanted{
+					isLabelBased: true,
+				}
+			},
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/pkg/policy/distillery_test.go b/pkg/policy/distillery_test.go
index a9b8888b52..9214aba4dc 100644
--- a/pkg/policy/distillery_test.go
+++ b/pkg/policy/distillery_test.go
@@ -43,32 +43,10 @@ type DistilleryTestSuite struct{}
 var (
 	_ = Suite(&DistilleryTestSuite{})
 
-	ep1 = newTestEP()
-	ep2 = newTestEP()
+	ep1 = testutils.NewTestEndpoint()
+	ep2 = testutils.NewTestEndpoint()
 )
 
-// testEP wraps the testutils endpoint implementation to provide
-// LookupRedirectPort() until tproxy support makes this redundant.
-// This avoids import cycles when adding policy to the imports in testutils.
-type testEP struct {
-	testutils.TestEndpoint
-}
-
-func newTestEP() *testEP {
-	return &testEP{
-		testutils.NewTestEndpoint(),
-	}
-}
-
-func (ep *testEP) WithIdentity(id int64) *testEP {
-	ep.SetIdentity(id, true)
-	return ep
-}
-
-func (ep *testEP) LookupRedirectPort(l4 *L4Filter) uint16 {
-	return 42
-}
-
 func (s *DistilleryTestSuite) TestCacheManagement(c *C) {
 	repo := NewPolicyRepository(nil, nil)
 	cache := repo.policyCache
@@ -93,7 +71,8 @@ func (s *DistilleryTestSuite) TestCacheManagement(c *C) {
 
 	// Insert two distinct identities, then delete one. Other should still
 	// be there.
-	ep3 := newTestEP().WithIdentity(1234)
+	ep3 := testutils.NewTestEndpoint()
+	ep3.SetIdentity(1234, true)
 	identity3 := ep3.GetSecurityIdentity()
 	c.Assert(identity3, Not(Equals), identity)
 	policy1, _ = cache.insert(identity)
@@ -134,7 +113,8 @@ func (s *DistilleryTestSuite) TestCachePopulation(c *C) {
 	c.Assert(err, NotNil)
 
 	// Attempt to update policy for non-cached endpoint and observe failure
-	ep3 := newTestEP().WithIdentity(1234)
+	ep3 := testutils.NewTestEndpoint()
+	ep3.SetIdentity(1234, true)
 	_, err = cache.updateSelectorPolicy(ep3.GetSecurityIdentity())
 	c.Assert(err, NotNil)
 	c.Assert(updated, Equals, false)
@@ -187,6 +167,11 @@ var (
 			{Port: "80", Protocol: api.ProtoTCP},
 		},
 	}}
+	allowNamedPort80 = []api.PortRule{{
+		Ports: []api.PortProtocol{
+			{Port: "port-80", Protocol: api.ProtoTCP},
+		},
+	}}
 	// L7 rule sections
 	allowHTTPRoot = &api.L7Rules{
 		HTTP: []api.PortRuleHTTP{
@@ -213,6 +198,12 @@ var (
 			FromEndpoints: []api.EndpointSelector{allowFooL3_},
 			ToPorts:       allowPort80,
 		}})
+	ruleL3npL4__Allow = api.NewRule().
+				WithLabels(lblsL3L4__Allow).
+				WithIngressRules([]api.IngressRule{{
+			FromEndpoints: []api.EndpointSelector{allowFooL3_},
+			ToPorts:       allowNamedPort80,
+		}})
 	lblsL3L4L7Allow = labels.ParseLabelArray("l3l4l7-allow")
 	ruleL3L4L7Allow = api.NewRule().
 			WithLabels(lblsL3L4L7Allow).
@@ -220,18 +211,34 @@ var (
 			FromEndpoints: []api.EndpointSelector{allowFooL3_},
 			ToPorts:       combineL4L7(allowPort80, allowHTTPRoot),
 		}})
+	ruleL3npL4L7Allow = api.NewRule().
+				WithLabels(lblsL3L4L7Allow).
+				WithIngressRules([]api.IngressRule{{
+			FromEndpoints: []api.EndpointSelector{allowFooL3_},
+			ToPorts:       combineL4L7(allowNamedPort80, allowHTTPRoot),
+		}})
 	lbls__L4__Allow = labels.ParseLabelArray("l4-allow")
 	rule__L4__Allow = api.NewRule().
 			WithLabels(lbls__L4__Allow).
 			WithIngressRules([]api.IngressRule{{
 			ToPorts: allowPort80,
 		}})
+	rule__npL4__Allow = api.NewRule().
+				WithLabels(lbls__L4__Allow).
+				WithIngressRules([]api.IngressRule{{
+			ToPorts: allowNamedPort80,
+		}})
 	lbls__L4L7Allow = labels.ParseLabelArray("l4l7-allow")
 	rule__L4L7Allow = api.NewRule().
 			WithLabels(lbls__L4L7Allow).
 			WithIngressRules([]api.IngressRule{{
 			ToPorts: combineL4L7(allowPort80, allowHTTPRoot),
 		}})
+	rule__npL4L7Allow = api.NewRule().
+				WithLabels(lbls__L4L7Allow).
+				WithIngressRules([]api.IngressRule{{
+			ToPorts: combineL4L7(allowNamedPort80, allowHTTPRoot),
+		}})
 	lbls__L3AllowFoo = labels.ParseLabelArray("l3-allow-foo")
 	rule__L3AllowFoo = api.NewRule().
 				WithLabels(lbls__L3AllowFoo).
@@ -310,7 +317,7 @@ func (d *policyDistillery) WithLogBuffer(w io.Writer) *policyDistillery {
 
 // distillPolicy distills the policy repository into a set of bpf map state
 // entries for an endpoint with the specified labels.
-func (d *policyDistillery) distillPolicy(epLabels labels.LabelArray) (MapState, error) {
+func (d *policyDistillery) distillPolicy(npMap NamedPortsMap, epLabels labels.LabelArray) (MapState, error) {
 	result := make(MapState)
 
 	endpointSelected, _ := d.Repository.GetRulesMatching(epLabels)
@@ -338,7 +345,7 @@ func (d *policyDistillery) distillPolicy(epLabels labels.LabelArray) (MapState,
 	io.WriteString(d.log, "[distill] Producing L4 filter keys\n")
 	for _, l4 := range l4IngressPolicy {
 		io.WriteString(d.log, fmt.Sprintf("[distill] Processing L4Filter (l4: %d/%s), (l3/7: %+v)\n", l4.Port, l4.Protocol, l4.L7RulesPerSelector))
-		for key, entry := range l4.ToMapState(0) {
+		for key, entry := range l4.ToMapState(npMap, 0) {
 			io.WriteString(d.log, fmt.Sprintf("[distill] L4 ingress allow %+v (parser=%s, redirect=%t)\n", key, l4.L7Parser, entry.IsRedirectEntry()))
 			result[key] = entry
 		}
@@ -374,7 +381,7 @@ func Test_MergeL3(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(labelsFoo)
+			mapstate, err := repo.distillPolicy(nil, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
@@ -446,7 +453,83 @@ func Test_MergeRules(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(labelsFoo)
+			mapstate, err := repo.distillPolicy(nil, labelsFoo)
+			if err != nil {
+				t.Errorf("Policy resolution failure: %s", err)
+			}
+			if equal, err := checker.DeepEqual(mapstate, tt.result); !equal {
+				t.Logf("Rules:\n%s\n\n", tt.rules.String())
+				t.Logf("Policy Trace: \n%s\n", logBuffer.String())
+				t.Errorf("Policy obtained didn't match expected for endpoint %s:\n%s", labelsFoo, err)
+			}
+		})
+	}
+}
+
+func Test_MergeRulesWithNamedPorts(t *testing.T) {
+	identityCache := cache.IdentityCache{
+		identity.NumericIdentity(identityFoo): labelsFoo,
+	}
+	selectorCache := testNewSelectorCache(identityCache)
+
+	npMap := NamedPortsMap{
+		"port-80": NamedPort{Proto: uint8(0), Port: uint16(80)},
+	}
+
+	tests := []struct {
+		test   int
+		rules  api.Rules
+		result MapState
+	}{
+		// The following table is derived from the Google Doc here:
+		// https://docs.google.com/spreadsheets/d/1WANIoZGB48nryylQjjOw6lKjI80eVgPShrdMTMalLEw/edit?usp=sharing
+		//
+		//  Rule 0                   | Rule 1         | Rule 2         | Rule 3         | Rule 4         | Desired BPF map state
+		{0, api.Rules{rule____NoAllow, rule____NoAllow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{}},
+		{1, api.Rules{rule____NoAllow, rule____NoAllow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{2, api.Rules{rule____NoAllow, rule____NoAllow, rule____NoAllow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7None_(lbls__L4__Allow)}},
+		{3, api.Rules{rule____NoAllow, rule____NoAllow, rule____NoAllow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7None_(lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{4, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3npL4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7None_(lblsL3L4__Allow)}},
+		{5, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3npL4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7None_(lblsL3L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{6, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3npL4__Allow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7None_(lblsL3L4__Allow, lbls__L4__Allow)}},                                                     // identical L3L4 entry suppressed
+		{7, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3npL4__Allow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7None_(lblsL3L4__Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}}, // identical L3L4 entry suppressed
+		{8, api.Rules{rule____NoAllow, rule__npL4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow)}},
+		{9, api.Rules{rule____NoAllow, rule__npL4L7Allow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{10, api.Rules{rule____NoAllow, rule__npL4L7Allow, rule____NoAllow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow, lbls__L4__Allow)}},
+		{11, api.Rules{rule____NoAllow, rule__npL4L7Allow, rule____NoAllow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{12, api.Rules{rule____NoAllow, rule__npL4L7Allow, ruleL3npL4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow, lblsL3L4__Allow)}},                                                                        // L3L4 entry suppressed to allow L4-only entry to redirect
+		{13, api.Rules{rule____NoAllow, rule__npL4L7Allow, ruleL3npL4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow, lblsL3L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},                    // L3L4 entry suppressed to allow L4-only entry to redirect
+		{14, api.Rules{rule____NoAllow, rule__npL4L7Allow, ruleL3npL4__Allow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow)}},                                                     // L3L4 entry suppressed to allow L4-only entry to redirect
+		{15, api.Rules{rule____NoAllow, rule__npL4L7Allow, ruleL3npL4__Allow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lbls__L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}}, // L3L4 entry suppressed to allow L4-only entry to redirect
+		{16, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow)}},
+		{17, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{18, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, rule____NoAllow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4__Allow), mapKeyAllow___L4: mapEntryL7None_(lblsL3L4L7Allow, lbls__L4__Allow)}},
+		{19, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, rule____NoAllow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4__Allow), mapKeyAllow___L4: mapEntryL7None_(lblsL3L4L7Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{20, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, ruleL3npL4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow, lblsL3L4__Allow)}},
+		{21, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, ruleL3npL4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow, lblsL3L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{22, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, ruleL3npL4__Allow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow), mapKeyAllow___L4: mapEntryL7None_(lblsL3L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow)}},
+		{23, api.Rules{ruleL3npL4L7Allow, rule____NoAllow, ruleL3npL4__Allow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy(lblsL3L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow), mapKeyAllow___L4: mapEntryL7None_(lblsL3L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},
+		{24, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow)}},                                                                                           // identical L3L4 entry suppressed
+		{25, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},                                       // identical L3L4 entry suppressed
+		{26, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, rule____NoAllow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow, lbls__L4__Allow)}},                                                                        // identical L3L4 entry suppressed
+		{27, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, rule____NoAllow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},                    // identical L3L4 entry suppressed
+		{28, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, ruleL3npL4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow, lblsL3L4__Allow)}},                                                                        // identical L3L4 entry suppressed
+		{29, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, ruleL3npL4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow, lblsL3L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}},                    // identical L3L4 entry suppressed
+		{30, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, ruleL3npL4__Allow, rule__npL4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow)}},                                                     // identical L3L4 entry suppressed
+		{31, api.Rules{ruleL3npL4L7Allow, rule__npL4L7Allow, ruleL3npL4__Allow, rule__npL4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy(lblsL3L4L7Allow, lbls__L4L7Allow, lblsL3L4__Allow, lbls__L4__Allow), mapKeyAllowFoo__: mapEntryL7None_(lblsL3____Allow)}}, // identical L3L4 entry suppressed
+	}
+	for _, tt := range tests {
+		repo := newPolicyDistillery(selectorCache)
+		for _, r := range tt.rules {
+			if r != nil {
+				rule := r.WithEndpointSelector(selectFoo_)
+				_, _ = repo.AddList(api.Rules{rule})
+			}
+		}
+		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
+			logBuffer := new(bytes.Buffer)
+			repo = repo.WithLogBuffer(logBuffer)
+			mapstate, err := repo.distillPolicy(npMap, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
@@ -487,7 +570,7 @@ func Test_AllowAll(t *testing.T) {
 		t.Run(fmt.Sprintf("permutation_%d", tt.test), func(t *testing.T) {
 			logBuffer := new(bytes.Buffer)
 			repo = repo.WithLogBuffer(logBuffer)
-			mapstate, err := repo.distillPolicy(labelsFoo)
+			mapstate, err := repo.distillPolicy(nil, labelsFoo)
 			if err != nil {
 				t.Errorf("Policy resolution failure: %s", err)
 			}
diff --git a/pkg/policy/resolve_test.go b/pkg/policy/resolve_test.go
index c0327f681a..ab4e249cf8 100644
--- a/pkg/policy/resolve_test.go
+++ b/pkg/policy/resolve_test.go
@@ -170,7 +170,7 @@ func GenerateCIDRRules(numRules int) api.Rules {
 
 type DummyOwner struct{}
 
-func (d DummyOwner) LookupRedirectPort(l4 *L4Filter) uint16 {
+func (d DummyOwner) LookupRedirectPortLocked(npMap NamedPortsMap, l4 *L4Filter) uint16 {
 	// Return a fake non-0 listening port number for redirect filters.
 	if l4.IsRedirect() {
 		return 4242
@@ -178,6 +178,10 @@ func (d DummyOwner) LookupRedirectPort(l4 *L4Filter) uint16 {
 	return 0
 }
 
+func (d DummyOwner) GetID() uint64 {
+	return 1234
+}
+
 func bootstrapRepo(ruleGenFunc func(int) api.Rules, numRules int, c *C) *Repository {
 	mgr := cache.NewCachingIdentityAllocator(&allocator.IdentityAllocatorOwnerMock{})
 	testRepo := NewPolicyRepository(mgr.GetIdentityCache(), nil)
@@ -212,7 +216,7 @@ func (ds *PolicyTestSuite) BenchmarkRegenerateCIDRPolicyRules(c *C) {
 	c.ResetTimer()
 	for i := 0; i < c.N; i++ {
 		ip, _ := testRepo.resolvePolicyLocked(fooIdentity)
-		_ = ip.DistillPolicy(DummyOwner{})
+		_ = ip.DistillPolicy(DummyOwner{}, nil)
 		ip.Detach()
 	}
 }
@@ -222,7 +226,7 @@ func (ds *PolicyTestSuite) BenchmarkRegenerateL3IngressPolicyRules(c *C) {
 	c.ResetTimer()
 	for i := 0; i < c.N; i++ {
 		ip, _ := testRepo.resolvePolicyLocked(fooIdentity)
-		_ = ip.DistillPolicy(DummyOwner{})
+		_ = ip.DistillPolicy(DummyOwner{}, nil)
 		ip.Detach()
 	}
 }
@@ -232,7 +236,7 @@ func (ds *PolicyTestSuite) BenchmarkRegenerateL3EgressPolicyRules(c *C) {
 	c.ResetTimer()
 	for i := 0; i < c.N; i++ {
 		ip, _ := testRepo.resolvePolicyLocked(fooIdentity)
-		_ = ip.DistillPolicy(DummyOwner{})
+		_ = ip.DistillPolicy(DummyOwner{}, nil)
 		ip.Detach()
 	}
 }
@@ -274,7 +278,7 @@ func (ds *PolicyTestSuite) TestL7WithIngressWildcard(c *C) {
 	defer repo.Mutex.RUnlock()
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{})
+	policy := selPolicy.DistillPolicy(DummyOwner{}, nil)
 
 	expectedEndpointPolicy := EndpointPolicy{
 		selectorPolicy: &selectorPolicy{
@@ -363,7 +367,7 @@ func (ds *PolicyTestSuite) TestL7WithLocalHostWildcardd(c *C) {
 
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{})
+	policy := selPolicy.DistillPolicy(DummyOwner{}, nil)
 
 	cachedSelectorHost := testSelectorCache.FindCachedIdentitySelector(api.ReservedEndpointSelectors[labels.IDNameHost])
 	c.Assert(cachedSelectorHost, Not(IsNil))
@@ -451,7 +455,7 @@ func (ds *PolicyTestSuite) TestMapStateWithIngressWildcard(c *C) {
 	defer repo.Mutex.RUnlock()
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{})
+	policy := selPolicy.DistillPolicy(DummyOwner{}, nil)
 
 	rule1MapStateEntry := NewMapStateEntry(labels.LabelArrayList{ruleLabel}, false)
 	allowEgressMapStateEntry := NewMapStateEntry(labels.LabelArrayList{ruleLabelAllowAnyEgress}, false)
@@ -556,7 +560,7 @@ func (ds *PolicyTestSuite) TestMapStateWithIngress(c *C) {
 	defer repo.Mutex.RUnlock()
 	selPolicy, err := repo.resolvePolicyLocked(fooIdentity)
 	c.Assert(err, IsNil)
-	policy := selPolicy.DistillPolicy(DummyOwner{})
+	policy := selPolicy.DistillPolicy(DummyOwner{}, nil)
 
 	// Add new identity to test accumulation of PolicyMapChanges
 	added1 := cache.IdentityCache{
diff --git a/pkg/policy/rule_test.go b/pkg/policy/rule_test.go
index f8a2c83ba6..9b77cd3692 100644
--- a/pkg/policy/rule_test.go
+++ b/pkg/policy/rule_test.go
@@ -1395,7 +1395,7 @@ func (ds *PolicyTestSuite) TestIngressAllowAll(c *C) {
 	checkIngress(c, repo, &ctxAToC80, api.Allowed)
 
 	ctxAToC90 := ctxAToC
-	ctxAToC90.DPorts = []*models.Port{{Port: 90, Protocol: models.PortProtocolTCP}}
+	ctxAToC90.DPorts = []*models.Port{{Name: "port-90", Protocol: models.PortProtocolTCP}}
 	checkIngress(c, repo, &ctxAToC90, api.Allowed)
 }
 
@@ -1433,6 +1433,40 @@ func (ds *PolicyTestSuite) TestIngressAllowAllL4Overlap(c *C) {
 	checkIngress(c, repo, &ctxAToC90, api.Allowed)
 }
 
+func (ds *PolicyTestSuite) TestIngressAllowAllL4OverlapNamedPort(c *C) {
+	repo := parseAndAddRules(c, api.Rules{
+		&api.Rule{
+			EndpointSelector: endpointSelectorC,
+			Ingress: []api.IngressRule{
+				{
+					// Allow all L3&L4 ingress rule
+					FromEndpoints: []api.EndpointSelector{
+						api.WildcardEndpointSelector,
+					},
+				},
+				{
+					// This rule is a subset of the above
+					// rule and should *NOT* restrict to
+					// port 80 only
+					ToPorts: []api.PortRule{{
+						Ports: []api.PortProtocol{
+							{Port: "port-80", Protocol: api.ProtoTCP},
+						},
+					}},
+				},
+			},
+		},
+	})
+
+	ctxAToC80 := ctxAToC
+	ctxAToC80.DPorts = []*models.Port{{Name: "port-80", Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToC80, api.Allowed)
+
+	ctxAToC90 := ctxAToC
+	ctxAToC90.DPorts = []*models.Port{{Port: 90, Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToC90, api.Allowed)
+}
+
 func (ds *PolicyTestSuite) TestIngressL4AllowAll(c *C) {
 	repo := parseAndAddRules(c, api.Rules{
 		&api.Rule{
@@ -1457,6 +1491,10 @@ func (ds *PolicyTestSuite) TestIngressL4AllowAll(c *C) {
 	ctxAToC90.DPorts = []*models.Port{{Port: 90, Protocol: models.PortProtocolTCP}}
 	checkIngress(c, repo, &ctxAToC90, api.Denied)
 
+	ctxAToCNamed90 := ctxAToC
+	ctxAToCNamed90.DPorts = []*models.Port{{Name: "port-90", Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToCNamed90, api.Denied)
+
 	l4IngressPolicy, err := repo.ResolveL4IngressPolicy(&ctxAToC80)
 	c.Assert(err, IsNil)
 
@@ -1470,6 +1508,52 @@ func (ds *PolicyTestSuite) TestIngressL4AllowAll(c *C) {
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 }
 
+func (ds *PolicyTestSuite) TestIngressL4AllowAllNamedPort(c *C) {
+	repo := parseAndAddRules(c, api.Rules{
+		&api.Rule{
+			EndpointSelector: endpointSelectorC,
+			Ingress: []api.IngressRule{
+				{
+					ToPorts: []api.PortRule{{
+						Ports: []api.PortProtocol{
+							{Port: "port-80", Protocol: api.ProtoTCP},
+						},
+					}},
+				},
+			},
+		},
+	})
+
+	ctxAToCNamed80 := ctxAToC
+	ctxAToCNamed80.DPorts = []*models.Port{{Name: "port-80", Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToCNamed80, api.Allowed)
+
+	ctxAToC80 := ctxAToC
+	ctxAToC80.DPorts = []*models.Port{{Port: 80, Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToC80, api.Denied)
+
+	ctxAToC90 := ctxAToC
+	ctxAToC90.DPorts = []*models.Port{{Port: 90, Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToC90, api.Denied)
+
+	ctxAToCNamed90 := ctxAToC
+	ctxAToCNamed90.DPorts = []*models.Port{{Name: "port-90", Protocol: models.PortProtocolTCP}}
+	checkIngress(c, repo, &ctxAToCNamed90, api.Denied)
+
+	l4IngressPolicy, err := repo.ResolveL4IngressPolicy(&ctxAToCNamed80)
+	c.Assert(err, IsNil)
+
+	filter, ok := l4IngressPolicy["port-80/TCP"]
+	c.Assert(ok, Equals, true)
+	c.Assert(filter.Port, Equals, 0)
+	c.Assert(filter.PortName, Equals, "port-80")
+	c.Assert(filter.Ingress, Equals, true)
+
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], IsNil)
+	l4IngressPolicy.Detach(repo.GetSelectorCache())
+}
+
 func (ds *PolicyTestSuite) TestEgressAllowAll(c *C) {
 	repo := parseAndAddRules(c, api.Rules{
 		&api.Rule{
diff --git a/pkg/proxy/mock_test.go b/pkg/proxy/mock_test.go
index 72e70ffbb2..47e799528a 100644
--- a/pkg/proxy/mock_test.go
+++ b/pkg/proxy/mock_test.go
@@ -48,7 +48,9 @@ func (m *proxyUpdaterMock) GetLabels() []string                   { return m.lab
 func (m *proxyUpdaterMock) GetEgressPolicyEnabledLocked() bool    { return true }
 func (m *proxyUpdaterMock) GetIngressPolicyEnabledLocked() bool   { return true }
 func (m *proxyUpdaterMock) GetIdentity() identity.NumericIdentity { return m.identity }
-func (m *proxyUpdaterMock) ProxyID(l4 *policy.L4Filter) string    { return "" }
+func (m *proxyUpdaterMock) ProxyID(npMap policy.NamedPortsMap, l4 *policy.L4Filter) (string, error) {
+	return "", nil
+}
 func (m *proxyUpdaterMock) GetLabelsSHA() string {
 	return labels.NewLabelsFromModel(m.labels).SHA256Sum()
 }
diff --git a/test/helpers/kubectl.go b/test/helpers/kubectl.go
index 9e664f494c..68cb673aad 100644
--- a/test/helpers/kubectl.go
+++ b/test/helpers/kubectl.go
@@ -2736,6 +2736,7 @@ func (kub *Kubectl) GatherLogs(ctx context.Context) {
 		"kubectl describe pods --all-namespaces":                     "pods_status.txt",
 		"kubectl get replicationcontroller --all-namespaces -o json": "replicationcontroller.txt",
 		"kubectl get deployment --all-namespaces -o json":            "deployment.txt",
+		"kubectl get crd ciliumnetworkpolicies.cilium.io -o json":    "cilium-network-policies-crd.json",
 
 		fmt.Sprintf("kubectl get cm cilium-config -n %s -o json", GetCiliumNamespace(GetCurrentIntegration())): "cilium-config.json",
 	}
diff --git a/test/k8sT/Policies.go b/test/k8sT/Policies.go
index 2f1ed396d9..9dcaa83bb9 100644
--- a/test/k8sT/Policies.go
+++ b/test/k8sT/Policies.go
@@ -41,7 +41,9 @@ var _ = Describe("K8sPolicyTest", func() {
 		ciliumFilename       string
 		demoPath             string
 		l3Policy             string
+		l3NamedPortPolicy    string
 		l7Policy             string
+		l7NamedPortPolicy    string
 		l7PolicyKafka        string
 		l7PolicyTLS          string
 		TLSCaCerts           string
@@ -70,9 +72,11 @@ var _ = Describe("K8sPolicyTest", func() {
 		kubectl = helpers.CreateKubectl(helpers.K8s1VMName(), logger)
 
 		ciliumFilename = helpers.TimestampFilename("cilium.yaml")
-		demoPath = helpers.ManifestGet(kubectl.BasePath(), "demo.yaml")
+		demoPath = helpers.ManifestGet(kubectl.BasePath(), "demo-named-port.yaml")
 		l3Policy = helpers.ManifestGet(kubectl.BasePath(), "l3-l4-policy.yaml")
+		l3NamedPortPolicy = helpers.ManifestGet(kubectl.BasePath(), "l3-l4-policy-named-port.yaml")
 		l7Policy = helpers.ManifestGet(kubectl.BasePath(), "l7-policy.yaml")
+		l7NamedPortPolicy = helpers.ManifestGet(kubectl.BasePath(), "l7-policy-named-port.yaml")
 		l7PolicyKafka = helpers.ManifestGet(kubectl.BasePath(), "l7-policy-kafka.yaml")
 		l7PolicyTLS = helpers.ManifestGet(kubectl.BasePath(), "l7-policy-TLS.yaml")
 		TLSCaCerts = helpers.ManifestGet(kubectl.BasePath(), "testCA.crt")
@@ -301,6 +305,77 @@ var _ = Describe("K8sPolicyTest", func() {
 				appPods[helpers.App3], clusterIP)
 		}, 500)
 
+		It("checks policies with named ports", func() {
+
+			logger.Infof("PolicyRulesTest: cluster service ip '%s'", clusterIP)
+
+			By("Testing L3/L4 rules with named ports")
+
+			_, err := kubectl.CiliumPolicyAction(
+				namespaceForTest, l3NamedPortPolicy, helpers.KubectlApply, helpers.HelperTimeout)
+			Expect(err).Should(BeNil())
+
+			for _, appName := range []string{helpers.App1, helpers.App2, helpers.App3} {
+				err = kubectl.WaitForCEPIdentity(namespaceForTest, appPods[appName])
+				Expect(err).Should(BeNil())
+			}
+
+			trace := kubectl.CiliumExecMustSucceed(context.TODO(), ciliumPod, fmt.Sprintf(
+				"cilium policy trace --src-k8s-pod %s:%s --dst-k8s-pod %s:%s --dport http-80/TCP",
+				namespaceForTest, appPods[helpers.App2], namespaceForTest, appPods[helpers.App1]))
+			trace.ExpectContains("Final verdict: ALLOWED", "Policy trace output mismatch")
+
+			trace = kubectl.CiliumExecMustSucceed(context.TODO(), ciliumPod, fmt.Sprintf(
+				"cilium policy trace --src-k8s-pod %s:%s --dst-k8s-pod %s:%s",
+				namespaceForTest, appPods[helpers.App3], namespaceForTest, appPods[helpers.App1]))
+			trace.ExpectContains("Final verdict: DENIED", "Policy trace output mismatch")
+
+			res := kubectl.ExecPodCmd(
+				namespaceForTest, appPods[helpers.App2],
+				helpers.CurlFail(fmt.Sprintf("http://%s/public", clusterIP)))
+			res.ExpectSuccess("%q cannot curl clusterIP %q", appPods[helpers.App2], clusterIP)
+
+			res = kubectl.ExecPodCmd(
+				namespaceForTest, appPods[helpers.App3],
+				helpers.CurlFail(fmt.Sprintf("http://%s/public", clusterIP)))
+			res.ExpectFail("%q can curl to %q", appPods[helpers.App3], clusterIP)
+
+			_, err = kubectl.CiliumPolicyAction(
+				namespaceForTest, l3NamedPortPolicy,
+				helpers.KubectlDelete, helpers.HelperTimeout)
+			Expect(err).Should(BeNil(), "Cannot delete L3 Policy")
+
+			By("Testing L7 Policy with named port")
+
+			_, err = kubectl.CiliumPolicyAction(
+				namespaceForTest, l7NamedPortPolicy, helpers.KubectlApply, helpers.HelperTimeout)
+			Expect(err).Should(BeNil(), "Cannot install %q policy", l7NamedPortPolicy)
+
+			res = kubectl.ExecPodCmd(
+				namespaceForTest, appPods[helpers.App2],
+				helpers.CurlFail("http://%s/public", clusterIP))
+			res.ExpectSuccess("Cannot connect from %q to 'http://%s/public'",
+				appPods[helpers.App2], clusterIP)
+
+			res = kubectl.ExecPodCmd(
+				namespaceForTest, appPods[helpers.App2],
+				helpers.CurlFail(fmt.Sprintf("http://%s/private", clusterIP)))
+			res.ExpectFail("Unexpected connection from %q to 'http://%s/private'",
+				appPods[helpers.App2], clusterIP)
+
+			res = kubectl.ExecPodCmd(
+				namespaceForTest, appPods[helpers.App3],
+				helpers.CurlFail(fmt.Sprintf("http://%s/public", clusterIP)))
+			res.ExpectFail("Unexpected connection from %q to 'http://%s/public'",
+				appPods[helpers.App3], clusterIP)
+
+			res = kubectl.ExecPodCmd(
+				namespaceForTest, appPods[helpers.App3],
+				helpers.CurlFail("http://%s/private", clusterIP))
+			res.ExpectFail("Unexpected connection from %q to 'http://%s/private'",
+				appPods[helpers.App3], clusterIP)
+		}, 500)
+
 		PIt("TLS policy", func() {
 			By("Testing L7 Policy with TLS")
 
@@ -955,6 +1030,24 @@ var _ = Describe("K8sPolicyTest", func() {
 
 				By("Checking that proxy visibility annotation is re-added after policy is removed")
 				checkProxyRedirection(app1PodIP, true, policy.ParserTypeHTTP)
+
+				By("Importing policy using named ports which selects app1; proxy-visibility annotation should be removed")
+
+				_, err = kubectl.CiliumPolicyAction(
+					namespaceForTest, l3NamedPortPolicy, helpers.KubectlApply, helpers.HelperTimeout)
+				Expect(err).Should(BeNil(),
+					"policy %s cannot be applied in %q namespace", l3NamedPortPolicy, namespaceForTest)
+
+				By("Checking that proxy visibility annotation is removed due to policy being added")
+				checkProxyRedirection(app1PodIP, false, policy.ParserTypeHTTP)
+
+				_, err = kubectl.CiliumPolicyAction(
+					namespaceForTest, l3NamedPortPolicy, helpers.KubectlDelete, helpers.HelperTimeout)
+				Expect(err).Should(BeNil(),
+					"policy %s cannot be deleted in %q namespace", l3NamedPortPolicy, namespaceForTest)
+
+				By("Checking that proxy visibility annotation is re-added after policy is removed")
+				checkProxyRedirection(app1PodIP, true, policy.ParserTypeHTTP)
 			})
 		})
 
diff --git a/test/k8sT/manifests/demo-named-port.yaml b/test/k8sT/manifests/demo-named-port.yaml
new file mode 100644
index 0000000000..06f742442f
--- /dev/null
+++ b/test/k8sT/manifests/demo-named-port.yaml
@@ -0,0 +1,123 @@
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: app1-account
+---
+kind: ServiceAccount
+apiVersion: v1
+metadata:
+  name: app2-account
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: app1-service
+spec:
+  ports:
+  - name: http
+    port: 80
+    protocol: TCP
+  - name: tftp
+    port: 69
+    protocol: UDP
+  selector:
+    id: app1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: app1
+spec:
+  selector:
+    matchLabels:
+      id: app1
+      zgroup: testapp
+  replicas: 2
+  template:
+    metadata:
+      labels:
+        id: app1
+        zgroup: testapp
+    spec:
+      serviceAccountName: app1-account
+      terminationGracePeriodSeconds: 0
+      containers:
+      - name: web
+        image: docker.io/cilium/demo-httpd:latest
+        imagePullPolicy: IfNotPresent
+        ports:
+        - containerPort: 80
+          name: "http-80"
+        readinessProbe:
+          httpGet:
+            path: /
+            port: 80
+      - name: udp
+        image: docker.io/cilium/echoserver-udp:v2020.01.30
+        imagePullPolicy: IfNotPresent
+        ports:
+        - containerPort: 69
+          protocol: UDP
+      nodeSelector:
+        "cilium.io/ci-node": k8s1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: app2
+spec:
+  selector:
+    matchLabels:
+      id: app2
+      zgroup: testapp
+      appSecond: "true"
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        id: app2
+        zgroup: testapp
+        appSecond: "true"
+    spec:
+      serviceAccountName: app2-account
+      terminationGracePeriodSeconds: 0
+      containers:
+      - name: app-frontend
+        image: docker.io/cilium/demo-client:latest
+        imagePullPolicy: IfNotPresent
+        command: [ "sleep" ]
+        args:
+          - "1000h"
+      # k8sT/Services.go:"Checks service on same node" requires the pod to be
+      # scheduled on the same node as app1
+      nodeSelector:
+        "cilium.io/ci-node": k8s1
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: app3
+spec:
+  selector:
+    matchLabels:
+      id: app3
+      zgroup: testapp
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        id: app3
+        zgroup: testapp
+    spec:
+      terminationGracePeriodSeconds: 0
+      containers:
+      - name: app-frontend
+        image: docker.io/cilium/demo-client:latest
+        imagePullPolicy: IfNotPresent
+        command: [ "sleep" ]
+        args:
+          - "1000h"
+        ports:
+        - containerPort: 80
+      nodeSelector:
+        "cilium.io/ci-node": k8s1
diff --git a/test/k8sT/manifests/l3-l4-policy-named-port.yaml b/test/k8sT/manifests/l3-l4-policy-named-port.yaml
new file mode 100644
index 0000000000..5fa174499f
--- /dev/null
+++ b/test/k8sT/manifests/l3-l4-policy-named-port.yaml
@@ -0,0 +1,17 @@
+apiVersion: "cilium.io/v2"
+kind: CiliumNetworkPolicy
+description: "L3-L4 policy with named l4 port"
+metadata:
+  name: "l3-l4-policy"
+spec:
+  endpointSelector:
+    matchLabels:
+      id: app1
+  ingress:
+  - fromEndpoints:
+    - matchLabels:
+        id: app2
+    toPorts:
+    - ports:
+      - port: "http-80"
+        protocol: TCP
diff --git a/test/k8sT/manifests/l7-policy-named-port.yaml b/test/k8sT/manifests/l7-policy-named-port.yaml
new file mode 100644
index 0000000000..432c09d358
--- /dev/null
+++ b/test/k8sT/manifests/l7-policy-named-port.yaml
@@ -0,0 +1,21 @@
+apiVersion: "cilium.io/v2"
+kind: CiliumNetworkPolicy
+description: "L7 policy with a named L4 port for getting started using Kubernetes guide"
+metadata:
+  name: "l7-policy"
+spec:
+  endpointSelector:
+    matchLabels:
+      id: app1
+  ingress:
+  - fromEndpoints:
+    - matchLabels:
+        id: app2
+    toPorts:
+    - ports:
+      - port: "http-80"
+        protocol: TCP
+      rules:
+        http:
+        - method: "GET"
+          path: "/public"
diff --git a/test/kubernetes-test.sh b/test/kubernetes-test.sh
index 8f93870ac9..14e5a7ed12 100755
--- a/test/kubernetes-test.sh
+++ b/test/kubernetes-test.sh
@@ -73,4 +73,4 @@ export KUBE_MASTER=192.168.36.11
 export KUBE_MASTER_IP=192.168.36.11
 export KUBE_MASTER_URL="https://192.168.36.11:6443"
 
-${HOME}/go/bin/kubetest --test --test_args="--ginkgo.focus=NetworkPolicy --e2e-verify-service-account=false --host ${KUBE_MASTER_URL} --ginkgo.skip=(should.allow.egress.access.to.server.in.CIDR.block)|(should.allow.ingress.access.from.updated.pod)|(named.port)"
+${HOME}/go/bin/kubetest --test --test_args="--ginkgo.focus=NetworkPolicy.* --e2e-verify-service-account=false --host ${KUBE_MASTER_URL} --ginkgo.skip=(should.allow.egress.access.to.server.in.CIDR.block)|(should.allow.ingress.access.from.updated.pod)"