Repository: cilium/cilium
Pull request #10115 (2020-02-10 14:19:23)

test: Add Kubernetes Service CI tests for IPv6
This adds initial connectivity tests for IPv6 in the Kubernetes service test suite. The deployed Kubernetes is not yet running in dual-stack mode - therefore we install the Cilium service rules manually via CLI. Because of this, the IPv6 tests are only ran in kube-proxy-free mode, as kube-proxy only installs IPv4 rules.

NodePort and ExternalIP tests have yet to be implemented in a follow-up PR. This PR here lays the groundwork by upgrading all images to listen on IPv6, providing helpers to install services via Cilium CLI, and adding basic ClusterIP tests.

Ref #9362 

<!-- Reviewable:start -->
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/cilium/cilium/10115)
<!-- Reviewable:end -->


gandro: test-focus K8sService*
coveralls: 
[![Coverage Status](https://coveralls.io/builds/28632599/badge)](https://coveralls.io/builds/28632599)

Coverage increased (+0.03%) to 44.64% when pulling **d333b82d96e9efdf32755801613b3b58ee02222f on pr/gandro/test-ipv6-svc** into **20f7a79b53ad16b467b41e9e0a3c8f86d72e64d3 on master**.

gandro: test-focus K8sService*
gandro: test-focus K8sService*
gandro: test-focus K8sService*
gandro: test-focus K8sService*
gandro: test-focus K8sService*
gandro: test-me-please
gandro: test-me-please

Patch:
diff --git a/Dockerfile b/Dockerfile
index 9e89db2e82..4385aa79ec 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,7 +1,7 @@
 #
 # cilium-envoy from github.com/cilium/proxy
 #
-FROM quay.io/cilium/cilium-envoy:c16d0f195d4fa5e26c3a7cff9a27fc69a13437c5 as cilium-envoy
+FROM quay.io/cilium/cilium-envoy:c31482c3e49670980c05cafc914320f7949b266f as cilium-envoy
 
 #
 # Cilium incremental build. Should be fast given builder-deps is up-to-date!
diff --git a/Documentation/gettingstarted/encryption.rst b/Documentation/gettingstarted/encryption.rst
index ce94774d85..9282c13875 100644
--- a/Documentation/gettingstarted/encryption.rst
+++ b/Documentation/gettingstarted/encryption.rst
@@ -6,9 +6,9 @@
 
 .. _encryption:
 
-*****************************
-Transparent Encryption (beta)
-*****************************
+************************************
+Transparent Encryption (stable/beta)
+************************************
 
 This guide explains how to configure Cilium to use IPsec based transparent
 encryption using Kubernetes secrets to distribute the IPsec keys. After this
@@ -19,8 +19,9 @@ keys may be manually distributed but that is not shown here.
 
 .. note::
 
-    This is a beta feature. Please provide feedback and file a GitHub issue
-    if you experience any problems.
+    The encryption feature is stable in combination with the direct-routing and
+    ENI datapath mode. In combination with encapsulation/tunneling, the feature
+    is still in beta phase.
 
 Generate & import the PSK
 =========================
diff --git a/Documentation/gettingstarted/k8s-install-azure-cni-validate.rst b/Documentation/gettingstarted/k8s-install-azure-cni-validate.rst
index a417419d71..e10a5d1b2b 100644
--- a/Documentation/gettingstarted/k8s-install-azure-cni-validate.rst
+++ b/Documentation/gettingstarted/k8s-install-azure-cni-validate.rst
@@ -26,21 +26,25 @@ You can deploy the "connectivity-check" to test connectivity between pods.
 
     kubectl apply -f \ |SCM_WEB|\/examples/kubernetes/connectivity-check/connectivity-check.yaml
 
-It will deploy a simple probe and echo server running with multiple replicas.
-The probe will only report readiness while it can successfully reach the echo
-server:
+It will deploy a series of deployments which will use various connectivity
+paths to connect to each other. Connectivity paths include with and without
+service load-balancing and various network policy combinations. The pod name
+indicates the connectivity variant and the readiness and liveness gate
+indicates success or failure of the test:
 
 .. code:: bash
 
     kubectl get pods
-    NAME                     READY   STATUS    RESTARTS   AGE
-    echo-585798dd9d-ck5xc    1/1     Running   0          75s
-    echo-585798dd9d-jkdjx    1/1     Running   0          75s
-    echo-585798dd9d-mk5q8    1/1     Running   0          75s
-    echo-585798dd9d-tn9t4    1/1     Running   0          75s
-    echo-585798dd9d-xmr4p    1/1     Running   0          75s
-    probe-866bb6f696-9lhfw   1/1     Running   0          75s
-    probe-866bb6f696-br4dr   1/1     Running   0          75s
-    probe-866bb6f696-gv5kf   1/1     Running   0          75s
-    probe-866bb6f696-qg2b7   1/1     Running   0          75s
-    probe-866bb6f696-tb926   1/1     Running   0          75s
+    NAME                                                     READY   STATUS             RESTARTS   AGE
+    echo-a-9b85dd869-292s2                                   1/1     Running            0          8m37s
+    echo-b-c7d9f4686-gdwcs                                   1/1     Running            0          8m37s
+    host-to-b-multi-node-clusterip-6d496f7cf9-956jb          1/1     Running            0          8m37s
+    host-to-b-multi-node-headless-bd589bbcf-jwbh2            1/1     Running            0          8m37s
+    pod-to-a-7cc4b6c5b8-9jfjb                                1/1     Running            0          8m36s
+    pod-to-a-allowed-cnp-6cc776bb4d-2cszk                    1/1     Running            0          8m36s
+    pod-to-a-external-1111-5c75bd66db-sxfck                  1/1     Running            0          8m35s
+    pod-to-a-l3-denied-cnp-7fdd9975dd-2pp96                  1/1     Running            0          8m36s
+    pod-to-b-intra-node-9d9d4d6f9-qccfs                      1/1     Running            0          8m35s
+    pod-to-b-multi-node-clusterip-5956c84b7c-hwzfg           1/1     Running            0          8m35s
+    pod-to-b-multi-node-headless-6698899447-xlhfw            1/1     Running            0          8m35s
+    pod-to-external-fqdn-allow-google-cnp-667649bbf6-v6rf8   0/1     Running            0          8m35s
diff --git a/Documentation/gettingstarted/k8s-install-validate.rst b/Documentation/gettingstarted/k8s-install-validate.rst
index 3abe66a122..95645c4442 100644
--- a/Documentation/gettingstarted/k8s-install-validate.rst
+++ b/Documentation/gettingstarted/k8s-install-validate.rst
@@ -30,21 +30,25 @@ You can deploy the "connectivity-check" to test connectivity between pods.
 
     kubectl apply -f \ |SCM_WEB|\/examples/kubernetes/connectivity-check/connectivity-check.yaml
 
-It will deploy a simple probe and echo server running with multiple replicas.
-The probe will only report readiness while it can successfully reach the echo
-server:
+It will deploy a series of deployments which will use various connectivity
+paths to connect to each other. Connectivity paths include with and without
+service load-balancing and various network policy combinations. The pod name
+indicates the connectivity variant and the readiness and liveness gate
+indicates success or failure of the test:
 
 .. code:: bash
 
     kubectl get pods
-    NAME                     READY   STATUS    RESTARTS   AGE
-    echo-585798dd9d-ck5xc    1/1     Running   0          75s
-    echo-585798dd9d-jkdjx    1/1     Running   0          75s
-    echo-585798dd9d-mk5q8    1/1     Running   0          75s
-    echo-585798dd9d-tn9t4    1/1     Running   0          75s
-    echo-585798dd9d-xmr4p    1/1     Running   0          75s
-    probe-866bb6f696-9lhfw   1/1     Running   0          75s
-    probe-866bb6f696-br4dr   1/1     Running   0          75s
-    probe-866bb6f696-gv5kf   1/1     Running   0          75s
-    probe-866bb6f696-qg2b7   1/1     Running   0          75s
-    probe-866bb6f696-tb926   1/1     Running   0          75s
+    NAME                                                     READY   STATUS             RESTARTS   AGE
+    echo-a-9b85dd869-292s2                                   1/1     Running            0          8m37s
+    echo-b-c7d9f4686-gdwcs                                   1/1     Running            0          8m37s
+    host-to-b-multi-node-clusterip-6d496f7cf9-956jb          1/1     Running            0          8m37s
+    host-to-b-multi-node-headless-bd589bbcf-jwbh2            1/1     Running            0          8m37s
+    pod-to-a-7cc4b6c5b8-9jfjb                                1/1     Running            0          8m36s
+    pod-to-a-allowed-cnp-6cc776bb4d-2cszk                    1/1     Running            0          8m36s
+    pod-to-a-external-1111-5c75bd66db-sxfck                  1/1     Running            0          8m35s
+    pod-to-a-l3-denied-cnp-7fdd9975dd-2pp96                  1/1     Running            0          8m36s
+    pod-to-b-intra-node-9d9d4d6f9-qccfs                      1/1     Running            0          8m35s
+    pod-to-b-multi-node-clusterip-5956c84b7c-hwzfg           1/1     Running            0          8m35s
+    pod-to-b-multi-node-headless-6698899447-xlhfw            1/1     Running            0          8m35s
+    pod-to-external-fqdn-allow-google-cnp-667649bbf6-v6rf8   0/1     Running            0          8m35s
diff --git a/bpf/lib/policy.h b/bpf/lib/policy.h
index 9d54ab2fcc..743029feff 100644
--- a/bpf/lib/policy.h
+++ b/bpf/lib/policy.h
@@ -42,38 +42,38 @@ policy_sk_egress(__u32 identity, __u32 ip,  __u16 dport)
 	};
 
 	if (!map)
-		return 0;
+		return TC_ACT_OK;
 
+	/* Start with L3/L4 lookup. */
 	policy = map_lookup_elem(map, &key);
 	if (likely(policy)) {
 		/* FIXME: Need byte counter */
 		__sync_fetch_and_add(&policy->packets, 1);
-		goto get_proxy_port;
+		return policy->proxy_port;
 	}
 
-	/* If L4 policy check misses, fall back to L3. */
-	key.dport = 0;
-	key.protocol = 0;
+	/* L4-only lookup. */
+	key.sec_label = 0;
 	policy = map_lookup_elem(map, &key);
 	if (likely(policy)) {
 		/* FIXME: Need byte counter */
 		__sync_fetch_and_add(&policy->packets, 1);
-		return TC_ACT_OK;
+		return policy->proxy_port;
 	}
+	key.sec_label = identity;
 
-	key.sec_label = 0;
-	key.dport = dport;
-	key.protocol = proto;
+	/* If L4 policy check misses, fall back to L3. */
+	key.dport = 0;
+	key.protocol = 0;
 	policy = map_lookup_elem(map, &key);
 	if (likely(policy)) {
-		/* FIXME: Use per cpu counters */
+		/* FIXME: Need byte counter */
 		__sync_fetch_and_add(&policy->packets, 1);
-		goto get_proxy_port;
+		return TC_ACT_OK;
 	}
 
 	/* Final fallback if allow-all policy is in place. */
-	key.dport = 0;
-	key.protocol = 0;
+	key.sec_label = 0;
 	policy = map_lookup_elem(map, &key);
 	if (likely(policy)) {
 		/* FIXME: Need byte counter */
@@ -82,11 +82,6 @@ policy_sk_egress(__u32 identity, __u32 ip,  __u16 dport)
 	}
 
 	return DROP_POLICY;
-get_proxy_port:
-	if (likely(policy)) {
-		return policy->proxy_port;
-	}
-	return TC_ACT_OK;
 }
 #else
 
@@ -133,15 +128,26 @@ __policy_can_access(void *map, struct __sk_buff *skb, __u32 identity,
 		.pad = 0,
 	};
 
+	/* L4 lookup can't be done on fragments. */
 	if (!is_fragment) {
+		/* Start with L3/L4 lookup. */
 		policy = map_lookup_elem(map, &key);
 		if (likely(policy)) {
 			cilium_dbg3(skb, DBG_L4_CREATE, identity, SECLABEL,
 				    dport << 16 | proto);
 
 			account(skb, policy);
-			goto get_proxy_port;
+			return policy->proxy_port;
+		}
+
+		/* L4-only lookup. */
+		key.sec_label = 0;
+		policy = map_lookup_elem(map, &key);
+		if (likely(policy)) {
+			account(skb, policy);
+			return policy->proxy_port;
 		}
+		key.sec_label = identity;
 	}
 
 	/* If L4 policy check misses, fall back to L3. */
@@ -153,19 +159,6 @@ __policy_can_access(void *map, struct __sk_buff *skb, __u32 identity,
 		return TC_ACT_OK;
 	}
 
-	if (!is_fragment) {
-		key.sec_label = 0;
-		key.dport = dport;
-		key.protocol = proto;
-		policy = map_lookup_elem(map, &key);
-		if (likely(policy)) {
-			account(skb, policy);
-			goto get_proxy_port;
-		}
-		key.dport = 0;
-		key.protocol = 0;
-	}
-
 	/* Final fallback if allow-all policy is in place. */
 	key.sec_label = 0;
 	policy = map_lookup_elem(map, &key);
@@ -175,17 +168,11 @@ __policy_can_access(void *map, struct __sk_buff *skb, __u32 identity,
 	}
 
 	if (skb->cb[CB_POLICY])
-		goto allow;
+		return TC_ACT_OK;
 
 	if (is_fragment)
 		return DROP_FRAG_NOSUPPORT;
 	return DROP_POLICY;
-get_proxy_port:
-	if (likely(policy)) {
-		return policy->proxy_port;
-	}
-allow:
-	return TC_ACT_OK;
 }
 
 /**
diff --git a/daemon/bpf.sha b/daemon/bpf.sha
index 9274ac85dd..c2da6bb3b4 100644
--- a/daemon/bpf.sha
+++ b/daemon/bpf.sha
@@ -1,2 +1,2 @@
-GO_BINDATA_SHA1SUM=207cb3f96374de2ae0423c0cb679f19d8aa03b73
+GO_BINDATA_SHA1SUM=5f7d8a68d7fad472622563a8d75238b6d4166c66
 BPF_FILES=../bpf/COPYING ../bpf/Makefile ../bpf/Makefile.bpf ../bpf/bpf_alignchecker.c ../bpf/bpf_features.h ../bpf/bpf_hostdev_ingress.c ../bpf/bpf_ipsec.c ../bpf/bpf_lxc.c ../bpf/bpf_netdev.c ../bpf/bpf_network.c ../bpf/bpf_overlay.c ../bpf/bpf_sock.c ../bpf/bpf_xdp.c ../bpf/cilium-map-migrate.c ../bpf/filter_config.h ../bpf/include/bpf/api.h ../bpf/include/elf/elf.h ../bpf/include/elf/gelf.h ../bpf/include/elf/libelf.h ../bpf/include/iproute2/bpf_elf.h ../bpf/include/linux/bpf.h ../bpf/include/linux/bpf_common.h ../bpf/include/linux/byteorder.h ../bpf/include/linux/byteorder/big_endian.h ../bpf/include/linux/byteorder/little_endian.h ../bpf/include/linux/icmp.h ../bpf/include/linux/icmpv6.h ../bpf/include/linux/if_arp.h ../bpf/include/linux/if_ether.h ../bpf/include/linux/if_packet.h ../bpf/include/linux/in.h ../bpf/include/linux/in6.h ../bpf/include/linux/ioctl.h ../bpf/include/linux/ip.h ../bpf/include/linux/ipv6.h ../bpf/include/linux/perf_event.h ../bpf/include/linux/swab.h ../bpf/include/linux/tcp.h ../bpf/include/linux/type_mapper.h ../bpf/include/linux/udp.h ../bpf/init.sh ../bpf/lib/arp.h ../bpf/lib/common.h ../bpf/lib/config.h ../bpf/lib/conntrack.h ../bpf/lib/conntrack_map.h ../bpf/lib/conntrack_test.h ../bpf/lib/csum.h ../bpf/lib/dbg.h ../bpf/lib/drop.h ../bpf/lib/encap.h ../bpf/lib/eps.h ../bpf/lib/eth.h ../bpf/lib/events.h ../bpf/lib/icmp6.h ../bpf/lib/identity.h ../bpf/lib/ipv4.h ../bpf/lib/ipv6.h ../bpf/lib/ipv6_test.h ../bpf/lib/l3.h ../bpf/lib/l4.h ../bpf/lib/lb.h ../bpf/lib/lxc.h ../bpf/lib/maps.h ../bpf/lib/metrics.h ../bpf/lib/nat.h ../bpf/lib/nat46.h ../bpf/lib/nodeport.h ../bpf/lib/policy.h ../bpf/lib/signal.h ../bpf/lib/tailcall.h ../bpf/lib/trace.h ../bpf/lib/utils.h ../bpf/lib/xdp.h ../bpf/lxc_config.h ../bpf/netdev_config.h ../bpf/node_config.h ../bpf/probes/raw_change_tail.t ../bpf/probes/raw_fib_lookup.t ../bpf/probes/raw_insn.h ../bpf/probes/raw_invalidate_hash.t ../bpf/probes/raw_lpm_map.t ../bpf/probes/raw_lru_map.t ../bpf/probes/raw_main.c ../bpf/probes/raw_max_insn.t ../bpf/probes/raw_sock_cookie.t ../bpf/run_probes.sh ../bpf/sockops/Makefile ../bpf/sockops/bpf_redir.c ../bpf/sockops/bpf_sockops.c ../bpf/sockops/bpf_sockops.h ../bpf/sockops/sockops_config.h 
diff --git a/examples/kubernetes/connectivity-check/Makefile b/examples/kubernetes/connectivity-check/Makefile
new file mode 100644
index 0000000000..a1f8ed28d2
--- /dev/null
+++ b/examples/kubernetes/connectivity-check/Makefile
@@ -0,0 +1,10 @@
+OUT := connectivity-check.yaml
+SRC := $(wildcard *.yaml)
+SRC := $(filter-out $(OUT), $(SRC))
+
+all: $(SRC)
+	@echo '# Automatically generated by Makefile. DO NOT EDIT' > $(OUT)
+	for FILE in $(SRC); do \
+		cat $$FILE >> $(OUT); \
+		echo "---" >> $(OUT); \
+	done
diff --git a/examples/kubernetes/connectivity-check/connectivity-check.yaml b/examples/kubernetes/connectivity-check/connectivity-check.yaml
new file mode 100644
index 0000000000..cb7fcc3a67
--- /dev/null
+++ b/examples/kubernetes/connectivity-check/connectivity-check.yaml
@@ -0,0 +1,441 @@
+# Automatically generated by Makefile. DO NOT EDIT
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo-a
+spec:
+  type: ClusterIP
+  ports:
+  - port: 80
+  selector:
+    name: echo-a
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: echo-a
+spec:
+  selector:
+    matchLabels:
+      name: echo-a
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: echo-a
+    spec:
+      containers:
+      - name: echo-container
+        image: docker.io/cilium/json-mock:1.0
+        imagePullPolicy: IfNotPresent
+        readinessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "localhost"]
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo-b
+spec:
+  type: ClusterIP
+  ports:
+  - port: 80
+  selector:
+    name: echo-b
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo-b-headless
+spec:
+  type: ClusterIP
+  clusterIP: None
+  ports:
+  - port: 80
+  selector:
+    name: echo-b
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: echo-b
+spec:
+  selector:
+    matchLabels:
+      name: echo-b
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: echo-b
+    spec:
+      containers:
+      - name: echo-container
+        image: docker.io/cilium/json-mock:1.0
+        imagePullPolicy: IfNotPresent
+        readinessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "localhost"]
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: host-to-b-multi-node-clusterip
+spec:
+  selector:
+    matchLabels:
+      name: host-to-b-multi-node-clusterip
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: host-to-b-multi-node-clusterip
+    spec:
+      hostNetwork: true
+      dnsPolicy: ClusterFirstWithHostNet
+      containers:
+      - name: host-to-b-multi-node-container
+        imagePullPolicy: IfNotPresent
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: host-to-b-multi-node-headless
+spec:
+  selector:
+    matchLabels:
+      name: host-to-b-multi-node-headless
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: host-to-b-multi-node-headless
+    spec:
+      hostNetwork: true
+      dnsPolicy: ClusterFirstWithHostNet
+      containers:
+      - name: host-to-b-multi-node-container
+        imagePullPolicy: IfNotPresent
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b-headless"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-a-allowed-cnp
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-a-allowed-cnp
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-a-allowed-cnp
+    spec:
+      containers:
+      - name: pod-to-a-allowed-cnp-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-a"]
+        readinessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-a"]
+---
+apiVersion: "cilium.io/v2"
+kind: CiliumNetworkPolicy
+metadata:
+  name: "pod-to-a-allowed-cnp"
+spec:
+  endpointSelector:
+    matchLabels:
+      name: pod-to-a-allowed-cnp
+  egress:
+  - toEndpoints:
+    - matchLabels:
+        name: echo-a
+    toPorts:
+    - ports:
+      - port: "80"
+        protocol: TCP
+  - toEndpoints:
+    - matchLabels:
+        k8s:io.kubernetes.pod.namespace: kube-system
+        k8s:k8s-app: kube-dns
+    toPorts:
+    - ports:
+      - port: "53"
+        protocol: UDP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-a-l3-denied-cnp
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-a-l3-denied-cnp
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-a-l3-denied-cnp
+    spec:
+      containers:
+      - name: pod-to-a-l3-denied-cnp-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["ash", "-c", "! curl -sS --connect-timeout 5 -o /dev/null echo-a"]
+        readinessProbe:
+          exec:
+            command: ["ash", "-c", "! curl -sS --connect-timeout 5 -o /dev/null echo-a"]
+---
+apiVersion: "cilium.io/v2"
+kind: CiliumNetworkPolicy
+metadata:
+  name: "pod-to-a-l3-denied-cnp"
+spec:
+  endpointSelector:
+    matchLabels:
+      name: pod-to-a-l3-denied-cnp
+  egress:
+  - toEndpoints:
+    - matchLabels:
+        k8s:io.kubernetes.pod.namespace: kube-system
+        k8s:k8s-app: kube-dns
+    toPorts:
+    - ports:
+      - port: "53"
+        protocol: UDP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-a
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-a
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-a
+    spec:
+      containers:
+      - name: pod-to-a-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-a"]
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-b-intra-node
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-b-intra-node
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-intra-node
+    spec:
+      containers:
+      - name: pod-to-b-intra-node-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b"]
+      affinity:
+        podAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-b-multi-node-clusterip
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-b-multi-node-clusterip
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-multi-node-clusterip
+    spec:
+      containers:
+      - name: pod-to-b-multi-node-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-b-multi-node-headless
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-b-multi-node-headless
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-multi-node-headless
+    spec:
+      containers:
+      - name: pod-to-b-multi-node-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b-headless"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-a-external-1111
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-a-external-1111
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-a-external-1111
+    spec:
+      containers:
+      - name: pod-to-a-external-1111-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "1.1.1.1"]
+        readinessProbe:
+          exec:
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "1.1.1.1"]
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-external-fqdn-allow-google-cnp
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-external-fqdn-allow-google-cnp
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-external-fqdn-allow-google-cnp
+    spec:
+      containers:
+      - name: pod-to-external-fqdn-allow-google-cnp-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "www.google.com"]
+        readinessProbe:
+          exec:
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "www.google.com"]
+---
+apiVersion: "cilium.io/v2"
+kind: CiliumNetworkPolicy
+metadata:
+  name: "pod-to-external-fqdn-allow-google-cnp"
+spec:
+  endpointSelector:
+    matchLabels:
+      name: pod-to-external-fqdn-allow-google-cnp
+  egress:
+  - toEndpoints:
+    - matchLabels:
+       "k8s:io.kubernetes.pod.namespace": kube-system
+       "k8s:k8s-app": kube-dns
+    toPorts:
+    - ports:
+      - port: "53"
+        protocol: ANY
+      rules:
+        dns:
+        - matchPattern: "*"
+  - toFQDNs:
+    - matchPattern: "*.google.com"
+---
diff --git a/examples/kubernetes/connectivity-check/echo-b.yaml b/examples/kubernetes/connectivity-check/echo-b.yaml
index 27ad33073d..63d3c66695 100644
--- a/examples/kubernetes/connectivity-check/echo-b.yaml
+++ b/examples/kubernetes/connectivity-check/echo-b.yaml
@@ -9,6 +9,18 @@ spec:
   selector:
     name: echo-b
 ---
+apiVersion: v1
+kind: Service
+metadata:
+  name: echo-b-headless
+spec:
+  type: ClusterIP
+  clusterIP: None
+  ports:
+  - port: 80
+  selector:
+    name: echo-b
+---
 apiVersion: apps/v1
 kind: Deployment
 metadata:
diff --git a/examples/kubernetes/connectivity-check/host-to-b-multi-node-clusterip.yaml b/examples/kubernetes/connectivity-check/host-to-b-multi-node-clusterip.yaml
new file mode 100644
index 0000000000..5c685840ee
--- /dev/null
+++ b/examples/kubernetes/connectivity-check/host-to-b-multi-node-clusterip.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: host-to-b-multi-node-clusterip
+spec:
+  selector:
+    matchLabels:
+      name: host-to-b-multi-node-clusterip
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: host-to-b-multi-node-clusterip
+    spec:
+      hostNetwork: true
+      dnsPolicy: ClusterFirstWithHostNet
+      containers:
+      - name: host-to-b-multi-node-container
+        imagePullPolicy: IfNotPresent
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
diff --git a/examples/kubernetes/connectivity-check/host-to-b-multi-node-headless.yaml b/examples/kubernetes/connectivity-check/host-to-b-multi-node-headless.yaml
new file mode 100644
index 0000000000..4b13626ea5
--- /dev/null
+++ b/examples/kubernetes/connectivity-check/host-to-b-multi-node-headless.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: host-to-b-multi-node-headless
+spec:
+  selector:
+    matchLabels:
+      name: host-to-b-multi-node-headless
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: host-to-b-multi-node-headless
+    spec:
+      hostNetwork: true
+      dnsPolicy: ClusterFirstWithHostNet
+      containers:
+      - name: host-to-b-multi-node-container
+        imagePullPolicy: IfNotPresent
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b-headless"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
diff --git a/examples/kubernetes/connectivity-check/pod-to-a-allowed.yaml b/examples/kubernetes/connectivity-check/pod-to-a-allowed.yaml
index 8ee3680c53..47738d35c5 100644
--- a/examples/kubernetes/connectivity-check/pod-to-a-allowed.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-a-allowed.yaml
@@ -14,7 +14,8 @@ spec:
     spec:
       containers:
       - name: pod-to-a-allowed-cnp-container
-        image: docker.io/cilium/json-mock:1.0
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
diff --git a/examples/kubernetes/connectivity-check/pod-to-a-denied.yaml b/examples/kubernetes/connectivity-check/pod-to-a-denied.yaml
index 3ad23a7cc2..5f56e764ba 100644
--- a/examples/kubernetes/connectivity-check/pod-to-a-denied.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-a-denied.yaml
@@ -14,14 +14,15 @@ spec:
     spec:
       containers:
       - name: pod-to-a-l3-denied-cnp-container
-        image: docker.io/cilium/json-mock:1.0
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
-            command: ["bash", "-c", "! curl -sS --connect-timeout 5 -o /dev/null echo-a"]
+            command: ["ash", "-c", "! curl -sS --connect-timeout 5 -o /dev/null echo-a"]
         readinessProbe:
           exec:
-            command: ["bash", "-c", "! curl -sS --connect-timeout 5 -o /dev/null echo-a"]
+            command: ["ash", "-c", "! curl -sS --connect-timeout 5 -o /dev/null echo-a"]
 ---
 apiVersion: "cilium.io/v2"
 kind: CiliumNetworkPolicy
diff --git a/examples/kubernetes/connectivity-check/pod-to-a.yaml b/examples/kubernetes/connectivity-check/pod-to-a.yaml
index dc0c61b09c..f6e9366cf2 100644
--- a/examples/kubernetes/connectivity-check/pod-to-a.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-a.yaml
@@ -14,7 +14,8 @@ spec:
     spec:
       containers:
       - name: pod-to-a-container
-        image: docker.io/cilium/json-mock:1.0
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
diff --git a/examples/kubernetes/connectivity-check/pod-to-b-intra-node.yaml b/examples/kubernetes/connectivity-check/pod-to-b-intra-node.yaml
index 1296b562db..b9c1ab49e2 100644
--- a/examples/kubernetes/connectivity-check/pod-to-b-intra-node.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-b-intra-node.yaml
@@ -14,7 +14,8 @@ spec:
     spec:
       containers:
       - name: pod-to-b-intra-node-container
-        image: docker.io/cilium/json-mock:1.0
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
diff --git a/examples/kubernetes/connectivity-check/pod-to-b-multi-node.yaml b/examples/kubernetes/connectivity-check/pod-to-b-multi-node-clusterip.yaml
similarity index 74%
rename from examples/kubernetes/connectivity-check/pod-to-b-multi-node.yaml
rename to examples/kubernetes/connectivity-check/pod-to-b-multi-node-clusterip.yaml
index 9edb0c63ff..e601b8f314 100644
--- a/examples/kubernetes/connectivity-check/pod-to-b-multi-node.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-b-multi-node-clusterip.yaml
@@ -1,20 +1,21 @@
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: pod-to-b-multi-node
+  name: pod-to-b-multi-node-clusterip
 spec:
   selector:
     matchLabels:
-      name: pod-to-b-multi-node
+      name: pod-to-b-multi-node-clusterip
   replicas: 1
   template:
     metadata:
       labels:
-        name: pod-to-b-multi-node
+        name: pod-to-b-multi-node-clusterip
     spec:
       containers:
       - name: pod-to-b-multi-node-container
-        image: docker.io/cilium/json-mock:1.0
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
diff --git a/examples/kubernetes/connectivity-check/pod-to-b-multi-node-headless.yaml b/examples/kubernetes/connectivity-check/pod-to-b-multi-node-headless.yaml
new file mode 100644
index 0000000000..471cecf990
--- /dev/null
+++ b/examples/kubernetes/connectivity-check/pod-to-b-multi-node-headless.yaml
@@ -0,0 +1,32 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: pod-to-b-multi-node-headless
+spec:
+  selector:
+    matchLabels:
+      name: pod-to-b-multi-node-headless
+  replicas: 1
+  template:
+    metadata:
+      labels:
+        name: pod-to-b-multi-node-headless
+    spec:
+      containers:
+      - name: pod-to-b-multi-node-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
+        imagePullPolicy: IfNotPresent
+        livenessProbe:
+          exec:
+            command: ["curl", "-sS", "-o", "/dev/null", "echo-b-headless"]
+      affinity:
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+          - labelSelector:
+              matchExpressions:
+              - key: name
+                operator: In
+                values:
+                - echo-b
+            topologyKey: "kubernetes.io/hostname"
diff --git a/examples/kubernetes/connectivity-check/pod-to-external-1111.yaml b/examples/kubernetes/connectivity-check/pod-to-external-1111.yaml
index 1a67714389..43da4dcc50 100644
--- a/examples/kubernetes/connectivity-check/pod-to-external-1111.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-external-1111.yaml
@@ -14,11 +14,12 @@ spec:
     spec:
       containers:
       - name: pod-to-a-external-1111-container
-        image: docker.io/cilium/json-mock:1.0
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
-            command: ["bash", "-c", "curl -sS --connect-timeout 5 -o /dev/null 1.1.1.1"]
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "1.1.1.1"]
         readinessProbe:
           exec:
-            command: ["bash", "-c", "curl -sS --connect-timeout 5 -o /dev/null 1.1.1.1"]
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "1.1.1.1"]
diff --git a/examples/kubernetes/connectivity-check/pod-to-external-fqdn-allow-google.yaml b/examples/kubernetes/connectivity-check/pod-to-external-fqdn-allow-google.yaml
index d8f6274c57..80fb55f125 100644
--- a/examples/kubernetes/connectivity-check/pod-to-external-fqdn-allow-google.yaml
+++ b/examples/kubernetes/connectivity-check/pod-to-external-fqdn-allow-google.yaml
@@ -1,36 +1,37 @@
 apiVersion: apps/v1
 kind: Deployment
 metadata:
-  name: pod-to-a-fqdn-allow-google-cnp
+  name: pod-to-external-fqdn-allow-google-cnp
 spec:
   selector:
     matchLabels:
-      name: pod-to-a-fqdn-allow-google-cnp
+      name: pod-to-external-fqdn-allow-google-cnp
   replicas: 1
   template:
     metadata:
       labels:
-        name: pod-to-a-fqdn-allow-google-cnp
+        name: pod-to-external-fqdn-allow-google-cnp
     spec:
       containers:
-      - name: pod-to-a-fqdn-allow-google-cnp-container
-        image: docker.io/cilium/json-mock:1.0
+      - name: pod-to-external-fqdn-allow-google-cnp-container
+        image: docker.io/byrnedo/alpine-curl:0.1.8
+        command: ["/bin/ash", "-c", "sleep 1000000000"]
         imagePullPolicy: IfNotPresent
         livenessProbe:
           exec:
-            command: ["bash", "-c", "curl -sS --connect-timeout 5 -o /dev/null www.google.com"]
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "www.google.com"]
         readinessProbe:
           exec:
-            command: ["bash", "-c", "curl -sS --connect-timeout 5 -o /dev/null www.google.com"]
+            command: ["curl", "-sS", "--connect-timeout", "5", "-o", "/dev/null", "www.google.com"]
 ---
 apiVersion: "cilium.io/v2"
 kind: CiliumNetworkPolicy
 metadata:
-  name: "pod-to-a-fqdn-allow-google-cnp"
+  name: "pod-to-external-fqdn-allow-google-cnp"
 spec:
   endpointSelector:
     matchLabels:
-      name: pod-to-a-fqdn-allow-google-cnp
+      name: pod-to-external-fqdn-allow-google-cnp
   egress:
   - toEndpoints:
     - matchLabels:
diff --git a/ginkgo.Jenkinsfile b/ginkgo.Jenkinsfile
index aba0325993..44129b3330 100644
--- a/ginkgo.Jenkinsfile
+++ b/ginkgo.Jenkinsfile
@@ -10,11 +10,11 @@ pipeline {
         VM_MEMORY = "4096"
         SERVER_BOX = "cilium/ubuntu"
         NETNEXT=setIfLabel("ci/net-next", "true", "false")
-        GINKGO_TIMEOUT="108m"
+        GINKGO_TIMEOUT="300m"
     }
 
     options {
-        timeout(time: 240, unit: 'MINUTES')
+        timeout(time: 300, unit: 'MINUTES')
         timestamps()
         ansiColor('xterm')
     }
@@ -22,7 +22,7 @@ pipeline {
     stages {
         stage('Checkout') {
             options {
-                timeout(time: 20, unit: 'MINUTES')
+                timeout(time: 30, unit: 'MINUTES')
             }
 
             steps {
@@ -39,7 +39,7 @@ pipeline {
         }
         stage('Precheck') {
             options {
-                timeout(time: 20, unit: 'MINUTES')
+                timeout(time: 30, unit: 'MINUTES')
             }
 
             environment {
@@ -94,7 +94,7 @@ pipeline {
         }
         stage ("Copy code and boot vms"){
             options {
-                timeout(time: 60, unit: 'MINUTES')
+                timeout(time: 120, unit: 'MINUTES')
             }
 
             environment {
@@ -112,7 +112,7 @@ pipeline {
                         sh 'mkdir -p ${GOPATH}/src/github.com/cilium'
                         sh 'cp -a ${WORKSPACE}/${PROJ_PATH} ${GOPATH}/${PROJ_PATH}'
                         retry(3) {
-                            timeout(time: 20, unit: 'MINUTES'){
+                            timeout(time: 30, unit: 'MINUTES'){
                                 sh 'cd ${TESTDIR}; vagrant destroy runtime --force'
                                 sh 'cd ${TESTDIR}; vagrant up runtime --provision'
                             }
@@ -145,7 +145,7 @@ pipeline {
                         sh 'mkdir -p ${GOPATH}/src/github.com/cilium'
                         sh 'cp -a ${WORKSPACE}/${PROJ_PATH} ${GOPATH}/${PROJ_PATH}'
                         retry(3) {
-                            timeout(time: 30, unit: 'MINUTES'){
+                            timeout(time: 45, unit: 'MINUTES'){
                                 dir("${TESTDIR}") {
                                     sh 'CILIUM_REGISTRY="$(./print-node-ip)" ./vagrant-ci-start.sh'
                                 }
@@ -174,7 +174,7 @@ pipeline {
                         sh 'mkdir -p ${GOPATH}/src/github.com/cilium'
                         sh 'cp -a ${WORKSPACE}/${PROJ_PATH} ${GOPATH}/${PROJ_PATH}'
                         retry(3) {
-                            timeout(time: 20, unit: 'MINUTES'){
+                            timeout(time: 45, unit: 'MINUTES'){
                                 dir("${TESTDIR}") {
                                     sh 'CILIUM_REGISTRY="$(./print-node-ip)" ./vagrant-ci-start.sh'
                                 }
@@ -195,7 +195,7 @@ pipeline {
         }
         stage ("BDD-Test-PR"){
             options {
-                timeout(time: 110, unit: 'MINUTES')
+                timeout(time: 130, unit: 'MINUTES')
             }
             environment {
                 FAILFAST=setIfLabel("ci/fail-fast", "true", "false")
diff --git a/pkg/endpoint/bpf.go b/pkg/endpoint/bpf.go
index b58cc00dbc..e3dfc23061 100644
--- a/pkg/endpoint/bpf.go
+++ b/pkg/endpoint/bpf.go
@@ -216,16 +216,18 @@ func (e *Endpoint) addNewRedirectsFromDesiredPolicy(ingress bool, desiredRedirec
 				direction = trafficdirection.Egress
 			}
 
-			keysFromFilter := l4.ToKeys(direction)
+			keysFromFilter := l4.ToMapState(direction)
 
-			for _, keyFromFilter := range keysFromFilter {
+			for keyFromFilter, entry := range keysFromFilter {
 				if oldEntry, ok := e.desiredPolicy.PolicyMapState[keyFromFilter]; ok {
 					updatedDesiredMapState[keyFromFilter] = oldEntry
 				} else {
 					insertedDesiredMapState[keyFromFilter] = struct{}{}
 				}
-
-				e.desiredPolicy.PolicyMapState[keyFromFilter] = policy.MapStateEntry{ProxyPort: redirectPort}
+				if entry != policy.NoRedirectEntry {
+					entry.ProxyPort = redirectPort
+				}
+				e.desiredPolicy.PolicyMapState[keyFromFilter] = entry
 			}
 		}
 	}
@@ -1060,13 +1062,15 @@ func (e *Endpoint) applyPolicyMapChanges() (proxyChanges bool, err error) {
 	//  collected on the newly computed desired policy, which is
 	//  not fully realized yet. This is why we get the map changes
 	//  from the desired policy here.
-	adds, deletes := e.desiredPolicy.PolicyMapChanges.ConsumeMapChanges()
+	adds, deletes := e.desiredPolicy.ConsumeMapChanges()
 
 	for keyToAdd, entry := range adds {
 		// Keep the existing proxy port, if any
-		entry.ProxyPort = e.realizedRedirects[policy.ProxyIDFromKey(e.ID, keyToAdd)]
-		if entry.ProxyPort != 0 {
-			proxyChanges = true
+		if entry != policy.NoRedirectEntry {
+			entry.ProxyPort = e.realizedRedirects[policy.ProxyIDFromKey(e.ID, keyToAdd)]
+			if entry.ProxyPort != 0 {
+				proxyChanges = true
+			}
 		}
 		if !e.addPolicyKey(keyToAdd, entry, true) {
 			errors++
diff --git a/pkg/envoy/server.go b/pkg/envoy/server.go
index aa5f16e9fd..38620c0ff7 100644
--- a/pkg/envoy/server.go
+++ b/pkg/envoy/server.go
@@ -752,7 +752,7 @@ func GetEnvoyHTTPRules(certManager policy.CertificateManager, l7Rules *api.L7Rul
 	return nil, true
 }
 
-func getPortNetworkPolicyRule(sel policy.CachedSelector, l7Parser policy.L7ParserType, l7Rules *policy.PerEpData) (*cilium.PortNetworkPolicyRule, bool) {
+func getPortNetworkPolicyRule(sel policy.CachedSelector, l7Parser policy.L7ParserType, l7Rules *policy.PerSelectorPolicy) (*cilium.PortNetworkPolicyRule, bool) {
 	// Optimize the policy if the endpoint selector is a wildcard by
 	// keeping remote policies list empty to match all remote policies.
 	var remotePolicies []uint64
@@ -775,6 +775,11 @@ func getPortNetworkPolicyRule(sel policy.CachedSelector, l7Parser policy.L7Parse
 		RemotePolicies: remotePolicies,
 	}
 
+	if l7Rules == nil {
+		// L3/L4 only rule, everything in L7 is allowed
+		return r, true
+	}
+
 	if l7Rules.TerminatingTLS != nil {
 		r.DownstreamTlsContext = getCiliumTLSContext(l7Rules.TerminatingTLS)
 	}
@@ -831,6 +836,48 @@ func getPortNetworkPolicyRule(sel policy.CachedSelector, l7Parser policy.L7Parse
 	return r, canShortCircuit
 }
 
+func getWildcardNetworkPolicyRule(selectors policy.L7DataMap) *cilium.PortNetworkPolicyRule {
+	// Use map to remove duplicates
+	remoteMap := make(map[uint64]struct{})
+	wildcardFound := false
+	for sel, l7 := range selectors {
+		if sel.IsWildcard() {
+			wildcardFound = true
+			break
+		}
+
+		for _, id := range sel.GetSelections() {
+			remoteMap[uint64(id)] = struct{}{}
+		}
+
+		if l7 != nil {
+			log.Warningf("L3-only rule for selector %v surprisingly has L7 rules (%v)!", sel, *l7)
+		}
+	}
+
+	if wildcardFound {
+		// Optimize the policy if the endpoint selector is a wildcard by
+		// keeping remote policies list empty to match all remote policies.
+		remoteMap = nil
+	} else if len(remoteMap) == 0 {
+		// No remote policies would match this rule. Discard it.
+		return nil
+	}
+
+	// Convert to a sorted slice
+	remotePolicies := make([]uint64, 0, len(remoteMap))
+	for id := range remoteMap {
+		remotePolicies = append(remotePolicies, id)
+	}
+	sort.Slice(remotePolicies, func(i, j int) bool {
+		return remotePolicies[i] < remotePolicies[j]
+	})
+
+	return &cilium.PortNetworkPolicyRule{
+		RemotePolicies: remotePolicies,
+	}
+}
+
 func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool) []*cilium.PortNetworkPolicy {
 	if !policyEnforced {
 		// Return an allow-all policy.
@@ -855,32 +902,43 @@ func getDirectionNetworkPolicy(l4Policy policy.L4PolicyMap, policyEnforced bool)
 		pnp := &cilium.PortNetworkPolicy{
 			Port:     uint32(l4.Port),
 			Protocol: protocol,
-			Rules:    make([]*cilium.PortNetworkPolicyRule, 0, len(l4.L7RulesPerEp)),
+			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
-		for sel, l7 := range l4.L7RulesPerEp {
-			rule, cs := getPortNetworkPolicyRule(sel, l4.L7Parser, l7)
-			if !cs {
-				canShortCircuit = false
-			}
+
+		if l4.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)
 			if rule != nil {
 				if len(rule.RemotePolicies) == 0 && rule.L7 == nil {
-					// Got an allow-all rule, which would short-circuit all of
-					// the other rules. Just set no rules, which has the same
-					// effect of allowing all.
+					// Got an allow-all rule, which can short-circuit all of
+					// the other rules.
 					allowAll = true
 				}
-
 				pnp.Rules = append(pnp.Rules, rule)
 			}
+		} else {
+			for sel, l7 := range l4.L7RulesPerSelector {
+				rule, cs := getPortNetworkPolicyRule(sel, l4.L7Parser, l7)
+				if rule != nil {
+					if !cs {
+						canShortCircuit = false
+					}
+					if len(rule.RemotePolicies) == 0 && rule.L7 == nil {
+						// Got an allow-all rule, which can short-circuit all of
+						// the other rules.
+						allowAll = true
+					}
+					pnp.Rules = append(pnp.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")
diff --git a/pkg/fqdn/dnsproxy/proxy.go b/pkg/fqdn/dnsproxy/proxy.go
index d2c16a8bcd..f8eddf2407 100644
--- a/pkg/fqdn/dnsproxy/proxy.go
+++ b/pkg/fqdn/dnsproxy/proxy.go
@@ -34,6 +34,7 @@ import (
 	"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"
 	"github.com/cilium/cilium/pkg/spanstat"
 
 	"github.com/miekg/dns"
@@ -149,6 +150,9 @@ func (allow perEPAllow) setPortRulesForID(endpointID uint64, destPort uint16, ne
 
 	newRE := make(cachedSelectorREEntry)
 	for selector, l7Rules := range newRules {
+		if l7Rules == nil {
+			l7Rules = &policy.PerSelectorPolicy{L7Rules: api.L7Rules{DNS: []api.PortRuleDNS{{MatchPattern: "*"}}}}
+		}
 		reStrings := make([]string, 0, len(l7Rules.DNS))
 		for _, dnsRule := range l7Rules.DNS {
 			if len(dnsRule.MatchName) > 0 {
diff --git a/pkg/logging/logfields/logfields.go b/pkg/logging/logfields/logfields.go
index f0e506b57c..fae4d4a454 100644
--- a/pkg/logging/logfields/logfields.go
+++ b/pkg/logging/logfields/logfields.go
@@ -87,6 +87,9 @@ const (
 	// L4PolicyID is the identifier of a L4 Policy
 	L4PolicyID = "PolicyID.L4"
 
+	// IsRedirect is a boolean for if the entry is a redirect or not
+	IsRedirect = "IsRedirect"
+
 	// DNSName is a FQDN or not fully qualified name intended for DNS lookups
 	DNSName = "dnsName"
 
diff --git a/pkg/policy/l4.go b/pkg/policy/l4.go
index c1afdea1bb..660c6eb6e7 100644
--- a/pkg/policy/l4.go
+++ b/pkg/policy/l4.go
@@ -18,6 +18,7 @@ import (
 	"bytes"
 	"encoding/json"
 	"fmt"
+	"reflect"
 	"sort"
 	"strconv"
 	"sync/atomic"
@@ -66,7 +67,7 @@ func (t *TLSContext) MarshalJSON() ([]byte, error) {
 	return json.Marshal(&redacted)
 }
 
-type PerEpData struct {
+type PerSelectorPolicy struct {
 	// TerminatingTLS is the TLS context for the connection terminated by
 	// the L7 proxy.  For egress policy this specifies the server-side TLS
 	// parameters to be applied on the connections originated from the local
@@ -84,6 +85,7 @@ type PerEpData struct {
 	OriginatingTLS *TLSContext `json:"originatingTLS,omitempty"`
 
 	// Pre-computed HTTP rules with resolved k8s secrets
+	// Computed after rule merging is complete!
 	EnvoyHTTPRules *cilium.HttpNetworkPolicyRules `json:"-"`
 
 	// CanShortCircuit is true if all 'EnvoyHTTPRules' may be
@@ -93,8 +95,16 @@ type PerEpData struct {
 	api.L7Rules
 }
 
+// Equal returns true if 'a' and 'b' represent the same L7 Rules
+func (a *PerSelectorPolicy) Equal(b *PerSelectorPolicy) bool {
+	return a == nil && b == nil || a != nil && b != nil &&
+		a.TerminatingTLS.Equal(b.TerminatingTLS) &&
+		a.OriginatingTLS.Equal(b.OriginatingTLS) &&
+		reflect.DeepEqual(a.L7Rules, b.L7Rules)
+}
+
 // L7DataMap contains a map of L7 rules per endpoint where key is a CachedSelector
-type L7DataMap map[CachedSelector]*PerEpData
+type L7DataMap map[CachedSelector]*PerSelectorPolicy
 
 func (l7 L7DataMap) MarshalJSON() ([]byte, error) {
 	if len(l7) == 0 {
@@ -175,21 +185,19 @@ type L4Filter struct {
 	Protocol api.L4Proto `json:"protocol"`
 	// U8Proto is the Protocol in numeric format, or 0 for NONE
 	U8Proto u8proto.U8proto `json:"-"`
-	// allowsAllAtL3 indicates whether this filter allows all traffic at L3.
-	// This can be determined by checking whether 'Endpoints' contains
-	// 'wildcardCachedSelector', but caching this information instead is
-	// much more performant.
-	allowsAllAtL3 bool
-	// CachedSelectors limits the labels for allowing traffic (to / from).
-	// This includes selectors for destinations affected by entity-based
-	// and CIDR-based policy.
-	// Holds references to the CachedSelectors, which must be released!
-	CachedSelectors CachedSelectorSlice `json:"l3-selectors,omitempty"`
+	// wildcard is the cached selector representing a wildcard in this filter, if any.
+	// This is nil the wildcard selector in not in 'L7RulesPerSelector'.
+	// When the wildcard selector is in 'L7RulesPerSelector' this is set to that
+	// same selector, which can then be used as a map key to find the corresponding
+	// L4-only L7 policy (which can be nil).
+	wildcard CachedSelector
+	// L7RulesPerSelector is a list of L7 rules per endpoint passed to the L7 proxy.
+	// nil values represent cached selectors that have no L7 restriction.
+	// Holds references to the cached selectors, which must be released!
+	L7RulesPerSelector L7DataMap `json:"l7-rules,omitempty"`
 	// L7Parser specifies the L7 protocol parser (optional). If specified as
 	// an empty string, then means that no L7 proxy redirect is performed.
 	L7Parser L7ParserType `json:"-"`
-	// L7RulesPerEp is a list of L7 rules per endpoint passed to the L7 proxy (optional)
-	L7RulesPerEp L7DataMap `json:"l7-rules,omitempty"`
 	// Ingress is true if filter applies at ingress; false if it applies at egress.
 	Ingress bool `json:"-"`
 	// The rule labels of this Filter
@@ -199,10 +207,22 @@ type L4Filter struct {
 	policy unsafe.Pointer // *L4Policy
 }
 
-// CopyL7RulesPerEndpoint returns a shallow copy of the L7RulesPerEp of the
+// SelectsAllEndpoints returns whether the L4Filter selects all
+// endpoints, which is true if the wildcard endpoint selector is present in the
+// map.
+func (l4 *L4Filter) SelectsAllEndpoints() bool {
+	for cs := range l4.L7RulesPerSelector {
+		if cs.IsWildcard() {
+			return true
+		}
+	}
+	return false
+}
+
+// CopyL7RulesPerEndpoint returns a shallow copy of the L7RulesPerSelector of the
 // L4Filter.
 func (l4 *L4Filter) CopyL7RulesPerEndpoint() L7DataMap {
-	return l4.L7RulesPerEp.ShallowCopy()
+	return l4.L7RulesPerSelector.ShallowCopy()
 }
 
 // GetL7Parser returns the L7ParserType of the L4Filter.
@@ -220,86 +240,92 @@ func (l4 *L4Filter) GetPort() uint16 {
 	return uint16(l4.Port)
 }
 
-// AllowsAllAtL3 returns whether this L4Filter applies to all endpoints at L3.
-func (l4 *L4Filter) AllowsAllAtL3() bool {
-	return l4.allowsAllAtL3
-}
-
-// HasL3DependentL7Rules returns true if this L4Filter is created from rules
-// that require an L3 match as well as specific L7 rules.
-func (l4 *L4Filter) HasL3DependentL7Rules() bool {
-	switch len(l4.L7RulesPerEp) {
-	case 0:
-		// No L7 rules.
-		return false
-	case 1:
-		// loop to get access to the first and only key in the map
-		for cs := range l4.L7RulesPerEp {
-			// If L3 is wildcarded, this filter corresponds to L4-only rule(s).
-			return !cs.IsWildcard()
-		}
-	}
-	return true
-}
-
-// ToKeys converts filter into a list of Keys.
-func (l4 *L4Filter) ToKeys(direction trafficdirection.TrafficDirection) []Key {
-	keysToAdd := []Key{}
+// ToMapState converts filter into a MapState with two possible values:
+// - NoRedirectEntry (ProxyPort = 0): No proxy redirection is needed for this key
+// - Entry with any other port #: Proxy redirection is required for this key,
+//                                caller must replace the ProxyPort with the actual
+//                                listening port number.
+// 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 {
 	port := uint16(l4.Port)
 	proto := uint8(l4.U8Proto)
 
-	if l4.AllowsAllAtL3() {
-		if l4.Port == 0 {
-			// Allow-all
-			log.WithFields(logrus.Fields{
-				logfields.TrafficDirection: direction,
-			}).Debug("ToKeys: allow all")
-
-			keyToAdd := Key{
-				DestPort:         0,
-				Nexthdr:          0,
-				TrafficDirection: direction.Uint8(),
-			}
-			keysToAdd = append(keysToAdd, keyToAdd)
-		} else {
-			// L4 allow
+	keysToAdd := MapState{}
+	keyToAdd := Key{
+		Identity:         0,    // Set in the loop below (if not wildcard)
+		DestPort:         port, // NOTE: Port is in host byte-order!
+		Nexthdr:          proto,
+		TrafficDirection: direction.Uint8(),
+	}
+
+	// find the L7 rules for the wildcard entry, if any
+	var wildcardL7Policy *PerSelectorPolicy
+	if l4.wildcard != nil {
+		wildcardL7Policy = l4.L7RulesPerSelector[l4.wildcard]
+	}
+
+	for cs, l7 := range l4.L7RulesPerSelector {
+		// Skip generating L3/L4 keys if L4-only key (for the same L4 port and
+		// protocol) has the same effect w.r.t. redirecting to the proxy or not,
+		// considering that L3/L4 key should redirect if L4-only key does. In
+		// summary, if have both L3/L4 and L4-only keys:
+		//
+		// L3/L4        L4-only      Skip generating L3/L4 key
+		// redirect     none         no
+		// no redirect  none         no
+		// redirect     no redirect  no   (this case tested below)
+		// no redirect  no redirect  yes  (same effect)
+		// redirect     redirect     yes  (same effect)
+		// no redirect  redirect     yes  (must redirect if L4-only redirects)
+		//
+		// 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("ToKeys: L4 allow all")
-
-			keyToAdd := Key{
-				Identity: 0,
-				// NOTE: Port is in host byte-order!
-				DestPort:         port,
-				Nexthdr:          proto,
-				TrafficDirection: direction.Uint8(),
+			}).Debug("ToMapState: Skipping L3/L4 key due to existing L4-only key")
+			continue
+		}
+		entry := NoRedirectEntry
+		if l7 != nil {
+			entry = redirectEntry
+		}
+
+		if cs.IsWildcard() {
+			keyToAdd.Identity = 0
+			keysToAdd.RedirectPreferredInsert(keyToAdd, entry)
+
+			if port == 0 {
+				// Allow-all
+				log.WithFields(logrus.Fields{
+					logfields.TrafficDirection: direction,
+				}).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")
 			}
-			keysToAdd = append(keysToAdd, keyToAdd)
+			continue
 		}
-		if !l4.HasL3DependentL7Rules() {
-			return keysToAdd
-		} // else we need to calculate all L3-dependent L4 peers below.
-	}
 
-	for _, cs := range l4.CachedSelectors {
 		identities := cs.GetSelections()
 		log.WithFields(logrus.Fields{
 			logfields.TrafficDirection: direction,
 			logfields.EndpointSelector: cs,
 			logfields.PolicyID:         identities,
-		}).Debug("ToKeys: Allowed remote IDs")
+		}).Debug("ToMapState: Allowed remote IDs")
+
 		for _, id := range identities {
-			srcID := id.Uint32()
-			keyToAdd := Key{
-				Identity: srcID,
-				// NOTE: Port is in host byte-order!
-				DestPort:         port,
-				Nexthdr:          proto,
-				TrafficDirection: direction.Uint8(),
-			}
-			keysToAdd = append(keysToAdd, keyToAdd)
+			keyToAdd.Identity = id.Uint32()
+			keysToAdd.RedirectPreferredInsert(keyToAdd, entry)
 		}
 	}
 
@@ -319,9 +345,10 @@ func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, selections
 		logfields.DeletedPolicyID:  deleted,
 	}).Debug("identities selected by L4Filter updated")
 
-	// Skip updates on filter that wildcards L3.
-	// This logic mirrors the one in ToKeys().
-	if l4.AllowsAllAtL3() && !l4.HasL3DependentL7Rules() {
+	// Skip updates on wildcard selectors, as datapath and L7
+	// proxies do not need enumeration of all ids for L3 wildcard.
+	// This mirrors the per-selector logic in ToMapState().
+	if selector.IsWildcard() {
 		return
 	}
 
@@ -335,14 +362,15 @@ func (l4 *L4Filter) IdentitySelectionUpdated(selector CachedSelector, selections
 		if l4.Ingress {
 			direction = trafficdirection.Ingress
 		}
-		l4Policy.AccumulateMapChanges(added, deleted, uint16(l4.Port), uint8(l4.U8Proto), direction)
+		l4Policy.AccumulateMapChanges(added, deleted, uint16(l4.Port), uint8(l4.U8Proto), direction,
+			l4.L7RulesPerSelector[selector] != nil)
 	}
 }
 
 func (l4 *L4Filter) cacheIdentitySelector(sel api.EndpointSelector, selectorCache *SelectorCache) CachedSelector {
 	cs, added := selectorCache.AddIdentitySelector(l4, sel)
 	if added {
-		l4.CachedSelectors = append(l4.CachedSelectors, cs)
+		l4.L7RulesPerSelector[cs] = nil // no l7 rules (yet)
 	}
 	return cs
 }
@@ -362,7 +390,7 @@ func (l4 *L4Filter) cacheFQDNSelectors(selectors api.FQDNSelectorSlice, selector
 func (l4 *L4Filter) cacheFQDNSelector(sel api.FQDNSelector, selectorCache *SelectorCache) CachedSelector {
 	cs, added := selectorCache.AddFQDNSelector(l4, sel)
 	if added {
-		l4.CachedSelectors = append(l4.CachedSelectors, cs)
+		l4.L7RulesPerSelector[cs] = nil // no l7 rules (yet)
 	}
 	return cs
 }
@@ -373,20 +401,24 @@ func (l7 L7DataMap) GetRelevantRulesForKafka(nid identity.NumericIdentity) []api
 
 	for cs, r := range l7 {
 		if cs.IsWildcard() || cs.Selects(nid) {
+			if r == nil {
+				r = &PerSelectorPolicy{L7Rules: api.L7Rules{Kafka: []api.PortRuleKafka{}}}
+			}
 			rules = append(rules, r.Kafka...)
 		}
 	}
 	return rules
 }
 
-func (l7 L7DataMap) addRulesForEndpoints(rules api.L7Rules, endpoints []CachedSelector, terminatingTLS, originatingTLS *TLSContext) {
-	perEpData := &PerEpData{
+// add L7 rules for all endpoints in the L7DataMap
+func (l7 L7DataMap) addRulesForEndpoints(rules api.L7Rules, terminatingTLS, originatingTLS *TLSContext) {
+	l7policy := &PerSelectorPolicy{
 		L7Rules:        rules,
 		TerminatingTLS: terminatingTLS,
 		OriginatingTLS: originatingTLS,
 	}
-	for _, epsel := range endpoints {
-		l7[epsel] = perEpData
+	for epsel := range l7 {
+		l7[epsel] = l7policy
 	}
 }
 
@@ -441,19 +473,17 @@ func createL4Filter(policyCtx PolicyContext, peerEndpoints api.EndpointSelectorS
 	u8p, _ := u8proto.ParseProtocol(string(protocol))
 
 	l4 := &L4Filter{
-		Port:             int(p),
-		Protocol:         protocol,
-		U8Proto:          u8p,
-		L7RulesPerEp:     make(L7DataMap),
-		DerivedFromRules: labels.LabelArrayList{ruleLabels},
-		Ingress:          ingress,
+		Port:               int(p),
+		Protocol:           protocol,
+		U8Proto:            u8p,
+		L7RulesPerSelector: make(L7DataMap),
+		DerivedFromRules:   labels.LabelArrayList{ruleLabels},
+		Ingress:            ingress,
 	}
 
 	if peerEndpoints.SelectsAllEndpoints() {
-		l4.cacheIdentitySelector(api.WildcardEndpointSelector, selectorCache)
-		l4.allowsAllAtL3 = true
+		l4.wildcard = l4.cacheIdentitySelector(api.WildcardEndpointSelector, selectorCache)
 	} else {
-		l4.CachedSelectors = make(CachedSelectorSlice, 0, len(peerEndpoints))
 		l4.cacheIdentitySelectors(peerEndpoints, selectorCache)
 		l4.cacheFQDNSelectors(fqdns, selectorCache)
 	}
@@ -485,32 +515,42 @@ func createL4Filter(policyCtx PolicyContext, peerEndpoints api.EndpointSelectorS
 				l4.L7Parser = (L7ParserType)(rule.Rules.L7Proto)
 			}
 			if !rule.Rules.IsEmpty() {
-				l4.L7RulesPerEp.addRulesForEndpoints(*rule.Rules, l4.CachedSelectors, terminatingTLS, originatingTLS)
+				l4.L7RulesPerSelector.addRulesForEndpoints(*rule.Rules, terminatingTLS, originatingTLS)
 			}
 		}
 
 		// we need this to redirect DNS UDP (or ANY, which is more useful)
 		if len(rule.Rules.DNS) > 0 {
 			l4.L7Parser = ParserTypeDNS
-			l4.L7RulesPerEp.addRulesForEndpoints(*rule.Rules, l4.CachedSelectors, terminatingTLS, originatingTLS)
+			l4.L7RulesPerSelector.addRulesForEndpoints(*rule.Rules, terminatingTLS, originatingTLS)
 		}
 	}
 
 	return l4, nil
 }
 
+func (l4 *L4Filter) removeSelectors(selectorCache *SelectorCache) {
+	selectors := make(CachedSelectorSlice, 0, len(l4.L7RulesPerSelector))
+	for cs := range l4.L7RulesPerSelector {
+		selectors = append(selectors, cs)
+	}
+	selectorCache.RemoveSelectors(selectors, l4)
+}
+
 // detach releases the references held in the L4Filter and must be called before
 // the filter is left to be garbage collected.
 func (l4 *L4Filter) detach(selectorCache *SelectorCache) {
-	selectorCache.RemoveSelectors(l4.CachedSelectors, l4)
+	l4.removeSelectors(selectorCache)
 	l4.attach(nil, nil)
 }
 
 func (l4 *L4Filter) attach(ctx PolicyContext, l4Policy *L4Policy) {
 	// Compute Envoy policies when a policy is ready to be used
 	if ctx != nil {
-		for _, perEpData := range l4.L7RulesPerEp {
-			perEpData.EnvoyHTTPRules, perEpData.CanShortCircuit = ctx.GetEnvoyHTTPRules(&perEpData.L7Rules)
+		for _, l7policy := range l4.L7RulesPerSelector {
+			if l7policy != nil {
+				l7policy.EnvoyHTTPRules, l7policy.CanShortCircuit = ctx.GetEnvoyHTTPRules(&l7policy.L7Rules)
+			}
 		}
 	}
 
@@ -535,11 +575,10 @@ func createL4IngressFilter(policyCtx PolicyContext, fromEndpoints api.EndpointSe
 	// If the filter would apply L7 rules for the Host, when we should accept everything from host,
 	// then wildcard Host at L7.
 	if !rule.Rules.IsEmpty() && hostWildcardL7 {
-		for _, cs := range filter.CachedSelectors {
+		for cs := range filter.L7RulesPerSelector {
 			if cs.Selects(identity.ReservedIdentityHost) {
 				hostSelector := api.ReservedEndpointSelectors[labels.IDNameHost]
-				hcs := filter.cacheIdentitySelector(hostSelector, policyCtx.GetSelectorCache())
-				filter.L7RulesPerEp[hcs] = &PerEpData{} // TODO: use nil instead?
+				filter.cacheIdentitySelector(hostSelector, policyCtx.GetSelectorCache())
 			}
 		}
 	}
@@ -592,13 +631,13 @@ func (l4 *L4Filter) String() string {
 
 // Note: Only used for policy tracing
 func (l4 *L4Filter) matchesLabels(labels labels.LabelArray) bool {
-	if l4.AllowsAllAtL3() {
+	if l4.wildcard != nil {
 		return true
 	} else if len(labels) == 0 {
 		return false
 	}
 
-	for _, sel := range l4.CachedSelectors {
+	for sel := range l4.L7RulesPerSelector {
 		// slow, but OK for tracing
 		if idSel, ok := sel.(*labelIdentitySelector); ok && idSel.xxxMatches(labels) {
 			return true
@@ -762,10 +801,10 @@ 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) {
+	port uint16, proto uint8, direction trafficdirection.TrafficDirection, redirect bool) {
 	l4.mutex.RLock()
 	for epPolicy := range l4.users {
-		epPolicy.PolicyMapChanges.AccumulateMapChanges(adds, deletes, port, proto, direction)
+		epPolicy.policyMapChanges.AccumulateMapChanges(adds, deletes, port, proto, direction, redirect)
 	}
 	l4.mutex.RUnlock()
 }
diff --git a/pkg/policy/mapstate.go b/pkg/policy/mapstate.go
index c4f1cddd05..4c6478668e 100644
--- a/pkg/policy/mapstate.go
+++ b/pkg/policy/mapstate.go
@@ -66,16 +66,39 @@ func (k Key) IsEgress() bool {
 type MapStateEntry struct {
 	// The proxy port, in host byte order.
 	// If 0 (default), there is no proxy redirection for the corresponding
-	// Key.
+	// Key. Any other value signifies proxy redirection.
 	ProxyPort uint16
 }
 
+// NoRedirectEntry is a special MapStateEntry used to signify that the
+// entry is not redirecting to a proxy.
+var NoRedirectEntry = MapStateEntry{ProxyPort: 0}
+
+// redirectEntry is a MapStateEntry used to signify that the entry
+// must redirect to a proxy. Any non-zero value would do, as the
+// callers replace this with the actual proxy listening port number
+// before the entry is added to the actual bpf map.
+var redirectEntry = MapStateEntry{ProxyPort: 1}
+
+// RedirectPreferredInsert inserts a new entry giving priority to L7-redirects by
+// not overwriting a L7-redirect entry with a non-redirect entry
+func (keys MapState) RedirectPreferredInsert(key Key, entry MapStateEntry) {
+	if entry == NoRedirectEntry {
+		if _, ok := keys[key]; ok {
+			// Key already exist, keep the existing entry so that
+			// a redirect entry is never overwritten by a non-redirect
+			// entry
+			return
+		}
+	}
+	keys[key] = entry
+}
+
 // DetermineAllowLocalhostIngress determines whether communication should be allowed
 // from the localhost. It inserts the Key corresponding to the localhost in
 // the desiredPolicyKeys if the localhost is allowed to communicate with the
 // endpoint.
 func (keys MapState) DetermineAllowLocalhostIngress(l4Policy *L4Policy) {
-
 	if option.Config.AlwaysAllowLocalhost() {
 		keys[localHostKey] = MapStateEntry{}
 	}
@@ -124,7 +147,7 @@ type MapChanges struct {
 // cases where an identity is first added and then deleted, or first
 // deleted and then added.
 func (mc *MapChanges) AccumulateMapChanges(adds, deletes []identity.NumericIdentity,
-	port uint16, proto uint8, direction trafficdirection.TrafficDirection) {
+	port uint16, proto uint8, direction trafficdirection.TrafficDirection, redirect bool) {
 	key := Key{
 		// The actual identity is set in the loops below
 		Identity: 0,
@@ -133,8 +156,9 @@ func (mc *MapChanges) AccumulateMapChanges(adds, deletes []identity.NumericIdent
 		Nexthdr:          proto,
 		TrafficDirection: direction.Uint8(),
 	}
-	value := MapStateEntry{
-		ProxyPort: 0, // Will be updated by the caller when applicable
+	var value MapStateEntry // defaults to ProxyPort 0 == no redirect
+	if redirect {
+		value = redirectEntry // Special entry to mark the need for redirection
 	}
 
 	if option.Config.Debug {
@@ -144,6 +168,7 @@ func (mc *MapChanges) AccumulateMapChanges(adds, deletes []identity.NumericIdent
 			logfields.Port:             port,
 			logfields.Protocol:         proto,
 			logfields.TrafficDirection: direction,
+			logfields.IsRedirect:       redirect,
 		}).Debug("AccumulateMapChanges")
 	}
 
@@ -154,7 +179,9 @@ func (mc *MapChanges) AccumulateMapChanges(adds, deletes []identity.NumericIdent
 		}
 		for _, id := range adds {
 			key.Identity = id.Uint32()
-			mc.adds[key] = value
+			// insert but do not allow NoRedirectEntry to overwrite a redirect entry
+			mc.adds.RedirectPreferredInsert(key, value)
+
 			// Remove a potential previously deleted key
 			if mc.deletes != nil {
 				delete(mc.deletes, key)
@@ -177,9 +204,9 @@ func (mc *MapChanges) AccumulateMapChanges(adds, deletes []identity.NumericIdent
 	mc.mutex.Unlock()
 }
 
-// ConsumeMapChanges transfers the changes from MapChanges to the caller.
+// consumeMapChanges transfers the changes from MapChanges to the caller.
 // May return nil maps.
-func (mc *MapChanges) ConsumeMapChanges() (adds, deletes MapState) {
+func (mc *MapChanges) consumeMapChanges() (adds, deletes MapState) {
 	mc.mutex.Lock()
 	adds = mc.adds
 	mc.adds = nil
diff --git a/pkg/policy/repository.go b/pkg/policy/repository.go
index c71545835d..ecfa2ca9ca 100644
--- a/pkg/policy/repository.go
+++ b/pkg/policy/repository.go
@@ -184,66 +184,6 @@ func (state *traceState) trace(rules int, ctx *SearchContext) {
 	}
 }
 
-// This belongs to l4.go as this manipulates L4Filters
-func wildcardL3L4Rule(proto api.L4Proto, port int, endpoints api.EndpointSelectorSlice,
-	ruleLabels labels.LabelArray, l4Policy L4PolicyMap, selectorCache *SelectorCache) {
-	for _, filter := range l4Policy {
-		if proto != filter.Protocol || (port != 0 && port != filter.Port) {
-			continue
-		}
-		switch filter.L7Parser {
-		case ParserTypeNone:
-			continue
-		case ParserTypeHTTP:
-			// Wildcard at L7 all the endpoints allowed at L3 or L4.
-			for _, sel := range endpoints {
-				cs := filter.cacheIdentitySelector(sel, selectorCache)
-				filter.L7RulesPerEp[cs] = &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					}}
-			}
-		case ParserTypeKafka:
-			// Wildcard at L7 all the endpoints allowed at L3 or L4.
-			for _, sel := range endpoints {
-				rule := api.PortRuleKafka{}
-				rule.Sanitize()
-				cs := filter.cacheIdentitySelector(sel, selectorCache)
-				filter.L7RulesPerEp[cs] = &PerEpData{
-					L7Rules: api.L7Rules{
-						Kafka: []api.PortRuleKafka{rule},
-					}}
-			}
-		case ParserTypeDNS:
-			// Wildcard at L7 all the endpoints allowed at L3 or L4.
-			for _, sel := range endpoints {
-				// Wildcarding at L7 for DNS is specified via allowing all via
-				// MatchPattern!
-				rule := api.PortRuleDNS{
-					MatchPattern: "*",
-				}
-				rule.Sanitize()
-				cs := filter.cacheIdentitySelector(sel, selectorCache)
-				filter.L7RulesPerEp[cs] = &PerEpData{
-					L7Rules: api.L7Rules{
-						DNS: []api.PortRuleDNS{rule},
-					}}
-			}
-		default:
-			// Wildcard at L7 all the endpoints allowed at L3 or L4.
-			for _, sel := range endpoints {
-				cs := filter.cacheIdentitySelector(sel, selectorCache)
-				filter.L7RulesPerEp[cs] = &PerEpData{
-					L7Rules: api.L7Rules{
-						L7Proto: filter.L7Parser.String(),
-						L7:      []api.PortRuleL7{},
-					}}
-			}
-		}
-		filter.DerivedFromRules = append(filter.DerivedFromRules, ruleLabels)
-	}
-}
-
 // ResolveL4IngressPolicy resolves the L4 ingress policy for a set of endpoints
 // by searching the policy repository for `PortRule` rules that are attached to
 // a `Rule` where the EndpointSelector matches `ctx.To`. `ctx.From` takes no effect and
diff --git a/pkg/policy/resolve.go b/pkg/policy/resolve.go
index 71f0b2067d..f6d33a59da 100644
--- a/pkg/policy/resolve.go
+++ b/pkg/policy/resolve.go
@@ -66,8 +66,8 @@ type EndpointPolicy struct {
 	// All fields within the Key and the proxy port must be in host byte-order.
 	PolicyMapState MapState
 
-	// PolicyMapChanges collects pending changes to the PolicyMapState
-	PolicyMapChanges MapChanges
+	// policyMapChanges collects pending changes to the PolicyMapState
+	policyMapChanges MapChanges
 
 	// PolicyOwner describes any type which consumes this EndpointPolicy object.
 	PolicyOwner PolicyOwner
@@ -125,10 +125,10 @@ func (p *selectorPolicy) DistillPolicy(policyOwner PolicyOwner) *EndpointPolicy
 	// computeDesiredL4PolicyMapEntires() call finishes may
 	// already be applied to the PolicyMapState, specifically:
 	//
-	// - PolicyMapChanges may contain an addition of an entry that
+	// - policyMapChanges may contain an addition of an entry that
 	//   is already added to the PolicyMapState
 	//
-	// - PolicyMapChanges may congtain a deletion of an entry that
+	// - policyMapChanges may contain a deletion of an entry that
 	//   has already been deleted from PolicyMapState
 	p.insertUser(calculatedPolicy)
 
@@ -154,26 +154,33 @@ func (p *EndpointPolicy) computeDesiredL4PolicyMapEntries() {
 
 func (p *EndpointPolicy) computeDirectionL4PolicyMapEntries(l4PolicyMap L4PolicyMap, direction trafficdirection.TrafficDirection) {
 	for _, filter := range l4PolicyMap {
-		keysFromFilter := filter.ToKeys(direction)
-		for _, keyFromFilter := range keysFromFilter {
-			var proxyPort uint16
-			// Preserve the already-allocated proxy ports for redirects that
-			// already exist.
-			if filter.IsRedirect() {
-				proxyPort = p.PolicyOwner.LookupRedirectPort(filter)
+		keysFromFilter := filter.ToMapState(direction)
+		for keyFromFilter, entry := range keysFromFilter {
+			// Fix up the proxy port for entries that need proxy redirection
+			if entry != NoRedirectEntry {
+				entry.ProxyPort = p.PolicyOwner.LookupRedirectPort(filter)
 				// 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
-				// e.addNewRedirectsFromMap once the port has been allocated.
-				if proxyPort == 0 {
+				// e.addNewRedirectsFromDesiredPolicy() once the port has been allocated.
+				if entry == NoRedirectEntry {
 					continue
 				}
 			}
-			p.PolicyMapState[keyFromFilter] = MapStateEntry{ProxyPort: proxyPort}
+			p.PolicyMapState[keyFromFilter] = entry
 		}
 	}
 }
 
+// ConsumeMapChanges transfers the changes from MapChanges to the caller,
+// locking the selector cache to make sure concurrent identity updates
+// have completed.
+func (p *EndpointPolicy) ConsumeMapChanges() (adds, deletes MapState) {
+	p.selectorPolicy.SelectorCache.mutex.Lock()
+	defer p.selectorPolicy.SelectorCache.mutex.Unlock()
+	return p.policyMapChanges.consumeMapChanges()
+}
+
 // NewEndpointPolicy returns an empty EndpointPolicy stub.
 func NewEndpointPolicy(repo *Repository) *EndpointPolicy {
 	return &EndpointPolicy{
diff --git a/pkg/policy/rule.go b/pkg/policy/rule.go
index 1b0c78b386..3ab3122179 100644
--- a/pkg/policy/rule.go
+++ b/pkg/policy/rule.go
@@ -58,46 +58,52 @@ func (r *rule) String() string {
 	return fmt.Sprintf("%v", r.EndpointSelector)
 }
 
-func (l4 *L4Filter) mergeCachedSelectors(from *L4Filter, selectorCache *SelectorCache) {
-	for _, cs := range from.CachedSelectors {
-		if l4.CachedSelectors.Insert(cs) {
-			// Update selector owner to the existingFilter
-			selectorCache.ChangeUser(cs, from, l4)
+func (epd *PerSelectorPolicy) appendL7WildcardRule(ctx *SearchContext) *PerSelectorPolicy {
+	// Wildcard rule only needs to be appended if some rules already exist
+	switch {
+	case len(epd.L7Rules.HTTP) > 0:
+		rule := api.PortRuleHTTP{}
+		if !rule.Exists(epd.L7Rules) {
+			ctx.PolicyTrace("   Merging HTTP wildcard rule: %+v\n", rule)
+			epd.L7Rules.HTTP = append(epd.L7Rules.HTTP, rule)
 		} else {
-			// selector was already in existingFilter.CachedSelectors, release
-			selectorCache.RemoveSelector(cs, from)
+			ctx.PolicyTrace("   Merging HTTP wildcard rule, equal rule already exists: %+v\n", rule)
+		}
+	case len(epd.L7Rules.Kafka) > 0:
+		rule := api.PortRuleKafka{}
+		rule.Sanitize()
+		if !rule.Exists(epd.L7Rules) {
+			ctx.PolicyTrace("   Merging Kafka wildcard rule: %+v\n", rule)
+			epd.L7Rules.Kafka = append(epd.L7Rules.Kafka, rule)
+		} else {
+			ctx.PolicyTrace("   Merging Kafka wildcard rule, equal rule already exists: %+v\n", rule)
+		}
+	case len(epd.L7Rules.DNS) > 0:
+		// Wildcarding at L7 for DNS is specified via allowing all via
+		// MatchPattern!
+		rule := api.PortRuleDNS{MatchPattern: "*"}
+		rule.Sanitize()
+		if !rule.Exists(epd.L7Rules) {
+			ctx.PolicyTrace("   Merging DNS wildcard rule: %+v\n", rule)
+			epd.L7Rules.DNS = append(epd.L7Rules.DNS, rule)
+		} else {
+			ctx.PolicyTrace("   Merging DNS wildcard rule, equal rule already exists: %+v\n", rule)
+		}
+	case epd.L7Rules.L7Proto != "" && len(epd.L7Rules.L7) > 0:
+		rule := api.PortRuleL7{}
+		if !rule.Exists(epd.L7Rules) {
+			ctx.PolicyTrace("   Merging L7 wildcard rule: %+v\n", rule)
+			epd.L7Rules.L7 = append(epd.L7Rules.L7, rule)
+		} else {
+			ctx.PolicyTrace("   Merging L7 wildcard rule, equal rule already exists: %+v\n", rule)
 		}
 	}
-	from.CachedSelectors = nil
+	return epd
 }
 
 func mergePortProto(ctx *SearchContext, existingFilter, filterToMerge *L4Filter, selectorCache *SelectorCache) error {
-	// Handle cases where filter we are merging new rule with, new rule itself
-	// allows all traffic on L3, or both rules allow all traffic on L3.
-	if existingFilter.AllowsAllAtL3() {
-		// Case 1: existing filter selects all endpoints, which means that this filter
-		// can now simply select all endpoints.
-		//
-		// Release references held by filterToMerge.CachedSelectors
-		if !filterToMerge.HasL3DependentL7Rules() {
-			selectorCache.RemoveSelectors(filterToMerge.CachedSelectors, filterToMerge)
-			filterToMerge.CachedSelectors = nil
-		}
-	} else {
-		// Case 2: Merge selectors from filterToMerge to the existingFilter.
-		if filterToMerge.AllowsAllAtL3() {
-			// Allowing all, release selectors from existingFilter
-			if !existingFilter.HasL3DependentL7Rules() {
-				selectorCache.RemoveSelectors(existingFilter.CachedSelectors, existingFilter)
-				existingFilter.CachedSelectors = nil
-			}
-			existingFilter.allowsAllAtL3 = true
-		}
-	}
-	existingFilter.mergeCachedSelectors(filterToMerge, selectorCache)
-
-	// Merge the L7-related data from the arguments provided to this function
-	// with the existing L7-related data already in the filter.
+	// Merge the L7-related data from the filter to merge
+	// with the L7-related data already in the existing filter.
 	if filterToMerge.L7Parser != ParserTypeNone {
 		if existingFilter.L7Parser == ParserTypeNone {
 			existingFilter.L7Parser = filterToMerge.L7Parser
@@ -107,16 +113,41 @@ func mergePortProto(ctx *SearchContext, existingFilter, filterToMerge *L4Filter,
 		}
 	}
 
-	for cs, newL7Rules := range filterToMerge.L7RulesPerEp {
-		// skip merging for reserved:none, as it is never
-		// selected, and toFQDN rules currently translate to
-		// reserved:none as an endpoint selector, causing a
-		// merge conflict for different toFQDN destinations
-		// with different TLS contexts.
-		if cs.IsNone() {
-			continue
-		}
-		if l7Rules, ok := existingFilter.L7RulesPerEp[cs]; ok {
+	for cs, newL7Rules := range filterToMerge.L7RulesPerSelector {
+		// 'cs' will be merged or moved (see below), either way it needs
+		// to be removed from the map it is in now.
+		delete(filterToMerge.L7RulesPerSelector, cs)
+
+		if l7Rules, ok := existingFilter.L7RulesPerSelector[cs]; ok {
+			// existing filter already has 'cs', release and merge L7 rules
+			selectorCache.RemoveSelector(cs, filterToMerge)
+
+			// skip merging for reserved:none, as it is never
+			// selected, and toFQDN rules currently translate to
+			// reserved:none as an endpoint selector, causing a
+			// merge conflict for different toFQDN destinations
+			// with different TLS contexts.
+			if cs.IsNone() {
+				continue
+			}
+
+			if l7Rules.Equal(newL7Rules) {
+				continue // identical rules need no merging
+			}
+
+			// nil L7 rules wildcard L7. When merging with a non-nil rule, the nil must be expanded
+			// to an actual wildcard rule for the specific L7
+			if l7Rules == nil && newL7Rules != nil {
+				existingFilter.L7RulesPerSelector[cs] = newL7Rules.appendL7WildcardRule(ctx)
+				continue
+			}
+			if l7Rules != nil && newL7Rules == nil {
+				existingFilter.L7RulesPerSelector[cs] = l7Rules.appendL7WildcardRule(ctx)
+				continue
+			}
+
+			// Merge two non-identical sets of non-nil rules
+
 			if !newL7Rules.TerminatingTLS.Equal(l7Rules.TerminatingTLS) {
 				ctx.PolicyTrace("   Merge conflict: mismatching terminating TLS contexts %v/%v\n", newL7Rules.TerminatingTLS, l7Rules.TerminatingTLS)
 				return fmt.Errorf("cannot merge conflicting terminating TLS contexts for cached selector %s: (%v/%v)", cs.String(), newL7Rules.TerminatingTLS, l7Rules.TerminatingTLS)
@@ -178,11 +209,20 @@ func mergePortProto(ctx *SearchContext, existingFilter, filterToMerge *L4Filter,
 			default:
 				ctx.PolicyTrace("   No L7 rules to merge.\n")
 			}
-			existingFilter.L7RulesPerEp[cs] = l7Rules
-		} else {
-			existingFilter.L7RulesPerEp[cs] = newL7Rules
+			existingFilter.L7RulesPerSelector[cs] = l7Rules
+		} else { // 'cs' is not in the existing filter yet
+			// Update selector owner to the existing filter
+			selectorCache.ChangeUser(cs, filterToMerge, existingFilter)
+
+			// Move L7 rules over.
+			existingFilter.L7RulesPerSelector[cs] = newL7Rules
+
+			if cs.IsWildcard() {
+				existingFilter.wildcard = cs
+			}
 		}
 	}
+
 	return nil
 }
 
diff --git a/pkg/policy/rules.go b/pkg/policy/rules.go
index 1934bbd4fb..2dfa03f695 100644
--- a/pkg/policy/rules.go
+++ b/pkg/policy/rules.go
@@ -16,9 +16,6 @@ package policy
 
 import (
 	"fmt"
-	"strconv"
-
-	"github.com/cilium/cilium/pkg/policy/api"
 
 	v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
@@ -27,81 +24,6 @@ import (
 // to be written with []*rule as a receiver.
 type ruleSlice []*rule
 
-func (rules ruleSlice) wildcardL3L4Rules(ingress bool, l4Policy L4PolicyMap, requirements []v1.LabelSelectorRequirement, selectorCache *SelectorCache) {
-	// Duplicate L3-only rules into wildcard L7 rules.
-	for _, r := range rules {
-		if ingress {
-			for _, rule := range r.Ingress {
-				// Non-label-based rule. Ignore.
-				if !rule.AllowsWildcarding() {
-					continue
-				}
-
-				fromEndpoints := rule.GetSourceEndpointSelectorsWithRequirements(requirements)
-				ruleLabels := r.Rule.Labels.DeepCopy()
-
-				// L3-only rule.
-				if len(rule.ToPorts) == 0 && len(fromEndpoints) > 0 {
-					wildcardL3L4Rule(api.ProtoTCP, 0, fromEndpoints, ruleLabels, l4Policy, selectorCache)
-					wildcardL3L4Rule(api.ProtoUDP, 0, fromEndpoints, ruleLabels, l4Policy, selectorCache)
-				} else {
-					// L4-only or L3-dependent L4 rule.
-					//
-					// "fromEndpoints" may be empty here, which indicates that all L3 peers should
-					// be selected. If so, add the wildcard selector.
-					if len(fromEndpoints) == 0 {
-						fromEndpoints = append(fromEndpoints, api.WildcardEndpointSelector)
-					}
-					for _, toPort := range rule.ToPorts {
-						// L3/L4-only rule
-						if toPort.Rules.IsEmpty() {
-							for _, p := range toPort.Ports {
-								// Already validated via PortRule.Validate().
-								port, _ := strconv.ParseUint(p.Port, 0, 16)
-								wildcardL3L4Rule(p.Protocol, int(port), fromEndpoints, ruleLabels, l4Policy, selectorCache)
-							}
-						}
-					}
-				}
-			}
-		} else {
-			for _, rule := range r.Egress {
-				// Non-label-based rule. Ignore.
-				if !rule.AllowsWildcarding() {
-					continue
-				}
-
-				toEndpoints := rule.GetDestinationEndpointSelectorsWithRequirements(requirements)
-				ruleLabels := r.Rule.Labels.DeepCopy()
-
-				// L3-only rule.
-				if len(rule.ToPorts) == 0 && len(toEndpoints) > 0 {
-					wildcardL3L4Rule(api.ProtoTCP, 0, toEndpoints, ruleLabels, l4Policy, selectorCache)
-					wildcardL3L4Rule(api.ProtoUDP, 0, toEndpoints, ruleLabels, l4Policy, selectorCache)
-				} else {
-					// L4-only or L3-dependent L4 rule.
-					//
-					// "toEndpoints" may be empty here, which indicates that all L3 peers should
-					// be selected. If so, add the wildcard selector.
-					if len(toEndpoints) == 0 {
-						toEndpoints = append(toEndpoints, api.WildcardEndpointSelector)
-					}
-					for _, toPort := range rule.ToPorts {
-						// L3/L4-only rule
-						if toPort.Rules.IsEmpty() {
-							for _, p := range toPort.Ports {
-								// Already validated via PortRule.Validate().
-								port, _ := strconv.ParseUint(p.Port, 0, 16)
-								wildcardL3L4Rule(p.Protocol, int(port), toEndpoints, ruleLabels, l4Policy, selectorCache)
-							}
-						}
-					}
-				}
-			}
-		}
-	}
-}
-
 func (rules ruleSlice) resolveL4IngressPolicy(policyCtx PolicyContext, ctx *SearchContext) (L4PolicyMap, error) {
 	result := L4PolicyMap{}
 
@@ -142,8 +64,6 @@ func (rules ruleSlice) resolveL4IngressPolicy(policyCtx PolicyContext, ctx *Sear
 		}
 	}
 
-	matchedRules.wildcardL3L4Rules(true, result, requirements, policyCtx.GetSelectorCache())
-
 	state.trace(len(rules), ctx)
 
 	// Restore ctx in case caller uses it again.
@@ -193,8 +113,6 @@ func (rules ruleSlice) resolveL4EgressPolicy(policyCtx PolicyContext, ctx *Searc
 		}
 	}
 
-	matchedRules.wildcardL3L4Rules(false, result, requirements, policyCtx.GetSelectorCache())
-
 	state.trace(len(rules), ctx)
 
 	// Restore ctx in case caller uses it again.
diff --git a/pkg/policy/selectorcache.go b/pkg/policy/selectorcache.go
index f3f23a56d8..2d01ed3eae 100644
--- a/pkg/policy/selectorcache.go
+++ b/pkg/policy/selectorcache.go
@@ -97,17 +97,6 @@ func (s CachedSelectorSlice) SelectsAllEndpoints() bool {
 	return false
 }
 
-// Insert in a sorted order? Returns true if inserted, false if cs was already in
-func (s *CachedSelectorSlice) Insert(cs CachedSelector) bool {
-	for _, selector := range *s {
-		if selector == cs {
-			return false
-		}
-	}
-	*s = append(*s, cs)
-	return true
-}
-
 // CachedSelectionUser inserts selectors into the cache and gets update
 // callbacks whenever the set of selected numeric identities change for
 // the CachedSelectors pushed by it.
@@ -151,6 +140,10 @@ type CachedSelectionUser interface {
 // identitySelector is used as a map key, so it must not be implemented by a
 // map, slice, or a func, or a runtime panic will be triggered. In all
 // cases below identitySelector is being implemented by structs.
+//
+// Because the selector exposed to the user is used as a map key, it must always
+// be passed to the user as a pointer to the actual implementation type.
+// For this reason 'notifyUsers' must be implemented by each type separately.
 type identitySelector interface {
 	CachedSelector
 	addUser(CachedSelectionUser) (added bool)
@@ -334,16 +327,6 @@ func (f *fqdnSelector) removeUser(user CachedSelectionUser) (last bool) {
 	return removed
 }
 
-// lock must be held
-//
-// The caller is responsible for making sure the same identity is not
-// present in both 'added' and 'deleted'.
-func (s *selectorManager) notifyUsers(added, deleted []identity.NumericIdentity) {
-	for user := range s.users {
-		user.IdentitySelectionUpdated(s, s.GetSelections(), added, deleted)
-	}
-}
-
 // lock must be held
 func (s *selectorManager) numUsers() int {
 	return len(s.users)
@@ -384,6 +367,17 @@ type fqdnSelector struct {
 	dnsProxy identityNotifier
 }
 
+// lock must be held
+//
+// The caller is responsible for making sure the same identity is not
+// present in both 'added' and 'deleted'.
+func (f *fqdnSelector) notifyUsers(added, deleted []identity.NumericIdentity) {
+	for user := range f.users {
+		// pass 'f' to the user as '*fqdnSelector'
+		user.IdentitySelectionUpdated(f, f.GetSelections(), added, deleted)
+	}
+}
+
 // identityNotifier provides a means for other subsystems to be made aware of a
 // given FQDNSelector (currently pkg/fqdn) so that said subsystems can notify
 // the SelectorCache about new IPs (via CIDR Identities) which correspond to
@@ -424,6 +418,17 @@ type labelIdentitySelector struct {
 	namespaces []string // allowed namespaces, or ""
 }
 
+// lock must be held
+//
+// The caller is responsible for making sure the same identity is not
+// present in both 'added' and 'deleted'.
+func (l *labelIdentitySelector) notifyUsers(added, deleted []identity.NumericIdentity) {
+	for user := range l.users {
+		// pass 'l' to the user as '*labelIdentitySelector'
+		user.IdentitySelectionUpdated(l, l.GetSelections(), added, deleted)
+	}
+}
+
 // xxxMatches returns true if the CachedSelector matches given labels.
 // This is slow, but only used for policy tracing, so it's OK.
 func (l *labelIdentitySelector) xxxMatches(labels labels.LabelArray) bool {
diff --git a/pkg/policy/visibility.go b/pkg/policy/visibility.go
index 70db67e2cd..f54b07dfcc 100644
--- a/pkg/policy/visibility.go
+++ b/pkg/policy/visibility.go
@@ -146,7 +146,7 @@ func generateL7AllowAllRules(parser L7ParserType) L7DataMap {
 		m = L7DataMap{}
 		// Create an entry to explicitly allow all at L7 for DNS.
 		emptyL3Selector := &labelIdentitySelector{selectorManager: selectorManager{key: wildcardSelectorKey}, selector: api.WildcardEndpointSelector}
-		m[emptyL3Selector] = &PerEpData{
+		m[emptyL3Selector] = &PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{
 					{


Test patch:
diff --git a/daemon/policy_test.go b/daemon/policy_test.go
index d494b5e649..aacdac06f0 100644
--- a/daemon/policy_test.go
+++ b/daemon/policy_test.go
@@ -35,6 +35,7 @@ import (
 	v2 "github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
 	"github.com/cilium/cilium/pkg/k8s/types"
 	"github.com/cilium/cilium/pkg/labels"
+	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/metrics"
 	"github.com/cilium/cilium/pkg/option"
 	"github.com/cilium/cilium/pkg/policy"
@@ -89,6 +90,21 @@ var (
 			},
 		},
 	}
+	CNPAllowGETbarLog = api.PortRule{
+		Ports: CNPAllowTCP80.Ports,
+		Rules: &api.L7Rules{
+			HTTP: []api.PortRuleHTTP{
+				{
+					Method: "GET",
+					HeaderMatches: []*api.HeaderMatch{{
+						Mismatch: api.MismatchActionLog,
+						Name:     ":path",
+						Value:    "/bar",
+					}},
+				},
+			},
+		},
+	}
 
 	PNPAllowAll = cilium.PortNetworkPolicyRule_HttpRules{
 		HttpRules: &cilium.HttpNetworkPolicyRules{
@@ -128,6 +144,41 @@ var (
 			},
 		},
 	}
+
+	PNPAllowGETbarLog = cilium.PortNetworkPolicyRule_HttpRules{
+		HttpRules: &cilium.HttpNetworkPolicyRules{
+			HttpRules: []*cilium.HttpNetworkPolicyRule{
+				{
+					Headers: []*envoy_api_v2_route.HeaderMatcher{
+						{
+							Name: ":method",
+							HeaderMatchSpecifier: &envoy_api_v2_route.HeaderMatcher_SafeRegexMatch{
+								SafeRegexMatch: &envoy_type_matcher.RegexMatcher{
+									EngineType: googleRe2,
+									Regex:      "GET",
+								}},
+						},
+					},
+					HeaderMatches: []*cilium.HeaderMatch{
+						{
+							Name:           ":path",
+							Value:          "/bar",
+							MismatchAction: cilium.HeaderMatch_CONTINUE_ON_MISMATCH,
+						},
+					},
+				},
+			},
+		},
+	}
+
+	PNPAllowWildcardGETbar = cilium.PortNetworkPolicyRule_HttpRules{
+		HttpRules: &cilium.HttpNetworkPolicyRules{
+			HttpRules: []*cilium.HttpNetworkPolicyRule{
+				{},
+				PNPAllowGETbar.HttpRules.HttpRules[0],
+			},
+		},
+	}
 )
 
 // getXDSNetworkPolicies returns the representation of the xDS network policies
@@ -178,6 +229,7 @@ func (ds *DaemonSuite) regenerateEndpoint(c *C, e *endpoint.Endpoint) {
 }
 
 func (ds *DaemonSuite) TestUpdateConsumerMap(c *C) {
+	logging.ConfigureLogLevel(false) // Use 'true' for debugging
 	rules := api.Rules{
 		{
 			EndpointSelector: api.NewESFromLabels(lblBar),
@@ -290,18 +342,23 @@ func (ds *DaemonSuite) TestUpdateConsumerMap(c *C) {
 		Policy:           uint64(qaBarSecLblsCtx.ID),
 		ConntrackMapName: "global",
 		IngressPerPortPolicies: []*cilium.PortNetworkPolicy{
+			{
+				Port:     0,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules: []*cilium.PortNetworkPolicyRule{
+					{
+						RemotePolicies: expectedRemotePolicies,
+					},
+				},
+			},
 			{
 				Port:     80,
 				Protocol: envoy_api_v2_core.SocketAddress_TCP,
 				Rules: []*cilium.PortNetworkPolicyRule{
 					{
 						RemotePolicies: expectedRemotePolicies,
-						L7:             &PNPAllowAll,
+						L7:             &PNPAllowGETbar,
 					},
-					//{
-					//	RemotePolicies: expectedRemotePolicies,
-					//	L7:             &PNPAllowGETbar,
-					//},
 				},
 			},
 		},
@@ -337,21 +394,22 @@ func (ds *DaemonSuite) TestUpdateConsumerMap(c *C) {
 		ConntrackMapName: "global",
 		IngressPerPortPolicies: []*cilium.PortNetworkPolicy{
 			{
-				Port:     80,
+				Port:     0,
 				Protocol: envoy_api_v2_core.SocketAddress_TCP,
 				Rules: []*cilium.PortNetworkPolicyRule{
 					{
-						RemotePolicies: expectedRemotePolicies2,
-						L7:             &PNPAllowAll,
+						RemotePolicies: expectedRemotePolicies,
 					},
+				},
+			},
+			{
+				Port:     80,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules: []*cilium.PortNetworkPolicyRule{
 					{
 						RemotePolicies: expectedRemotePolicies,
-						L7:             &PNPAllowAll,
+						L7:             &PNPAllowGETbar,
 					},
-					//{
-					//	RemotePolicies: expectedRemotePolicies,
-					//	L7:             &PNPAllowGETbar,
-					//},
 				},
 			},
 		},
@@ -364,6 +422,90 @@ func (ds *DaemonSuite) TestUpdateConsumerMap(c *C) {
 }
 
 func (ds *DaemonSuite) TestL4_L7_Shadowing(c *C) {
+	logging.ConfigureLogLevel(false) // Use 'true' for debugging
+	// Prepare the identities necessary for testing
+	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
+	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
+	c.Assert(err, Equals, nil)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
+	qaFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
+	c.Assert(err, Equals, nil)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx)
+
+	rules := api.Rules{
+		{
+			EndpointSelector: api.NewESFromLabels(lblBar),
+			Ingress: []api.IngressRule{
+				{
+					ToPorts: []api.PortRule{
+						// Allow all on port 80 (no proxy)
+						CNPAllowTCP80,
+					},
+				},
+				{
+					FromEndpoints: []api.EndpointSelector{
+						api.NewESFromLabels(lblFoo),
+					},
+					ToPorts: []api.PortRule{
+						// Allow Port 80 GET /bar
+						CNPAllowGETbarLog,
+					},
+				},
+			},
+		},
+	}
+
+	ds.d.l7Proxy.RemoveAllNetworkPolicies()
+
+	_, err = ds.d.PolicyAdd(rules, nil)
+	c.Assert(err, Equals, nil)
+
+	// Prepare endpoints
+	cleanup, err := prepareEndpointDirs()
+	c.Assert(err, Equals, nil)
+	defer cleanup()
+
+	e := ds.prepareEndpoint(c, qaBarSecLblsCtx, true)
+	c.Assert(e.Allows(qaBarSecLblsCtx.ID), Equals, false)
+	c.Assert(e.Allows(qaFooSecLblsCtx.ID), Equals, false)
+
+	// Check that both policies have been updated in the xDS cache for the L7
+	// proxies.
+	networkPolicies := ds.getXDSNetworkPolicies(c, nil)
+	c.Assert(networkPolicies, HasLen, 2)
+
+	qaBarNetworkPolicy := networkPolicies[QAIPv4Addr.String()]
+	expectedNetworkPolicy := &cilium.NetworkPolicy{
+		Name:             QAIPv4Addr.String(),
+		Policy:           uint64(qaBarSecLblsCtx.ID),
+		ConntrackMapName: "global",
+		IngressPerPortPolicies: []*cilium.PortNetworkPolicy{
+			{
+				Port:     80,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules: []*cilium.PortNetworkPolicyRule{
+					{},
+					{
+						RemotePolicies: []uint64{uint64(qaFooSecLblsCtx.ID)},
+						L7:             &PNPAllowGETbarLog,
+					},
+				},
+			},
+		},
+		EgressPerPortPolicies: []*cilium.PortNetworkPolicy{ // Allow-all policy.
+			{Protocol: envoy_api_v2_core.SocketAddress_TCP},
+			{Protocol: envoy_api_v2_core.SocketAddress_UDP},
+		},
+	}
+	c.Assert(qaBarNetworkPolicy, checker.Equals, expectedNetworkPolicy)
+}
+
+// HTTP rules here have no side effects, so the L4 allow-all rule is
+// short-circuiting the HTTP rules (i.e., the network policy sent to
+// envoy does not even have the HTTP rules).
+func (ds *DaemonSuite) TestL4_L7_ShadowingShortCircuit(c *C) {
+	logging.ConfigureLogLevel(false) // Use 'true' for debugging
 	// Prepare the identities necessary for testing
 	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
 	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
@@ -425,11 +567,101 @@ func (ds *DaemonSuite) TestL4_L7_Shadowing(c *C) {
 			{
 				Port:     80,
 				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules:    nil,
+			},
+		},
+		EgressPerPortPolicies: []*cilium.PortNetworkPolicy{ // Allow-all policy.
+			{Protocol: envoy_api_v2_core.SocketAddress_TCP},
+			{Protocol: envoy_api_v2_core.SocketAddress_UDP},
+		},
+	}
+	c.Assert(qaBarNetworkPolicy, checker.Equals, expectedNetworkPolicy)
+}
+
+func (ds *DaemonSuite) TestL3_dependent_L7(c *C) {
+	logging.ConfigureLogLevel(true) // Use 'true' for debugging
+	defer logging.ConfigureLogLevel(false)
+
+	// Prepare the identities necessary for testing
+	qaBarLbls := labels.Labels{lblBar.Key: lblBar, lblQA.Key: lblQA}
+	qaBarSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaBarLbls, true)
+	c.Assert(err, Equals, nil)
+	defer ds.d.identityAllocator.Release(context.Background(), qaBarSecLblsCtx)
+	qaFooLbls := labels.Labels{lblFoo.Key: lblFoo, lblQA.Key: lblQA}
+	qaFooSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaFooLbls, true)
+	c.Assert(err, Equals, nil)
+	defer ds.d.identityAllocator.Release(context.Background(), qaFooSecLblsCtx)
+	qaJoeLbls := labels.Labels{lblJoe.Key: lblJoe, lblQA.Key: lblQA}
+	qaJoeSecLblsCtx, _, err := ds.d.identityAllocator.AllocateIdentity(context.Background(), qaJoeLbls, true)
+	c.Assert(err, Equals, nil)
+	defer ds.d.identityAllocator.Release(context.Background(), qaJoeSecLblsCtx)
+
+	rules := api.Rules{
+		{
+			EndpointSelector: api.NewESFromLabels(lblBar),
+			Ingress: []api.IngressRule{
+				{
+					FromEndpoints: []api.EndpointSelector{
+						api.NewESFromLabels(lblFoo),
+					},
+					ToPorts: []api.PortRule{
+						// Allow Port 80 GET /bar
+						CNPAllowGETbar,
+					},
+				},
+			},
+		},
+		{
+			EndpointSelector: api.NewESFromLabels(lblBar),
+			Ingress: []api.IngressRule{
+				{
+					FromEndpoints: []api.EndpointSelector{
+						api.NewESFromLabels(lblJoe),
+					},
+				},
+			},
+		},
+	}
+
+	ds.d.l7Proxy.RemoveAllNetworkPolicies()
+
+	_, err = ds.d.PolicyAdd(rules, nil)
+	c.Assert(err, Equals, nil)
+
+	// Prepare endpoints
+	cleanup, err := prepareEndpointDirs()
+	c.Assert(err, Equals, nil)
+	defer cleanup()
+
+	e := ds.prepareEndpoint(c, qaBarSecLblsCtx, true)
+	c.Assert(e.Allows(qaBarSecLblsCtx.ID), Equals, false)
+	c.Assert(e.Allows(qaFooSecLblsCtx.ID), Equals, false)
+	c.Assert(e.Allows(qaJoeSecLblsCtx.ID), Equals, true)
+
+	// Check that both policies have been updated in the xDS cache for the L7
+	// proxies.
+	networkPolicies := ds.getXDSNetworkPolicies(c, nil)
+	c.Assert(networkPolicies, HasLen, 2)
+
+	qaBarNetworkPolicy := networkPolicies[QAIPv4Addr.String()]
+	expectedNetworkPolicy := &cilium.NetworkPolicy{
+		Name:             QAIPv4Addr.String(),
+		Policy:           uint64(qaBarSecLblsCtx.ID),
+		ConntrackMapName: "global",
+		IngressPerPortPolicies: []*cilium.PortNetworkPolicy{
+			{
+				Port:     0,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
 				Rules: []*cilium.PortNetworkPolicyRule{
 					{
-						RemotePolicies: nil,
-						L7:             &PNPAllowAll,
+						RemotePolicies: []uint64{uint64(qaJoeSecLblsCtx.ID)},
 					},
+				},
+			},
+			{
+				Port:     80,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules: []*cilium.PortNetworkPolicyRule{
 					{
 						RemotePolicies: []uint64{uint64(qaFooSecLblsCtx.ID)},
 						L7:             &PNPAllowGETbar,
@@ -684,12 +916,39 @@ func (ds *DaemonSuite) TestIncrementalPolicy(c *C) {
 			return false
 		}
 		qaBarNetworkPolicy = networkPolicies[QAIPv4Addr.String()]
-		return qaBarNetworkPolicy != nil && len(qaBarNetworkPolicy.IngressPerPortPolicies) == 1
+		return qaBarNetworkPolicy != nil && len(qaBarNetworkPolicy.IngressPerPortPolicies) == 2
 	}, time.Second*1)
 	c.Assert(err, IsNil)
-	c.Assert(qaBarNetworkPolicy.IngressPerPortPolicies[0].Rules, HasLen, 1)
-	c.Assert(qaBarNetworkPolicy.IngressPerPortPolicies[0].Rules[0].RemotePolicies, HasLen, 1)
-	c.Assert(qaBarNetworkPolicy.IngressPerPortPolicies[0].Rules[0].RemotePolicies[0], Equals, uint64(qaFooID.ID))
+	c.Assert(qaBarNetworkPolicy, checker.Equals, &cilium.NetworkPolicy{
+		Name:             QAIPv4Addr.String(),
+		Policy:           uint64(qaBarSecLblsCtx.ID),
+		ConntrackMapName: "global",
+		IngressPerPortPolicies: []*cilium.PortNetworkPolicy{
+			{
+				Port:     0,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules: []*cilium.PortNetworkPolicyRule{
+					{
+						RemotePolicies: []uint64{uint64(qaFooID.ID)},
+					},
+				},
+			},
+			{
+				Port:     80,
+				Protocol: envoy_api_v2_core.SocketAddress_TCP,
+				Rules: []*cilium.PortNetworkPolicyRule{
+					{
+						RemotePolicies: []uint64{uint64(qaFooID.ID)},
+						L7:             &PNPAllowGETbar,
+					},
+				},
+			},
+		},
+		EgressPerPortPolicies: []*cilium.PortNetworkPolicy{ // Allow-all policy.
+			{Protocol: envoy_api_v2_core.SocketAddress_TCP},
+			{Protocol: envoy_api_v2_core.SocketAddress_UDP},
+		},
+	})
 
 	// Delete the endpoint.
 	e.Delete(ds.d, ds.d.ipam, &dummyManager{}, endpoint.DeleteConfig{})
diff --git a/pkg/envoy/server_test.go b/pkg/envoy/server_test.go
index 272cf3fa3e..c6e7871083 100644
--- a/pkg/envoy/server_test.go
+++ b/pkg/envoy/server_test.go
@@ -202,11 +202,11 @@ var (
 	cachedRequiresV2Selector, _ = testSelectorCache.AddIdentitySelector(dummySelectorCacheUser, RequiresV2Selector)
 )
 
-var L7Rules1 = &policy.PerEpData{L7Rules: api.L7Rules{HTTP: []api.PortRuleHTTP{*PortRuleHTTP1, *PortRuleHTTP2}}}
+var L7Rules1 = &policy.PerSelectorPolicy{L7Rules: api.L7Rules{HTTP: []api.PortRuleHTTP{*PortRuleHTTP1, *PortRuleHTTP2}}}
 
-var L7Rules1HeaderMatch = &policy.PerEpData{L7Rules: api.L7Rules{HTTP: []api.PortRuleHTTP{*PortRuleHTTP1, *PortRuleHTTP2HeaderMatch}}}
+var L7Rules1HeaderMatch = &policy.PerSelectorPolicy{L7Rules: api.L7Rules{HTTP: []api.PortRuleHTTP{*PortRuleHTTP1, *PortRuleHTTP2HeaderMatch}}}
 
-var L7Rules2 = &policy.PerEpData{L7Rules: api.L7Rules{HTTP: []api.PortRuleHTTP{*PortRuleHTTP1}}}
+var L7Rules2 = &policy.PerSelectorPolicy{L7Rules: api.L7Rules{HTTP: []api.PortRuleHTTP{*PortRuleHTTP1}}}
 
 var ExpectedPortNetworkPolicyRule1 = &cilium.PortNetworkPolicyRule{
 	RemotePolicies: []uint64{1001, 1002},
@@ -288,7 +288,7 @@ var L4PolicyMap1 = map[string]*policy.L4Filter{
 		Port:     80,
 		Protocol: api.ProtoTCP,
 		L7Parser: policy.ParserTypeHTTP,
-		L7RulesPerEp: policy.L7DataMap{
+		L7RulesPerSelector: policy.L7DataMap{
 			cachedSelector1: L7Rules1,
 		},
 	},
@@ -299,7 +299,7 @@ var L4PolicyMap1HeaderMatch = map[string]*policy.L4Filter{
 		Port:     80,
 		Protocol: api.ProtoTCP,
 		L7Parser: policy.ParserTypeHTTP,
-		L7RulesPerEp: policy.L7DataMap{
+		L7RulesPerSelector: policy.L7DataMap{
 			cachedSelector1: L7Rules1HeaderMatch,
 		},
 	},
@@ -310,40 +310,40 @@ var L4PolicyMap1RequiresV2 = map[string]*policy.L4Filter{
 		Port:     80,
 		Protocol: api.ProtoTCP,
 		L7Parser: policy.ParserTypeHTTP,
-		L7RulesPerEp: policy.L7DataMap{
+		L7RulesPerSelector: policy.L7DataMap{
 			cachedRequiresV2Selector1: L7Rules1,
 		},
 	},
 }
 
 var L4PolicyMap2 = map[string]*policy.L4Filter{
-	"8080/UDP": {
+	"8080/TCP": {
 		Port:     8080,
-		Protocol: api.ProtoUDP,
+		Protocol: api.ProtoTCP,
 		L7Parser: policy.ParserTypeHTTP,
-		L7RulesPerEp: policy.L7DataMap{
+		L7RulesPerSelector: policy.L7DataMap{
 			cachedSelector2: L7Rules2,
 		},
 	},
 }
 
 var L4PolicyMap3 = map[string]*policy.L4Filter{
-	"80/UDP": {
+	"80/TCP": {
 		Port:     80,
 		Protocol: api.ProtoTCP,
 		L7Parser: policy.ParserTypeHTTP,
-		L7RulesPerEp: policy.L7DataMap{
+		L7RulesPerSelector: policy.L7DataMap{
 			wildcardCachedSelector: L7Rules1,
 		},
 	},
 }
 
 var L4PolicyMap3RequiresV2 = map[string]*policy.L4Filter{
-	"80/UDP": {
+	"80/TCP": {
 		Port:     80,
 		Protocol: api.ProtoTCP,
 		L7Parser: policy.ParserTypeHTTP,
-		L7RulesPerEp: policy.L7DataMap{
+		L7RulesPerSelector: policy.L7DataMap{
 			cachedRequiresV2Selector: L7Rules1,
 		},
 	},
@@ -354,8 +354,8 @@ var L4PolicyMap4 = map[string]*policy.L4Filter{
 	"80/TCP": {
 		Port:     80,
 		Protocol: api.ProtoTCP,
-		L7RulesPerEp: policy.L7DataMap{
-			cachedSelector1: &policy.PerEpData{L7Rules: api.L7Rules{}},
+		L7RulesPerSelector: policy.L7DataMap{
+			cachedSelector1: &policy.PerSelectorPolicy{L7Rules: api.L7Rules{}},
 		},
 	},
 }
@@ -365,8 +365,8 @@ var L4PolicyMap5 = map[string]*policy.L4Filter{
 	"80/TCP": {
 		Port:     80,
 		Protocol: api.ProtoTCP,
-		L7RulesPerEp: policy.L7DataMap{
-			wildcardCachedSelector: &policy.PerEpData{L7Rules: api.L7Rules{}},
+		L7RulesPerSelector: policy.L7DataMap{
+			wildcardCachedSelector: &policy.PerSelectorPolicy{L7Rules: api.L7Rules{}},
 		},
 	},
 }
@@ -394,7 +394,7 @@ var ExpectedPerPortPolicies1HeaderMatch = []*cilium.PortNetworkPolicy{
 var ExpectedPerPortPolicies2 = []*cilium.PortNetworkPolicy{
 	{
 		Port:     8080,
-		Protocol: envoy_api_v2_core.SocketAddress_UDP,
+		Protocol: envoy_api_v2_core.SocketAddress_TCP,
 		Rules: []*cilium.PortNetworkPolicyRule{
 			ExpectedPortNetworkPolicyRule2,
 		},
@@ -592,8 +592,8 @@ var L4PolicyL7 = &policy.L4Policy{
 		"9090/TCP": {
 			Port: 9090, Protocol: api.ProtoTCP,
 			L7Parser: "tester",
-			L7RulesPerEp: policy.L7DataMap{
-				cachedSelector1: &policy.PerEpData{L7Rules: api.L7Rules{
+			L7RulesPerSelector: policy.L7DataMap{
+				cachedSelector1: &policy.PerSelectorPolicy{L7Rules: api.L7Rules{
 					L7Proto: "tester",
 					L7: []api.PortRuleL7{
 						map[string]string{
diff --git a/pkg/fqdn/dnsproxy/proxy_test.go b/pkg/fqdn/dnsproxy/proxy_test.go
index 3b8caf4bf4..9347116a02 100644
--- a/pkg/fqdn/dnsproxy/proxy_test.go
+++ b/pkg/fqdn/dnsproxy/proxy_test.go
@@ -144,12 +144,18 @@ var (
 	cachedDstID1Selector, _ = testSelectorCache.AddIdentitySelector(dummySelectorCacheUser, DstID1Selector)
 	DstID2Selector          = api.NewESFromLabels(labels.ParseSelectLabel("k8s:Dst2=test"))
 	cachedDstID2Selector, _ = testSelectorCache.AddIdentitySelector(dummySelectorCacheUser, DstID2Selector)
+	DstID3Selector          = api.NewESFromLabels(labels.ParseSelectLabel("k8s:Dst3=test"))
+	cachedDstID3Selector, _ = testSelectorCache.AddIdentitySelector(dummySelectorCacheUser, DstID3Selector)
+	DstID4Selector          = api.NewESFromLabels(labels.ParseSelectLabel("k8s:Dst4=test"))
+	cachedDstID4Selector, _ = testSelectorCache.AddIdentitySelector(dummySelectorCacheUser, DstID4Selector)
 
 	epID1   = uint64(111)
 	epID2   = uint64(222)
 	epID3   = uint64(333)
 	dstID1  = identity.NumericIdentity(1001)
 	dstID2  = identity.NumericIdentity(2002)
+	dstID3  = identity.NumericIdentity(3003)
+	dstID4  = identity.NumericIdentity(4004)
 	dstPort = uint16(53) // Set below when we setup the server!
 )
 
@@ -158,6 +164,8 @@ func (s *DNSProxyTestSuite) SetUpSuite(c *C) {
 	testSelectorCache.UpdateIdentities(cache.IdentityCache{
 		dstID1: labels.Labels{"Dst1": labels.NewLabel("Dst1", "test", labels.LabelSourceK8s)}.LabelArray(),
 		dstID2: labels.Labels{"Dst2": labels.NewLabel("Dst2", "test", labels.LabelSourceK8s)}.LabelArray(),
+		dstID3: labels.Labels{"Dst3": labels.NewLabel("Dst3", "test", labels.LabelSourceK8s)}.LabelArray(),
+		dstID4: labels.Labels{"Dst4": labels.NewLabel("Dst4", "test", labels.LabelSourceK8s)}.LabelArray(),
 	}, nil)
 
 	s.repo = policy.NewPolicyRepository(nil, nil)
@@ -218,7 +226,7 @@ func (s *DNSProxyTestSuite) TearDownSuite(c *C) {
 func (s *DNSProxyTestSuite) TestRejectFromDifferentEndpoint(c *C) {
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -237,7 +245,7 @@ func (s *DNSProxyTestSuite) TestRejectFromDifferentEndpoint(c *C) {
 func (s *DNSProxyTestSuite) TestAcceptFromMatchingEndpoint(c *C) {
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -256,7 +264,7 @@ func (s *DNSProxyTestSuite) TestAcceptFromMatchingEndpoint(c *C) {
 func (s *DNSProxyTestSuite) TestAcceptNonRegex(c *C) {
 	name := "simple.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -275,7 +283,7 @@ func (s *DNSProxyTestSuite) TestAcceptNonRegex(c *C) {
 func (s *DNSProxyTestSuite) TestRejectNonRegex(c *C) {
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -294,7 +302,7 @@ func (s *DNSProxyTestSuite) TestRejectNonRegex(c *C) {
 func (s *DNSProxyTestSuite) TestRejectNonMatchingRefusedResponse(c *C) {
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -330,7 +338,7 @@ func (s *DNSProxyTestSuite) TestRespondViaCorrectProtocol(c *C) {
 
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -359,7 +367,7 @@ func (s *DNSProxyTestSuite) TestRespondMixedCaseInRequestResponse(c *C) {
 	// vector :( )
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -390,7 +398,7 @@ func (s *DNSProxyTestSuite) TestRespondMixedCaseInRequestResponse(c *C) {
 func (s *DNSProxyTestSuite) TestCheckAllowedTwiceRemovedOnce(c *C) {
 	name := "cilium.io."
 	l7map := policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{{MatchName: name}},
 			},
@@ -435,6 +443,8 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 	// | EP1  | DstID2 |      53 | cilium.io      |
 	// | EP1  | DstID1 |      54 | example.com    |
 	// | EP3  | DstID1 |      53 | example.com    |
+	// | EP3  | DstID3 |      53 | *              |
+	// | EP3  | DstID4 |      53 | nil            |
 	// +------+--------+---------+----------------+
 	//
 	// Cases:
@@ -451,6 +461,8 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 	// |    8 | EPID3 | DstID1 |   53 | aws.amazon.com | Rejected | EPID3 is only allowed to ask DstID1 on Port 53 for example.com |
 	// |    8 | EPID3 | DstID1 |   54 | example.com    | Rejected | EPID3 is only allowed to ask DstID1 on Port 53 for example.com |
 	// |    9 | EPID3 | DstID2 |   53 | example.com    | Rejected | EPID3 is only allowed to ask DstID1 on Port 53 for example.com |
+	// |   10 | EPID3 | DstID3 |   53 | example.com    | Allowed  | Allowed due to wildcard match pattern                          |
+	// |   11 | EPID3 | DstID4 |   53 | example.com    | Allowed  | Allowed due to a nil rule                                      |
 	// +------+-------+--------+------+----------------+----------+----------------------------------------------------------------+
 
 	// Setup rules
@@ -458,7 +470,7 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 	//	| EP1  | DstID1 |      53 | aws.amazon.com |
 	//	| EP1  | DstID2 |      53 | cilium.io      |
 	err := s.proxy.UpdateAllowed(epID1, 53, policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{
 					{MatchPattern: "*.ubuntu.com."},
@@ -466,7 +478,7 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 				},
 			},
 		},
-		cachedDstID2Selector: &policy.PerEpData{
+		cachedDstID2Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{
 					{MatchPattern: "cilium.io."},
@@ -478,7 +490,7 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 
 	//	| EP1  | DstID1 |      54 | example.com    |
 	err = s.proxy.UpdateAllowed(epID1, 54, policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{
 					{MatchPattern: "example.com."},
@@ -489,14 +501,24 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 	c.Assert(err, Equals, nil, Commentf("Could not update with rules"))
 
 	// | EP3  | DstID1 |      53 | example.com    |
+	// | EP3  | DstID3 |      53 | *              |
+	// | EP3  | DstID4 |      53 | nil            |
 	err = s.proxy.UpdateAllowed(epID3, 53, policy.L7DataMap{
-		cachedDstID1Selector: &policy.PerEpData{
+		cachedDstID1Selector: &policy.PerSelectorPolicy{
 			L7Rules: api.L7Rules{
 				DNS: []api.PortRuleDNS{
 					{MatchPattern: "example.com."},
 				},
 			},
 		},
+		cachedDstID3Selector: &policy.PerSelectorPolicy{
+			L7Rules: api.L7Rules{
+				DNS: []api.PortRuleDNS{
+					{MatchPattern: "*"},
+				},
+			},
+		},
+		cachedDstID4Selector: nil,
 	})
 	c.Assert(err, Equals, nil, Commentf("Could not update with rules"))
 
@@ -550,4 +572,14 @@ func (s *DNSProxyTestSuite) TestFullPathDependence(c *C) {
 	allowed, err = s.proxy.CheckAllowed(epID3, 53, dstID2, "example.com")
 	c.Assert(err, Equals, nil, Commentf("Error when checking allowed"))
 	c.Assert(allowed, Equals, false, Commentf("request was allowed when it should be rejected"))
+
+	// Case 10 | EPID3 | DstID3 |   53 | example.com    | Allowed due to wildcard match pattern
+	allowed, err = s.proxy.CheckAllowed(epID3, 53, dstID3, "example.com")
+	c.Assert(err, Equals, nil, Commentf("Error when checking allowed"))
+	c.Assert(allowed, Equals, true, Commentf("request was rejected when it should be allowed"))
+
+	// Case 11 | EPID3 | DstID4 |   53 | example.com    | Allowed due to a nil rule
+	allowed, err = s.proxy.CheckAllowed(epID3, 53, dstID4, "example.com")
+	c.Assert(err, Equals, nil, Commentf("Error when checking allowed"))
+	c.Assert(allowed, Equals, true, Commentf("request was rejected when it should be allowed"))
 }
diff --git a/pkg/k8s/network_policy_test.go b/pkg/k8s/network_policy_test.go
index da405f6a78..918d196dbd 100644
--- a/pkg/k8s/network_policy_test.go
+++ b/pkg/k8s/network_policy_test.go
@@ -175,10 +175,9 @@ func (s *K8sSuite) TestParseNetworkPolicyIngress(c *C) {
 	c.Assert(ingressL4Policy, checker.Equals, policy.L4PolicyMap{
 		"80/TCP": {
 			Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-			CachedSelectors: policy.CachedSelectorSlice{cachedEPSelector},
-			L7Parser:        policy.ParserTypeNone,
-			L7RulesPerEp:    policy.L7DataMap{},
-			Ingress:         true,
+			L7Parser:           policy.ParserTypeNone,
+			L7RulesPerSelector: policy.L7DataMap{cachedEPSelector: nil},
+			Ingress:            true,
 			DerivedFromRules: []labels.LabelArray{
 				labels.ParseLabelArray(
 					"k8s:"+k8sConst.PolicyLabelName,
@@ -503,10 +502,9 @@ func (s *K8sSuite) TestParseNetworkPolicyEgress(c *C) {
 	c.Assert(egressL4Policy, checker.DeepEquals, policy.L4PolicyMap{
 		"80/TCP": {
 			Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-			CachedSelectors: policy.CachedSelectorSlice{cachedEPSelector},
-			L7Parser:        policy.ParserTypeNone,
-			L7RulesPerEp:    policy.L7DataMap{},
-			Ingress:         false,
+			L7Parser:           policy.ParserTypeNone,
+			L7RulesPerSelector: policy.L7DataMap{cachedEPSelector: nil},
+			Ingress:            false,
 			DerivedFromRules: []labels.LabelArray{
 				labels.ParseLabelArray(
 					"k8s:"+k8sConst.PolicyLabelName,
diff --git a/pkg/policy/distillery_test.go b/pkg/policy/distillery_test.go
index 659d823adf..f69277c40e 100644
--- a/pkg/policy/distillery_test.go
+++ b/pkg/policy/distillery_test.go
@@ -246,14 +246,14 @@ var (
 	mapKeyAllow___L4 = Key{0, 80, 6, dirIngress}
 	mapKeyAllowAll__ = Key{0, 0, 0, dirIngress}
 	// Desired map entries for no L7 redirect / redirect to Proxy
-	mapEntryL7None_ = MapStateEntry{l7RedirectNone_}
-	mapEntryL7Proxy = MapStateEntry{l7RedirectProxy}
+	mapEntryL7None_ = NoRedirectEntry
+	mapEntryL7Proxy = redirectEntry
 )
 
 // combineL4L7 returns a new PortRule that refers to the specified l4 ports and
 // l7 rules.
 func combineL4L7(l4 []api.PortRule, l7 *api.L7Rules) []api.PortRule {
-	result := make([]api.PortRule, len(l4))
+	result := make([]api.PortRule, 0, len(l4))
 	for _, pr := range l4 {
 		result = append(result, api.PortRule{
 			Ports: pr.Ports,
@@ -314,52 +314,13 @@ func (d *policyDistillery) distillPolicy(epLabels labels.LabelArray) (MapState,
 	// Handle L4 ingress from each identity in the cache to the endpoint.
 	io.WriteString(d.log, "[distill] Producing L4 filter keys\n")
 	for _, l4 := range l4IngressPolicy {
-		io.WriteString(d.log, fmt.Sprintf("[distill] Processing L4Filter (l3: %+v), (l4: %d/%s), (l7: %+v)\n", l4.CachedSelectors, l4.Port, l4.Protocol, l4.L7RulesPerEp))
-		for _, key := range l4.ToKeys(0) {
-			io.WriteString(d.log, fmt.Sprintf("[distill] L4 ingress allow %+v (parser=%s, redirect=%t)\n", key, l4.L7Parser, l4.IsRedirect()))
-			if l4.IsRedirect() {
-				result[key] = MapStateEntry{l7RedirectProxy}
-			} else {
-				result[key] = MapStateEntry{l7RedirectNone_}
-			}
+		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) {
+			io.WriteString(d.log, fmt.Sprintf("[distill] L4 ingress allow %+v (parser=%s, redirect=%t)\n", key, l4.L7Parser, entry != NoRedirectEntry))
+			result[key] = entry
 		}
 	}
 	l4IngressPolicy.Detach(d.Repository.GetSelectorCache())
-
-	// Handle L3-wildcard of L7 destinations
-	// Eg, when you have L4+L7 "allow /public on 80" with L3 "allow all from foo"
-	// Initially, three keys would be generated: L4+L7, L3+L4+L7, and L3.
-	// Here, we remove the L3+L4+L7 key if it overlaps with the L4+L7,
-	// but only if they have the same L7 redirect.
-	//
-	// For these the BPF policy order of attempting L3+L4 lookup, L4 lookup,
-	// then L3 lookup means that three keys are not strictly necessary; the
-	// correct behaviour can be encoded with two keys - an L4 key and L3 key.
-	nKeys := 0
-	l3l4keys := make([]Key, len(result))
-	for k := range result {
-		if k.Identity > 0 && k.DestPort > 0 {
-			l3l4keys[nKeys] = k
-			nKeys++
-		}
-	}
-	for i := 0; i < nKeys; i++ {
-		k := l3l4keys[i]
-		io.WriteString(d.log, fmt.Sprintf("[distill] Squashing L3-dependent L4 key %+v\n", k))
-		wildcardL3 := Key{DestPort: k.DestPort, Nexthdr: k.Nexthdr, TrafficDirection: k.TrafficDirection}
-		wildcardL4 := Key{Identity: k.Identity, TrafficDirection: k.TrafficDirection}
-		if _, ok := result[wildcardL4]; ok {
-			io.WriteString(d.log, fmt.Sprintf("[distill] -> Found L3 overlap %+v\n", wildcardL4))
-			if entry, ok := result[wildcardL3]; ok {
-				io.WriteString(d.log, fmt.Sprintf("[distill] -> Found L4 overlap %+v:%+v\n", wildcardL3, entry))
-				if entry.ProxyPort == result[k].ProxyPort {
-					io.WriteString(d.log, fmt.Sprintf("[distill] -> Removing L3-dependent L4 %+v\n", k))
-					delete(result, k)
-				}
-			}
-		}
-	}
-
 	return result, nil
 }
 
@@ -424,34 +385,32 @@ func Test_MergeRules(t *testing.T) {
 		{3, api.Rules{rule____NoAllow, rule____NoAllow, rule____NoAllow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}},
 		{4, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3L4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7None_}},
 		{5, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3L4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}},
-		{6, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7None_}},                                    // Differs from spreadsheet(!)
-		{7, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
+		{6, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7None_}},                                    // identical L3L4 entry suppressed
+		{7, api.Rules{rule____NoAllow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}}, // identical L3L4 entry suppressed
 		{8, api.Rules{rule____NoAllow, rule__L4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},
 		{9, api.Rules{rule____NoAllow, rule__L4L7Allow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}},
 		{10, api.Rules{rule____NoAllow, rule__L4L7Allow, rule____NoAllow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},
 		{11, api.Rules{rule____NoAllow, rule__L4L7Allow, rule____NoAllow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}},
-		{12, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7Proxy}},
-		{13, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
-		{14, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7Proxy}},
-		{15, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
+		{12, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},                                    // L3L4 entry suppressed to allow L4-only entry to redirect
+		{13, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // L3L4 entry suppressed to allow L4-only entry to redirect
+		{14, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},                                    // L3L4 entry suppressed to allow L4-only entry to redirect
+		{15, api.Rules{rule____NoAllow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // L3L4 entry suppressed to allow L4-only entry to redirect
 		{16, api.Rules{ruleL3L4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy}},
 		{17, api.Rules{ruleL3L4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}},
-		// TODO: Tests 22-23 reveal a bug in the redirect logic (GH-7438).
-		//{18, api.Rules{ruleL3L4L7Allow, rule____NoAllow, rule____NoAllow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_}},
-		//{19, api.Rules{ruleL3L4L7Allow, rule____NoAllow, rule____NoAllow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}},
+		{18, api.Rules{ruleL3L4L7Allow, rule____NoAllow, rule____NoAllow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_}},
+		{19, api.Rules{ruleL3L4L7Allow, rule____NoAllow, rule____NoAllow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}},
 		{20, api.Rules{ruleL3L4L7Allow, rule____NoAllow, ruleL3L4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy}},
 		{21, api.Rules{ruleL3L4L7Allow, rule____NoAllow, ruleL3L4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}},
-		// TODO: Tests 22-23 reveal a bug in the redirect logic (GH-7438).
-		//{22, api.Rules{ruleL3L4L7Allow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_}},
-		//{23, api.Rules{ruleL3L4L7Allow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}},
-		{24, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7Proxy}},
-		{25, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
-		{26, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7Proxy}},
-		{27, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
-		{28, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7Proxy}},
-		{29, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
-		{30, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7Proxy}},
-		{31, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // Differs from spreadsheet(!)
+		{22, api.Rules{ruleL3L4L7Allow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_}},
+		{23, api.Rules{ruleL3L4L7Allow, rule____NoAllow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllowFooL4: mapEntryL7Proxy, mapKeyAllow___L4: mapEntryL7None_, mapKeyAllowFoo__: mapEntryL7None_}},
+		{24, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},                                    // identical L3L4 entry suppressed
+		{25, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // identical L3L4 entry suppressed
+		{26, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},                                    // identical L3L4 entry suppressed
+		{27, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, rule____NoAllow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // identical L3L4 entry suppressed
+		{28, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},                                    // identical L3L4 entry suppressed
+		{29, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule____NoAllow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // identical L3L4 entry suppressed
+		{30, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, rule____NoAllow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy}},                                    // identical L3L4 entry suppressed
+		{31, api.Rules{ruleL3L4L7Allow, rule__L4L7Allow, ruleL3L4__Allow, rule__L4__Allow, ruleL3____Allow}, MapState{mapKeyAllow___L4: mapEntryL7Proxy, mapKeyAllowFoo__: mapEntryL7None_}}, // identical L3L4 entry suppressed
 	}
 	for _, tt := range tests {
 		repo := newPolicyDistillery(selectorCache)
diff --git a/pkg/policy/l4_filter_test.go b/pkg/policy/l4_filter_test.go
index 509f8ba79b..7ecae2a09e 100644
--- a/pkg/policy/l4_filter_test.go
+++ b/pkg/policy/l4_filter_test.go
@@ -67,8 +67,14 @@ func (p *testPolicyContextType) GetSelectorCache() *SelectorCache {
 	return testSelectorCache
 }
 
-func (p *testPolicyContextType) GetTLSContext(*api.TLSContext) (ca, public, private string, err error) {
-	return "", "", "", fmt.Errorf("Not supported")
+func (p *testPolicyContextType) GetTLSContext(tls *api.TLSContext) (ca, public, private string, err error) {
+	switch tls.Secret.Name {
+	case "tls-cert":
+		return "", "fake public cert", "fake private key", nil
+	case "tls-ca-certs":
+		return "fake CA certs", "", "", nil
+	}
+	return "", "", "", fmt.Errorf("Unknown test secret '%s'", tls.Secret.Name)
 }
 
 func (p *testPolicyContextType) GetEnvoyHTTPRules(*api.L7Rules) (*cilium.HttpNetworkPolicyRules, bool) {
@@ -149,10 +155,10 @@ func (ds *PolicyTestSuite) TestMergeAllowAllL3AndAllowAllL7(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(filter.CachedSelectors.SelectsAllEndpoints(), Equals, true)
+	c.Assert(filter.SelectsAllEndpoints(), Equals, true)
 
 	c.Assert(filter.L7Parser, Equals, ParserTypeNone)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 0)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 
 	// Case1B: implicitly wildcard all endpoints.
@@ -192,10 +198,10 @@ func (ds *PolicyTestSuite) TestMergeAllowAllL3AndAllowAllL7(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(filter.CachedSelectors.SelectsAllEndpoints(), Equals, true)
+	c.Assert(filter.SelectsAllEndpoints(), Equals, true)
 
 	c.Assert(filter.L7Parser, Equals, ParserTypeNone)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 0)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -243,22 +249,16 @@ func (ds *PolicyTestSuite) TestMergeAllowAllL3AndShadowedL7(c *C) {
 
 	c.Log(buffer)
 
-	// The expected policy contains the L7 Rules below, but in practice
-	// when the policy is being resolved and sent to the proxy, it actually
-	// allows all at L7, based on the first API rule imported above. We
-	// just set the expected set of L7 rules below to include this to match
-	// the current implementation.
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:        "http",
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "http",
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
-					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
+					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}, {}},
 				},
 			},
 		},
@@ -315,10 +315,10 @@ func (ds *PolicyTestSuite) TestMergeAllowAllL3AndShadowedL7(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(filter.CachedSelectors.SelectsAllEndpoints(), Equals, true)
+	c.Assert(filter.SelectsAllEndpoints(), Equals, true)
 
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 1)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -359,14 +359,13 @@ func (ds *PolicyTestSuite) TestMergeIdenticalAllowAllL3AndRestrictedL7HTTP(c *C)
 		}}
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -441,14 +440,13 @@ func (ds *PolicyTestSuite) TestMergeIdenticalAllowAllL3AndRestrictedL7Kafka(c *C
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"9092/TCP": &L4Filter{
-		Port:            9092,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:        ParserTypeKafka,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     9092,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeKafka,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					Kafka: []api.PortRuleKafka{{Topic: "foo"}},
 				},
@@ -663,6 +661,94 @@ func (ds *PolicyTestSuite) TestMergeIdenticalAllowAllL3AndMismatchingParsers(c *
 	c.Assert(res, IsNil)
 }
 
+// TLS policies with and without interception
+func (ds *PolicyTestSuite) TestMergeTLSPolicies(c *C) {
+	egressRule := &rule{
+		Rule: api.Rule{
+			EndpointSelector: fooSelector,
+			Egress: []api.EgressRule{
+				{
+					ToEndpoints: []api.EndpointSelector{endpointSelectorA},
+					ToPorts: []api.PortRule{{
+						Ports: []api.PortProtocol{
+							{Port: "443", Protocol: api.ProtoTCP},
+						},
+					}},
+				},
+				{
+					ToEndpoints: []api.EndpointSelector{endpointSelectorC},
+					ToPorts: []api.PortRule{{
+						Ports: []api.PortProtocol{
+							{Port: "443", Protocol: api.ProtoTCP},
+						},
+						TerminatingTLS: &api.TLSContext{
+							Secret: &api.Secret{
+								Name: "tls-cert",
+							},
+						},
+						OriginatingTLS: &api.TLSContext{
+							Secret: &api.Secret{
+								Name: "tls-ca-certs",
+							},
+						},
+						Rules: &api.L7Rules{
+							HTTP: []api.PortRuleHTTP{{}},
+						},
+					}},
+				},
+			},
+		}}
+
+	buffer := new(bytes.Buffer)
+	ctxFromFoo := SearchContext{From: labels.ParseSelectLabelArray("foo"), Trace: TRACE_VERBOSE}
+	ctxFromFoo.Logging = logging.NewLogBackend(buffer, "", 0)
+	c.Log(buffer)
+
+	err := egressRule.Sanitize()
+	c.Assert(err, IsNil)
+
+	state := traceState{}
+	res, err := egressRule.resolveEgressPolicy(testPolicyContext, &ctxFromFoo, &state, L4PolicyMap{}, nil)
+	c.Log(buffer)
+	c.Assert(err, IsNil)
+	c.Assert(res, Not(IsNil))
+
+	// Since cachedSelectorA's map entry is 'nil', it will not be redirected to the proxy.
+	expected := L4PolicyMap{"443/TCP": &L4Filter{
+		Port:     443,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: nil,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorA: nil, // no proxy redirect
+			cachedSelectorC: &PerSelectorPolicy{
+				TerminatingTLS: &TLSContext{
+					CertificateChain: "fake public cert",
+					PrivateKey:       "fake private key",
+				},
+				OriginatingTLS: &TLSContext{
+					TrustedCA: "fake CA certs",
+				},
+				EnvoyHTTPRules:  nil,
+				CanShortCircuit: false,
+				L7Rules: api.L7Rules{
+					HTTP: []api.PortRuleHTTP{{}},
+				},
+			},
+		},
+		Ingress:          false,
+		DerivedFromRules: labels.LabelArrayList{nil, nil},
+	}}
+
+	c.Assert(res, checker.Equals, expected)
+
+	l4Filter := res["443/TCP"]
+	c.Assert(l4Filter, Not(IsNil))
+	c.Assert(l4Filter.L7Parser, Equals, ParserTypeHTTP)
+	log.Infof("res: %v", res)
+}
+
 // Case 6: allow all at L3/L7 in one rule, and select an endpoint and allow all on L7
 // in another rule. Should resolve to just allowing all on L3/L7 (first rule
 // shadows the second).
@@ -697,13 +783,15 @@ func (ds *PolicyTestSuite) TestL3RuleShadowedByL3AllowAll(c *C) {
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:             80,
-		Protocol:         api.ProtoTCP,
-		U8Proto:          6,
-		allowsAllAtL3:    true,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:         ParserTypeNone,
-		L7RulesPerEp:     L7DataMap{},
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeNone,
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorA:        nil,
+			wildcardCachedSelector: nil,
+		},
 		Ingress:          true,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
@@ -755,13 +843,15 @@ func (ds *PolicyTestSuite) TestL3RuleShadowedByL3AllowAll(c *C) {
 	c.Log(buffer)
 
 	expected = L4PolicyMap{"80/TCP": &L4Filter{
-		Port:             80,
-		Protocol:         api.ProtoTCP,
-		U8Proto:          6,
-		allowsAllAtL3:    true,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:         ParserTypeNone,
-		L7RulesPerEp:     L7DataMap{},
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeNone,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+			cachedSelectorA:        nil,
+		},
 		Ingress:          true,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
@@ -826,14 +916,14 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RulePartiallyShadowedByL3AllowAll(c *
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{cachedSelectorA, wildcardCachedSelector},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			cachedSelectorA: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+			cachedSelectorA: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -897,14 +987,14 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RulePartiallyShadowedByL3AllowAll(c *
 	c.Log(buffer)
 
 	expected = L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedSelectorA},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			cachedSelectorA: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+			cachedSelectorA: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -940,7 +1030,7 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RuleShadowedByL3AllowAll(c *C) {
 
 	// Case 8A: selects specific endpoint with L7 restrictions rule first, then
 	// rule which selects all endpoints and restricts on the same resource on L7.
-	// L7RulesPerEp contains entries for both endpoints selected in each rule
+	// L7RulesPerSelector contains entries for both endpoints selected in each rule
 	// on L7 restriction.
 	case8Rule := &rule{
 		Rule: api.Rule{
@@ -981,19 +1071,18 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RuleShadowedByL3AllowAll(c *C) {
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{cachedSelectorA, wildcardCachedSelector},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
 			},
-			cachedSelectorA: &PerEpData{
+			cachedSelectorA: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -1022,7 +1111,7 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RuleShadowedByL3AllowAll(c *C) {
 
 	// Case 8B: first insert rule which selects all endpoints and restricts on
 	// the same resource on L7. Then, insert rule which  selects specific endpoint
-	// with L7 restrictions rule. L7RulesPerEp contains entries for both
+	// with L7 restrictions rule. L7RulesPerSelector contains entries for both
 	// endpoints selected in each rule on L7 restriction.
 	case8Rule = &rule{
 		Rule: api.Rule{
@@ -1060,22 +1149,20 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RuleShadowedByL3AllowAll(c *C) {
 	buffer = new(bytes.Buffer)
 	ctxToA = SearchContext{To: labelsA, Trace: TRACE_VERBOSE}
 	ctxToA.Logging = logging.NewLogBackend(buffer, "", 0)
-	c.Log(buffer)
 
 	expected = L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedSelectorA},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
 			},
-			cachedSelectorA: &PerEpData{
+			cachedSelectorA: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -1087,6 +1174,7 @@ func (ds *PolicyTestSuite) TestL3RuleWithL7RuleShadowedByL3AllowAll(c *C) {
 
 	state = traceState{}
 	res, err = case8Rule.resolveIngressPolicy(testPolicyContext, &ctxToA, &state, L4PolicyMap{}, nil)
+	c.Log(buffer)
 	c.Assert(err, IsNil)
 	c.Assert(res, Not(IsNil))
 	c.Assert(res, checker.Equals, expected)
@@ -1254,18 +1342,18 @@ func (ds *PolicyTestSuite) TestMergingWithDifferentEndpointsSelectedAllowSameL7(
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		CachedSelectors: CachedSelectorSlice{cachedSelectorA, cachedSelectorC},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			cachedSelectorC: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: nil,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorC: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
 			},
-			cachedSelectorA: &PerEpData{
+			cachedSelectorA: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -1330,12 +1418,15 @@ func (ds *PolicyTestSuite) TestMergingWithDifferentEndpointSelectedAllowAllL7(c
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:             80,
-		Protocol:         api.ProtoTCP,
-		U8Proto:          6,
-		CachedSelectors:  CachedSelectorSlice{cachedSelectorA, cachedSelectorC},
-		L7Parser:         ParserTypeNone,
-		L7RulesPerEp:     L7DataMap{},
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: nil,
+		L7Parser: ParserTypeNone,
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorA: nil,
+			cachedSelectorC: nil,
+		},
 		Ingress:          true,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
@@ -1402,21 +1493,18 @@ func (ds *PolicyTestSuite) TestAllowingLocalhostShadowsL7(c *C) {
 	ctxToA.Logging = logging.NewLogBackend(buffer, "", 0)
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedSelectorHost},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
 			},
-			cachedSelectorHost: &PerEpData{
-				L7Rules: api.L7Rules{}, // Empty => Allow all
-			},
+			cachedSelectorHost: nil, // no proxy redirect
 		},
 		Ingress:          true,
 		DerivedFromRules: labels.LabelArrayList{nil},
@@ -1465,14 +1553,15 @@ func (ds *PolicyTestSuite) TestEntitiesL3(c *C) {
 	c.Log(buffer)
 
 	expected := L4PolicyMap{"0/ANY": &L4Filter{
-		Port:             0,
-		Protocol:         api.ProtoAny,
-		U8Proto:          0,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:         ParserTypeNone,
-		L7RulesPerEp:     L7DataMap{},
+		Port:     0,
+		Protocol: api.ProtoAny,
+		U8Proto:  0,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeNone,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+		},
 		Ingress:          false,
-		allowsAllAtL3:    true,
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}}
 
diff --git a/pkg/policy/l4_test.go b/pkg/policy/l4_test.go
index 24fc0ed378..2a8ebd5362 100644
--- a/pkg/policy/l4_test.go
+++ b/pkg/policy/l4_test.go
@@ -50,13 +50,13 @@ func (s *PolicyTestSuite) TestCreateL4Filter(c *C) {
 		// or if it is based on specific labels.
 		filter, err := createL4IngressFilter(testPolicyContext, eps, false, portrule, tuple, tuple.Protocol, nil)
 		c.Assert(err, IsNil)
-		c.Assert(len(filter.L7RulesPerEp), Equals, 1)
+		c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 		c.Assert(filter.IsEnvoyRedirect(), Equals, true)
 		c.Assert(filter.IsProxylibRedirect(), Equals, false)
 
 		filter, err = createL4EgressFilter(testPolicyContext, eps, portrule, tuple, tuple.Protocol, nil, nil)
 		c.Assert(err, IsNil)
-		c.Assert(len(filter.L7RulesPerEp), Equals, 1)
+		c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 		c.Assert(filter.IsEnvoyRedirect(), Equals, true)
 		c.Assert(filter.IsProxylibRedirect(), Equals, false)
 	}
@@ -117,10 +117,9 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 		Ingress: L4PolicyMap{
 			"80/TCP": {
 				Port: 80, Protocol: api.ProtoTCP,
-				CachedSelectors: CachedSelectorSlice{cachedFooSelector},
-				L7Parser:        "http",
-				L7RulesPerEp: L7DataMap{
-					cachedFooSelector: &PerEpData{
+				L7Parser: "http",
+				L7RulesPerSelector: L7DataMap{
+					cachedFooSelector: &PerSelectorPolicy{
 						L7Rules: api.L7Rules{
 							HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 						},
@@ -130,10 +129,9 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 			},
 			"9090/TCP": {
 				Port: 9090, Protocol: api.ProtoTCP,
-				CachedSelectors: CachedSelectorSlice{cachedFooSelector},
-				L7Parser:        "tester",
-				L7RulesPerEp: L7DataMap{
-					cachedFooSelector: &PerEpData{
+				L7Parser: "tester",
+				L7RulesPerSelector: L7DataMap{
+					cachedFooSelector: &PerSelectorPolicy{
 						L7Rules: api.L7Rules{
 							L7Proto: "tester",
 							L7: []api.PortRuleL7{
@@ -151,10 +149,9 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 			},
 			"8080/TCP": {
 				Port: 8080, Protocol: api.ProtoTCP,
-				CachedSelectors: CachedSelectorSlice{cachedFooSelector},
-				L7Parser:        "http",
-				L7RulesPerEp: L7DataMap{
-					cachedFooSelector: &PerEpData{
+				L7Parser: "http",
+				L7RulesPerSelector: L7DataMap{
+					cachedFooSelector: &PerSelectorPolicy{
 						L7Rules: api.L7Rules{
 							HTTP: []api.PortRuleHTTP{
 								{Path: "/", Method: "GET"},
@@ -162,7 +159,7 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 							},
 						},
 					},
-					wildcardCachedSelector: &PerEpData{
+					wildcardCachedSelector: &PerSelectorPolicy{
 						L7Rules: api.L7Rules{
 							HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 						},
@@ -190,9 +187,6 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 	expectedIngress := []string{`{
   "port": 80,
   "protocol": "TCP",
-  "l3-selectors": [
-    "\u0026LabelSelector{MatchLabels:map[string]string{any.foo: ,},MatchExpressions:[]LabelSelectorRequirement{},}"
-  ],
   "l7-rules": [
     {
       "\u0026LabelSelector{MatchLabels:map[string]string{any.foo: ,},MatchExpressions:[]LabelSelectorRequirement{},}": {
@@ -209,9 +203,6 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 		`{
   "port": 9090,
   "protocol": "TCP",
-  "l3-selectors": [
-    "\u0026LabelSelector{MatchLabels:map[string]string{any.foo: ,},MatchExpressions:[]LabelSelectorRequirement{},}"
-  ],
   "l7-rules": [
     {
       "\u0026LabelSelector{MatchLabels:map[string]string{any.foo: ,},MatchExpressions:[]LabelSelectorRequirement{},}": {
@@ -233,9 +224,6 @@ func (s *PolicyTestSuite) TestJSONMarshal(c *C) {
 		`{
   "port": 8080,
   "protocol": "TCP",
-  "l3-selectors": [
-    "\u0026LabelSelector{MatchLabels:map[string]string{any.foo: ,},MatchExpressions:[]LabelSelectorRequirement{},}"
-  ],
   "l7-rules": [
     {
       "\u0026LabelSelector{MatchLabels:map[string]string{any.foo: ,},MatchExpressions:[]LabelSelectorRequirement{},}": {
diff --git a/pkg/policy/repository_test.go b/pkg/policy/repository_test.go
index f79b748707..ba13617d88 100644
--- a/pkg/policy/repository_test.go
+++ b/pkg/policy/repository_test.go
@@ -656,78 +656,60 @@ func (ds *PolicyTestSuite) TestWildcardL3RulesIngress(c *C) {
 
 	expectedPolicy := L4PolicyMap{
 		"0/ANY": {
-			Port:             0,
-			Protocol:         api.ProtoAny,
-			U8Proto:          0x0,
-			CachedSelectors:  CachedSelectorSlice{cachedSelectorBar1},
-			L7RulesPerEp:     L7DataMap{},
+			Port:     0,
+			Protocol: api.ProtoAny,
+			U8Proto:  0x0,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar1: nil,
+			},
 			Ingress:          true,
 			DerivedFromRules: labels.LabelArrayList{labelsL3},
 		},
 		"9092/TCP": {
-			Port:            9092,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorBar1},
-			L7Parser:        ParserTypeKafka,
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar2: &PerEpData{
+			Port:     9092,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeKafka,
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						Kafka: []api.PortRuleKafka{kafkaRule.Ingress[0].ToPorts[0].Rules.Kafka[0]},
 					},
 				},
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						Kafka: []api.PortRuleKafka{{}},
-					},
-				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsKafka, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsKafka},
 		},
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorBar1},
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar2: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						HTTP: []api.PortRuleHTTP{httpRule.Ingress[0].ToPorts[0].Rules.HTTP[0]},
 					},
 				},
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					},
-				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsHTTP, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsHTTP},
 		},
 		"9090/TCP": {
-			Port:            9090,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorBar1},
-			L7Parser:        L7ParserType("tester"),
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar2: &PerEpData{
+			Port:     9090,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: L7ParserType("tester"),
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						L7Proto: "tester",
 						L7:      []api.PortRuleL7{l7Rule.Ingress[0].ToPorts[0].Rules.L7[0]},
 					},
 				},
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						L7Proto: "tester",
-						L7:      []api.PortRuleL7{},
-					},
-				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsL7, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsL7},
 		},
 	}
 	c.Assert(policy, checker.Equals, expectedPolicy)
@@ -742,9 +724,10 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesIngress(c *C) {
 	selBar1 := api.NewESFromLabels(labels.ParseSelectLabel("id=bar1"))
 	selBar2 := api.NewESFromLabels(labels.ParseSelectLabel("id=bar2"))
 
-	labelsL4 := labels.LabelArray{labels.ParseLabel("L4")}
-	labelsKafka := labels.LabelArray{labels.ParseLabel("kafka")}
-	labelsHTTP := labels.LabelArray{labels.ParseLabel("http")}
+	labelsL4Kafka := labels.LabelArray{labels.ParseLabel("L4-kafka")}
+	labelsL7Kafka := labels.LabelArray{labels.ParseLabel("kafka")}
+	labelsL4HTTP := labels.LabelArray{labels.ParseLabel("L4-http")}
+	labelsL7HTTP := labels.LabelArray{labels.ParseLabel("http")}
 
 	l49092Rule := api.Rule{
 		EndpointSelector: selFoo,
@@ -758,7 +741,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesIngress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsL4,
+		Labels: labelsL4Kafka,
 	}
 	l49092Rule.Sanitize()
 	_, _, err := repo.Add(l49092Rule, []Endpoint{})
@@ -781,7 +764,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesIngress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsKafka,
+		Labels: labelsL7Kafka,
 	}
 	kafkaRule.Sanitize()
 	_, _, err = repo.Add(kafkaRule, []Endpoint{})
@@ -799,7 +782,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesIngress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsL4,
+		Labels: labelsL4HTTP,
 	}
 	l480Rule.Sanitize()
 	_, _, err = repo.Add(l480Rule, []Endpoint{})
@@ -822,7 +805,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesIngress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsHTTP,
+		Labels: labelsL7HTTP,
 	}
 	_, _, err = repo.Add(httpRule, []Endpoint{})
 	c.Assert(err, IsNil)
@@ -839,46 +822,36 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesIngress(c *C) {
 
 	expectedPolicy := L4PolicyMap{
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar1, cachedSelectorBar2},
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar1: nil,
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						HTTP: []api.PortRuleHTTP{httpRule.Ingress[0].ToPorts[0].Rules.HTTP[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsL4, labelsHTTP, labelsL4},
+			DerivedFromRules: labels.LabelArrayList{labelsL4HTTP, labelsL7HTTP},
 		},
 		"9092/TCP": {
-			Port:            9092,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar1, cachedSelectorBar2},
-			L7Parser:        ParserTypeKafka,
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						Kafka: []api.PortRuleKafka{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     9092,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeKafka,
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar1: nil,
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						Kafka: []api.PortRuleKafka{kafkaRule.Ingress[0].ToPorts[0].Rules.Kafka[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsL4, labelsKafka, labelsL4},
+			DerivedFromRules: labels.LabelArrayList{labelsL4Kafka, labelsL7Kafka},
 		},
 	}
 	c.Assert(policy, checker.Equals, expectedPolicy)
@@ -939,10 +912,9 @@ func (ds *PolicyTestSuite) TestL3DependentL4IngressFromRequires(c *C) {
 			Port:     80,
 			Protocol: api.ProtoTCP,
 			U8Proto:  0x6,
-			CachedSelectors: CachedSelectorSlice{
-				expectedCachedSelector,
+			L7RulesPerSelector: L7DataMap{
+				expectedCachedSelector: nil,
 			},
-			L7RulesPerEp:     L7DataMap{},
 			Ingress:          true,
 			DerivedFromRules: labels.LabelArrayList{nil},
 		},
@@ -1014,24 +986,21 @@ func (ds *PolicyTestSuite) TestL3DependentL4EgressFromRequires(c *C) {
 
 	expectedPolicy := L4PolicyMap{
 		"0/ANY": &L4Filter{
-			Port:          0,
-			Protocol:      "ANY",
-			U8Proto:       0x0,
-			allowsAllAtL3: false,
-			CachedSelectors: CachedSelectorSlice{
-				expectedCachedSelector2,
-			},
-			L7RulesPerEp:     L7DataMap{},
+			Port:     0,
+			Protocol: "ANY",
+			U8Proto:  0x0,
+			L7RulesPerSelector: L7DataMap{
+				expectedCachedSelector2: nil,
+			},
 			DerivedFromRules: labels.LabelArrayList{nil},
 		},
 		"80/TCP": &L4Filter{
 			Port:     80,
 			Protocol: api.ProtoTCP,
 			U8Proto:  0x6,
-			CachedSelectors: CachedSelectorSlice{
-				expectedCachedSelector,
+			L7RulesPerSelector: L7DataMap{
+				expectedCachedSelector: nil,
 			},
-			L7RulesPerEp:     L7DataMap{},
 			DerivedFromRules: labels.LabelArrayList{nil},
 		},
 	}
@@ -1118,65 +1087,60 @@ func (ds *PolicyTestSuite) TestWildcardL3RulesEgress(c *C) {
 	repo.Mutex.RLock()
 	defer repo.Mutex.RUnlock()
 
-	policy, err := repo.ResolveL4EgressPolicy(ctx)
+	logBuffer := new(bytes.Buffer)
+	policy, err := repo.ResolveL4EgressPolicy(ctx.WithLogger(logBuffer))
 	c.Assert(err, IsNil)
 
+	// Traffic to bar1 should not be forwarded to the DNS or HTTP
+	// proxy at all, but if it is (e.g., for visibility, the
+	// "0/ANY" rule should allow such traffic through.
 	expectedPolicy := L4PolicyMap{
 		"53/UDP": {
-			Port:            53,
-			Protocol:        api.ProtoUDP,
-			U8Proto:         0x11,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorBar1},
-			L7Parser:        ParserTypeDNS,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						DNS: []api.PortRuleDNS{{MatchPattern: "*"}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     53,
+			Protocol: api.ProtoUDP,
+			U8Proto:  0x11,
+			L7Parser: ParserTypeDNS,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						DNS: []api.PortRuleDNS{dnsRule.Egress[0].ToPorts[0].Rules.DNS[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsDNS, labelsL4},
+			DerivedFromRules: labels.LabelArrayList{labelsDNS},
 		},
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorBar1},
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						HTTP: []api.PortRuleHTTP{httpRule.Egress[0].ToPorts[0].Rules.HTTP[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsHTTP, labelsL4},
+			DerivedFromRules: labels.LabelArrayList{labelsHTTP},
 		},
 		"0/ANY": {
-			Port:             0,
-			Protocol:         "ANY",
-			U8Proto:          0x0,
-			allowsAllAtL3:    false,
-			CachedSelectors:  CachedSelectorSlice{cachedSelectorBar1},
-			L7Parser:         "",
-			L7RulesPerEp:     L7DataMap{},
+			Port:     0,
+			Protocol: "ANY",
+			U8Proto:  0x0,
+			L7Parser: "",
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar1: nil,
+			},
 			Ingress:          false,
 			DerivedFromRules: labels.LabelArrayList{labelsL4},
 		},
 	}
-	c.Assert(policy, checker.Equals, expectedPolicy)
+	if equal, err := checker.Equal(policy, expectedPolicy); !equal {
+		c.Logf("%s", logBuffer.String())
+		c.Errorf("Resolved policy did not match expected: \n%s", err)
+	}
 	policy.Detach(repo.GetSelectorCache())
 }
 
@@ -1188,9 +1152,10 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesEgress(c *C) {
 	selBar1 := api.NewESFromLabels(labels.ParseSelectLabel("id=bar1"))
 	selBar2 := api.NewESFromLabels(labels.ParseSelectLabel("id=bar2"))
 
-	labelsL3 := labels.LabelArray{labels.ParseLabel("L3")}
-	labelsDNS := labels.LabelArray{labels.ParseLabel("dns")}
-	labelsHTTP := labels.LabelArray{labels.ParseLabel("http")}
+	labelsL3DNS := labels.LabelArray{labels.ParseLabel("L3-dns")}
+	labelsL7DNS := labels.LabelArray{labels.ParseLabel("dns")}
+	labelsL3HTTP := labels.LabelArray{labels.ParseLabel("L3-http")}
+	labelsL7HTTP := labels.LabelArray{labels.ParseLabel("http")}
 
 	l453Rule := api.Rule{
 		EndpointSelector: selFoo,
@@ -1204,7 +1169,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesEgress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsL3,
+		Labels: labelsL3DNS,
 	}
 	l453Rule.Sanitize()
 	_, _, err := repo.Add(l453Rule, []Endpoint{})
@@ -1227,7 +1192,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesEgress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsDNS,
+		Labels: labelsL7DNS,
 	}
 	dnsRule.Sanitize()
 	_, _, err = repo.Add(dnsRule, []Endpoint{})
@@ -1245,7 +1210,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesEgress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsL3,
+		Labels: labelsL3HTTP,
 	}
 	l480Rule.Sanitize()
 	_, _, err = repo.Add(l480Rule, []Endpoint{})
@@ -1268,7 +1233,7 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesEgress(c *C) {
 				}},
 			},
 		},
-		Labels: labelsHTTP,
+		Labels: labelsL7HTTP,
 	}
 	_, _, err = repo.Add(httpRule, []Endpoint{})
 	c.Assert(err, IsNil)
@@ -1280,54 +1245,50 @@ func (ds *PolicyTestSuite) TestWildcardL4RulesEgress(c *C) {
 	repo.Mutex.RLock()
 	defer repo.Mutex.RUnlock()
 
-	policy, err := repo.ResolveL4EgressPolicy(ctx)
+	logBuffer := new(bytes.Buffer)
+	policy, err := repo.ResolveL4EgressPolicy(ctx.WithLogger(logBuffer))
 	c.Assert(err, IsNil)
 
+	// Bar1 should not be forwarded to the proxy, but if it is (e.g., for visibility),
+	// the L3/L4 allow should pass it without an explicit L7 wildcard.
 	expectedPolicy := L4PolicyMap{
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar1, cachedSelectorBar2},
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar1: nil,
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						HTTP: []api.PortRuleHTTP{httpRule.Egress[0].ToPorts[0].Rules.HTTP[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsL3, labelsHTTP, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsL3HTTP, labelsL7HTTP},
 		},
 		"53/UDP": {
-			Port:            53,
-			Protocol:        api.ProtoUDP,
-			U8Proto:         0x11,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar1, cachedSelectorBar2},
-			L7Parser:        ParserTypeDNS,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorBar1: &PerEpData{
-					L7Rules: api.L7Rules{
-						DNS: []api.PortRuleDNS{{MatchPattern: "*"}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     53,
+			Protocol: api.ProtoUDP,
+			U8Proto:  0x11,
+			L7Parser: ParserTypeDNS,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar1: nil,
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						DNS: []api.PortRuleDNS{dnsRule.Egress[0].ToPorts[0].Rules.DNS[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsL3, labelsDNS, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsL3DNS, labelsL7DNS},
 		},
 	}
-	c.Assert(policy, checker.Equals, expectedPolicy)
+	if equal, err := checker.Equal(policy, expectedPolicy); !equal {
+		c.Logf("%s", logBuffer.String())
+		c.Errorf("Resolved policy did not match expected: \n%s", err)
+	}
 	policy.Detach(repo.GetSelectorCache())
 }
 
@@ -1398,38 +1359,48 @@ func (ds *PolicyTestSuite) TestWildcardCIDRRulesEgress(c *C) {
 	repo.Mutex.RLock()
 	defer repo.Mutex.RUnlock()
 
-	policy, err := repo.ResolveL4EgressPolicy(ctx)
+	logBuffer := new(bytes.Buffer)
+	policy, err := repo.ResolveL4EgressPolicy(ctx.WithLogger(logBuffer))
 	c.Assert(err, IsNil)
 
+	// Port 80 policy does not need the wildcard, as the "0" port policy will allow the traffic.
+	// HTTP rules can have side-effects, so they need to be retained even if shadowed by a wildcard.
 	expectedPolicy := L4PolicyMap{
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: cachedSelectors,
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectors[0]: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectors[0]: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
+						HTTP: []api.PortRuleHTTP{{
+							Headers: []string{"X-My-Header: true"},
+							Method:  "GET",
+							Path:    "/",
+						}},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsHTTP, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsHTTP},
 		},
 		"0/ANY": {
-			Port:             0,
-			Protocol:         api.ProtoAny,
-			U8Proto:          0x0,
-			CachedSelectors:  cachedSelectors,
-			L7Parser:         ParserTypeNone,
-			Ingress:          false,
+			Port:     0,
+			Protocol: api.ProtoAny,
+			U8Proto:  0x0,
+			L7Parser: ParserTypeNone,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectors[0]: nil,
+			},
 			DerivedFromRules: labels.LabelArrayList{labelsL3},
-			L7RulesPerEp:     L7DataMap{},
 		},
 	}
-	c.Assert(policy, checker.Equals, expectedPolicy)
+	if equal, err := checker.Equal(policy, expectedPolicy); !equal {
+		c.Logf("%s", logBuffer.String())
+		c.Errorf("Resolved policy did not match expected: \n%s", err)
+	}
 	policy.Detach(repo.GetSelectorCache())
 }
 
@@ -1513,63 +1484,51 @@ func (ds *PolicyTestSuite) TestWildcardL3RulesIngressFromEntities(c *C) {
 	c.Assert(err, IsNil)
 	c.Assert(len(policy), Equals, 3)
 	selWorld := api.EntitySelectorMapping[api.EntityWorld][0]
-	c.Assert(len(policy["80/TCP"].CachedSelectors), Equals, 2)
+	c.Assert(len(policy["80/TCP"].L7RulesPerSelector), Equals, 1)
 	cachedSelectorWorld := testSelectorCache.FindCachedIdentitySelector(selWorld)
 	c.Assert(cachedSelectorWorld, Not(IsNil))
 
 	expectedPolicy := L4PolicyMap{
 		"0/ANY": {
-			Port:             0,
-			Protocol:         "ANY",
-			U8Proto:          0x0,
-			allowsAllAtL3:    false,
-			CachedSelectors:  CachedSelectorSlice{cachedSelectorWorld},
-			L7Parser:         "",
-			L7RulesPerEp:     L7DataMap{},
+			Port:     0,
+			Protocol: "ANY",
+			U8Proto:  0x0,
+			L7Parser: "",
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorWorld: nil,
+			},
 			Ingress:          true,
 			DerivedFromRules: labels.LabelArrayList{labelsL3},
 		},
 		"9092/TCP": {
-			Port:            9092,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorWorld},
-			L7Parser:        ParserTypeKafka,
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorWorld: &PerEpData{
-					L7Rules: api.L7Rules{
-						Kafka: []api.PortRuleKafka{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     9092,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeKafka,
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						Kafka: []api.PortRuleKafka{kafkaRule.Ingress[0].ToPorts[0].Rules.Kafka[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsKafka, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsKafka},
 		},
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorWorld},
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         true,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorWorld: &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  true,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						HTTP: []api.PortRuleHTTP{httpRule.Ingress[0].ToPorts[0].Rules.HTTP[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsHTTP, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsHTTP},
 		},
 	}
 
@@ -1657,63 +1616,51 @@ func (ds *PolicyTestSuite) TestWildcardL3RulesEgressToEntities(c *C) {
 	c.Assert(err, IsNil)
 	c.Assert(len(policy), Equals, 3)
 	selWorld := api.EntitySelectorMapping[api.EntityWorld][0]
-	c.Assert(len(policy["80/TCP"].CachedSelectors), Equals, 2)
+	c.Assert(len(policy["80/TCP"].L7RulesPerSelector), Equals, 1)
 	cachedSelectorWorld := testSelectorCache.FindCachedIdentitySelector(selWorld)
 	c.Assert(cachedSelectorWorld, Not(IsNil))
 
 	expectedPolicy := L4PolicyMap{
 		"0/ANY": {
-			Port:             0,
-			Protocol:         "ANY",
-			U8Proto:          0x0,
-			allowsAllAtL3:    false,
-			CachedSelectors:  CachedSelectorSlice{cachedSelectorWorld},
-			L7Parser:         "",
-			L7RulesPerEp:     L7DataMap{},
+			Port:     0,
+			Protocol: "ANY",
+			U8Proto:  0x0,
+			L7Parser: "",
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorWorld: nil,
+			},
 			Ingress:          false,
 			DerivedFromRules: labels.LabelArrayList{labelsL3},
 		},
 		"53/UDP": {
-			Port:            53,
-			Protocol:        api.ProtoUDP,
-			U8Proto:         0x11,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorWorld},
-			L7Parser:        ParserTypeDNS,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorWorld: &PerEpData{
-					L7Rules: api.L7Rules{
-						DNS: []api.PortRuleDNS{{MatchPattern: "*"}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     53,
+			Protocol: api.ProtoUDP,
+			U8Proto:  0x11,
+			L7Parser: ParserTypeDNS,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						DNS: []api.PortRuleDNS{dnsRule.Egress[0].ToPorts[0].Rules.DNS[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsDNS, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsDNS},
 		},
 		"80/TCP": {
-			Port:            80,
-			Protocol:        api.ProtoTCP,
-			U8Proto:         0x6,
-			CachedSelectors: CachedSelectorSlice{cachedSelectorBar2, cachedSelectorWorld},
-			L7Parser:        ParserTypeHTTP,
-			Ingress:         false,
-			L7RulesPerEp: L7DataMap{
-				cachedSelectorWorld: &PerEpData{
-					L7Rules: api.L7Rules{
-						HTTP: []api.PortRuleHTTP{{}},
-					},
-				},
-				cachedSelectorBar2: &PerEpData{
+			Port:     80,
+			Protocol: api.ProtoTCP,
+			U8Proto:  0x6,
+			L7Parser: ParserTypeHTTP,
+			Ingress:  false,
+			L7RulesPerSelector: L7DataMap{
+				cachedSelectorBar2: &PerSelectorPolicy{
 					L7Rules: api.L7Rules{
 						HTTP: []api.PortRuleHTTP{httpRule.Egress[0].ToPorts[0].Rules.HTTP[0]},
 					},
 				},
 			},
-			DerivedFromRules: labels.LabelArrayList{labelsHTTP, labelsL3},
+			DerivedFromRules: labels.LabelArrayList{labelsHTTP},
 		},
 	}
 
@@ -1821,17 +1768,16 @@ func (ds *PolicyTestSuite) TestMinikubeGettingStarted(c *C) {
 	expected := NewL4Policy(repo.GetRevision())
 	expected.Ingress["80/TCP"] = &L4Filter{
 		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		CachedSelectors: CachedSelectorSlice{cachedSelectorApp2},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			cachedSelectorApp2: &PerEpData{
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorApp2: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
-					HTTP: []api.PortRuleHTTP{{}},
+					HTTP: []api.PortRuleHTTP{{Method: "GET", Path: "/"}, {}},
 				},
 			},
 		},
 		Ingress:          true,
-		DerivedFromRules: []labels.LabelArray{nil, nil, nil, nil},
+		DerivedFromRules: []labels.LabelArray{nil, nil, nil},
 	}
 
 	if equal, err := checker.Equal(l4IngressPolicy, expected.Ingress); !equal {
diff --git a/pkg/policy/resolve_test.go b/pkg/policy/resolve_test.go
index f06f19c1d5..ad6a4cf95b 100644
--- a/pkg/policy/resolve_test.go
+++ b/pkg/policy/resolve_test.go
@@ -171,6 +171,10 @@ func GenerateCIDRRules(numRules int) api.Rules {
 type DummyOwner struct{}
 
 func (d DummyOwner) LookupRedirectPort(l4 *L4Filter) uint16 {
+	// Return a fake non-0 listening port number for redirect filters.
+	if l4.IsRedirect() {
+		return 4242
+	}
 	return 0
 }
 
@@ -283,14 +287,11 @@ func (ds *PolicyTestSuite) TestL7WithIngressWildcard(c *C) {
 						Port:     80,
 						Protocol: api.ProtoTCP,
 						U8Proto:  0x6,
-						CachedSelectors: CachedSelectorSlice{
-							wildcardCachedSelector,
-						},
-						allowsAllAtL3: true,
-						L7Parser:      ParserTypeHTTP,
-						Ingress:       true,
-						L7RulesPerEp: L7DataMap{
-							wildcardCachedSelector: &PerEpData{
+						wildcard: wildcardCachedSelector,
+						L7Parser: ParserTypeHTTP,
+						Ingress:  true,
+						L7RulesPerSelector: L7DataMap{
+							wildcardCachedSelector: &PerSelectorPolicy{
 								L7Rules: api.L7Rules{
 									HTTP: []api.PortRuleHTTP{{Method: "GET", Path: "/good"}},
 								},
@@ -378,24 +379,17 @@ func (ds *PolicyTestSuite) TestL7WithLocalHostWildcardd(c *C) {
 						Port:     80,
 						Protocol: api.ProtoTCP,
 						U8Proto:  0x6,
-						CachedSelectors: CachedSelectorSlice{
-							wildcardCachedSelector,
-							cachedSelectorHost,
-						},
-						allowsAllAtL3: true,
-						L7Parser:      ParserTypeHTTP,
-						Ingress:       true,
-						L7RulesPerEp: L7DataMap{
-							wildcardCachedSelector: &PerEpData{
+						wildcard: wildcardCachedSelector,
+						L7Parser: ParserTypeHTTP,
+						Ingress:  true,
+						L7RulesPerSelector: L7DataMap{
+							wildcardCachedSelector: &PerSelectorPolicy{
 								L7Rules: api.L7Rules{
 									HTTP: []api.PortRuleHTTP{{Method: "GET", Path: "/good"}},
 								},
 								CanShortCircuit: true,
 							},
-							cachedSelectorHost: &PerEpData{
-								L7Rules:         api.L7Rules{},
-								CanShortCircuit: true,
-							},
+							cachedSelectorHost: nil,
 						},
 						DerivedFromRules: labels.LabelArrayList{nil},
 					},
@@ -464,13 +458,12 @@ func (ds *PolicyTestSuite) TestMapStateWithIngressWildcard(c *C) {
 						Port:     80,
 						Protocol: api.ProtoTCP,
 						U8Proto:  0x6,
-						CachedSelectors: CachedSelectorSlice{
-							wildcardCachedSelector,
+						wildcard: wildcardCachedSelector,
+						L7Parser: ParserTypeNone,
+						Ingress:  true,
+						L7RulesPerSelector: L7DataMap{
+							wildcardCachedSelector: nil,
 						},
-						allowsAllAtL3:    true,
-						L7Parser:         ParserTypeNone,
-						Ingress:          true,
-						L7RulesPerEp:     L7DataMap{},
 						DerivedFromRules: labels.LabelArrayList{nil},
 					},
 				},
@@ -492,8 +485,8 @@ func (ds *PolicyTestSuite) TestMapStateWithIngressWildcard(c *C) {
 		identity.NumericIdentity(192): labels.ParseSelectLabelArray("id=resolve_test_1"),
 	}
 	testSelectorCache.UpdateIdentities(added1, nil)
-	c.Assert(policy.PolicyMapChanges.adds, HasLen, 0)
-	c.Assert(policy.PolicyMapChanges.deletes, HasLen, 0)
+	c.Assert(policy.policyMapChanges.adds, HasLen, 0)
+	c.Assert(policy.policyMapChanges.deletes, HasLen, 0)
 
 	// Have to remove circular reference before testing to avoid an infinite loop
 	policy.selectorPolicy.Detach()
@@ -557,15 +550,15 @@ func (ds *PolicyTestSuite) TestMapStateWithIngress(c *C) {
 		identity.NumericIdentity(194): labels.ParseSelectLabelArray("id=resolve_test_1", "num=3"),
 	}
 	testSelectorCache.UpdateIdentities(added1, nil)
-	c.Assert(policy.PolicyMapChanges.adds, HasLen, 3)
-	c.Assert(policy.PolicyMapChanges.deletes, HasLen, 0)
+	c.Assert(policy.policyMapChanges.adds, HasLen, 3)
+	c.Assert(policy.policyMapChanges.deletes, HasLen, 0)
 
 	deleted1 := cache.IdentityCache{
 		identity.NumericIdentity(193): labels.ParseSelectLabelArray("id=resolve_test_1", "num=2"),
 	}
 	testSelectorCache.UpdateIdentities(nil, deleted1)
-	c.Assert(policy.PolicyMapChanges.adds, HasLen, 2)
-	c.Assert(policy.PolicyMapChanges.deletes, HasLen, 1)
+	c.Assert(policy.policyMapChanges.adds, HasLen, 2)
+	c.Assert(policy.policyMapChanges.deletes, HasLen, 1)
 
 	cachedSelectorWorld := testSelectorCache.FindCachedIdentitySelector(api.ReservedEndpointSelectors[labels.IDNameWorld])
 	c.Assert(cachedSelectorWorld, Not(IsNil))
@@ -584,14 +577,12 @@ func (ds *PolicyTestSuite) TestMapStateWithIngress(c *C) {
 						Port:     80,
 						Protocol: api.ProtoTCP,
 						U8Proto:  0x6,
-						CachedSelectors: CachedSelectorSlice{
-							cachedSelectorWorld,
-							cachedSelectorTest,
+						L7Parser: ParserTypeNone,
+						Ingress:  true,
+						L7RulesPerSelector: L7DataMap{
+							cachedSelectorWorld: nil,
+							cachedSelectorTest:  nil,
 						},
-						allowsAllAtL3:    false,
-						L7Parser:         ParserTypeNone,
-						Ingress:          true,
-						L7RulesPerEp:     L7DataMap{},
 						DerivedFromRules: labels.LabelArrayList{nil, nil},
 					},
 				},
@@ -606,7 +597,7 @@ func (ds *PolicyTestSuite) TestMapStateWithIngress(c *C) {
 			{TrafficDirection: trafficdirection.Egress.Uint8()}:                          {},
 			{Identity: uint32(identity.ReservedIdentityWorld), DestPort: 80, Nexthdr: 6}: {},
 		},
-		PolicyMapChanges: MapChanges{
+		policyMapChanges: MapChanges{
 			adds: MapState{
 				{Identity: 192, DestPort: 80, Nexthdr: 6}: {},
 				{Identity: 194, DestPort: 80, Nexthdr: 6}: {},
@@ -626,10 +617,10 @@ func (ds *PolicyTestSuite) TestMapStateWithIngress(c *C) {
 
 	c.Assert(policy, checker.Equals, &expectedEndpointPolicy)
 
-	adds, deletes := policy.PolicyMapChanges.ConsumeMapChanges()
+	adds, deletes := policy.ConsumeMapChanges()
 	// maps on the policy got cleared
-	c.Assert(policy.PolicyMapChanges.adds, IsNil)
-	c.Assert(policy.PolicyMapChanges.deletes, IsNil)
+	c.Assert(policy.policyMapChanges.adds, IsNil)
+	c.Assert(policy.policyMapChanges.deletes, IsNil)
 
 	c.Assert(adds, checker.Equals, MapState{
 		{Identity: 192, DestPort: 80, Nexthdr: 6}: {},
diff --git a/pkg/policy/rule_test.go b/pkg/policy/rule_test.go
index fb3c11bdcb..763fd7f0d1 100644
--- a/pkg/policy/rule_test.go
+++ b/pkg/policy/rule_test.go
@@ -71,7 +71,7 @@ func (ds *PolicyTestSuite) TestL4Policy(c *C) {
 		HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 	}
 	l7map := L7DataMap{
-		wildcardCachedSelector: &PerEpData{
+		wildcardCachedSelector: &PerSelectorPolicy{
 			L7Rules: l7rules,
 		},
 	}
@@ -79,31 +79,31 @@ func (ds *PolicyTestSuite) TestL4Policy(c *C) {
 	expected := NewL4Policy(0)
 	expected.Ingress["80/TCP"] = &L4Filter{
 		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:        "http", L7RulesPerEp: l7map, Ingress: true,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "http", L7RulesPerSelector: l7map, Ingress: true,
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}
 	expected.Ingress["8080/TCP"] = &L4Filter{
 		Port: 8080, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:        "http", L7RulesPerEp: l7map, Ingress: true,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "http", L7RulesPerSelector: l7map, Ingress: true,
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}
 
 	expected.Egress["3000/TCP"] = &L4Filter{
 		Port: 3000, Protocol: api.ProtoTCP, U8Proto: 6, Ingress: false,
-		allowsAllAtL3:    true,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7RulesPerEp:     L7DataMap{},
+		wildcard: wildcardCachedSelector,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+		},
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}
 	expected.Egress["3000/UDP"] = &L4Filter{
 		Port: 3000, Protocol: api.ProtoUDP, U8Proto: 17, Ingress: false,
-		allowsAllAtL3:    true,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7RulesPerEp:     L7DataMap{},
+		wildcard: wildcardCachedSelector,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+		},
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}
 
@@ -151,6 +151,7 @@ func (ds *PolicyTestSuite) TestL4Policy(c *C) {
 			EndpointSelector: api.NewESFromLabels(labels.ParseSelectLabel("bar")),
 			Ingress: []api.IngressRule{
 				{
+					// Note that this allows all on 80, so the result should wildcard HTTP
 					ToPorts: []api.PortRule{{
 						Ports: []api.PortProtocol{
 							{Port: "80", Protocol: api.ProtoTCP},
@@ -184,16 +185,15 @@ func (ds *PolicyTestSuite) TestL4Policy(c *C) {
 
 	expected = NewL4Policy(0)
 	expected.Ingress["80/TCP"] = &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
-					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
+					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}, {}},
 				},
 			},
 		},
@@ -202,26 +202,35 @@ func (ds *PolicyTestSuite) TestL4Policy(c *C) {
 	}
 	expected.Egress["3000/TCP"] = &L4Filter{
 		Port: 3000, Protocol: api.ProtoTCP, U8Proto: 6, Ingress: false,
-		allowsAllAtL3:    true,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7RulesPerEp:     L7DataMap{},
+		wildcard: wildcardCachedSelector,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+		},
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}
 	expected.Egress["3000/UDP"] = &L4Filter{
 		Port: 3000, Protocol: api.ProtoUDP, U8Proto: 17, Ingress: false,
-		allowsAllAtL3:    true,
-		CachedSelectors:  CachedSelectorSlice{wildcardCachedSelector},
-		L7RulesPerEp:     L7DataMap{},
+		wildcard: wildcardCachedSelector,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+		},
 		DerivedFromRules: labels.LabelArrayList{nil},
 	}
 
 	ingressState = traceState{}
 	egressState = traceState{}
 	res = NewL4Policy(0)
-	res.Ingress, err = rule2.resolveIngressPolicy(testPolicyContext, toBar, &ingressState, L4PolicyMap{}, nil)
+
+	buffer := new(bytes.Buffer)
+	ctx := SearchContext{To: labels.ParseSelectLabelArray("bar"), Trace: TRACE_VERBOSE}
+	ctx.Logging = logging.NewLogBackend(buffer, "", 0)
+
+	res.Ingress, err = rule2.resolveIngressPolicy(testPolicyContext, &ctx, &ingressState, L4PolicyMap{}, nil)
 	c.Assert(err, IsNil)
 	c.Assert(res.Ingress, Not(IsNil))
 
+	c.Log(buffer)
+
 	res.Egress, err = rule2.resolveEgressPolicy(testPolicyContext, fromBar, &egressState, L4PolicyMap{}, nil)
 	c.Assert(err, IsNil)
 	c.Assert(res.Egress, Not(IsNil))
@@ -282,10 +291,13 @@ func (ds *PolicyTestSuite) TestMergeL4PolicyIngress(c *C) {
 		},
 	}
 
-	mergedES := CachedSelectorSlice{cachedFooSelector, cachedBazSelector}
+	mergedES := L7DataMap{
+		cachedFooSelector: nil,
+		cachedBazSelector: nil,
+	}
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6, CachedSelectors: mergedES,
-		L7Parser: ParserTypeNone, L7RulesPerEp: L7DataMap{}, Ingress: true,
+		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
+		L7Parser: ParserTypeNone, L7RulesPerSelector: mergedES, Ingress: true,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
 
@@ -334,10 +346,13 @@ func (ds *PolicyTestSuite) TestMergeL4PolicyEgress(c *C) {
 		},
 	}
 
-	mergedES := CachedSelectorSlice{cachedFooSelector, cachedBazSelector}
+	mergedES := L7DataMap{
+		cachedFooSelector: nil,
+		cachedBazSelector: nil,
+	}
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6, CachedSelectors: mergedES,
-		L7Parser: ParserTypeNone, L7RulesPerEp: L7DataMap{}, Ingress: false,
+		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
+		L7Parser: ParserTypeNone, L7RulesPerSelector: mergedES, Ingress: false,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
 
@@ -367,6 +382,7 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyIngress(c *C) {
 			EndpointSelector: api.NewESFromLabels(labels.ParseSelectLabel("bar")),
 			Ingress: []api.IngressRule{
 				{
+					// Note that this allows all on 80, so the result should wildcard HTTP
 					ToPorts: []api.PortRule{{
 						Ports: []api.PortProtocol{
 							{Port: "80", Protocol: api.ProtoTCP},
@@ -403,19 +419,18 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyIngress(c *C) {
 	}
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
-		Port:            80,
-		Protocol:        api.ProtoTCP,
-		U8Proto:         6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedFooSelector},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		Port:     80,
+		Protocol: api.ProtoTCP,
+		U8Proto:  6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
-					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
+					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}, {}},
 				},
 			},
-			cachedFooSelector: &PerEpData{
+			cachedFooSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
 				},
@@ -479,19 +494,18 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyIngress(c *C) {
 		Kafka: []api.PortRuleKafka{{Topic: "foo"}},
 	}
 	l7map := L7DataMap{
-		wildcardCachedSelector: &PerEpData{
+		wildcardCachedSelector: &PerSelectorPolicy{
 			L7Rules: l7rules,
 		},
-		cachedFooSelector: &PerEpData{
+		cachedFooSelector: &PerSelectorPolicy{
 			L7Rules: l7rules,
 		},
 	}
 
 	expected = L4PolicyMap{"80/TCP": &L4Filter{
 		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedFooSelector},
-		L7Parser:        "kafka", L7RulesPerEp: l7map, Ingress: true,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "kafka", L7RulesPerSelector: l7map, Ingress: true,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
 
@@ -569,18 +583,17 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyIngress(c *C) {
 
 	// The L3-dependent L7 rules are not merged together.
 	l7map = L7DataMap{
-		cachedFooSelector: &PerEpData{
+		cachedFooSelector: &PerSelectorPolicy{
 			L7Rules: fooRules,
 		},
-		wildcardCachedSelector: &PerEpData{
+		wildcardCachedSelector: &PerSelectorPolicy{
 			L7Rules: barRules,
 		},
 	}
 	expected = L4PolicyMap{"80/TCP": &L4Filter{
 		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{cachedFooSelector, wildcardCachedSelector},
-		L7Parser:        "kafka", L7RulesPerEp: l7map, Ingress: true,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "kafka", L7RulesPerSelector: l7map, Ingress: true,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
 
@@ -608,6 +621,7 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyEgress(c *C) {
 			EndpointSelector: api.NewESFromLabels(labels.ParseSelectLabel("bar")),
 			Egress: []api.EgressRule{
 				{
+					// Note that this allows all on 80, so the result should wildcard HTTP
 					ToPorts: []api.PortRule{{
 						Ports: []api.PortProtocol{
 							{Port: "80", Protocol: api.ProtoTCP},
@@ -645,16 +659,15 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyEgress(c *C) {
 
 	expected := L4PolicyMap{"80/TCP": &L4Filter{
 		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedFooSelector},
-		L7Parser:        ParserTypeHTTP,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeHTTP,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
-					HTTP: []api.PortRuleHTTP{{Path: "/public", Method: "GET"}},
+					HTTP: []api.PortRuleHTTP{{Path: "/public", Method: "GET"}, {}},
 				},
 			},
-			cachedFooSelector: &PerEpData{
+			cachedFooSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					HTTP: []api.PortRuleHTTP{{Path: "/private", Method: "GET"}},
 				},
@@ -686,16 +699,17 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyEgress(c *C) {
 			EndpointSelector: api.NewESFromLabels(labels.ParseSelectLabel("bar")),
 			Egress: []api.EgressRule{
 				{
+					// Note that this allows all on 9092, so the result should wildcard Kafka
 					ToPorts: []api.PortRule{{
 						Ports: []api.PortProtocol{
-							{Port: "80", Protocol: api.ProtoTCP},
+							{Port: "9092", Protocol: api.ProtoTCP},
 						},
 					}},
 				},
 				{
 					ToPorts: []api.PortRule{{
 						Ports: []api.PortProtocol{
-							{Port: "80", Protocol: api.ProtoTCP},
+							{Port: "9092", Protocol: api.ProtoTCP},
 						},
 						Rules: &api.L7Rules{
 							Kafka: []api.PortRuleKafka{
@@ -708,7 +722,7 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyEgress(c *C) {
 					ToEndpoints: fooSelector,
 					ToPorts: []api.PortRule{{
 						Ports: []api.PortProtocol{
-							{Port: "80", Protocol: api.ProtoTCP},
+							{Port: "9092", Protocol: api.ProtoTCP},
 						},
 						Rules: &api.L7Rules{
 							Kafka: []api.PortRuleKafka{
@@ -721,18 +735,17 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyEgress(c *C) {
 		},
 	}
 
-	expected = L4PolicyMap{"80/TCP": &L4Filter{
-		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{wildcardCachedSelector, cachedFooSelector},
-		L7Parser:        ParserTypeKafka,
-		L7RulesPerEp: L7DataMap{
-			wildcardCachedSelector: &PerEpData{
+	expected = L4PolicyMap{"9092/TCP": &L4Filter{
+		Port: 9092, Protocol: api.ProtoTCP, U8Proto: 6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: ParserTypeKafka,
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
-					Kafka: []api.PortRuleKafka{{Topic: "foo"}},
+					Kafka: []api.PortRuleKafka{{Topic: "foo"}, {}},
 				},
 			},
-			cachedFooSelector: &PerEpData{
+			cachedFooSelector: &PerSelectorPolicy{
 				L7Rules: api.L7Rules{
 					Kafka: []api.PortRuleKafka{{Topic: "foo"}},
 				},
@@ -809,18 +822,17 @@ func (ds *PolicyTestSuite) TestMergeL7PolicyEgress(c *C) {
 
 	// The l3-dependent l7 rules are not merged together.
 	l7map := L7DataMap{
-		cachedFooSelector: &PerEpData{
+		cachedFooSelector: &PerSelectorPolicy{
 			L7Rules: fooRules,
 		},
-		wildcardCachedSelector: &PerEpData{
+		wildcardCachedSelector: &PerSelectorPolicy{
 			L7Rules: barRules,
 		},
 	}
 	expected = L4PolicyMap{"80/TCP": &L4Filter{
 		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
-		allowsAllAtL3:   true,
-		CachedSelectors: CachedSelectorSlice{cachedFooSelector, wildcardCachedSelector},
-		L7Parser:        "kafka", L7RulesPerEp: l7map, Ingress: false,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "kafka", L7RulesPerSelector: l7map, Ingress: false,
 		DerivedFromRules: labels.LabelArrayList{nil, nil},
 	}}
 
@@ -1453,8 +1465,8 @@ func (ds *PolicyTestSuite) TestIngressL4AllowAll(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 1)
-	c.Assert(filter.CachedSelectors[0], checker.Equals, wildcardCachedSelector)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], IsNil)
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -1522,8 +1534,8 @@ func (ds *PolicyTestSuite) TestEgressL4AllowAll(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, false)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 1)
-	c.Assert(filter.CachedSelectors[0], checker.Equals, wildcardCachedSelector)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], IsNil)
 	l4EgressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -1576,7 +1588,7 @@ func (ds *PolicyTestSuite) TestEgressL4AllowWorld(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, false)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 1)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 	l4EgressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -1629,7 +1641,7 @@ func (ds *PolicyTestSuite) TestEgressL4AllowAllEntity(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, false)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 1)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 1)
 	l4EgressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -1735,9 +1747,47 @@ func (ds *PolicyTestSuite) TestL4WildcardMerge(c *C) {
 					},
 				}},
 			},
+			{
+				FromEndpoints: []api.EndpointSelector{endpointSelectorC},
+				ToPorts: []api.PortRule{{
+					Ports: []api.PortProtocol{
+						{Port: "7000", Protocol: api.ProtoTCP},
+					},
+					Rules: &api.L7Rules{
+						L7Proto: "testparser",
+						L7: []api.PortRuleL7{
+							{"Key": "Value"},
+						},
+					},
+				}},
+			},
+			{
+				FromEndpoints: []api.EndpointSelector{endpointSelectorC},
+				ToPorts: []api.PortRule{{
+					Ports: []api.PortProtocol{
+						{Port: "7000", Protocol: api.ProtoTCP},
+					},
+				}},
+			},
 		},
 	}})
 
+	expected := &L4Filter{
+		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "http",
+		L7RulesPerSelector: L7DataMap{
+			wildcardCachedSelector: nil,
+			cachedSelectorC: &PerSelectorPolicy{
+				L7Rules: api.L7Rules{
+					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
+				},
+			},
+		},
+		Ingress:          true,
+		DerivedFromRules: labels.LabelArrayList{nil, nil},
+	}
+
 	buffer := new(bytes.Buffer)
 	ctx := SearchContext{To: labelsA, Trace: TRACE_VERBOSE}
 	ctx.Logging = logging.NewLogBackend(buffer, "", 0)
@@ -1752,12 +1802,38 @@ func (ds *PolicyTestSuite) TestL4WildcardMerge(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 2)
-	c.Assert(filter.CachedSelectors[0], checker.Equals, cachedSelectorC)
-	c.Assert(filter.CachedSelectors[1], checker.Equals, wildcardCachedSelector)
-
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter.L7RulesPerSelector[cachedSelectorC], Not(IsNil))
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], IsNil)
+	c.Assert(filter, checker.Equals, expected)
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 2)
+
+	expectedL7 := &L4Filter{
+		Port: 7000, Protocol: api.ProtoTCP, U8Proto: 6,
+		L7Parser: "testparser",
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorC: &PerSelectorPolicy{
+				L7Rules: api.L7Rules{
+					L7Proto: "testparser",
+					L7:      []api.PortRuleL7{{"Key": "Value"}, {}},
+				},
+			},
+		},
+		Ingress:          true,
+		DerivedFromRules: labels.LabelArrayList{nil, nil},
+	}
+
+	filterL7, ok := l4IngressPolicy["7000/TCP"]
+	c.Assert(ok, Equals, true)
+	c.Assert(filterL7.Port, Equals, 7000)
+	c.Assert(filterL7.Ingress, Equals, true)
+
+	c.Assert(len(filterL7.L7RulesPerSelector), Equals, 1)
+	c.Assert(filterL7.L7RulesPerSelector[cachedSelectorC], Not(IsNil))
+	c.Assert(filterL7.L7RulesPerSelector[wildcardCachedSelector], IsNil)
+	c.Assert(filterL7, checker.Equals, expectedL7)
+	c.Assert(filterL7.L7Parser, Equals, L7ParserType("testparser"))
+
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 
 	// Test the reverse order as well; ensure that we check both conditions
@@ -1767,6 +1843,28 @@ func (ds *PolicyTestSuite) TestL4WildcardMerge(c *C) {
 	repo = parseAndAddRules(c, api.Rules{&api.Rule{
 		EndpointSelector: endpointSelectorA,
 		Ingress: []api.IngressRule{
+			{
+				FromEndpoints: []api.EndpointSelector{endpointSelectorC},
+				ToPorts: []api.PortRule{{
+					Ports: []api.PortProtocol{
+						{Port: "7000", Protocol: api.ProtoTCP},
+					},
+				}},
+			},
+			{
+				FromEndpoints: []api.EndpointSelector{endpointSelectorC},
+				ToPorts: []api.PortRule{{
+					Ports: []api.PortProtocol{
+						{Port: "7000", Protocol: api.ProtoTCP},
+					},
+					Rules: &api.L7Rules{
+						L7Proto: "testparser",
+						L7: []api.PortRuleL7{
+							{"Key": "Value"},
+						},
+					},
+				}},
+			},
 			{
 				ToPorts: []api.PortRule{{
 					Ports: []api.PortProtocol{
@@ -1804,12 +1902,23 @@ func (ds *PolicyTestSuite) TestL4WildcardMerge(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 2)
-	c.Assert(filter.CachedSelectors[0], checker.Equals, wildcardCachedSelector)
-	c.Assert(filter.CachedSelectors[1], checker.Equals, cachedSelectorC)
-
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], IsNil)
+	c.Assert(filter.L7RulesPerSelector[cachedSelectorC], Not(IsNil))
+	c.Assert(filter, checker.Equals, expected)
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 2)
+
+	filterL7, ok = l4IngressPolicy["7000/TCP"]
+	c.Assert(ok, Equals, true)
+	c.Assert(filterL7.Port, Equals, 7000)
+	c.Assert(filterL7.Ingress, Equals, true)
+
+	c.Assert(len(filterL7.L7RulesPerSelector), Equals, 1)
+	c.Assert(filterL7.L7RulesPerSelector[cachedSelectorC], Not(IsNil))
+	c.Assert(filterL7.L7RulesPerSelector[wildcardCachedSelector], IsNil)
+	c.Assert(filterL7, checker.Equals, expectedL7)
+	c.Assert(filterL7.L7Parser, Equals, L7ParserType("testparser"))
+
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 
 	// Second, test the explicit allow at L3.
@@ -1854,9 +1963,9 @@ func (ds *PolicyTestSuite) TestL4WildcardMerge(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 2)
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 2)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter, checker.Equals, expected)
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 
 	// Test the reverse order as well; ensure that we check both conditions
@@ -1904,10 +2013,9 @@ func (ds *PolicyTestSuite) TestL4WildcardMerge(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 2)
-
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 2)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter, checker.Equals, expected)
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 }
 
@@ -1962,12 +2070,27 @@ func (ds *PolicyTestSuite) TestL3L4L7Merge(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 2)
-	c.Assert(filter.CachedSelectors[0], checker.Equals, wildcardCachedSelector)
-	c.Assert(filter.CachedSelectors[1], checker.Equals, cachedSelectorC)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], Not(IsNil))
+	c.Assert(filter.L7RulesPerSelector[cachedSelectorC], IsNil)
 
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 2)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter, checker.Equals, &L4Filter{
+		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "http",
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorC: nil,
+			wildcardCachedSelector: &PerSelectorPolicy{
+				L7Rules: api.L7Rules{
+					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
+				},
+			},
+		},
+		Ingress:          true,
+		DerivedFromRules: labels.LabelArrayList{nil, nil},
+	})
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 
 	repo = parseAndAddRules(c, api.Rules{&api.Rule{
@@ -2010,12 +2133,26 @@ func (ds *PolicyTestSuite) TestL3L4L7Merge(c *C) {
 	c.Assert(filter.Port, Equals, 80)
 	c.Assert(filter.Ingress, Equals, true)
 
-	c.Assert(len(filter.CachedSelectors), Equals, 2)
-	c.Assert(filter.CachedSelectors[0], checker.Equals, wildcardCachedSelector)
-	c.Assert(filter.CachedSelectors[1], checker.Equals, cachedSelectorC)
-
 	c.Assert(filter.L7Parser, Equals, ParserTypeHTTP)
-	c.Assert(len(filter.L7RulesPerEp), Equals, 2)
+	c.Assert(len(filter.L7RulesPerSelector), Equals, 2)
+	c.Assert(filter.L7RulesPerSelector[wildcardCachedSelector], Not(IsNil))
+	c.Assert(filter.L7RulesPerSelector[cachedSelectorC], IsNil)
+	c.Assert(filter, checker.Equals, &L4Filter{
+		Port: 80, Protocol: api.ProtoTCP, U8Proto: 6,
+		wildcard: wildcardCachedSelector,
+		L7Parser: "http",
+		L7RulesPerSelector: L7DataMap{
+			cachedSelectorC: nil,
+			wildcardCachedSelector: &PerSelectorPolicy{
+				L7Rules: api.L7Rules{
+					HTTP: []api.PortRuleHTTP{{Path: "/", Method: "GET"}},
+				},
+			},
+		},
+		Ingress:          true,
+		DerivedFromRules: labels.LabelArrayList{nil, nil},
+	})
+
 	l4IngressPolicy.Detach(repo.GetSelectorCache())
 }
 
diff --git a/pkg/policy/visibility_test.go b/pkg/policy/visibility_test.go
index e6db68e916..706a20f28d 100644
--- a/pkg/policy/visibility_test.go
+++ b/pkg/policy/visibility_test.go
@@ -34,14 +34,14 @@ func (ds *PolicyTestSuite) TestGenerateL7RulesByParser(c *C) {
 	c.Assert(m, Not(IsNil))
 	c.Assert(len(m), Equals, 1)
 
-	var l7Rules []*PerEpData
+	var l7Rules []*PerSelectorPolicy
 	for _, v := range m {
 		l7Rules = append(l7Rules, v)
 	}
 
 	// Check that we allow all at L7 for DNS for the one rule we should have
 	// generated.
-	c.Assert(l7Rules[0], checker.DeepEquals, &PerEpData{L7Rules: api.L7Rules{DNS: []api.PortRuleDNS{{MatchPattern: "*"}}}})
+	c.Assert(l7Rules[0], checker.DeepEquals, &PerSelectorPolicy{L7Rules: api.L7Rules{DNS: []api.PortRuleDNS{{MatchPattern: "*"}}}})
 }
 
 func (ds *PolicyTestSuite) TestVisibilityPolicyCreation(c *C) {
diff --git a/pkg/proxy/kafka_test.go b/pkg/proxy/kafka_test.go
index 98cd9be5da..0056052b9d 100644
--- a/pkg/proxy/kafka_test.go
+++ b/pkg/proxy/kafka_test.go
@@ -283,7 +283,7 @@ func (s *proxyTestSuite) TestKafkaRedirect(c *C) {
 	r := newRedirect(localEndpointMock, pp, uint16(portInt))
 
 	r.rules = policy.L7DataMap{
-		wildcardCachedSelector: &policy.PerEpData{L7Rules: api.L7Rules{
+		wildcardCachedSelector: &policy.PerSelectorPolicy{L7Rules: api.L7Rules{
 			Kafka: []api.PortRuleKafka{kafkaRule1, kafkaRule2},
 		}},
 	}
diff --git a/test/helpers/kubectl.go b/test/helpers/kubectl.go
index dd001a5d4d..a966c1d6a7 100644
--- a/test/helpers/kubectl.go
+++ b/test/helpers/kubectl.go
@@ -1568,6 +1568,16 @@ func (kub *Kubectl) CiliumEndpointsStatus(pod string) map[string]string {
 		"cilium endpoint list -o jsonpath='%s'", filter)).KVOutput()
 }
 
+// CiliumEndpointIPv6 returns the IPv6 address of each endpoint which matches
+// the given endpoint selector.
+func (kub *Kubectl) CiliumEndpointIPv6(pod string, endpoint string) map[string]string {
+	filter := `{range [*]}{@.status.external-identifiers.pod-name}{"="}{@.status.networking.addressing[*].ipv6}{"\n"}{end}`
+	ctx, cancel := context.WithTimeout(context.Background(), ShortCommandTimeout)
+	defer cancel()
+	return kub.CiliumExecContext(ctx, pod, fmt.Sprintf(
+		"cilium endpoint get %s -o jsonpath='%s'", endpoint, filter)).KVOutput()
+}
+
 // CiliumEndpointWaitReady waits until all endpoints managed by all Cilium pod
 // are ready. Returns an error if the Cilium pods cannot be retrieved via
 // Kubernetes, or endpoints are not ready after a specified timeout
@@ -2814,6 +2824,43 @@ CILIUM_SERVICES:
 	return validateCiliumSvcLB(*ciliumService, ciliumLB)
 }
 
+// CiliumServiceAdd adds the given service on a 'pod' running Cilium
+func (kub *Kubectl) CiliumServiceAdd(pod string, id int64, frontend string, backends []string, svcType, trafficPolicy string) error {
+	var opts []string
+	switch strings.ToLower(svcType) {
+	case "nodeport":
+		opts = append(opts, "--k8s-node-port")
+	case "externalip":
+		opts = append(opts, "--k8s-external")
+	case "clusterip":
+		// this is the default
+	default:
+		return fmt.Errorf("invalid service type: %q", svcType)
+	}
+
+	trafficPolicy = strings.Title(strings.ToLower(trafficPolicy))
+	switch trafficPolicy {
+	case "Cluster", "Local":
+		opts = append(opts, "--k8s-traffic-policy "+trafficPolicy)
+	default:
+		return fmt.Errorf("invalid traffic policy: %q", svcType)
+	}
+
+	optsStr := strings.Join(opts, " ")
+	backendsStr := strings.Join(backends, ",")
+	ctx, cancel := context.WithTimeout(context.Background(), ShortCommandTimeout)
+	defer cancel()
+	return kub.CiliumExecContext(ctx, pod, fmt.Sprintf("cilium service update --id %d --frontend %q --backends %q %s",
+		id, frontend, backendsStr, optsStr)).GetErr("cilium service update")
+}
+
+// CiliumServiceDel deletes the service with 'id' on a 'pod' running Cilium
+func (kub *Kubectl) CiliumServiceDel(pod string, id int64) error {
+	ctx, cancel := context.WithTimeout(context.Background(), ShortCommandTimeout)
+	defer cancel()
+	return kub.CiliumExecContext(ctx, pod, fmt.Sprintf("cilium service delete %d", id)).GetErr("cilium service delete")
+}
+
 // ciliumServicePreFlightCheck checks that k8s service is plumbed correctly
 func (kub *Kubectl) ciliumServicePreFlightCheck() error {
 	ginkgoext.By("Performing Cilium service preflight check")
diff --git a/test/k8sT/Services.go b/test/k8sT/Services.go
index 97acc0b6d9..00c2db2649 100644
--- a/test/k8sT/Services.go
+++ b/test/k8sT/Services.go
@@ -104,6 +104,37 @@ var _ = Describe("K8sServicesTest", func() {
 		kubectl.CloseSSHClient()
 	})
 
+	ciliumIPv6Backends := func(label string, port string) (backends []string) {
+		ciliumPods, err := kubectl.GetCiliumPods(helpers.CiliumNamespace)
+		Expect(err).To(BeNil(), "Cannot get cilium pods")
+		for _, pod := range ciliumPods {
+			endpointIPs := kubectl.CiliumEndpointIPv6(pod, label)
+			for _, ip := range endpointIPs {
+				backends = append(backends, net.JoinHostPort(ip, port))
+			}
+		}
+		Expect(backends).To(Not(BeEmpty()), "Cannot find any IPv6 backends")
+		return backends
+	}
+
+	ciliumAddService := func(id int64, frontend string, backends []string, svcType, trafficPolicy string) {
+		ciliumPods, err := kubectl.GetCiliumPods(helpers.CiliumNamespace)
+		Expect(err).To(BeNil(), "Cannot get cilium pods")
+		for _, pod := range ciliumPods {
+			err := kubectl.CiliumServiceAdd(pod, id, frontend, backends, svcType, trafficPolicy)
+			Expect(err).To(BeNil(), "Failed to add cilium service")
+		}
+	}
+
+	ciliumDelService := func(id int64) {
+		ciliumPods, err := kubectl.GetCiliumPods(helpers.CiliumNamespace)
+		Expect(err).To(BeNil(), "Cannot get cilium pods")
+		for _, pod := range ciliumPods {
+			// ignore result so tear down still continues on failures
+			_ = kubectl.CiliumServiceDel(pod, id)
+		}
+	}
+
 	testCurlRequest := func(clientPodLabel, url string) {
 		pods, err := kubectl.GetPodNames(helpers.DefaultNamespace, clientPodLabel)
 		ExpectWithOffset(1, err).Should(BeNil(), "cannot retrieve pod names by filter %q", testDSClient)
@@ -201,6 +232,58 @@ var _ = Describe("K8sServicesTest", func() {
 			url = fmt.Sprintf("tftp://%s/hello", clusterIP)
 			testCurlRequest(echoPodLabel, url)
 		}, 300)
+
+		SkipContextIf(helpers.RunsWithKubeProxy, "IPv6 Connectivity", func() {
+			// Because the deployed K8s does not have dual-stack mode enabled,
+			// we install the Cilium service rules manually via Cilium CLI.
+
+			demoClusterIPv6 := "fd03::100"
+			echoClusterIPv6 := "fd03::200"
+
+			BeforeEach(func() {
+				// Installs the IPv6 equivalent of app1-service (demo.yaml)
+				err := kubectl.WaitforPods(helpers.DefaultNamespace, "-l id=app1", helpers.HelperTimeout)
+				Expect(err).Should(BeNil())
+				httpBackends := ciliumIPv6Backends("-l k8s:id=app1", "80")
+				ciliumAddService(10080, net.JoinHostPort(demoClusterIPv6, "80"), httpBackends, "ClusterIP", "Cluster")
+				tftpBackends := ciliumIPv6Backends("-l k8s:id=app1", "69")
+				ciliumAddService(10069, net.JoinHostPort(demoClusterIPv6, "69"), tftpBackends, "ClusterIP", "Cluster")
+				// Installs the IPv6 equivalent of echo (echo-svc.yaml)
+				err = kubectl.WaitforPods(helpers.DefaultNamespace, "-l name=echo", helpers.HelperTimeout)
+				Expect(err).Should(BeNil())
+				httpBackends = ciliumIPv6Backends("-l k8s:name=echo", "80")
+				ciliumAddService(20080, net.JoinHostPort(echoClusterIPv6, "80"), httpBackends, "ClusterIP", "Cluster")
+				tftpBackends = ciliumIPv6Backends("-l k8s:name=echo", "69")
+				ciliumAddService(20069, net.JoinHostPort(echoClusterIPv6, "69"), tftpBackends, "ClusterIP", "Cluster")
+			})
+
+			AfterEach(func() {
+				ciliumDelService(10080)
+				ciliumDelService(10069)
+				ciliumDelService(20080)
+				ciliumDelService(20069)
+			})
+
+			It("Checks service on same node", func() {
+				k8s1Name, _ := getNodeInfo(helpers.K8s1)
+				status, err := kubectl.ExecInHostNetNS(context.TODO(), k8s1Name,
+					helpers.CurlFail(`"http://[%s]/"`, demoClusterIPv6))
+				Expect(err).To(BeNil(), "Cannot run curl in host netns")
+				status.ExpectSuccess("cannot curl to service IP from host")
+
+				status, err = kubectl.ExecInHostNetNS(context.TODO(), k8s1Name,
+					helpers.CurlFail(`"tftp://[%s]/hello"`, demoClusterIPv6))
+				Expect(err).To(BeNil(), "Cannot run curl in host netns")
+				status.ExpectSuccess("cannot curl to service IP from host")
+			})
+
+			It("Checks service accessing itself (hairpin flow)", func() {
+				url := fmt.Sprintf(`"http://[%s]/"`, echoClusterIPv6)
+				testCurlRequest(echoPodLabel, url)
+				url = fmt.Sprintf(`"tftp://[%s]/hello"`, echoClusterIPv6)
+				testCurlRequest(echoPodLabel, url)
+			})
+		})
 	})
 
 	Context("Checks service across nodes", func() {
@@ -237,6 +320,32 @@ var _ = Describe("K8sServicesTest", func() {
 			testCurlRequest(testDSClient, url)
 		})
 
+		SkipContextIf(helpers.RunsWithKubeProxy, "IPv6 Connectivity", func() {
+			testDSIPv6 := "fd03::310"
+
+			BeforeAll(func() {
+				// Install rules for testds-service (demo_ds.yaml)
+				waitPodsDs()
+				httpBackends := ciliumIPv6Backends("-l k8s:zgroup=testDS", "80")
+				ciliumAddService(31080, net.JoinHostPort(testDSIPv6, "80"), httpBackends, "ClusterIP", "Cluster")
+				tftpBackends := ciliumIPv6Backends("-l k8s:zgroup=testDS", "69")
+				ciliumAddService(31069, net.JoinHostPort(testDSIPv6, "69"), tftpBackends, "ClusterIP", "Cluster")
+			})
+
+			AfterAll(func() {
+				ciliumDelService(31080)
+				ciliumDelService(31069)
+			})
+
+			It("Checks ClusterIP Connectivity", func() {
+				url := fmt.Sprintf(`"http://[%s]/"`, testDSIPv6)
+				testCurlRequest(testDSClient, url)
+
+				url = fmt.Sprintf(`"tftp://[%s]/hello"`, testDSIPv6)
+				testCurlRequest(testDSClient, url)
+			})
+		})
+
 		getHTTPLink := func(host string, port int32) string {
 			return fmt.Sprintf("http://%s",
 				net.JoinHostPort(host, fmt.Sprintf("%d", port)))
@@ -309,7 +418,10 @@ var _ = Describe("K8sServicesTest", func() {
 					ExpectWithOffset(1, res).Should(helpers.CMDSuccess(),
 						"Can not connect to service %q from outside cluster", url)
 					if checkSourceIP {
-						Expect(strings.TrimSpace(strings.Split(res.GetStdOut(), "=")[1])).To(Equal(clientIP))
+						// Parse the IPs to avoid issues with 4-in-6 formats
+						sourceIP := net.ParseIP(strings.TrimSpace(strings.Split(res.GetStdOut(), "=")[1]))
+						clientIP := net.ParseIP(clientIP)
+						Expect(sourceIP).To(Equal(clientIP))
 					}
 				}
 			}
diff --git a/test/k8sT/manifests/demo_ds.yaml b/test/k8sT/manifests/demo_ds.yaml
index 63ae7cb823..6a4484445e 100644
--- a/test/k8sT/manifests/demo_ds.yaml
+++ b/test/k8sT/manifests/demo_ds.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: web
-        image: docker.io/cilium/echoserver:1.10
+        image: docker.io/cilium/echoserver:1.10.1
         imagePullPolicy: IfNotPresent
         ports:
         - containerPort: 80
@@ -76,7 +76,7 @@ spec:
     spec:
       containers:
       - name: web
-        image: docker.io/cilium/echoserver:1.10
+        image: docker.io/cilium/echoserver:1.10.1
         imagePullPolicy: IfNotPresent
         ports:
         - containerPort: 80
diff --git a/test/k8sT/manifests/echo-svc.yaml b/test/k8sT/manifests/echo-svc.yaml
index 5585792a53..27c75121ee 100644
--- a/test/k8sT/manifests/echo-svc.yaml
+++ b/test/k8sT/manifests/echo-svc.yaml
@@ -14,7 +14,7 @@ spec:
     spec:
       containers:
       - name: echo-container
-        image: docker.io/cilium/json-mock:1.1
+        image: docker.io/cilium/json-mock:1.2
         livenessProbe:
           exec:
             command:
diff --git a/test/runtime/Policies.go b/test/runtime/Policies.go
index 2f6597ad87..84e9f942b1 100644
--- a/test/runtime/Policies.go
+++ b/test/runtime/Policies.go
@@ -395,7 +395,7 @@ var _ = Describe("RuntimePolicies", func() {
 		_, err = vm.PolicyImportAndWait(vm.GetFullPath(multL7PoliciesJSON), helpers.HelperTimeout)
 		Expect(err).Should(BeNil())
 
-		//APP1 can connnect to public, but no to private
+		//APP1 can connect to public, but no to private
 
 		connectivityTest(httpRequestsPublic, helpers.App1, helpers.Httpd1, true)
 		connectivityTest(httpRequestsPrivate, helpers.App1, helpers.Httpd1, false)
@@ -403,6 +403,10 @@ var _ = Describe("RuntimePolicies", func() {
 		//App2 can't connect
 		connectivityTest(httpRequestsPublic, helpers.App2, helpers.Httpd1, false)
 
+		By("Multiple Ingress rules on same port")
+		// app1 can connect to /public on httpd2
+		connectivityTest(httpRequestsPublic, helpers.App1, helpers.Httpd2, true)
+
 		By("Multiple Egress")
 		// app2 can connect to /public, but not to /private
 		connectivityTest(httpRequestsPublic, helpers.App2, helpers.Httpd2, true)
@@ -528,31 +532,17 @@ var _ = Describe("RuntimePolicies", func() {
 
 		connectivityTest(httpRequestsPublic, helpers.App3, helpers.Httpd2, true)
 
-		// Since policy allows connectivity on L3 from app3 to httpd2, we expect:
-		// * two more requests to get received by the proxy because even though
-		// only L3 policy applies for connectivity from app3 to httpd2, because
-		// app3 has L7 policy applied to it, all traffic goes through the proxy.
-		// * two more requests to get forwarded by the proxy because policy allows
-		// app3 to talk to httpd2.
-		// * no increase in requests denied by the proxy.
-		// * two more corresponding responses forwarded / received to the aforementioned requests due to policy
-		// allowing connectivity via http / http6.
-		checkProxyStatistics(app3EndpointID, 4, 6, 2, 4, 4)
+		// Since policy allows connectivity on L3 from app3 to httpd2, and such
+		// packets are not forwarded to the proxy, we expect no changes in proxy
+		// stats.
+		checkProxyStatistics(app3EndpointID, 2, 4, 2, 2, 2)
 
 		connectivityTest(httpRequestsPrivate, helpers.App3, helpers.Httpd2, true)
 
-		// Since policy allows connectivity on L3 from app3 to httpd2, we expect:
-		// * two more requests to get received by the proxy because even though
-		// only L3 policy applies for connectivity from app3 to httpd2, because
-		// app3 has L7 policy applied to it, all traffic goes through the proxy.
-		// * two more requests to get forwarded by the proxy because policy allows
-		// app3 to talk to httpd2, even though it's restricted on L7 for connectivity
-		// to httpd1 from app3. This is what tests L3-dependent L7 policy is applied
-		// correctly.
-		// * no increase in requests denied by the proxy.
-		// * two more corresponding responses forwarded / received to the aforementioned requests due to policy
-		// allowing connectivity via http / http6.
-		checkProxyStatistics(app3EndpointID, 6, 8, 2, 6, 6)
+		// Since policy allows connectivity on L3 from app3 to httpd2, and such
+		// packets are not forwarded to the proxy, we expect no changes in proxy
+		// stats.
+		checkProxyStatistics(app3EndpointID, 2, 4, 2, 2, 2)
 	})
 
 	It("Checks CIDR L3 Policy", func() {
diff --git a/test/runtime/fqdn.go b/test/runtime/fqdn.go
index 504d22027b..2eb8b1a18b 100644
--- a/test/runtime/fqdn.go
+++ b/test/runtime/fqdn.go
@@ -177,8 +177,13 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 		ExpectCiliumReady(vm)
 
 		By("Create sample containers in %q docker network", worldNetwork)
-		vm.Exec(fmt.Sprintf("docker network create  %s", worldNetwork)).ExpectSuccess(
-			"%q network cant be created", worldNetwork)
+		res := vm.Exec(fmt.Sprintf("docker network create %s", worldNetwork))
+		if !res.WasSuccessful() {
+			if !strings.Contains(res.GetStdErr(), "network with name world already exists") {
+				res.ExpectSuccess(
+					"%q network cant be created", worldNetwork)
+			}
+		}
 
 		for name, image := range ciliumTestImages {
 			vm.ContainerCreate(name, image, worldNetwork, fmt.Sprintf("-l id.%s", name))
@@ -224,7 +229,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 		// Use a bridge network (The docker default) to be able to use this
 		// server from cilium agent. This should change when DNS proxy is in
 		// place.
-		res := vm.ContainerCreate(
+		res = vm.ContainerCreate(
 			bindContainerName,
 			constants.BindContainerImage,
 			"bridge",
@@ -346,7 +351,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
   }
 ]`
 		fqdnPolicyImport(fqdnPolicy)
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 
 		By("Denying egress to IPs of DNS names not in ToFQDNs, and normal IPs")
 		// www.cilium.io has a different IP than cilium.io (it is CNAMEd as well!),
@@ -402,7 +407,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 		_, err := vm.PolicyRenderAndImport(policy)
 		Expect(err).To(BeNil(), "Policy cannot be imported")
 
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 
 		allowVerdict := "verdict Forwarded DNS Query: world1.cilium.test"
 		deniedVerdict := "verdict Denied DNS Query: world2.cilium.test"
@@ -461,7 +466,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 		_, err := vm.PolicyRenderAndImport(fmt.Sprintf(policy, outsideIps[OutsideHttpd1]))
 		Expect(err).To(BeNil(), "Policy cannot be imported")
 
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 
 		By("Testing connectivity to Cilium.test domain")
 		res := vm.ContainerExec(helpers.App1, helpers.CurlFail(world1Target))
@@ -571,7 +576,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 ]`
 		_, err := vm.PolicyRenderAndImport(fqdnPolicy)
 		Expect(err).To(BeNil(), "Policy cannot be imported")
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 
 		By("Allowing egress to IPs of only the specified DNS names")
 		res := vm.ContainerExec(helpers.App1, helpers.CurlFail(world2Target))
@@ -657,7 +662,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 		_, err := vm.PolicyRenderAndImport(policy)
 		Expect(err).To(BeNil(), "Policy cannot be imported")
 
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 		target := "http://level1CNAME.cilium.test"
 		res := vm.ContainerExec(helpers.App1, helpers.CurlFail(target))
 		res.ExpectSuccess("Container %q cannot access to %q when should work", helpers.App1, target)
@@ -784,7 +789,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 ]`
 		_, err := vm.PolicyRenderAndImport(fqdnPolicy)
 		Expect(err).To(BeNil(), "Policy cannot be imported")
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 
 		By("Denying egress to any IPs or domains")
 		for _, allowedTarget := range []string{"world1.cilium.test", "world2.cilium.test", "world3.cilium.test", "level1CNAME.cilium.test", "level2CNAME.cilium.test"} {
@@ -833,7 +838,7 @@ var _ = Describe("RuntimeFQDNPolicies", func() {
 
 		// Selector cache is populated when a policy is applied on an endpoint.
 		// DNSSEC container is not running yet, so we can't expect the FQDNs to be applied yet.
-		// expectFQDNSareApplied("dnssec.test", 1)
+		// expectFQDNSareApplied("dnssec.test", 0)
 
 		By("Validate that allow target is working correctly")
 		res := vm.ContainerRun(
@@ -1043,7 +1048,7 @@ INITSYSTEM=SYSTEMD`
 		_, err := vm.PolicyRenderAndImport(policy)
 		Expect(err).To(BeNil(), "Policy cannot be imported")
 
-		expectFQDNSareApplied("cilium.test", 1)
+		expectFQDNSareApplied("cilium.test", 0)
 
 		By("Curl from %q to %q", helpers.App1, targetURL)
 		res := vm.ContainerExec(helpers.App1, helpers.CurlFail(targetURL))
diff --git a/test/runtime/manifests/Policies-l7-multiple.json b/test/runtime/manifests/Policies-l7-multiple.json
index 93f3c1cea9..6e5b1d0dd9 100644
--- a/test/runtime/manifests/Policies-l7-multiple.json
+++ b/test/runtime/manifests/Policies-l7-multiple.json
@@ -26,6 +26,30 @@
             {"matchLabels":{"reserved:host":""}},
             {"matchLabels":{"id.app2":""}}
         ]
+    },{
+        "fromEndpoints": [
+            {"matchLabels":{"reserved:host":""}},
+            {"matchLabels":{"id.app1":""}}
+        ],
+        "toPorts": [{
+            "ports": [
+                {"port": "80", "protocol": "tcp"}
+	    ]
+        }]	
+    }, {
+        "fromEndpoints": [
+            {"matchLabels":{"reserved:host":""}},
+            {"matchLabels":{"id.app1":""}}
+        ],
+        "toPorts": [{
+            "ports": [
+                {"port": "80", "protocol": "tcp"}
+	    ],
+            "rules": {
+                "HTTP": [
+                    {"method": "GET", "path": "/private"}]
+            }
+        }]	
     }]
 },
 {