Repository: cilium/cilium
Pull request #11298 (2020-05-01 22:22:57)

doc: Document render-docs target
Document the render-docs target. This target generates documentation
files and serves them locally in a container. Assuming that everybody
uses this make target to test docmentation changes, I'm removing the
requirement to run:

    sudo pip3 install -r Documentation/requirements.txt

to install Python packages locally.

Signed-off-by: Michi Mutsuzaki <michi@isovalent.com>


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/contributing/development/dev_setup.rst b/Documentation/contributing/development/dev_setup.rst
index d417077363..6ca9cccc01 100644
--- a/Documentation/contributing/development/dev_setup.rst
+++ b/Documentation/contributing/development/dev_setup.rst
@@ -43,12 +43,6 @@ contribute to Cilium:
 
 For `unit_testing`, you will need to run ``docker`` without privileges. You can usually achieve this by adding your current user to the ``docker`` group.
 
-To build the documentation, you will need to install its dependencies:
-
-::
-
-    $ sudo pip3 install -r Documentation/requirements.txt
-
 Finally, in order to run Cilium locally on VMs, you need:
 
 +----------------------------------------------------------------------------------+-----------------------+--------------------------------------------------------------------------------+
@@ -288,6 +282,10 @@ Making Changes
 #. See :ref:`unit_testing` on how to run unit tests.
 #. See :ref:`testsuite` for information how to run the end to end integration
    tests
+#. If you are making documentation changes, you can generate documentation files
+   and serve them locally on ``http://localhost:9081`` by running ``make render-docs``.
+   This make target works both inside and outside the Vagrant VM, assuming that ``docker``
+   is running in the environment.
 
 Add/update a golang dependency
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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)"