Repository: cilium/cilium
Pull request #11331 (2020-05-05 01:31:11)

Move all 'common' code to 'pkg'
This PR moves the common packages to the pkg folder.

Fixes: #11293
Signed-off-by: Swaminathan Vasudevan <svasudevan@suse.com>



maintainer-s-little-helper[bot]: Please set the appropriate release note label.
coveralls: 
[![Coverage Status](https://coveralls.io/builds/30572503/badge)](https://coveralls.io/builds/30572503)

Coverage decreased (-0.007%) to 44.445% when pulling **4a3df6a64a39edf3265de3b579b9525c477eab22 on soumynathan:move-all-common-code-to-pkg** into **9f325461c403c00c35151e01e04f7f102368e2c8 on cilium:master**.

tklauser: test-me-please
joestringer: Precheck failed,

```
15:13:25  + cd /home/jenkins/workspace/Cilium-PR-Ginkgo-Tests-Validated/src/github.com/cilium/cilium/
15:13:25  + make jenkins-precheck
15:13:25  docker-compose -f test/docker-compose.yml -p Cilium-PR-Ginkgo-Tests-Validated-$BUILD_NUMBER run --rm precheck
15:13:26  Creating network "cilium-pr-ginkgo-tests-validated-19489_default" with the default driver
15:13:29  ineffassign .
15:13:29  contrib/scripts/check-logging-subsys-field.sh
15:13:29  contrib/scripts/check-fmt.sh
15:13:30  Unformatted Go source code:
...
```
pchaigno: test-me-please

Patch:
diff --git a/.gitignore b/.gitignore
index 394b1d9d2e..361889eb03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -49,6 +49,7 @@ outgoing
 *cscope.in.out
 *cscope.po.out
 *tags
+.gdb_history
 
 man/
 
@@ -58,6 +59,8 @@ test/*_manifest.yaml
 test/*_policy.json
 test/*.xml
 test/.vagrant
+test/*.log
+test/bpf/_results
 
 # Emacs backup files
 *~
@@ -65,7 +68,6 @@ test/.vagrant
 # Temporary files that allow build containers/VMs work without git
 GIT_VERSION
 
-test/bpf/_results
-
 # generated from make targets
 *.ok
+*.build_all
diff --git a/CODEOWNERS b/CODEOWNERS
index 31ec259547..eea209194f 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -58,6 +58,7 @@ Dockerfile* @cilium/build
 /daemon/cmd/state.go @cilium/endpoint
 /daemon/cmd/sysctl_linux.go @cilium/bpf
 /Documentation/ @cilium/docs
+/Documentation/cmdref @cilium/nonexistantteam
 /Documentation/bpf.rst @cilium/bpf
 /Documentation/contributing/ @cilium/contributing
 /Documentation/envoy/ @cilium/proxy
diff --git a/Dockerfile b/Dockerfile
index d3a97d860c..b77627a6f8 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -15,7 +15,7 @@ LABEL cilium-sha=${CILIUM_SHA}
 # versions to be built while allowing the new versions to make changes
 # that are not backwards compatible.
 #
-FROM quay.io/cilium/cilium-builder:2020-05-05 as builder
+FROM quay.io/cilium/cilium-builder:2020-05-06 as builder
 ARG CILIUM_SHA=""
 LABEL cilium-sha=${CILIUM_SHA}
 LABEL maintainer="maintainer@cilium.io"
diff --git a/Dockerfile.builder b/Dockerfile.builder
index df67dfce5b..2636f6317b 100644
--- a/Dockerfile.builder
+++ b/Dockerfile.builder
@@ -28,11 +28,7 @@ RUN apt-get update && \
       git \
       libc6-dev \
       libelf-dev \
-      make \
-      rsync \
-      unzip \
-      wget \
-      zip && \
+      make && \
     apt-get purge --auto-remove && \
     apt-get clean && \
     rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
diff --git a/Documentation/cmdref/cilium-agent.md b/Documentation/cmdref/cilium-agent.md
index 7bbff1bc6d..c63236fae1 100644
--- a/Documentation/cmdref/cilium-agent.md
+++ b/Documentation/cmdref/cilium-agent.md
@@ -108,7 +108,6 @@ cilium-agent [flags]
       --install-iptables-rules                        Install base iptables rules for cilium to mainly interact with kube-proxy (and masquerading) (default true)
       --ip-allocation-timeout duration                Time after which an incomplete CIDR allocation is considered failed (default 2m0s)
       --ip-masq-agent-config-path string              ip-masq-agent configuration file path (default "/etc/config/ip-masq-agent")
-      --ip-masq-agent-sync-period duration            ip-masq-agent configuration file synchronization period (default 1m0s)
       --ipam string                                   Backend to use for IPAM (default "hostscope-legacy")
       --ipsec-key-file string                         Path to IPSec key file
       --ipv4-node string                              IPv4 address of node (default "auto")
diff --git a/Documentation/cmdref/cilium-operator.md b/Documentation/cmdref/cilium-operator.md
index f4a4e2efae..2f4385cb39 100644
--- a/Documentation/cmdref/cilium-operator.md
+++ b/Documentation/cmdref/cilium-operator.md
@@ -19,7 +19,7 @@ cilium-operator [flags]
       --aws-release-excess-ips                  Enable releasing excess free IP addresses from AWS ENI.
       --azure-resource-group string             Resource group to use for Azure IPAM
       --azure-subscription-id string            Subscription ID to access Azure API
-      --cilium-endpoint-gc-interval duration    GC interval for cilium endpoints (default 30m0s)
+      --cilium-endpoint-gc-interval duration    GC interval for cilium endpoints (default 10m0s)
       --cluster-id int                          Unique identifier of the cluster
       --cluster-name string                     Name of the cluster (default "default")
       --cnp-node-status-gc-interval duration    GC interval for nodes which have been removed from the cluster in CiliumNetworkPolicy Status (default 2m0s)
@@ -34,7 +34,7 @@ cilium-operator [flags]
   -h, --help                                    help for cilium-operator
       --identity-allocation-mode string         Method to use for identity allocation (default "kvstore")
       --identity-gc-interval duration           GC interval for security identities (default 15m0s)
-      --identity-heartbeat-timeout duration     Timeout after which identity expires on lack of heartbeat (default 15m0s)
+      --identity-heartbeat-timeout duration     Timeout after which identity expires on lack of heartbeat (default 31m0s)
       --ipam string                             Backend to use for IPAM (default "hostscope-legacy")
       --k8s-api-server string                   Kubernetes API server URL
       --k8s-client-burst int                    Burst value allowed for the K8s client
diff --git a/Documentation/concepts/networking.rst b/Documentation/concepts/networking.rst
index 515e18fa09..51823ba185 100644
--- a/Documentation/concepts/networking.rst
+++ b/Documentation/concepts/networking.rst
@@ -46,8 +46,23 @@ When no configuration is provided, Cilium automatically runs in this mode.
 
 In this mode, all cluster nodes form a mesh of tunnels using the UDP based
 encapsulation protocols `VXLAN` or `Geneve`. All container-to-container network
-traffic is routed through these tunnels. This mode has several major
-advantages:
+traffic is routed through these tunnels. 
+
+The overlay used by the mesh of tunnels is managed by the protocol of 
+your choice (`VXLAN` by default). It's up to the overlay implementation and
+the kernel's routing table to route traffic to the other nodes' overlays. 
+It's likely that the encapsulation protocol will pick up the default layer
+3 infrastructure for the host. 
+
+If you would like all traffic in cilium tunnels to go through a specific 
+network interface (e.g.: a private network for security purposes), 
+you should configure the host's default endpoint accordingly 
+(e.g.: the IP of the node in a Kubernetes setup). You may check 
+the endpoints used by cilium to set up the tunneling with
+the command [cilium bpf tunnel list](https://docs.cilium.io/en/stable/cmdref/cilium_bpf_tunnel_list/).
+
+
+This mode has several major advantages:
 
 - **Simplicity:** The network which connects the cluster nodes does not need to
   be made aware of the *cluster prefix*. Cluster nodes can spawn multiple
diff --git a/Vagrantfile b/Vagrantfile
index de2293a0e1..ef960ead31 100644
--- a/Vagrantfile
+++ b/Vagrantfile
@@ -65,7 +65,7 @@ set -o pipefail
 
 export PATH=/home/vagrant/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games
 #{$makeclean}
-~/go/src/github.com/cilium/cilium/common/build.sh
+~/go/src/github.com/cilium/cilium/pkg/common/build.sh
 rm -fr ~/go/bin/cilium*
 SCRIPT
 
diff --git a/bpf/lib/nodeport.h b/bpf/lib/nodeport.h
index 5d2e740910..5def70bba9 100644
--- a/bpf/lib/nodeport.h
+++ b/bpf/lib/nodeport.h
@@ -575,6 +575,8 @@ redo_local:
 			ep_tail_call(ctx, CILIUM_CALL_IPV6_NODEPORT_NAT);
 		}
 		return DROP_MISSED_TAIL_CALL;
+	} else {
+		ctx_set_xfer(ctx, XFER_PKT_NO_SVC);
 	}
 
 	return CTX_ACT_OK;
@@ -1181,6 +1183,8 @@ redo_local:
 			ep_tail_call(ctx, CILIUM_CALL_IPV4_NODEPORT_NAT);
 		}
 		return DROP_MISSED_TAIL_CALL;
+	} else {
+		ctx_set_xfer(ctx, XFER_PKT_NO_SVC);
 	}
 
 	return CTX_ACT_OK;
diff --git a/cilium/cmd/bpf_ct_flush.go b/cilium/cmd/bpf_ct_flush.go
index eaca9256bd..434099beee 100644
--- a/cilium/cmd/bpf_ct_flush.go
+++ b/cilium/cmd/bpf_ct_flush.go
@@ -19,7 +19,7 @@ import (
 	"os"
 	"strconv"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_ct_list.go b/cilium/cmd/bpf_ct_list.go
index b3fc08d57e..7224c73cd2 100644
--- a/cilium/cmd/bpf_ct_list.go
+++ b/cilium/cmd/bpf_ct_list.go
@@ -19,9 +19,9 @@ import (
 	"os"
 	"strconv"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_endpoint_delete.go b/cilium/cmd/bpf_endpoint_delete.go
index bb52494101..3d28521f3d 100644
--- a/cilium/cmd/bpf_endpoint_delete.go
+++ b/cilium/cmd/bpf_endpoint_delete.go
@@ -17,7 +17,7 @@ package cmd
 import (
 	"net"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/lxcmap"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_endpoint_list.go b/cilium/cmd/bpf_endpoint_list.go
index 75bc046088..40da24d19d 100644
--- a/cilium/cmd/bpf_endpoint_list.go
+++ b/cilium/cmd/bpf_endpoint_list.go
@@ -17,8 +17,8 @@ package cmd
 import (
 	"os"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/lxcmap"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_ipcache_get.go b/cilium/cmd/bpf_ipcache_get.go
index 3093e6f69f..fb3a12c04b 100644
--- a/cilium/cmd/bpf_ipcache_get.go
+++ b/cilium/cmd/bpf_ipcache_get.go
@@ -20,7 +20,7 @@ import (
 	"os"
 	"strings"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/ipcache"
 
 	"github.com/hashicorp/go-immutable-radix"
diff --git a/cilium/cmd/bpf_ipcache_list.go b/cilium/cmd/bpf_ipcache_list.go
index 3e6ffe2c6d..96b42247e9 100644
--- a/cilium/cmd/bpf_ipcache_list.go
+++ b/cilium/cmd/bpf_ipcache_list.go
@@ -18,8 +18,8 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/ipcache"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_ipmasq_list.go b/cilium/cmd/bpf_ipmasq_list.go
index eab2664de0..47ff04778b 100644
--- a/cilium/cmd/bpf_ipmasq_list.go
+++ b/cilium/cmd/bpf_ipmasq_list.go
@@ -18,8 +18,8 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/ipmasq"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_lb_list.go b/cilium/cmd/bpf_lb_list.go
index 4597be8780..4db9ad4452 100644
--- a/cilium/cmd/bpf_lb_list.go
+++ b/cilium/cmd/bpf_lb_list.go
@@ -17,9 +17,9 @@ package cmd
 import (
 	"fmt"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/loadbalancer"
 	"github.com/cilium/cilium/pkg/maps/lbmap"
 
diff --git a/cilium/cmd/bpf_metrics_list.go b/cilium/cmd/bpf_metrics_list.go
index ec143749e8..7d73f49c77 100644
--- a/cilium/cmd/bpf_metrics_list.go
+++ b/cilium/cmd/bpf_metrics_list.go
@@ -22,8 +22,8 @@ import (
 	"strings"
 	"text/tabwriter"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/metricsmap"
 	monitorAPI "github.com/cilium/cilium/pkg/monitor/api"
 
diff --git a/cilium/cmd/bpf_nat_flush.go b/cilium/cmd/bpf_nat_flush.go
index a0a4e2111b..1ebb6092d0 100644
--- a/cilium/cmd/bpf_nat_flush.go
+++ b/cilium/cmd/bpf_nat_flush.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/nat"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_nat_list.go b/cilium/cmd/bpf_nat_list.go
index 11d1be694c..8cea78112b 100644
--- a/cilium/cmd/bpf_nat_list.go
+++ b/cilium/cmd/bpf_nat_list.go
@@ -18,9 +18,9 @@ import (
 	"fmt"
 	"os"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/nat"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_policy_add.go b/cilium/cmd/bpf_policy_add.go
index 3e89f95543..2a1c5cc1c7 100644
--- a/cilium/cmd/bpf_policy_add.go
+++ b/cilium/cmd/bpf_policy_add.go
@@ -15,7 +15,7 @@
 package cmd
 
 import (
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 
 	"github.com/spf13/cobra"
 )
diff --git a/cilium/cmd/bpf_policy_delete.go b/cilium/cmd/bpf_policy_delete.go
index 0e20953a8e..855ceb435b 100644
--- a/cilium/cmd/bpf_policy_delete.go
+++ b/cilium/cmd/bpf_policy_delete.go
@@ -15,7 +15,7 @@
 package cmd
 
 import (
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 
 	"github.com/spf13/cobra"
 )
diff --git a/cilium/cmd/bpf_policy_get.go b/cilium/cmd/bpf_policy_get.go
index 791311a87e..6d414ba2ad 100644
--- a/cilium/cmd/bpf_policy_get.go
+++ b/cilium/cmd/bpf_policy_get.go
@@ -24,10 +24,10 @@ import (
 	"text/tabwriter"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/identity"
 	identitymodel "github.com/cilium/cilium/pkg/identity/model"
 	"github.com/cilium/cilium/pkg/maps/policymap"
diff --git a/cilium/cmd/bpf_proxy_flush.go b/cilium/cmd/bpf_proxy_flush.go
index 65d665aa36..2c09aa8931 100644
--- a/cilium/cmd/bpf_proxy_flush.go
+++ b/cilium/cmd/bpf_proxy_flush.go
@@ -17,7 +17,7 @@ package cmd
 import (
 	"fmt"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 
 	"github.com/spf13/cobra"
 )
diff --git a/cilium/cmd/bpf_proxy_list.go b/cilium/cmd/bpf_proxy_list.go
index 9a04c85285..fc0243215c 100644
--- a/cilium/cmd/bpf_proxy_list.go
+++ b/cilium/cmd/bpf_proxy_list.go
@@ -17,8 +17,8 @@ package cmd
 import (
 	"os"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/spf13/cobra"
 )
 
diff --git a/cilium/cmd/bpf_sha_get.go b/cilium/cmd/bpf_sha_get.go
index 086a212dfa..b5cd4e47f1 100644
--- a/cilium/cmd/bpf_sha_get.go
+++ b/cilium/cmd/bpf_sha_get.go
@@ -22,8 +22,8 @@ import (
 	"regexp"
 	"strings"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 
 	"github.com/spf13/cobra"
 )
diff --git a/cilium/cmd/bpf_sha_list.go b/cilium/cmd/bpf_sha_list.go
index cb002cfb99..5742a1bf5d 100644
--- a/cilium/cmd/bpf_sha_list.go
+++ b/cilium/cmd/bpf_sha_list.go
@@ -21,8 +21,8 @@ import (
 	"path/filepath"
 	"strconv"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/defaults"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/bpf_tunnel_list.go b/cilium/cmd/bpf_tunnel_list.go
index c55aedbe57..29f052f2cb 100644
--- a/cilium/cmd/bpf_tunnel_list.go
+++ b/cilium/cmd/bpf_tunnel_list.go
@@ -17,8 +17,8 @@ package cmd
 import (
 	"os"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/command"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/maps/tunnel"
 
 	"github.com/spf13/cobra"
diff --git a/cilium/cmd/cleanup.go b/cilium/cmd/cleanup.go
index 30e87e40d3..3c1c2a1e68 100644
--- a/cilium/cmd/cleanup.go
+++ b/cilium/cmd/cleanup.go
@@ -23,8 +23,8 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/defaults"
 	"github.com/cilium/cilium/pkg/maps/tunnel"
 	"github.com/cilium/cilium/pkg/netns"
diff --git a/daemon/cmd/daemon_main.go b/daemon/cmd/daemon_main.go
index 4196cacb1c..d32aa95eb4 100644
--- a/daemon/cmd/daemon_main.go
+++ b/daemon/cmd/daemon_main.go
@@ -29,10 +29,10 @@ import (
 
 	"github.com/cilium/cilium/api/v1/server"
 	"github.com/cilium/cilium/api/v1/server/restapi"
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/cgroups"
 	"github.com/cilium/cilium/pkg/cleanup"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/components"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/datapath/iptables"
@@ -547,9 +547,6 @@ func init() {
 	flags.String(option.IPMasqAgentConfigPath, "/etc/config/ip-masq-agent", "ip-masq-agent configuration file path")
 	option.BindEnv(option.IPMasqAgentConfigPath)
 
-	flags.Duration(option.IPMasqAgentSyncPeriod, 60*time.Second, "ip-masq-agent configuration file synchronization period")
-	option.BindEnv(option.IPMasqAgentSyncPeriod)
-
 	flags.Bool(option.InstallIptRules, true, "Install base iptables rules for cilium to mainly interact with kube-proxy (and masquerading)")
 	option.BindEnv(option.InstallIptRules)
 
@@ -1295,8 +1292,11 @@ func runDaemon() {
 	}
 
 	if option.Config.EnableIPMasqAgent {
-		ipmasq.Start(option.Config.IPMasqAgentConfigPath,
-			option.Config.IPMasqAgentSyncPeriod)
+		ipmasqAgent, err := ipmasq.NewIPMasqAgent(option.Config.IPMasqAgentConfigPath)
+		if err != nil {
+			log.WithError(err).Fatal("Failed to create ip-masq-agent")
+		}
+		ipmasqAgent.Start()
 	}
 
 	if !option.Config.DryMode {
diff --git a/install/kubernetes/cilium/charts/agent/templates/clusterrole.yaml b/install/kubernetes/cilium/charts/agent/templates/clusterrole.yaml
index 142b8998d4..6762c453a9 100644
--- a/install/kubernetes/cilium/charts/agent/templates/clusterrole.yaml
+++ b/install/kubernetes/cilium/charts/agent/templates/clusterrole.yaml
@@ -77,6 +77,5 @@ rules:
   - ciliumnodes
   - ciliumnodes/status
   - ciliumidentities
-  - ciliumidentities/status
   verbs:
   - '*'
diff --git a/install/kubernetes/cilium/charts/agent/templates/serviceaccount.yaml b/install/kubernetes/cilium/charts/agent/templates/serviceaccount.yaml
index fdb4dfef36..8cba04fce6 100644
--- a/install/kubernetes/cilium/charts/agent/templates/serviceaccount.yaml
+++ b/install/kubernetes/cilium/charts/agent/templates/serviceaccount.yaml
@@ -3,3 +3,7 @@ kind: ServiceAccount
 metadata:
   name: cilium
   namespace: {{ .Release.Namespace }}
+  {{- if .Values.serviceAccount.annotations }}
+  annotations:
+{{ toYaml .Values.serviceAccount.annotations | indent 4 }}
+  {{- end }}
\ No newline at end of file
diff --git a/install/kubernetes/cilium/charts/agent/values.yaml b/install/kubernetes/cilium/charts/agent/values.yaml
index 22d5655263..cea5f9a846 100644
--- a/install/kubernetes/cilium/charts/agent/values.yaml
+++ b/install/kubernetes/cilium/charts/agent/values.yaml
@@ -4,6 +4,10 @@ image: cilium
 # update process.
 maxUnavailable: 2
 
+# Specifies annotation for service accounts
+serviceAccount:
+  annotations: {}
+
 # Enables monitor sidecar container for specified event types
 monitor:
   enabled: false
diff --git a/install/kubernetes/cilium/charts/config/templates/configmap.yaml b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
index 5ead4485d8..927938ab24 100644
--- a/install/kubernetes/cilium/charts/config/templates/configmap.yaml
+++ b/install/kubernetes/cilium/charts/config/templates/configmap.yaml
@@ -45,6 +45,15 @@ data:
   #   the kvstore by commenting out the identity-allocation-mode below, or
   #   setting it to "kvstore".
   identity-allocation-mode: {{ .Values.global.identityAllocationMode }}
+{{- if .Values.global.identityHeartbeatTimeout }}
+  identity-heartbeat-timeout: "{{ .Values.global.identityHeartbeatTimeout }}"
+{{- end }}
+{{- if .Values.global.identityGCInterval }}
+  identity-gc-interval: "{{ .Values.global.identityGCInterval }}"
+{{- end }}
+{{- if .Values.global.endpointGCInterval }}
+  cilium-endpoint-gc-interval: "{{ .Values.global.endpointGCInterval }}"
+{{- end }}
 
   # identity-change-grace-period is the grace period that needs to pass
   # before an endpoint that has changed its identity will start using
diff --git a/install/kubernetes/cilium/charts/hubble-ui/templates/serviceaccount.yaml b/install/kubernetes/cilium/charts/hubble-ui/templates/serviceaccount.yaml
index 77d1b9d6cf..5d8adda0ba 100644
--- a/install/kubernetes/cilium/charts/hubble-ui/templates/serviceaccount.yaml
+++ b/install/kubernetes/cilium/charts/hubble-ui/templates/serviceaccount.yaml
@@ -3,5 +3,9 @@ apiVersion: v1
 kind: ServiceAccount
 metadata:
   namespace: {{ .Release.Namespace }}
-  name: hubble-ui 
+  name: hubble-ui
+  {{- if .Values.serviceAccount.annotations }}
+  annotations:
+{{ toYaml .Values.serviceAccount.annotations | indent 4 }}
+  {{- end }}
 {{- end }}
diff --git a/install/kubernetes/cilium/charts/hubble-ui/values.yaml b/install/kubernetes/cilium/charts/hubble-ui/values.yaml
index 5cdb11f6a1..bbb6098f4c 100644
--- a/install/kubernetes/cilium/charts/hubble-ui/values.yaml
+++ b/install/kubernetes/cilium/charts/hubble-ui/values.yaml
@@ -18,7 +18,10 @@ replicas: 1
 #         cpu: 100m
 #         memory: 64Mi
 resources: {}
-  
+
+serviceAccount:
+  annotations: {}
+
 ingress:
   enabled: false
   annotations: {}
diff --git a/install/kubernetes/cilium/charts/managed-etcd/templates/etcd-operator-serviceaccount.yaml b/install/kubernetes/cilium/charts/managed-etcd/templates/etcd-operator-serviceaccount.yaml
index 6a841cf529..9a46a7df34 100644
--- a/install/kubernetes/cilium/charts/managed-etcd/templates/etcd-operator-serviceaccount.yaml
+++ b/install/kubernetes/cilium/charts/managed-etcd/templates/etcd-operator-serviceaccount.yaml
@@ -3,3 +3,7 @@ kind: ServiceAccount
 metadata:
   name: cilium-etcd-sa
   namespace: {{ .Release.Namespace }}
+  {{- if .Values.serviceAccount.annotations }}
+  annotations:
+{{ toYaml .Values.serviceAccount.annotations | indent 4 }}
+  {{- end }}
\ No newline at end of file
diff --git a/install/kubernetes/cilium/charts/managed-etcd/values.yaml b/install/kubernetes/cilium/charts/managed-etcd/values.yaml
index ad4e0f1170..863b738e82 100644
--- a/install/kubernetes/cilium/charts/managed-etcd/values.yaml
+++ b/install/kubernetes/cilium/charts/managed-etcd/values.yaml
@@ -1,2 +1,5 @@
 image: cilium-etcd-operator
 tag: v2.0.7
+
+serviceAccount:
+  annotations: {}
\ No newline at end of file
diff --git a/install/kubernetes/cilium/charts/operator/templates/serviceaccount.yaml b/install/kubernetes/cilium/charts/operator/templates/serviceaccount.yaml
index 4afda64c17..e1883d4674 100644
--- a/install/kubernetes/cilium/charts/operator/templates/serviceaccount.yaml
+++ b/install/kubernetes/cilium/charts/operator/templates/serviceaccount.yaml
@@ -3,3 +3,7 @@ kind: ServiceAccount
 metadata:
   name: cilium-operator
   namespace: {{ .Release.Namespace }}
+  {{- if .Values.serviceAccount.annotations }}
+  annotations:
+{{ toYaml .Values.serviceAccount.annotations | indent 4 }}
+  {{- end }}
\ No newline at end of file
diff --git a/install/kubernetes/cilium/charts/operator/values.yaml b/install/kubernetes/cilium/charts/operator/values.yaml
index dbf3513e2c..0ff9afbf13 100644
--- a/install/kubernetes/cilium/charts/operator/values.yaml
+++ b/install/kubernetes/cilium/charts/operator/values.yaml
@@ -2,5 +2,9 @@ image: operator
 # Deprecated: please use synchronizeK8sNodes in install/kubernetes/cilium/values.yaml
 synchronizeK8sNodes: true
 
+# Service account annotations
+serviceAccount:
+  annotations: {}
+
 # Specifies the resources for the operator container
 resources: {}
diff --git a/install/kubernetes/quick-install.yaml b/install/kubernetes/quick-install.yaml
index 11084b9a4e..4def67c4b1 100644
--- a/install/kubernetes/quick-install.yaml
+++ b/install/kubernetes/quick-install.yaml
@@ -242,7 +242,6 @@ rules:
   - ciliumnodes
   - ciliumnodes/status
   - ciliumidentities
-  - ciliumidentities/status
   verbs:
   - '*'
 ---
diff --git a/operator/flags.go b/operator/flags.go
index 011944a226..65d19c6398 100644
--- a/operator/flags.go
+++ b/operator/flags.go
@@ -131,7 +131,7 @@ func init() {
 	option.BindEnv(operatorOption.EnableCEPGC)
 	flags.MarkDeprecated(operatorOption.EnableCEPGC, fmt.Sprintf("Please use %s=0 to disable CEP GC", operatorOption.EndpointGCInterval))
 
-	flags.Duration(operatorOption.EndpointGCInterval, 30*time.Minute, "GC interval for cilium endpoints")
+	flags.Duration(operatorOption.EndpointGCInterval, 10*time.Minute, "GC interval for cilium endpoints")
 	option.BindEnv(operatorOption.EndpointGCInterval)
 
 	flags.Bool(operatorOption.EnableMetrics, false, "Enable Prometheus metrics")
@@ -140,7 +140,8 @@ func init() {
 	flags.String(option.IPAM, option.IPAMHostScopeLegacy, "Backend to use for IPAM")
 	option.BindEnv(option.IPAM)
 
-	flags.Duration(operatorOption.IdentityHeartbeatTimeout, 15*time.Minute, "Timeout after which identity expires on lack of heartbeat")
+	// 31 minutes is the default to be slightly larger than 3x EndpointGCInterval
+	flags.Duration(operatorOption.IdentityHeartbeatTimeout, 31*time.Minute, "Timeout after which identity expires on lack of heartbeat")
 	option.BindEnv(operatorOption.IdentityHeartbeatTimeout)
 
 	flags.String(option.IdentityAllocationMode, option.IdentityAllocationModeKVstore, "Method to use for identity allocation")
diff --git a/operator/identity/heartbeat.go b/operator/identity/heartbeat.go
new file mode 100644
index 0000000000..90f60f7474
--- /dev/null
+++ b/operator/identity/heartbeat.go
@@ -0,0 +1,102 @@
+// Copyright 2019-2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package identity
+
+import (
+	"time"
+
+	"github.com/cilium/cilium/pkg/lock"
+	"github.com/cilium/cilium/pkg/logging"
+	"github.com/cilium/cilium/pkg/logging/logfields"
+)
+
+var (
+	log = logging.DefaultLogger.WithField(logfields.LogSubsys, "identity-heartbeat")
+)
+
+// IdentityHeartbeatStore keeps track of the heartbeat of identities
+type IdentityHeartbeatStore struct {
+	mutex        lock.RWMutex
+	lastLifesign map[string]time.Time
+	firstRun     time.Time
+	timeout      time.Duration
+}
+
+// NewIdentityHeartbeatStore returns a new identity heartbeat store
+func NewIdentityHeartbeatStore(timeout time.Duration) *IdentityHeartbeatStore {
+	i := &IdentityHeartbeatStore{
+		timeout:      timeout,
+		lastLifesign: map[string]time.Time{},
+		firstRun:     time.Now(),
+	}
+	return i
+}
+
+// MarkAlive marks an identity as alive
+func (i *IdentityHeartbeatStore) MarkAlive(identity string, t time.Time) {
+	log.WithField("identity", identity).Debug("Marking identity alive")
+	i.mutex.Lock()
+	i.lastLifesign[identity] = t
+	i.mutex.Unlock()
+}
+
+// IsAlive returns true if the identity is still alive
+func (i *IdentityHeartbeatStore) IsAlive(identity string) bool {
+	i.mutex.RLock()
+	defer i.mutex.RUnlock()
+
+	lifesign, ok := i.lastLifesign[identity]
+	if ok {
+		// A lifesign has been recorded, check if the lifesign is older
+		// than the stale period
+		if time.Since(lifesign) < i.timeout {
+			return true
+		}
+	} else {
+		// No lifesign has ever been recorded. If the operator has not
+		// been up for longer than the stale period, then the identity
+		// is still considered alive
+		log.Debugf("No lifesign exists %s > %s", time.Since(i.firstRun), i.timeout)
+		if time.Since(i.firstRun) < i.timeout {
+			return true
+		}
+	}
+
+	return false
+}
+
+// Delete deletes an identity from the store
+func (i *IdentityHeartbeatStore) Delete(identity string) {
+	log.WithField("identity", identity).Debug("Deleting identity in heartbeat lifesign table")
+	i.mutex.Lock()
+	defer i.mutex.Unlock()
+	delete(i.lastLifesign, identity)
+}
+
+// GC removes all lifesign entries which have exceeded the heartbeat by a large
+// amount. This happens when the CiliumIdentity is deleted before the
+// CiliumEndpoint that refers to it. In that case, the lifesign entry will
+// continue to exist. Remove it once has not been updated for a long time.
+func (i *IdentityHeartbeatStore) GC() {
+	i.mutex.Lock()
+	defer i.mutex.Unlock()
+
+	for identity, lifesign := range i.lastLifesign {
+		if time.Since(lifesign) > 10*i.timeout {
+			log.WithField("identity", identity).Debug("Removing unused heartbeat entry")
+			delete(i.lastLifesign, identity)
+		}
+	}
+}
diff --git a/operator/k8s_cep_gc.go b/operator/k8s_cep_gc.go
index 140b067597..0cbff83c10 100644
--- a/operator/k8s_cep_gc.go
+++ b/operator/k8s_cep_gc.go
@@ -16,6 +16,7 @@ package main
 
 import (
 	"context"
+	"strconv"
 	"time"
 
 	operatorOption "github.com/cilium/cilium/operator/option"
@@ -70,6 +71,7 @@ func enableCiliumEndpointSyncGC() {
 					podsCache[pod.Namespace+"/"+pod.Name] = &pod
 				}
 
+				timeNow := time.Now()
 			perCEPFetch:
 				for time.Now().Before(loopStop) { // Guard against no-break bugs
 					time.Sleep(time.Second) // Throttle lookups in case of a busy loop
@@ -96,6 +98,10 @@ func enableCiliumEndpointSyncGC() {
 
 					// For each CEP we fetched, check if we know about it
 					for _, cep := range ceps.Items {
+						if cep.Status.Identity != nil {
+							identityHeartbeat.MarkAlive(strconv.FormatInt(cep.Status.Identity.ID, 10), timeNow)
+						}
+
 						cepFullName := cep.Namespace + "/" + cep.Name
 						_, exists := podsCache[cepFullName]
 						if !exists {
diff --git a/operator/k8s_identity.go b/operator/k8s_identity.go
index 1f3880dcce..06e0eb8bdc 100644
--- a/operator/k8s_identity.go
+++ b/operator/k8s_identity.go
@@ -18,6 +18,7 @@ import (
 	"context"
 	"time"
 
+	"github.com/cilium/cilium/operator/identity"
 	operatorOption "github.com/cilium/cilium/operator/option"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/k8s/apis/cilium.io/v2"
@@ -56,12 +57,11 @@ func deleteIdentity(identity *types.Identity) error {
 	return err
 }
 
+var identityHeartbeat *identity.IdentityHeartbeatStore
+
 // identityGCIteration is a single iteration of a garbage collection. It will
-// delete identities that have node status entries that are all older than
-// k8sIdentityHeartbeatTimeout.
-// Note: cilium-operator deletes identities in the OnDelete handler when they
-// have no nodes using them (status is empty). This generally means that
-// deletes here are for longer lived identities with no active users.
+// delete identities that have not had its heartbeat lifesign updated since
+// option.Config.IdentityHeartbeatTimeout
 func identityGCIteration() {
 	log.Debug("Running CRD identity garbage collector")
 
@@ -70,7 +70,6 @@ func identityGCIteration() {
 		return
 	}
 
-nextIdentity:
 	for _, identityObject := range identityStore.List() {
 		identity, ok := identityObject.(*types.Identity)
 		if !ok {
@@ -79,21 +78,23 @@ nextIdentity:
 			continue
 		}
 
-		for _, heartbeat := range identity.Status.Nodes {
-			if time.Since(heartbeat.Time) < operatorOption.Config.IdentityHeartbeatTimeout {
-				continue nextIdentity
-			}
+		if !identityHeartbeat.IsAlive(identity.Name) {
+			log.WithFields(logrus.Fields{
+				logfields.Identity: identity,
+				"nodes":            identity.Status.Nodes,
+			}).Debug("Deleting unused identity")
+			deleteIdentity(identity)
 		}
-
-		log.WithFields(logrus.Fields{
-			logfields.Identity: identity,
-			"nodes":            identity.Status.Nodes,
-		}).Debug("Deleting unused identity")
-		deleteIdentity(identity)
 	}
+
+	identityHeartbeat.GC()
 }
 
 func startCRDIdentityGC() {
+	if operatorOption.Config.EndpointGCInterval == 0 {
+		log.Fatal("The CiliumIdentity garabge collector requires the CiliumEndpoint garbage collector to be enabled")
+	}
+
 	log.Infof("Starting CRD identity garbage collector with %s interval...", operatorOption.Config.IdentityGCInterval)
 
 	controller.NewManager().UpdateController("crd-identity-gc",
@@ -106,15 +107,9 @@ func startCRDIdentityGC() {
 		})
 }
 
-func handleIdentityUpdate(identity *types.Identity) {
-	// If no more nodes are using this identity, release the ID for reuse.
-	// If deleteIdentity fails the identity will be removed by the periodic GC.
-	if len(identity.Status.Nodes) == 0 {
-		deleteIdentity(identity)
-	}
-}
-
 func startManagingK8sIdentities() {
+	identityHeartbeat = identity.NewIdentityHeartbeatStore(operatorOption.Config.IdentityHeartbeatTimeout)
+
 	identityStore = cache.NewStore(cache.DeletionHandlingMetaNamespaceKeyFunc)
 	identityInformer := informer.NewInformerWithStore(
 		cache.NewListWatchFromClient(ciliumK8sClient.CiliumV2().RESTClient(),
@@ -122,10 +117,35 @@ func startManagingK8sIdentities() {
 		&v2.CiliumIdentity{},
 		0,
 		cache.ResourceEventHandlerFuncs{
+			AddFunc: func(obj interface{}) {
+				if identity, ok := obj.(*types.Identity); ok {
+					// A new identity is always alive
+					identityHeartbeat.MarkAlive(identity.Name, time.Now())
+				}
+			},
 			UpdateFunc: func(oldObj, newObj interface{}) {
 				if identity, ok := newObj.(*types.Identity); ok {
-					handleIdentityUpdate(identity)
+					// Any update to the identity marks it as alive
+					identityHeartbeat.MarkAlive(identity.Name, time.Now())
+				}
+			},
+			DeleteFunc: func(obj interface{}) {
+				identity, ok := obj.(*types.Identity)
+				if !ok {
+					deletedObj, ok := obj.(cache.DeletedFinalStateUnknown)
+					if ok {
+						identity, ok = deletedObj.Obj.(*types.Identity)
+					}
+					if !ok {
+						return
+					}
 				}
+				// When the identity is deleted, delete the
+				// heartbeat entry as well. This will not be
+				// 100% accurate as the CiliumEndpoint can live
+				// longer than the CiliumIdentity. See
+				// identityHeartbeat.GC()
+				identityHeartbeat.Delete(identity.Name)
 			},
 		},
 		types.ConvertToIdentity,
diff --git a/common/addressing/ip.go b/pkg/addressing/ip.go
similarity index 100%
rename from common/addressing/ip.go
rename to pkg/addressing/ip.go
diff --git a/common/addressing/ip_test.go b/pkg/addressing/ip_test.go
similarity index 100%
rename from common/addressing/ip_test.go
rename to pkg/addressing/ip_test.go
diff --git a/pkg/azure/api/api.go b/pkg/azure/api/api.go
index c446d76d24..3d7eb710b2 100644
--- a/pkg/azure/api/api.go
+++ b/pkg/azure/api/api.go
@@ -17,6 +17,7 @@ package api
 import (
 	"context"
 	"fmt"
+	"net"
 	"strings"
 	"time"
 
@@ -147,7 +148,7 @@ func (c *Client) describeNetworkInterfaces(ctx context.Context) ([]network.Inter
 
 // parseInterfaces parses a network.Interface as returned by the Azure API
 // converts it into a types.AzureInterface
-func parseInterface(iface *network.Interface) (instanceID string, i *types.AzureInterface) {
+func parseInterface(iface *network.Interface, subnets ipamTypes.SubnetMap) (instanceID string, i *types.AzureInterface) {
 	i = &types.AzureInterface{}
 
 	if iface.VirtualMachine != nil && iface.VirtualMachine.ID != nil {
@@ -183,6 +184,11 @@ func parseInterface(iface *network.Interface) (instanceID string, i *types.Azure
 
 				if ip.Subnet != nil {
 					addr.Subnet = *ip.Subnet.ID
+					if subnet, ok := subnets[addr.Subnet]; ok {
+						if gateway := deriveGatewayIP(subnet.CIDR.IP); gateway != "" {
+							i.GatewayIP = gateway
+						}
+					}
 				}
 
 				i.Addresses = append(i.Addresses, addr)
@@ -193,9 +199,17 @@ func parseInterface(iface *network.Interface) (instanceID string, i *types.Azure
 	return
 }
 
+// deriveGatewayIP finds the default gateway for a given Azure subnet.
+// inspired by pkg/ipam/crd.go (as AWS, Azure reserves the first subnet IP for the gw).
+// Ref: https://docs.microsoft.com/en-us/azure/virtual-network/virtual-networks-faq#are-there-any-restrictions-on-using-ip-addresses-within-these-subnets
+func deriveGatewayIP(subnetIP net.IP) string {
+	addr := subnetIP.To4()
+	return net.IPv4(addr[0], addr[1], addr[2], addr[3]+1).String()
+}
+
 // GetInstances returns the list of all instances including all attached
 // interfaces as instanceMap
-func (c *Client) GetInstances(ctx context.Context) (*ipamTypes.InstanceMap, error) {
+func (c *Client) GetInstances(ctx context.Context, subnets ipamTypes.SubnetMap) (*ipamTypes.InstanceMap, error) {
 	instances := ipamTypes.NewInstanceMap()
 
 	networkInterfaces, err := c.describeNetworkInterfaces(ctx)
@@ -204,7 +218,7 @@ func (c *Client) GetInstances(ctx context.Context) (*ipamTypes.InstanceMap, erro
 	}
 
 	for _, iface := range networkInterfaces {
-		if id, azureInterface := parseInterface(&iface); id != "" {
+		if id, azureInterface := parseInterface(&iface, subnets); id != "" {
 			instances.Update(id, ipamTypes.InterfaceRevision{Resource: azureInterface})
 		}
 	}
@@ -217,8 +231,8 @@ func (c *Client) describeVpcs(ctx context.Context) ([]network.VirtualNetwork, er
 	c.limiter.Limit(ctx, "VirtualNetworks.List")
 
 	sinceStart := spanstat.Start()
-	result, err := c.virtualnetworks.ListComplete(ctx, c.resourceGroup)
-	c.metricsAPI.ObserveAPICall("Interfaces.ListAll", deriveStatus(err), sinceStart.Seconds())
+	result, err := c.virtualnetworks.ListAllComplete(ctx)
+	c.metricsAPI.ObserveAPICall("virtualnetworks.ListAll", deriveStatus(err), sinceStart.Seconds())
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/azure/api/mock/mock.go b/pkg/azure/api/mock/mock.go
index 9a5f78153a..6046fe1bff 100644
--- a/pkg/azure/api/mock/mock.go
+++ b/pkg/azure/api/mock/mock.go
@@ -136,7 +136,7 @@ func (a *API) rateLimit() {
 	}
 }
 
-func (a *API) GetInstances(ctx context.Context) (*ipamTypes.InstanceMap, error) {
+func (a *API) GetInstances(ctx context.Context, subnets ipamTypes.SubnetMap) (*ipamTypes.InstanceMap, error) {
 	a.rateLimit()
 	a.delaySim.Delay(GetInstances)
 
diff --git a/pkg/azure/ipam/instances.go b/pkg/azure/ipam/instances.go
index 71a211df1e..48c7102f35 100644
--- a/pkg/azure/ipam/instances.go
+++ b/pkg/azure/ipam/instances.go
@@ -28,7 +28,7 @@ import (
 
 // AzureAPI is the API surface used of the Azure API
 type AzureAPI interface {
-	GetInstances(ctx context.Context) (*ipamTypes.InstanceMap, error)
+	GetInstances(ctx context.Context, subnets ipamTypes.SubnetMap) (*ipamTypes.InstanceMap, error)
 	GetVpcsAndSubnets(ctx context.Context) (ipamTypes.VirtualNetworkMap, ipamTypes.SubnetMap, error)
 	AssignPrivateIpAddresses(ctx context.Context, instanceID, vmssName, subnetID, interfaceName string, addresses int) error
 }
@@ -88,7 +88,7 @@ func (m *InstancesManager) Resync(ctx context.Context) time.Time {
 		return time.Time{}
 	}
 
-	instances, err := m.api.GetInstances(ctx)
+	instances, err := m.api.GetInstances(ctx, subnets)
 	if err != nil {
 		log.WithError(err).Warning("Unable to synchronize Azure instances list")
 		return time.Time{}
diff --git a/pkg/azure/types/types.go b/pkg/azure/types/types.go
index 0dd6482dff..fb338448e7 100644
--- a/pkg/azure/types/types.go
+++ b/pkg/azure/types/types.go
@@ -110,6 +110,11 @@ type AzureInterface struct {
 	// SecurityGroup is the security group associated with the interface
 	SecurityGroup string `json:"security-group,omitempty"`
 
+	// GatewayIP is the interface subnet's default route
+	//
+	// +optional
+	GatewayIP string
+
 	// vmssName is the name of the virtual machine scale set. This field is
 	// set by extractIDs()
 	vmssName string
diff --git a/pkg/bpf/endpoint.go b/pkg/bpf/endpoint.go
index a363dd4108..6d20491b80 100644
--- a/pkg/bpf/endpoint.go
+++ b/pkg/bpf/endpoint.go
@@ -19,7 +19,7 @@ import (
 	"net"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 // Must be in sync with ENDPOINT_KEY_* in <bpf/lib/common.h>
diff --git a/common/build.sh b/pkg/common/build.sh
similarity index 100%
rename from common/build.sh
rename to pkg/common/build.sh
diff --git a/common/const.go b/pkg/common/const.go
similarity index 100%
rename from common/const.go
rename to pkg/common/const.go
diff --git a/common/utils.go b/pkg/common/utils.go
similarity index 100%
rename from common/utils.go
rename to pkg/common/utils.go
diff --git a/common/utils_test.go b/pkg/common/utils_test.go
similarity index 100%
rename from common/utils_test.go
rename to pkg/common/utils_test.go
diff --git a/pkg/datapath/config.go b/pkg/datapath/config.go
index d96fd14cd8..57de141206 100644
--- a/pkg/datapath/config.go
+++ b/pkg/datapath/config.go
@@ -17,7 +17,7 @@ package datapath
 import (
 	"io"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/mac"
 	"github.com/cilium/cilium/pkg/option"
diff --git a/pkg/datapath/loader/base.go b/pkg/datapath/loader/base.go
index bed4b2d9f5..e4ce0ceef3 100644
--- a/pkg/datapath/loader/base.go
+++ b/pkg/datapath/loader/base.go
@@ -22,10 +22,10 @@ import (
 	"path/filepath"
 	"strings"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/cgroups"
 	"github.com/cilium/cilium/pkg/command/exec"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/datapath"
 	"github.com/cilium/cilium/pkg/datapath/alignchecker"
 	"github.com/cilium/cilium/pkg/datapath/prefilter"
diff --git a/pkg/datapath/loader/cache.go b/pkg/datapath/loader/cache.go
index 62e4cfc936..58ab2afcdb 100644
--- a/pkg/datapath/loader/cache.go
+++ b/pkg/datapath/loader/cache.go
@@ -20,7 +20,7 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/datapath"
 	"github.com/cilium/cilium/pkg/datapath/loader/metrics"
diff --git a/pkg/datapath/loader/template.go b/pkg/datapath/loader/template.go
index 0b55502e2e..f39ae3073e 100644
--- a/pkg/datapath/loader/template.go
+++ b/pkg/datapath/loader/template.go
@@ -18,7 +18,7 @@ import (
 	"fmt"
 	"reflect"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/datapath"
diff --git a/pkg/endpoint/api.go b/pkg/endpoint/api.go
index 6add3492c6..8c3a6605a1 100644
--- a/pkg/endpoint/api.go
+++ b/pkg/endpoint/api.go
@@ -23,7 +23,7 @@ import (
 	"sort"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/endpoint/regeneration"
 	"github.com/cilium/cilium/pkg/fqdn"
diff --git a/pkg/endpoint/bpf.go b/pkg/endpoint/bpf.go
index 22106071d8..7d3c10ecac 100644
--- a/pkg/endpoint/bpf.go
+++ b/pkg/endpoint/bpf.go
@@ -25,8 +25,8 @@ import (
 	"time"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/endpoint/regeneration"
diff --git a/pkg/endpoint/cache.go b/pkg/endpoint/cache.go
index 0251cae556..c58adfe381 100644
--- a/pkg/endpoint/cache.go
+++ b/pkg/endpoint/cache.go
@@ -17,7 +17,7 @@ package endpoint
 import (
 	"fmt"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/mac"
 	"github.com/cilium/cilium/pkg/option"
diff --git a/pkg/endpoint/directory.go b/pkg/endpoint/directory.go
index f3c15657b7..cf8b450d25 100644
--- a/pkg/endpoint/directory.go
+++ b/pkg/endpoint/directory.go
@@ -19,7 +19,7 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
 
diff --git a/pkg/endpoint/endpoint.go b/pkg/endpoint/endpoint.go
index efe31ecfc6..3f4ce5889d 100644
--- a/pkg/endpoint/endpoint.go
+++ b/pkg/endpoint/endpoint.go
@@ -28,7 +28,7 @@ import (
 	"unsafe"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/annotation"
 	enirouting "github.com/cilium/cilium/pkg/aws/eni/routing"
 	"github.com/cilium/cilium/pkg/bpf"
diff --git a/pkg/endpoint/policy.go b/pkg/endpoint/policy.go
index c3b522736c..002e22a38b 100644
--- a/pkg/endpoint/policy.go
+++ b/pkg/endpoint/policy.go
@@ -22,7 +22,7 @@ import (
 	"strings"
 	"time"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/controller"
 	endpointid "github.com/cilium/cilium/pkg/endpoint/id"
diff --git a/pkg/endpoint/restore.go b/pkg/endpoint/restore.go
index 9bcce70459..c68eb539c6 100644
--- a/pkg/endpoint/restore.go
+++ b/pkg/endpoint/restore.go
@@ -24,8 +24,8 @@ import (
 	"time"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common"
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/endpoint/regeneration"
 	"github.com/cilium/cilium/pkg/fqdn"
 	"github.com/cilium/cilium/pkg/identity"
diff --git a/pkg/hubble/container/ring.go b/pkg/hubble/container/ring.go
index ef0e978d72..1d15c06e44 100644
--- a/pkg/hubble/container/ring.go
+++ b/pkg/hubble/container/ring.go
@@ -16,6 +16,8 @@ package container
 
 import (
 	"context"
+	"errors"
+	"io"
 	"sync/atomic"
 	"unsafe"
 
@@ -24,6 +26,11 @@ import (
 	"github.com/cilium/cilium/pkg/lock"
 )
 
+var (
+	// ErrInvalidRead indicates that the requested position can no longer be read.
+	ErrInvalidRead = errors.New("read position is no longer valid")
+)
+
 // Ring is a ring buffer that stores *v1.Event
 type Ring struct {
 	// mask to calculate the index to write into 'data'.
@@ -38,6 +45,8 @@ type Ring struct {
 	cycleExp uint8
 	// cycleMask is the mask used to calculate the correct cycle of a position.
 	cycleMask uint64
+	// halfCycle is half the total number of cycles
+	halfCycle uint64
 	// dataLen is the length of the internal buffer.
 	dataLen uint64
 	// data is the internal buffer of this ring buffer.
@@ -66,10 +75,14 @@ func NewRing(n int) *Ring {
 	l := math.GetMask(msb)
 	dataLen := uint64(l + 1)
 	cycleExp := uint8(math.MSB(l+1)) - 1
+	// half cycle is (^uint64(0)/dataLen)/2 == (^uint64(0)>>cycleExp)>>1
+	halfCycle := (^uint64(0) >> cycleExp) >> 1
+
 	return &Ring{
 		mask:      l,
 		cycleExp:  cycleExp,
 		cycleMask: ^uint64(0) >> cycleExp,
+		halfCycle: halfCycle,
 		dataLen:   dataLen,
 		data:      make([]*v1.Event, dataLen, dataLen),
 		notifyMu:  lock.Mutex{},
@@ -153,9 +166,12 @@ func (r *Ring) LastWrite() uint64 {
 	return atomic.LoadUint64(&r.write) - 1
 }
 
-// read reads the *v1.Event from the given read position. Returns false if
-// that position is no longer available to be read, returns true otherwise.
-func (r *Ring) read(read uint64) (*v1.Event, bool) {
+// read the *v1.Event from the given read position. Returns an error if
+// the position is not valid. A position is invalid either because it has
+// already been overwritten by the writer (in which case ErrInvalidRead is
+// returned) or because the position is ahead of the writer (in which case
+// io.EOF is returned).
+func (r *Ring) read(read uint64) (*v1.Event, error) {
 	readIdx := read & r.mask
 	event := r.dataLoadAtomic(readIdx)
 
@@ -180,11 +196,34 @@ func (r *Ring) read(read uint64) (*v1.Event, bool) {
 	// cycle: 1f 1f 1f 1f 1f 1f 1f 1f  0  0  0  0  0  0  0  0  1  1  1  1  1  1  1  1
 	readCycle := read >> r.cycleExp
 	writeCycle := lastWrite >> r.cycleExp
-	if (readCycle == writeCycle && readIdx < lastWriteIdx) ||
-		(readCycle == (writeCycle-1)&r.cycleMask && readIdx > lastWriteIdx) {
-		return event, true
+
+	prevWriteCycle := (writeCycle - 1) & r.cycleMask
+	maxWriteCycle := (writeCycle + r.halfCycle) & r.cycleMask
+
+	switch {
+	// Case: Reader in current cycle and accessing valid indices
+	case readCycle == writeCycle && readIdx < lastWriteIdx:
+		if event == nil {
+			// This case should never happen, as the writer must never write
+			// a nil value. In case it happens anyway, we just stop reading.
+			return nil, io.EOF
+		}
+		return event, nil
+	// Case: Reader in previous cycle and accessing valid indices
+	case readCycle == prevWriteCycle && readIdx > lastWriteIdx:
+		if event == nil {
+			// If the ring buffer is not yet fully populated, we treat nil
+			// as a value which is about to be overwritten
+			return nil, ErrInvalidRead
+		}
+		return event, nil
+	// Case: Reader ahead of writer
+	case readCycle >= writeCycle && readCycle < maxWriteCycle:
+		return nil, io.EOF
+	// Case: Reader behind writer
+	default:
+		return nil, ErrInvalidRead
 	}
-	return nil, false
 }
 
 // readFrom continues to read from the given position until the context is
@@ -194,10 +233,6 @@ func (r *Ring) readFrom(ctx context.Context, read uint64) <-chan *v1.Event {
 	const returnedBufferChLen = 1000
 	ch := make(chan *v1.Event, returnedBufferChLen)
 	go func() {
-		// halfCycle is the middle of a cycle.
-		// a half cycle is (^uint64(0)/r.dataLen)/2
-		// which translates into (^uint64(0)>>r.dataLen)>>1
-		halfCycle := (^uint64(0) >> r.cycleExp) >> 1
 		defer func() {
 			close(ch)
 		}()
@@ -260,7 +295,7 @@ func (r *Ring) readFrom(ctx context.Context, read uint64) <-chan *v1.Event {
 			// write: f0 f1 f2 f3 f4 f5 f6 f7 f8 f9 fa fb fc fd fe ff  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
 			// index:  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f  0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  f
 			// cycle: 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f 1f  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0
-			case event == nil || readCycle >= (writeCycle+1)&r.cycleMask && readCycle < (halfCycle+writeCycle)&r.cycleMask:
+			case event == nil || readCycle >= (writeCycle+1)&r.cycleMask && readCycle < (r.halfCycle+writeCycle)&r.cycleMask:
 				// The writer has already written a new event so there's no
 				// need to stop the reader.
 
diff --git a/pkg/hubble/container/ring_reader.go b/pkg/hubble/container/ring_reader.go
index fe8f207e9b..b56d53a9f3 100644
--- a/pkg/hubble/container/ring_reader.go
+++ b/pkg/hubble/container/ring_reader.go
@@ -39,27 +39,36 @@ func NewRingReader(ring *Ring, start uint64) *RingReader {
 }
 
 // Previous reads the event at the current position and decrement the read
-// position. When no more event can be read, Previous returns nil.
-func (r *RingReader) Previous() *v1.Event {
-	var e *v1.Event
-	// when the ring is not full, ring.read() may return <nil>, true
-	// in such a case, one should continue reading
-	for ok := true; e == nil && ok; r.idx-- {
-		e, ok = r.ring.read(r.idx)
+// position. Returns ErrInvalidRead if there are no older entries.
+func (r *RingReader) Previous() (*v1.Event, error) {
+	// We only expect ErrInvalidRead to be returned when reading backwards,
+	// therefore we don't try to handle any errors here.
+	e, err := r.ring.read(r.idx)
+	if err != nil {
+		return nil, err
 	}
-	return e
+	r.idx--
+	return e, nil
 }
 
 // Next reads the event at the current position and increment the read position.
-// When no more event can be read, Next returns nil.
-func (r *RingReader) Next() *v1.Event {
-	var e *v1.Event
-	// when the ring is not full, ring.read() may return <nil>, true
-	// in such a case, one should continue reading
-	for ok := true; e == nil && ok; r.idx++ {
-		e, ok = r.ring.read(r.idx)
+// Returns io.EOF if there are no more entries. May return ErrInvalidRead
+// if the writer overtook this RingReader.
+func (r *RingReader) Next() (*v1.Event, error) {
+	// There are two possible errors returned by read():
+	//
+	// Reader ahead of writer (io.EOF): We have read past the writer.
+	// In this case, we want to return nil and don't bump the index, as we have
+	// read all existing values that exist now.
+	// Writer ahead of reader (ErrInvalidRead): The writer has already
+	// overwritten the values we wanted to read. In this case, we want to
+	// propagate the error, as trying to catch up would be very racy.
+	e, err := r.ring.read(r.idx)
+	if err != nil {
+		return nil, err
 	}
-	return e
+	r.idx++
+	return e, nil
 }
 
 // NextFollow reads the event at the current position and increment the read
diff --git a/pkg/hubble/observer/local_observer.go b/pkg/hubble/observer/local_observer.go
index d25a1edf6e..6ce03e2a31 100644
--- a/pkg/hubble/observer/local_observer.go
+++ b/pkg/hubble/observer/local_observer.go
@@ -16,6 +16,7 @@ package observer
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"io"
 	"strings"
@@ -29,7 +30,7 @@ import (
 	"github.com/cilium/cilium/pkg/hubble/metrics"
 	"github.com/cilium/cilium/pkg/hubble/observer/observeroption"
 	"github.com/cilium/cilium/pkg/hubble/parser"
-	"github.com/cilium/cilium/pkg/hubble/parser/errors"
+	parserErrors "github.com/cilium/cilium/pkg/hubble/parser/errors"
 
 	"github.com/golang/protobuf/ptypes"
 	"github.com/golang/protobuf/ptypes/timestamp"
@@ -152,7 +153,7 @@ nextEvent:
 
 		flow, err := decodeFlow(s.payloadParser, pl)
 		if err != nil {
-			if !errors.IsErrInvalidType(err) {
+			if !parserErrors.IsErrInvalidType(err) {
 				s.log.WithError(err).WithField("data", pl.Data).Debug("failed to decode payload")
 			}
 			continue
@@ -392,13 +393,21 @@ func (r *flowsReader) Next(ctx context.Context) (*pb.Flow, error) {
 		default:
 		}
 		var e *v1.Event
+		var err error
 		if r.follow {
 			e = r.ringReader.NextFollow(ctx)
 		} else {
 			if r.maxFlows > 0 && (r.flowsCount >= r.maxFlows) {
 				return nil, io.EOF
 			}
-			e = r.ringReader.Next()
+			e, err = r.ringReader.Next()
+			if err != nil {
+				if err == container.ErrInvalidRead {
+					// this error is sent over the wire and presented to the user
+					return nil, errors.New("requested data has been overwritten and is no longer available")
+				}
+				return nil, err
+			}
 		}
 		if e == nil {
 			return nil, io.EOF
@@ -450,9 +459,12 @@ func newRingReader(ring *container.Ring, req *observerpb.GetFlowsRequest, whitel
 	// In order to avoid buffering events, we have to rewind first to find the
 	// correct index, then create a new reader that starts from there
 	for i := ring.Len(); i > 0; i, idx = i-1, idx-1 {
-		e := reader.Previous()
-		if e == nil {
+		e, err := reader.Previous()
+		if err == container.ErrInvalidRead {
+			idx++ // we went backward 1 too far
 			break
+		} else if err != nil {
+			return nil, err
 		}
 		_, ok := e.Event.(*pb.Flow)
 		if !ok || !filters.Apply(whitelist, blacklist, e) {
diff --git a/pkg/hubble/relay/observer.go b/pkg/hubble/relay/observer.go
index 01888a7d4d..f8b33b19c4 100644
--- a/pkg/hubble/relay/observer.go
+++ b/pkg/hubble/relay/observer.go
@@ -17,13 +17,11 @@ package relay
 import (
 	"context"
 	"io"
-	"time"
 
 	observerpb "github.com/cilium/cilium/api/v1/observer"
 	"github.com/sirupsen/logrus"
 
 	"golang.org/x/sync/errgroup"
-	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 )
@@ -31,17 +29,6 @@ import (
 // ensure that Server implements the observer.ObserverServer interface.
 var _ observerpb.ObserverServer = (*Server)(nil)
 
-func newObserverClient(target string, dialTimeout time.Duration) (observerpb.ObserverClient, *grpc.ClientConn, error) {
-	ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
-	defer cancel()
-	//FIXME: remove WithInsecure once mutual TLS is implemented
-	conn, err := grpc.DialContext(ctx, target, grpc.WithInsecure(), grpc.WithBlock())
-	if err != nil {
-		return nil, nil, err
-	}
-	return observerpb.NewObserverClient(conn), conn, nil
-}
-
 // GetFlows implements observer.ObserverServer.GetFlows by proxying requests to
 // the hubble instance the proxy is connected to.
 func (s *Server) GetFlows(req *observerpb.GetFlowsRequest, stream observerpb.Observer_GetFlowsServer) error {
@@ -61,26 +48,23 @@ func (s *Server) GetFlows(req *observerpb.GetFlowsRequest, stream observerpb.Obs
 	flows := make(chan *observerpb.GetFlowsResponse, 10*len(peers))
 	for _, p := range peers {
 		p := p
+		if p.conn == nil || p.connErr != nil {
+			s.log.WithField("address", p.Address.String()).Infof(
+				"No connection to peer %s, skipping", p.Name,
+			)
+			go s.connectPeer(p.Name, p.Address.String())
+			continue
+		}
 		g.Go(func() error {
-			//FIXME: establishing a new connection to every peer for every
-			// request is costly. Possible solutions:
-			// - selective connection: only connect to peers that have the
-			//   requested information (it still has the cost of connect/disconnect per
-			//   GetFlows request but it's at least limited in terms of scope).
-			// - Maintain a connection with all peers and reconnect when necessary.
-			cl, conn, err := newObserverClient(p.Address.String(), s.opts.DialTimeout)
+			client := observerpb.NewObserverClient(p.conn)
+			c, err := client.GetFlows(ctx, req)
 			if err != nil {
 				s.log.WithFields(logrus.Fields{
 					"error": err,
 					"peer":  p,
-				}).Error("Failed to connect to peer")
+				}).Warning("Failed to retrieve flows from peer")
 				return nil
 			}
-			defer conn.Close()
-			c, err := cl.GetFlows(ctx, req)
-			if err != nil {
-				return err
-			}
 			for {
 				flow, err := c.Recv()
 				switch err {
@@ -137,28 +121,21 @@ func (s *Server) ServerStatus(ctx context.Context, req *observerpb.ServerStatusR
 	statuses := make(chan *observerpb.ServerStatusResponse, len(peers))
 	for _, p := range peers {
 		p := p
+		if p.conn == nil || p.connErr != nil {
+			s.log.WithField("address", p.Address.String()).Infof(
+				"No connection to peer %s, skipping", p.Name,
+			)
+			go s.connectPeer(p.Name, p.Address.String())
+			continue
+		}
 		g.Go(func() error {
-			//FIXME: establishing a new connection to every peer for every
-			// request is costly. Possible solutions:
-			// - selective connection: only connect to peers that have the
-			//   requested information (it still has the cost of connect/disconnect per
-			//   ServerStatus request but it's at least limited in terms of scope).
-			// - Maintain a connection with all peers and reconnect when necessary.
-			cl, conn, err := newObserverClient(p.Address.String(), s.opts.DialTimeout)
-			if err != nil {
-				s.log.WithFields(logrus.Fields{
-					"error": err,
-					"peer":  p,
-				}).Error("Failed to connect to peer")
-				return nil
-			}
-			defer conn.Close()
-			status, err := cl.ServerStatus(ctx, req)
+			client := observerpb.NewObserverClient(p.conn)
+			status, err := client.ServerStatus(ctx, req)
 			if err != nil {
 				s.log.WithFields(logrus.Fields{
 					"error": err,
 					"peer":  p,
-				}).Error("Failed to retrieve server status")
+				}).Warning("Failed to retrieve server status")
 				return nil
 			}
 			select {
diff --git a/pkg/hubble/relay/peer.go b/pkg/hubble/relay/peer.go
index 37fa4e2a7b..b92a549d4b 100644
--- a/pkg/hubble/relay/peer.go
+++ b/pkg/hubble/relay/peer.go
@@ -25,11 +25,15 @@ import (
 	"google.golang.org/grpc"
 )
 
-func newPeerClient(target string, dialTimeout time.Duration) (peerpb.PeerClient, *grpc.ClientConn, error) {
+func newConn(target string, dialTimeout time.Duration, opts ...grpc.DialOption) (*grpc.ClientConn, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), dialTimeout)
 	defer cancel()
+	return grpc.DialContext(ctx, target, opts...)
+}
+
+func newPeerClient(target string, dialTimeout time.Duration) (peerpb.PeerClient, *grpc.ClientConn, error) {
 	// the connection is assumed to be local
-	conn, err := grpc.DialContext(ctx, target, grpc.WithInsecure(), grpc.WithBlock())
+	conn, err := newConn(target, dialTimeout, grpc.WithInsecure(), grpc.WithBlock())
 	if err != nil {
 		return nil, nil, err
 	}
@@ -106,19 +110,81 @@ connect:
 	}
 }
 
+func (s *Server) connectPeer(name, addr string) {
+	s.log.WithFields(logrus.Fields{
+		"address":      addr,
+		"dial timeout": s.opts.DialTimeout,
+	}).Debugf("Connecting peer %s...", name)
+
+	//FIXME: remove WithInsecure once mutual TLS is implemented
+	conn, connErr := newConn(addr, s.opts.DialTimeout, grpc.WithInsecure(), grpc.WithBlock())
+	var err error
+	s.mu.Lock()
+	if hp, ok := s.peers[name]; ok {
+		if hp.conn != nil { // make sure to close existing connection, if any
+			err = hp.conn.Close()
+		}
+		hp.conn = conn
+		hp.connErr = connErr
+	}
+	s.mu.Unlock()
+
+	if err != nil {
+		s.log.WithFields(logrus.Fields{
+			"error": err,
+		}).Warningf("Failed to properly close gRPC client connection to peer %s", name)
+	}
+	if connErr != nil {
+		s.log.WithFields(logrus.Fields{
+			"address":      addr,
+			"dial timeout": s.opts.DialTimeout,
+			"error":        connErr,
+		}).Warningf("Failed to create gRPC client connection to peer %s", name)
+	} else {
+		s.log.Debugf("Peer %s connected", name)
+	}
+}
+
+func (s *Server) disconnectPeer(name string) {
+	s.log.Debugf("Disconnecting peer %s...", name)
+
+	var err error
+	s.mu.Lock()
+	if hp, ok := s.peers[name]; ok && hp.conn != nil {
+		err = hp.conn.Close()
+		hp.conn = nil
+	}
+	s.mu.Unlock()
+
+	if err != nil {
+		s.log.WithFields(logrus.Fields{
+			"error": err,
+		}).Warningf("Failed to properly close gRPC client connection to peer %s", name)
+	}
+	s.log.Debugf("Peer %s disconnected", name)
+}
+
 func (s *Server) addPeer(p *peer.Peer) {
 	if p == nil {
 		return
 	}
+
+	hp := &hubblePeer{*p, nil, nil}
 	s.mu.Lock()
-	s.peers[p.Name] = *p
+	s.peers[p.Name] = hp
 	s.mu.Unlock()
+
+	// we don't want to block while waiting to establish a connection with the
+	// peer thus attempt to connect in the background
+	go s.connectPeer(p.Name, p.Address.String())
 }
 
 func (s *Server) deletePeer(p *peer.Peer) {
 	if p == nil {
 		return
 	}
+
+	s.disconnectPeer(p.Name)
 	s.mu.Lock()
 	delete(s.peers, p.Name)
 	s.mu.Unlock()
@@ -128,16 +194,17 @@ func (s *Server) updatePeer(p *peer.Peer) {
 	if p == nil {
 		return
 	}
-	s.mu.Lock()
-	s.peers[p.Name] = *p
-	s.mu.Unlock()
+
+	s.disconnectPeer(p.Name)
+	s.addPeer(p)
 }
 
-func (s *Server) peerList() []peer.Peer {
+func (s *Server) peerList() []hubblePeer {
 	s.mu.Lock()
-	p := make([]peer.Peer, 0, len(s.peers))
+	p := make([]hubblePeer, 0, len(s.peers))
 	for _, v := range s.peers {
-		p = append(p, v)
+		// note: there shouldn't be null entries in the map
+		p = append(p, *v)
 	}
 	s.mu.Unlock()
 	return p
diff --git a/pkg/hubble/relay/server.go b/pkg/hubble/relay/server.go
index aa28fea9d0..0a36468bf4 100644
--- a/pkg/hubble/relay/server.go
+++ b/pkg/hubble/relay/server.go
@@ -32,12 +32,18 @@ import (
 	healthpb "google.golang.org/grpc/health/grpc_health_v1"
 )
 
+type hubblePeer struct {
+	peer.Peer
+	conn    *grpc.ClientConn
+	connErr error
+}
+
 // Server is a proxy that connects to a running instance of hubble gRPC server
 // via unix domain socket.
 type Server struct {
 	server *grpc.Server
 	log    logrus.FieldLogger
-	peers  map[string]peer.Peer
+	peers  map[string]*hubblePeer
 	opts   relayoption.Options
 	mu     lock.Mutex
 	stop   chan struct{}
@@ -55,7 +61,7 @@ func NewServer(options ...relayoption.Option) (*Server, error) {
 	logging.ConfigureLogLevel(opts.Debug)
 	return &Server{
 		log:   logger,
-		peers: make(map[string]peer.Peer),
+		peers: make(map[string]*hubblePeer),
 		stop:  make(chan struct{}),
 		opts:  opts,
 	}, nil
diff --git a/pkg/ipam/crd.go b/pkg/ipam/crd.go
index ba8d7a8ac3..6c1dbff347 100644
--- a/pkg/ipam/crd.go
+++ b/pkg/ipam/crd.go
@@ -503,7 +503,7 @@ func (a *crdAllocator) buildAllocationResult(ip net.IP, ipInfo *ipamTypes.Alloca
 		for _, iface := range a.store.ownNode.Status.Azure.Interfaces {
 			if iface.ID == ipInfo.Resource {
 				result.Master = iface.MAC
-
+				result.GatewayIP = iface.GatewayIP
 				return
 			}
 		}
diff --git a/pkg/ipmasq/ipmasq.go b/pkg/ipmasq/ipmasq.go
index b4bddefdb0..5fa0463deb 100644
--- a/pkg/ipmasq/ipmasq.go
+++ b/pkg/ipmasq/ipmasq.go
@@ -15,17 +15,17 @@
 package ipmasq
 
 import (
-	"context"
+	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"net"
 	"os"
+	"path/filepath"
 	"strings"
-	"time"
 
-	"gopkg.in/yaml.v2"
+	"github.com/fsnotify/fsnotify"
+	"k8s.io/apimachinery/pkg/util/yaml"
 
-	"github.com/cilium/cilium/pkg/controller"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/maps/ipmasq"
@@ -38,10 +38,11 @@ var (
 // ipnet is a wrapper type for net.IPNet to enable de-serialization of CIDRs
 type Ipnet net.IPNet
 
-func (c *Ipnet) UnmarshalYAML(unmarshal func(interface{}) error) error {
-	var str string
-	if err := unmarshal(&str); err != nil {
-		return err
+func (c *Ipnet) UnmarshalJSON(json []byte) error {
+	str := string(json)
+
+	if json[0] != '"' {
+		return fmt.Errorf("Invalid CIDR: %s", str)
 	}
 
 	ip, n, err := net.ParseCIDR(strings.Trim(str, `"`))
@@ -54,12 +55,11 @@ func (c *Ipnet) UnmarshalYAML(unmarshal func(interface{}) error) error {
 
 	*c = Ipnet(*n)
 	return nil
-
 }
 
 // config represents the ip-masq-agent configuration file encoded as YAML
 type config struct {
-	NonMasqCIDRs []Ipnet `yaml:"nonMasqueradeCIDRs"`
+	NonMasqCIDRs []Ipnet `json:"nonMasqueradeCIDRs"`
 }
 
 // IPMasqMap is an interface describing methods for manipulating an ipmasq map
@@ -75,35 +75,86 @@ type IPMasqAgent struct {
 	nonMasqCIDRsFromConfig map[string]net.IPNet
 	nonMasqCIDRsInMap      map[string]net.IPNet
 	ipMasqMap              IPMasqMap
+	watcher                *fsnotify.Watcher
+	stop                   chan struct{}
+	handlerFinished        chan struct{}
 }
 
-// Start starts the "ip-masq-agent" controller which is used to sync the ipmasq
-// BPF maps.
-func Start(configPath string, syncPeriod time.Duration) {
-	start(configPath, syncPeriod, &ipmasq.IPMasqBPFMap{}, controller.NewManager())
+func NewIPMasqAgent(configPath string) (*IPMasqAgent, error) {
+	return newIPMasqAgent(configPath, &ipmasq.IPMasqBPFMap{})
 }
 
-func start(configPath string, syncPeriod time.Duration,
-	ipMasqMap IPMasqMap, manager *controller.Manager) {
+func newIPMasqAgent(configPath string, ipMasqMap IPMasqMap) (*IPMasqAgent, error) {
+	watcher, err := fsnotify.NewWatcher()
+	if err != nil {
+		return nil, fmt.Errorf("Failed to create fsnotify watcher: %s", err)
+	}
+
+	configDir := filepath.Dir(configPath)
+	// The directory of the config should exist at this time, otherwise
+	// the watcher will fail to add
+	if err := watcher.Add(configDir); err != nil {
+		watcher.Close()
+		return nil, fmt.Errorf("Failed to add %q dir to fsnotify watcher: %s", configDir, err)
+	}
 
 	a := &IPMasqAgent{
 		configPath:        configPath,
 		nonMasqCIDRsInMap: map[string]net.IPNet{},
 		ipMasqMap:         ipMasqMap,
+		watcher:           watcher,
 	}
 
+	return a, nil
+}
+
+// Start starts the ip-masq-agent goroutine which tracks the config file and
+// updates the BPF map accordingly.
+func (a *IPMasqAgent) Start() {
 	if err := a.restore(); err != nil {
-		log.WithError(err).Warn("ip-masq-agent failed to restore")
+		log.WithError(err).Warn("Failed to restore")
+	}
+	if err := a.Update(); err != nil {
+		log.WithError(err).Warn("Failed to update")
 	}
 
-	manager.UpdateController("ip-masq-agent",
-		controller.ControllerParams{
-			DoFunc: func(ctx context.Context) error {
-				return a.Update()
-			},
-			RunInterval: syncPeriod,
-		},
-	)
+	a.stop = make(chan struct{})
+	a.handlerFinished = make(chan struct{})
+
+	go func() {
+		for {
+			select {
+			case event := <-a.watcher.Events:
+				log.Debugf("Received fsnotify event: %+v", event)
+
+				if event.Name != a.configPath {
+					continue
+				}
+
+				switch event.Op {
+				case fsnotify.Create, fsnotify.Write, fsnotify.Chmod, fsnotify.Remove, fsnotify.Rename:
+					if err := a.Update(); err != nil {
+						log.WithError(err).Warn("Failed to update")
+					}
+				default:
+					log.Warnf("Watcher received unknown event: %s. Ignoring.", event)
+				}
+			case err := <-a.watcher.Errors:
+				log.WithError(err).Warn("Watcher received an error")
+			case <-a.stop:
+				log.Info("Stopping ip-masq-agent")
+				close(a.handlerFinished)
+				return
+			}
+		}
+	}()
+}
+
+// Stop stops the ip-masq-agent goroutine and the watcher.
+func (a *IPMasqAgent) Stop() {
+	close(a.stop)
+	<-a.handlerFinished
+	a.watcher.Close()
 }
 
 // Update updates the ipmasq BPF map entries with ones from the config file.
@@ -146,8 +197,13 @@ func (a *IPMasqAgent) readConfig() error {
 		return fmt.Errorf("Failed to read %s: %s", a.configPath, err)
 	}
 
-	if err := yaml.Unmarshal(raw, &cfg); err != nil {
-		return fmt.Errorf("Failed to de-serialize yaml: %s", err)
+	jsonStr, err := yaml.ToJSON(raw)
+	if err != nil {
+		return fmt.Errorf("Failed to convert to json: %s", err)
+	}
+
+	if err := json.Unmarshal(jsonStr, &cfg); err != nil {
+		return fmt.Errorf("Failed to de-serialize json: %s", err)
 	}
 
 	nonMasqCIDRs := map[string]net.IPNet{}
diff --git a/pkg/k8s/apis/cilium.io/v2/types.go b/pkg/k8s/apis/cilium.io/v2/types.go
index 98cd18e9cd..3e44be495e 100644
--- a/pkg/k8s/apis/cilium.io/v2/types.go
+++ b/pkg/k8s/apis/cilium.io/v2/types.go
@@ -518,10 +518,13 @@ type CiliumIdentity struct {
 	// SecurityLabels is the source-of-truth set of labels for this identity.
 	SecurityLabels map[string]string `json:"security-labels"`
 
+	// Status is deprecated and no longer used, it will be removed in Cilium 1.9
 	Status IdentityStatus `json:"status"`
 }
 
 // IdentityStatus is the status of an identity
+//
+// This structure is deprecated, do not use.
 type IdentityStatus struct {
 	Nodes map[string]metav1.Time `json:"nodes,omitempty"`
 }
diff --git a/pkg/k8s/identitybackend/identity.go b/pkg/k8s/identitybackend/identity.go
index 4d4a3a1981..f7178ed9c0 100644
--- a/pkg/k8s/identitybackend/identity.go
+++ b/pkg/k8s/identitybackend/identity.go
@@ -16,7 +16,6 @@ package identitybackend
 
 import (
 	"context"
-	"encoding/json"
 	"fmt"
 	"reflect"
 	"strconv"
@@ -28,7 +27,6 @@ import (
 	clientset "github.com/cilium/cilium/pkg/k8s/client/clientset/versioned"
 	"github.com/cilium/cilium/pkg/k8s/informer"
 	"github.com/cilium/cilium/pkg/k8s/types"
-	k8sversion "github.com/cilium/cilium/pkg/k8s/version"
 	"github.com/cilium/cilium/pkg/kvstore"
 	"github.com/cilium/cilium/pkg/labels"
 	"github.com/cilium/cilium/pkg/logging"
@@ -38,7 +36,6 @@ import (
 	"k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/fields"
-	k8sTypes "k8s.io/apimachinery/pkg/types"
 	"k8s.io/client-go/tools/cache"
 )
 
@@ -87,8 +84,6 @@ func sanitizeK8sLabels(old map[string]string) (selected, skipped map[string]stri
 
 // AllocateID will create an identity CRD, thus creating the identity for this
 // key-> ID mapping.
-// Note: This does not create a reference to this node to indicate that it is
-// using this identity. That must be done with AcquireReference.
 // Note: the lock field is not supported with the k8s CRD allocator.
 func (c *crdBackend) AllocateID(ctx context.Context, id idpool.ID, key allocator.AllocatorKey) error {
 	selectedLabels, skippedLabels := sanitizeK8sLabels(key.GetAsMap())
@@ -100,11 +95,6 @@ func (c *crdBackend) AllocateID(ctx context.Context, id idpool.ID, key allocator
 			Labels: selectedLabels,
 		},
 		SecurityLabels: key.GetAsMap(),
-		Status: v2.IdentityStatus{
-			Nodes: map[string]metav1.Time{
-				c.NodeName: metav1.Now(),
-			},
-		},
 	}
 
 	_, err := c.Client.CiliumV2().CiliumIdentities().Create(ctx, identity, metav1.CreateOptions{})
@@ -115,95 +105,13 @@ func (c *crdBackend) AllocateIDIfLocked(ctx context.Context, id idpool.ID, key a
 	return c.AllocateID(ctx, id, key)
 }
 
-// JSONPatch structure based on the RFC 6902
-// Note: This mirros pkg/k8s/json_patch.go but using that directly would cause
-// an import loop.
-type JSONPatch struct {
-	OP    string      `json:"op,omitempty"`
-	Path  string      `json:"path,omitempty"`
-	Value interface{} `json:"value"`
-}
-
-// AcquireReference updates the status field of the CRD corresponding to id
-// with this node. This marks that CRD as used by this node, and will stop it
-// being garbage collected.
-// Note: the lock field is not supported with the k8s CRD allocator.
+// AcquireReference acquires a reference to the identity.
 func (c *crdBackend) AcquireReference(ctx context.Context, id idpool.ID, key allocator.AllocatorKey, lock kvstore.KVLocker) error {
-	identity, exists, err := c.getById(ctx, id)
-	if err != nil {
-		return err
-	}
-	if !exists {
-		return fmt.Errorf("identity does not exist")
-	}
-
-	capabilities := k8sversion.Capabilities()
-	identityOps := c.Client.CiliumV2().CiliumIdentities()
-
-	if capabilities.Patch {
-		var patch []byte
-		patch, err = json.Marshal([]JSONPatch{
-			{
-				OP:    "test",
-				Path:  "/status",
-				Value: nil,
-			},
-			{
-				OP:   "add",
-				Path: "/status",
-				Value: v2.IdentityStatus{
-					Nodes: map[string]metav1.Time{
-						c.NodeName: metav1.Now(),
-					},
-				},
-			},
-		})
-		if err != nil {
-			return err
-		}
-
-		_, err = identityOps.Patch(ctx, identity.GetName(), k8sTypes.JSONPatchType, patch, metav1.PatchOptions{}, "status")
-		if err != nil {
-			patch, err = json.Marshal([]JSONPatch{
-				{
-					OP:    "replace",
-					Path:  "/status/nodes/" + c.NodeName,
-					Value: metav1.Now(),
-				},
-			})
-			if err != nil {
-				return err
-			}
-			_, err = identityOps.Patch(ctx, identity.GetName(), k8sTypes.JSONPatchType, patch, metav1.PatchOptions{}, "status")
-		}
-
-		if err == nil {
-			return nil
-		}
-		log.WithError(err).Debug("Error patching status. Continuing update via UpdateStatus")
-		/* fall through and attempt UpdateStatus() or Update() */
-	}
-
-	identityCopy := identity.DeepCopy()
-	if identityCopy.Status.Nodes == nil {
-		identityCopy.Status.Nodes = map[string]metav1.Time{
-			c.NodeName: metav1.Now(),
-		}
-	} else {
-		identityCopy.Status.Nodes[c.NodeName] = metav1.Now()
-	}
-
-	if capabilities.UpdateStatus {
-		_, err = identityOps.UpdateStatus(ctx, identityCopy.CiliumIdentity, metav1.UpdateOptions{})
-		if err == nil {
-			return nil
-		}
-		log.WithError(err).Debug("Error updating status. Continuing update via Update")
-		/* fall through and attempt Update() */
-	}
-
-	_, err = identityOps.Update(ctx, identityCopy.CiliumIdentity, metav1.UpdateOptions{})
-	return err
+	// For CiliumIdentity-based allocation, the reference counting is
+	// handled via CiliumEndpoint. Any CiliumEndpoint referring to a
+	// CiliumIdentity will keep the CiliumIdentity alive. No action is
+	// needed to acquire the reference here.
+	return nil
 }
 
 func (c *crdBackend) RunLocksGC(_ context.Context, _ map[string]kvstore.Value) (map[string]kvstore.Value, error) {
@@ -359,54 +267,11 @@ func (c *crdBackend) GetByID(ctx context.Context, id idpool.ID) (allocator.Alloc
 // Release dissociates this node from using the identity bound to the given ID.
 // When an identity has no references it may be garbage collected.
 func (c *crdBackend) Release(ctx context.Context, id idpool.ID, key allocator.AllocatorKey) (err error) {
-	identity, exists, err := c.getById(ctx, id)
-	if err != nil {
-		return err
-	}
-	if !exists || identity == nil {
-		return fmt.Errorf("unable to release identity %s: identity does not exist", key)
-	}
-
-	if _, ok := identity.Status.Nodes[c.NodeName]; !ok {
-		return fmt.Errorf("unable to release identity %s: identity is unused", key)
-	}
-
-	capabilities := k8sversion.Capabilities()
-
-	identityOps := c.Client.CiliumV2().CiliumIdentities()
-	if capabilities.Patch {
-		var patch []byte
-		patch, err = json.Marshal([]JSONPatch{
-			{
-				OP:   "delete",
-				Path: "/status/nodes/" + c.NodeName,
-			},
-		})
-		if err != nil {
-			return err
-		}
-		_, err = identityOps.Patch(ctx, identity.GetName(), k8sTypes.JSONPatchType, patch, metav1.PatchOptions{}, "status")
-		if err == nil {
-			return nil
-		}
-		log.WithError(err).Debug("Error patching status. Continuing update via UpdateStatus")
-		/* fall through and attempt UpdateStatus() or Update() */
-	}
-
-	identityCopy := identity.DeepCopy()
-	delete(identityCopy.Status.Nodes, c.NodeName)
-
-	if capabilities.UpdateStatus {
-		_, err = identityOps.UpdateStatus(ctx, identityCopy.CiliumIdentity, metav1.UpdateOptions{})
-		if err == nil {
-			return nil
-		}
-		log.WithError(err).Debug("Error updating status. Continuing update via Update")
-		/* fall through and attempt Update() */
-	}
-
-	_, err = identityOps.Update(ctx, identityCopy.CiliumIdentity, metav1.UpdateOptions{})
-	return err
+	// For CiliumIdentity-based allocation, the reference counting is
+	// handled via CiliumEndpoint. Any CiliumEndpoint referring to a
+	// CiliumIdentity will keep the CiliumIdentity alive. No action is
+	// needed to release the reference here.
+	return nil
 }
 
 func (c *crdBackend) ListAndWatch(ctx context.Context, handler allocator.CacheMutations, stopChan chan struct{}) {
diff --git a/pkg/kvstore/backwards_compat.go b/pkg/kvstore/backwards_compat.go
index 5787871f6e..2f20b0d0a4 100644
--- a/pkg/kvstore/backwards_compat.go
+++ b/pkg/kvstore/backwards_compat.go
@@ -17,7 +17,7 @@ package kvstore
 import (
 	"context"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 )
 
 // deleteLegacyPrefixes removes old kvstore prefixes of non-persistent keys
diff --git a/pkg/maps/eventsmap/eventsmap.go b/pkg/maps/eventsmap/eventsmap.go
index 62e980cb3f..633bee338f 100644
--- a/pkg/maps/eventsmap/eventsmap.go
+++ b/pkg/maps/eventsmap/eventsmap.go
@@ -18,8 +18,8 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 )
diff --git a/pkg/maps/fragmap/fragmap.go b/pkg/maps/fragmap/fragmap.go
index 8a4d63128c..d1a4054ed1 100644
--- a/pkg/maps/fragmap/fragmap.go
+++ b/pkg/maps/fragmap/fragmap.go
@@ -18,8 +18,8 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 const (
diff --git a/pkg/maps/ipcache/ipcache.go b/pkg/maps/ipcache/ipcache.go
index 01eed481a4..bc006ca42d 100644
--- a/pkg/maps/ipcache/ipcache.go
+++ b/pkg/maps/ipcache/ipcache.go
@@ -21,10 +21,10 @@ import (
 	"sync"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/types"
 
 	"golang.org/x/sys/unix"
 )
diff --git a/pkg/maps/ipmasq/ipmasq.go b/pkg/maps/ipmasq/ipmasq.go
index 1b0d714fec..86084a0379 100644
--- a/pkg/maps/ipmasq/ipmasq.go
+++ b/pkg/maps/ipmasq/ipmasq.go
@@ -19,8 +19,8 @@ import (
 	"net"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 const (
diff --git a/pkg/maps/lbmap/affinity.go b/pkg/maps/lbmap/affinity.go
index fcfe61c331..1d303c19d4 100644
--- a/pkg/maps/lbmap/affinity.go
+++ b/pkg/maps/lbmap/affinity.go
@@ -18,9 +18,9 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 const (
diff --git a/pkg/maps/lbmap/ipv4.go b/pkg/maps/lbmap/ipv4.go
index 52077c6adc..47131d98f1 100644
--- a/pkg/maps/lbmap/ipv4.go
+++ b/pkg/maps/lbmap/ipv4.go
@@ -19,10 +19,10 @@ import (
 	"net"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/loadbalancer"
+	"github.com/cilium/cilium/pkg/types"
 	"github.com/cilium/cilium/pkg/u8proto"
 )
 
diff --git a/pkg/maps/lbmap/ipv6.go b/pkg/maps/lbmap/ipv6.go
index 0a11c79010..10c1923f49 100644
--- a/pkg/maps/lbmap/ipv6.go
+++ b/pkg/maps/lbmap/ipv6.go
@@ -19,10 +19,10 @@ import (
 	"net"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/loadbalancer"
+	"github.com/cilium/cilium/pkg/types"
 	"github.com/cilium/cilium/pkg/u8proto"
 )
 
diff --git a/pkg/maps/lxcmap/lxcmap.go b/pkg/maps/lxcmap/lxcmap.go
index f08e3283a4..35e1beaea7 100644
--- a/pkg/maps/lxcmap/lxcmap.go
+++ b/pkg/maps/lxcmap/lxcmap.go
@@ -19,7 +19,7 @@ import (
 	"net"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/mac"
 )
diff --git a/pkg/maps/metricsmap/metricsmap.go b/pkg/maps/metricsmap/metricsmap.go
index ac33fbbbb9..caee8787ea 100644
--- a/pkg/maps/metricsmap/metricsmap.go
+++ b/pkg/maps/metricsmap/metricsmap.go
@@ -20,8 +20,8 @@ import (
 	"strconv"
 	"unsafe"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/metrics"
diff --git a/pkg/maps/nat/ipv4.go b/pkg/maps/nat/ipv4.go
index a9a627d438..dd95c54c54 100644
--- a/pkg/maps/nat/ipv4.go
+++ b/pkg/maps/nat/ipv4.go
@@ -18,9 +18,9 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/tuple"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 // NatEntry4 represents an IPv4 entry in the NAT table.
diff --git a/pkg/maps/nat/ipv6.go b/pkg/maps/nat/ipv6.go
index 34fe92bab0..54b69dca4f 100644
--- a/pkg/maps/nat/ipv6.go
+++ b/pkg/maps/nat/ipv6.go
@@ -18,9 +18,9 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/tuple"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 // NatEntry6 represents an IPv6 entry in the NAT table.
diff --git a/pkg/maps/neighborsmap/neighborsmap.go b/pkg/maps/neighborsmap/neighborsmap.go
index d335e67ef3..685b8ac278 100644
--- a/pkg/maps/neighborsmap/neighborsmap.go
+++ b/pkg/maps/neighborsmap/neighborsmap.go
@@ -17,9 +17,9 @@ package neighborsmap
 import (
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/option"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 const (
diff --git a/pkg/maps/signalmap/signalmap.go b/pkg/maps/signalmap/signalmap.go
index 25f8a91b74..eec5e2a2e8 100644
--- a/pkg/maps/signalmap/signalmap.go
+++ b/pkg/maps/signalmap/signalmap.go
@@ -18,8 +18,8 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/bpf"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 )
diff --git a/pkg/maps/sockmap/sockmap.go b/pkg/maps/sockmap/sockmap.go
index 35eec66ada..f6731d7fff 100644
--- a/pkg/maps/sockmap/sockmap.go
+++ b/pkg/maps/sockmap/sockmap.go
@@ -19,10 +19,10 @@ import (
 	"sync"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 // SockmapKey is the 5-tuple used to lookup a socket
diff --git a/pkg/monitor/datapath_trace.go b/pkg/monitor/datapath_trace.go
index a215d5f3bf..95d3c8d0a7 100644
--- a/pkg/monitor/datapath_trace.go
+++ b/pkg/monitor/datapath_trace.go
@@ -22,9 +22,9 @@ import (
 	"net"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/monitor/api"
+	"github.com/cilium/cilium/pkg/types"
 )
 
 const (
diff --git a/pkg/node/node_address.go b/pkg/node/node_address.go
index 7586ffc5cb..981ce77ef9 100644
--- a/pkg/node/node_address.go
+++ b/pkg/node/node_address.go
@@ -23,9 +23,9 @@ import (
 	"strings"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/cidr"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/defaults"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/pkg/option"
diff --git a/pkg/option/config.go b/pkg/option/config.go
index b1cd49fcf2..310375f2ab 100644
--- a/pkg/option/config.go
+++ b/pkg/option/config.go
@@ -29,9 +29,9 @@ import (
 	"time"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common"
 	"github.com/cilium/cilium/pkg/cidr"
 	clustermeshTypes "github.com/cilium/cilium/pkg/clustermesh/types"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/defaults"
 	"github.com/cilium/cilium/pkg/ip"
 	"github.com/cilium/cilium/pkg/lock"
@@ -276,9 +276,6 @@ const (
 	// IPMasqAgentConfigPath is the configuration file path
 	IPMasqAgentConfigPath = "ip-masq-agent-config-path"
 
-	// IPMasqAgentSyncPeriod is the configuration file synchronization period
-	IPMasqAgentSyncPeriod = "ip-masq-agent-sync-period"
-
 	// InstallIptRules sets whether Cilium should install any iptables in general
 	InstallIptRules = "install-iptables-rules"
 
@@ -1454,7 +1451,6 @@ type DaemonConfig struct {
 	EnableBPFMasquerade    bool
 	EnableIPMasqAgent      bool
 	IPMasqAgentConfigPath  string
-	IPMasqAgentSyncPeriod  time.Duration
 	InstallIptRules        bool
 	MonitorAggregation     string
 	PreAllocateMaps        bool
@@ -2231,7 +2227,6 @@ func (c *DaemonConfig) Populate() {
 	c.EnableBPFMasquerade = viper.GetBool(EnableBPFMasquerade)
 	c.EnableIPMasqAgent = viper.GetBool(EnableIPMasqAgent)
 	c.IPMasqAgentConfigPath = viper.GetString(IPMasqAgentConfigPath)
-	c.IPMasqAgentSyncPeriod = viper.GetDuration(IPMasqAgentSyncPeriod)
 	c.InstallIptRules = viper.GetBool(InstallIptRules)
 	c.IPSecKeyFile = viper.GetString(IPSecKeyFileName)
 	c.ModePreFilter = viper.GetString(PrefilterMode)
diff --git a/pkg/tuple/ipv4.go b/pkg/tuple/ipv4.go
index e006e1486f..0cf223907d 100644
--- a/pkg/tuple/ipv4.go
+++ b/pkg/tuple/ipv4.go
@@ -19,9 +19,9 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
+	"github.com/cilium/cilium/pkg/types"
 	"github.com/cilium/cilium/pkg/u8proto"
 )
 
diff --git a/pkg/tuple/ipv6.go b/pkg/tuple/ipv6.go
index 9e2c080dd5..67efcdc4dc 100644
--- a/pkg/tuple/ipv6.go
+++ b/pkg/tuple/ipv6.go
@@ -19,9 +19,9 @@ import (
 	"fmt"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/byteorder"
+	"github.com/cilium/cilium/pkg/types"
 	"github.com/cilium/cilium/pkg/u8proto"
 )
 
diff --git a/common/types/ipv4.go b/pkg/types/ipv4.go
similarity index 100%
rename from common/types/ipv4.go
rename to pkg/types/ipv4.go
diff --git a/common/types/ipv4_test.go b/pkg/types/ipv4_test.go
similarity index 100%
rename from common/types/ipv4_test.go
rename to pkg/types/ipv4_test.go
diff --git a/common/types/ipv6.go b/pkg/types/ipv6.go
similarity index 100%
rename from common/types/ipv6.go
rename to pkg/types/ipv6.go
diff --git a/common/types/ipv6_test.go b/pkg/types/ipv6_test.go
similarity index 100%
rename from common/types/ipv6_test.go
rename to pkg/types/ipv6_test.go
diff --git a/common/types/macaddr.go b/pkg/types/macaddr.go
similarity index 100%
rename from common/types/macaddr.go
rename to pkg/types/macaddr.go
diff --git a/common/types/macaddr_test.go b/pkg/types/macaddr_test.go
similarity index 100%
rename from common/types/macaddr_test.go
rename to pkg/types/macaddr_test.go
diff --git a/plugins/cilium-cni/cilium-cni.go b/plugins/cilium-cni/cilium-cni.go
index 84b16a4c92..8ecc7bb0cd 100644
--- a/plugins/cilium-cni/cilium-cni.go
+++ b/plugins/cilium-cni/cilium-cni.go
@@ -23,7 +23,7 @@ import (
 	"sort"
 
 	"github.com/cilium/cilium/api/v1/models"
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/client"
 	"github.com/cilium/cilium/pkg/datapath/connector"
 	"github.com/cilium/cilium/pkg/datapath/linux/route"
@@ -488,18 +488,12 @@ func cmdAdd(args *skel.CmdArgs) (err error) {
 		res.Routes = append(res.Routes, routes...)
 	}
 
-	switch conf.IpamMode {
-	case option.IPAMENI:
-		err = eniAdd(ipConfig, ipam.IPV4, conf)
+	if conf.IpamMode == option.IPAMENI || conf.IpamMode == option.IPAMAzure {
+		err = interfaceAdd(ipConfig, ipam.IPV4, conf)
 		if err != nil {
-			err = fmt.Errorf("unable to setup ENI datapath: %s", err)
+			err = fmt.Errorf("unable to setup interface datapath: %s", err)
 			return
 		}
-
-	case option.IPAMAzure:
-		// No specific action is required. The standard veth based
-		// approach is selected for now. The agent will set up the
-		// routes as necessary.
 	}
 
 	var macAddrStr string
diff --git a/plugins/cilium-cni/eni.go b/plugins/cilium-cni/interface.go
similarity index 94%
rename from plugins/cilium-cni/eni.go
rename to plugins/cilium-cni/interface.go
index 0f62e5565b..02138f636d 100644
--- a/plugins/cilium-cni/eni.go
+++ b/plugins/cilium-cni/interface.go
@@ -26,7 +26,7 @@ import (
 	"github.com/containernetworking/cni/pkg/types/current"
 )
 
-func eniAdd(ipConfig *current.IPConfig, ipam *models.IPAMAddressResponse, conf models.DaemonConfigurationStatus) error {
+func interfaceAdd(ipConfig *current.IPConfig, ipam *models.IPAMAddressResponse, conf models.DaemonConfigurationStatus) error {
 	allCIDRs := make([]*net.IPNet, 0, len(ipam.Cidrs))
 	for _, cidrString := range ipam.Cidrs {
 		_, cidr, err := net.ParseCIDR(cidrString)
diff --git a/plugins/cilium-docker/main.go b/plugins/cilium-docker/main.go
index bc810de13d..0f72bd5e8d 100644
--- a/plugins/cilium-docker/main.go
+++ b/plugins/cilium-docker/main.go
@@ -18,7 +18,7 @@ import (
 	"os"
 	"path/filepath"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/logging"
 	"github.com/cilium/cilium/pkg/logging/logfields"
 	"github.com/cilium/cilium/plugins/cilium-docker/driver"


Test patch:
diff --git a/cilium/cmd/bpf_ct_list_test.go b/cilium/cmd/bpf_ct_list_test.go
index 990cee85d8..129926c205 100644
--- a/cilium/cmd/bpf_ct_list_test.go
+++ b/cilium/cmd/bpf_ct_list_test.go
@@ -23,12 +23,12 @@ import (
 	"os"
 	"testing"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/checker"
 	"github.com/cilium/cilium/pkg/command"
 	"github.com/cilium/cilium/pkg/maps/ctmap"
 	"github.com/cilium/cilium/pkg/tuple"
+	"github.com/cilium/cilium/pkg/types"
 
 	. "gopkg.in/check.v1"
 )
diff --git a/cilium/cmd/bpf_nat_list_test.go b/cilium/cmd/bpf_nat_list_test.go
index 55a2c94ce8..9bc30fdd66 100644
--- a/cilium/cmd/bpf_nat_list_test.go
+++ b/cilium/cmd/bpf_nat_list_test.go
@@ -19,11 +19,11 @@ package cmd
 import (
 	"encoding/json"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/byteorder"
 	"github.com/cilium/cilium/pkg/checker"
 	"github.com/cilium/cilium/pkg/maps/nat"
 	"github.com/cilium/cilium/pkg/tuple"
+	"github.com/cilium/cilium/pkg/types"
 
 	. "gopkg.in/check.v1"
 )
diff --git a/daemon/cmd/daemon_test.go b/daemon/cmd/daemon_test.go
index 0e5c018014..70f589b213 100644
--- a/daemon/cmd/daemon_test.go
+++ b/daemon/cmd/daemon_test.go
@@ -25,7 +25,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/cilium/cilium/common"
+	"github.com/cilium/cilium/pkg/common"
 	"github.com/cilium/cilium/pkg/datapath"
 	fakedatapath "github.com/cilium/cilium/pkg/datapath/fake"
 	"github.com/cilium/cilium/pkg/endpoint"
diff --git a/daemon/cmd/policy_test.go b/daemon/cmd/policy_test.go
index ce6ceed76c..92b9d265f8 100644
--- a/daemon/cmd/policy_test.go
+++ b/daemon/cmd/policy_test.go
@@ -23,7 +23,7 @@ import (
 	"sort"
 	"time"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/checker"
 	"github.com/cilium/cilium/pkg/endpoint"
 	"github.com/cilium/cilium/pkg/endpoint/id"
diff --git a/operator/identity/heartbeat_test.go b/operator/identity/heartbeat_test.go
new file mode 100644
index 0000000000..de140cb0c1
--- /dev/null
+++ b/operator/identity/heartbeat_test.go
@@ -0,0 +1,74 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// +build !privileged_tests
+
+package identity
+
+import (
+	"testing"
+	"time"
+
+	"gopkg.in/check.v1"
+)
+
+func Test(t *testing.T) {
+	check.TestingT(t)
+}
+
+type OperatorTestSuite struct{}
+
+var _ = check.Suite(&OperatorTestSuite{})
+
+func (s *OperatorTestSuite) TestIdentityHeartbeatStore(c *check.C) {
+	store := NewIdentityHeartbeatStore(time.Second)
+
+	// mark lifesign to now, identity must be alive, run GC, identity
+	// should still exist
+	store.MarkAlive("foo", time.Now())
+	c.Assert(store.IsAlive("foo"), check.Equals, true)
+	store.GC()
+	c.Assert(store.IsAlive("foo"), check.Equals, true)
+
+	// mark lifesign in the past, identity should not be alive anymore
+	store.MarkAlive("foo", time.Now().Add(-time.Minute))
+	c.Assert(store.IsAlive("foo"), check.Equals, false)
+
+	// mark lifesign way in the past, run GC, validate that identity is no
+	// longer tracked
+	store.MarkAlive("foo", time.Now().Add(-24*time.Hour))
+	c.Assert(store.IsAlive("foo"), check.Equals, false)
+	store.GC()
+	store.mutex.RLock()
+	_, ok := store.lastLifesign["foo"]
+	c.Assert(ok, check.Equals, false)
+	store.mutex.RUnlock()
+
+	// mark lifesign to now and validate deletion
+	store.MarkAlive("foo", time.Now())
+	store.mutex.RLock()
+	_, ok = store.lastLifesign["foo"]
+	store.mutex.RUnlock()
+	c.Assert(ok, check.Equals, true)
+	store.Delete("foo")
+	store.mutex.RLock()
+	_, ok = store.lastLifesign["foo"]
+	store.mutex.RUnlock()
+	c.Assert(ok, check.Equals, false)
+
+	// identtity foo now doesn't exist, simulate start time of operator way
+	// in the past to check if an old, stale identity will be deleeted
+	store.firstRun = time.Now().Add(-24 * time.Hour)
+	c.Assert(store.IsAlive("foo"), check.Equals, false)
+}
diff --git a/pkg/azure/api/mock/mock_test.go b/pkg/azure/api/mock/mock_test.go
index 6fe40cd12c..fba086ed0d 100644
--- a/pkg/azure/api/mock/mock_test.go
+++ b/pkg/azure/api/mock/mock_test.go
@@ -42,7 +42,7 @@ func (e *MockSuite) TestMock(c *check.C) {
 	api := NewAPI([]*ipamTypes.Subnet{subnet}, []*ipamTypes.VirtualNetwork{{ID: "v-1"}})
 	c.Assert(api, check.Not(check.IsNil))
 
-	instances, err := api.GetInstances(context.Background())
+	instances, err := api.GetInstances(context.Background(), ipamTypes.SubnetMap{})
 	c.Assert(err, check.IsNil)
 	c.Assert(instances.NumInstances(), check.Equals, 0)
 
@@ -59,7 +59,7 @@ func (e *MockSuite) TestMock(c *check.C) {
 		Resource: &types.AzureInterface{ID: ifaceID, Name: "eth0"},
 	})
 	api.UpdateInstances(instances)
-	instances, err = api.GetInstances(context.Background())
+	instances, err = api.GetInstances(context.Background(), ipamTypes.SubnetMap{})
 	c.Assert(err, check.IsNil)
 	c.Assert(instances.NumInstances(), check.Equals, 1)
 	instances.ForeachInterface("", func(instanceID, interfaceID string, iface ipamTypes.InterfaceRevision) error {
@@ -70,7 +70,7 @@ func (e *MockSuite) TestMock(c *check.C) {
 
 	err = api.AssignPrivateIpAddresses(context.Background(), "vm1", "vmss1", "s-1", "eth0", 2)
 	c.Assert(err, check.IsNil)
-	instances, err = api.GetInstances(context.Background())
+	instances, err = api.GetInstances(context.Background(), ipamTypes.SubnetMap{})
 	c.Assert(err, check.IsNil)
 	c.Assert(instances.NumInstances(), check.Equals, 1)
 	instances.ForeachInterface("", func(instanceID, interfaceID string, revision ipamTypes.InterfaceRevision) error {
@@ -91,7 +91,7 @@ func (e *MockSuite) TestSetMockError(c *check.C) {
 	mockError := errors.New("error")
 
 	api.SetMockError(GetInstances, mockError)
-	_, err := api.GetInstances(context.Background())
+	_, err := api.GetInstances(context.Background(), ipamTypes.SubnetMap{})
 	c.Assert(err, check.Equals, mockError)
 
 	api.SetMockError(GetVpcsAndSubnets, mockError)
@@ -109,6 +109,6 @@ func (e *MockSuite) TestSetLimiter(c *check.C) {
 	c.Assert(api, check.Not(check.IsNil))
 
 	api.SetLimiter(10.0, 2)
-	_, err := api.GetInstances(context.Background())
+	_, err := api.GetInstances(context.Background(), ipamTypes.SubnetMap{})
 	c.Assert(err, check.IsNil)
 }
diff --git a/pkg/endpoint/endpoint_test.go b/pkg/endpoint/endpoint_test.go
index 72990171aa..7eb790f5ba 100644
--- a/pkg/endpoint/endpoint_test.go
+++ b/pkg/endpoint/endpoint_test.go
@@ -23,7 +23,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/datapath"
 	"github.com/cilium/cilium/pkg/datapath/fake"
 	"github.com/cilium/cilium/pkg/endpoint/id"
diff --git a/pkg/endpoint/restore_test.go b/pkg/endpoint/restore_test.go
index 6ed16ebc8c..8682f820e4 100644
--- a/pkg/endpoint/restore_test.go
+++ b/pkg/endpoint/restore_test.go
@@ -26,7 +26,7 @@ import (
 	"path/filepath"
 	"sort"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/checker"
 	linuxDatapath "github.com/cilium/cilium/pkg/datapath/linux"
 	"github.com/cilium/cilium/pkg/endpoint/regeneration"
diff --git a/pkg/endpointmanager/manager_test.go b/pkg/endpointmanager/manager_test.go
index 108b8c2889..cf6428ee54 100644
--- a/pkg/endpointmanager/manager_test.go
+++ b/pkg/endpointmanager/manager_test.go
@@ -22,7 +22,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/checker"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/datapath"
diff --git a/pkg/hubble/container/ring_reader_test.go b/pkg/hubble/container/ring_reader_test.go
index 48cfeb9c0a..5e3304f170 100644
--- a/pkg/hubble/container/ring_reader_test.go
+++ b/pkg/hubble/container/ring_reader_test.go
@@ -19,6 +19,7 @@ package container
 import (
 	"context"
 	"fmt"
+	"io"
 	"testing"
 	"time"
 
@@ -35,9 +36,10 @@ func TestRingReader_Previous(t *testing.T) {
 		ring.Write(&v1.Event{Timestamp: &timestamp.Timestamp{Seconds: int64(i)}})
 	}
 	tests := []struct {
-		start uint64
-		count int
-		want  []*v1.Event
+		start   uint64
+		count   int
+		want    []*v1.Event
+		wantErr error
 	}{
 		{
 			start: 13,
@@ -70,15 +72,19 @@ func TestRingReader_Previous(t *testing.T) {
 			},
 		}, {
 			start: 0,
-			count: 2,
+			count: 1,
 			want: []*v1.Event{
 				{Timestamp: &timestamp.Timestamp{Seconds: 0}},
-				nil,
 			},
 		}, {
-			start: 14,
-			count: 1,
-			want:  []*v1.Event{nil},
+			start:   14,
+			count:   1,
+			wantErr: io.EOF,
+		},
+		{
+			start:   ^uint64(0),
+			count:   1,
+			wantErr: ErrInvalidRead,
 		},
 	}
 	for _, tt := range tests {
@@ -87,7 +93,14 @@ func TestRingReader_Previous(t *testing.T) {
 			reader := NewRingReader(ring, tt.start)
 			var got []*v1.Event
 			for i := 0; i < tt.count; i++ {
-				got = append(got, reader.Previous())
+				event, err := reader.Previous()
+				if err != tt.wantErr {
+					t.Errorf(`"%s" error = %v, wantErr %v`, name, err, tt.wantErr)
+				}
+				if err != nil {
+					return
+				}
+				got = append(got, event)
 			}
 			assert.Equal(t, tt.want, got)
 		})
@@ -101,9 +114,10 @@ func TestRingReader_Next(t *testing.T) {
 	}
 
 	tests := []struct {
-		start uint64
-		count int
-		want  []*v1.Event
+		start   uint64
+		count   int
+		want    []*v1.Event
+		wantErr error
 	}{
 		{
 			start: 0,
@@ -135,9 +149,13 @@ func TestRingReader_Next(t *testing.T) {
 				{Timestamp: &timestamp.Timestamp{Seconds: 13}},
 			},
 		}, {
-			start: 14,
-			count: 1,
-			want:  []*v1.Event{nil},
+			start:   ^uint64(0),
+			count:   1,
+			wantErr: ErrInvalidRead,
+		}, {
+			start:   14,
+			count:   1,
+			wantErr: io.EOF,
 		},
 	}
 	for _, tt := range tests {
@@ -146,7 +164,14 @@ func TestRingReader_Next(t *testing.T) {
 			reader := NewRingReader(ring, tt.start)
 			var got []*v1.Event
 			for i := 0; i < tt.count; i++ {
-				got = append(got, reader.Next())
+				event, err := reader.Next()
+				if err != tt.wantErr {
+					t.Errorf(`"%s" error = %v, wantErr %v`, name, err, tt.wantErr)
+				}
+				if err != nil {
+					return
+				}
+				got = append(got, event)
 			}
 			assert.Equal(t, tt.want, got)
 		})
diff --git a/pkg/hubble/container/ring_test.go b/pkg/hubble/container/ring_test.go
index 2813c16647..d9d9161c4c 100644
--- a/pkg/hubble/container/ring_test.go
+++ b/pkg/hubble/container/ring_test.go
@@ -20,6 +20,7 @@ import (
 	"container/list"
 	"container/ring"
 	"context"
+	"io"
 	"reflect"
 	"testing"
 
@@ -98,11 +99,11 @@ func TestRing_Read(t *testing.T) {
 		read uint64
 	}
 	tests := []struct {
-		name   string
-		fields fields
-		args   args
-		want   *v1.Event
-		want1  bool
+		name    string
+		fields  fields
+		args    args
+		want    *v1.Event
+		wantErr error
 	}{
 		{
 			name: "normal read for the index 7",
@@ -125,8 +126,8 @@ func TestRing_Read(t *testing.T) {
 			args: args{
 				read: 0x7,
 			},
-			want:  &v1.Event{Timestamp: &timestamp.Timestamp{Seconds: 7}},
-			want1: true,
+			want:    &v1.Event{Timestamp: &timestamp.Timestamp{Seconds: 7}},
+			wantErr: nil,
 		},
 		{
 			name: "we can't read index 0 since we just wrote into it",
@@ -149,8 +150,8 @@ func TestRing_Read(t *testing.T) {
 			args: args{
 				read: 0x0,
 			},
-			want:  nil,
-			want1: false,
+			want:    nil,
+			wantErr: ErrInvalidRead,
 		},
 		{
 			name: "we can't read index 0x7 since we are one writing cycle ahead",
@@ -174,8 +175,8 @@ func TestRing_Read(t *testing.T) {
 				// The next possible entry that we can read is 0x10-0x7-0x1 = 0x8 (idx: 0)
 				read: 0x7,
 			},
-			want:  nil,
-			want1: false,
+			want:    nil,
+			wantErr: ErrInvalidRead,
 		},
 		{
 			name: "we can read index 0x8 since it's the last entry that we can read in this cycle",
@@ -199,8 +200,8 @@ func TestRing_Read(t *testing.T) {
 				// The next possible entry that we can read is 0x10-0x7-0x1 = 0x8 (idx: 0)
 				read: 0x8,
 			},
-			want:  &v1.Event{Timestamp: &timestamp.Timestamp{Seconds: 0}},
-			want1: true,
+			want:    &v1.Event{Timestamp: &timestamp.Timestamp{Seconds: 0}},
+			wantErr: nil,
 		},
 		{
 			name: "we overflow write and we are trying to read the previous writes, that we can't",
@@ -225,8 +226,8 @@ func TestRing_Read(t *testing.T) {
 				// next to be read: ^uint64(0) (idx: 7), last read: 0xfffffffffffffffe (idx: 6)
 				read: ^uint64(0),
 			},
-			want:  nil,
-			want1: false,
+			want:    nil,
+			wantErr: ErrInvalidRead,
 		},
 		{
 			name: "we overflow write and we are trying to read the previous writes, that we can",
@@ -250,8 +251,8 @@ func TestRing_Read(t *testing.T) {
 				// next to be read: ^uint64(0) (idx: 7), last read: 0xfffffffffffffffe (idx: 6)
 				read: ^uint64(0),
 			},
-			want:  &v1.Event{Timestamp: &timestamp.Timestamp{Seconds: 7}},
-			want1: true,
+			want:    &v1.Event{Timestamp: &timestamp.Timestamp{Seconds: 7}},
+			wantErr: nil,
 		},
 		{
 			name: "we overflow write and we are trying to read the 2 previously cycles",
@@ -277,8 +278,8 @@ func TestRing_Read(t *testing.T) {
 				// with a cycle that was already overwritten
 				read: ^uint64(0) - 0x7,
 			},
-			want:  nil,
-			want1: false,
+			want:    nil,
+			wantErr: ErrInvalidRead,
 		},
 	}
 	for _, tt := range tests {
@@ -295,8 +296,8 @@ func TestRing_Read(t *testing.T) {
 			if !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("Ring.read() got = %v, want %v", got, tt.want)
 			}
-			if got1 != tt.want1 {
-				t.Errorf("Ring.read() got1 = %v, want %v", got1, tt.want1)
+			if got1 != tt.wantErr {
+				t.Errorf("Ring.read() got1 = %v, want %v", got1, tt.wantErr)
 			}
 		})
 	}
@@ -503,20 +504,17 @@ func TestRingFunctionalityInParallel(t *testing.T) {
 		t.Errorf("lastWrite should be 0x0. Got %x", lastWrite)
 	}
 
-	entry, ok := r.read(lastWrite)
-	if !ok {
-		t.Errorf("Should be able to read position %x", lastWrite)
+	entry, err := r.read(lastWrite)
+	if err != nil {
+		t.Errorf("Should be able to read position %x, got %v", lastWrite, err)
 	}
 	if entry.Timestamp.Seconds != int64(0) {
 		t.Errorf("Read Event should be %+v, got %+v instead", &timestamp.Timestamp{Seconds: 0}, entry.Timestamp)
 	}
 	lastWrite--
-	entry, ok = r.read(lastWrite)
-	if !ok {
-		t.Errorf("Should be able to read position %x", lastWrite)
-	}
-	if entry != nil {
-		t.Errorf("Read Event should be %+v, got %+v instead", nil, entry)
+	entry, err = r.read(lastWrite)
+	if err != ErrInvalidRead {
+		t.Errorf("Should not be able to read position %x, got %v", lastWrite, err)
 	}
 }
 
@@ -545,14 +543,14 @@ func TestRingFunctionalitySerialized(t *testing.T) {
 		t.Errorf("lastWrite should be 0x1. Got %x", lastWrite)
 	}
 
-	entry, ok := r.read(lastWrite)
-	if ok {
-		t.Errorf("Should not be able to read position %x", lastWrite)
+	entry, err := r.read(lastWrite)
+	if err != io.EOF {
+		t.Errorf("Should not be able to read position %x, got %v", lastWrite, err)
 	}
 	lastWrite--
-	entry, ok = r.read(lastWrite)
-	if !ok {
-		t.Errorf("Should be able to read position %x", lastWrite)
+	entry, err = r.read(lastWrite)
+	if err != nil {
+		t.Errorf("Should be able to read position %x, got %v", lastWrite, err)
 	}
 	if entry.Timestamp.Seconds != int64(0) {
 		t.Errorf("Read Event should be %+v, got %+v instead", &timestamp.Timestamp{Seconds: 0}, entry.Timestamp)
diff --git a/pkg/ipam/allocator_test.go b/pkg/ipam/allocator_test.go
index 18b02d1da6..fff1645c1b 100644
--- a/pkg/ipam/allocator_test.go
+++ b/pkg/ipam/allocator_test.go
@@ -20,7 +20,7 @@ import (
 	"net"
 	"time"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/datapath/fake"
 
 	. "gopkg.in/check.v1"
diff --git a/pkg/ipam/ipam_test.go b/pkg/ipam/ipam_test.go
index 32ed2dc172..529766fb8b 100644
--- a/pkg/ipam/ipam_test.go
+++ b/pkg/ipam/ipam_test.go
@@ -20,7 +20,7 @@ import (
 	"net"
 	"testing"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/checker"
 	"github.com/cilium/cilium/pkg/cidr"
 	"github.com/cilium/cilium/pkg/datapath"
diff --git a/pkg/ipmasq/ipmasq_test.go b/pkg/ipmasq/ipmasq_test.go
index 436a3e66f8..f1b4b019e9 100644
--- a/pkg/ipmasq/ipmasq_test.go
+++ b/pkg/ipmasq/ipmasq_test.go
@@ -26,7 +26,7 @@ import (
 
 	"gopkg.in/check.v1"
 
-	"github.com/cilium/cilium/pkg/controller"
+	"github.com/cilium/cilium/pkg/lock"
 )
 
 func Test(t *testing.T) {
@@ -34,58 +34,83 @@ func Test(t *testing.T) {
 }
 
 type ipMasqMapMock struct {
+	lock.RWMutex
 	cidrs map[string]net.IPNet
 }
 
 func (m *ipMasqMapMock) Update(cidr net.IPNet) error {
+	m.Lock()
+	defer m.Unlock()
+
 	cidrStr := cidr.String()
 	if _, ok := m.cidrs[cidrStr]; ok {
 		return fmt.Errorf("CIDR already exists: %s", cidrStr)
 	}
 	m.cidrs[cidrStr] = cidr
+
 	return nil
 }
 
 func (m *ipMasqMapMock) Delete(cidr net.IPNet) error {
+	m.Lock()
+	defer m.Unlock()
+
 	cidrStr := cidr.String()
 	if _, ok := m.cidrs[cidrStr]; !ok {
 		return fmt.Errorf("CIDR not found: %s", cidrStr)
 	}
 	delete(m.cidrs, cidrStr)
+
 	return nil
 }
 
 func (m *ipMasqMapMock) Dump() ([]net.IPNet, error) {
+	m.RLock()
+	defer m.RUnlock()
+
 	cidrs := make([]net.IPNet, 0, len(m.cidrs))
 	for _, cidr := range m.cidrs {
 		cidrs = append(cidrs, cidr)
 	}
+
 	return cidrs, nil
 }
 
+func (m *ipMasqMapMock) dumpToSet() map[string]struct{} {
+	m.RLock()
+	defer m.RUnlock()
+
+	cidrs := make(map[string]struct{}, len(m.cidrs))
+	for cidrStr := range m.cidrs {
+		cidrs[cidrStr] = struct{}{}
+	}
+
+	return cidrs
+}
+
 type IPMasqTestSuite struct {
-	ipMasqMap  *ipMasqMapMock
-	manager    *controller.Manager
-	configFile *os.File
+	ipMasqMap   *ipMasqMapMock
+	ipMasqAgent *IPMasqAgent
+	configFile  *os.File
 }
 
 var _ = check.Suite(&IPMasqTestSuite{})
 
 func (i *IPMasqTestSuite) SetUpTest(c *check.C) {
 	i.ipMasqMap = &ipMasqMapMock{cidrs: map[string]net.IPNet{}}
-	i.manager = controller.NewManager()
 
 	configFile, err := ioutil.TempFile("", "ipmasq-test")
 	c.Assert(err, check.IsNil)
 	i.configFile = configFile
 
-	start(configFile.Name(), 100*time.Millisecond, i.ipMasqMap, i.manager)
+	agent, err := newIPMasqAgent(configFile.Name(), i.ipMasqMap)
+	c.Assert(err, check.IsNil)
+	i.ipMasqAgent = agent
+	i.ipMasqAgent.Start()
 }
 
 func (i *IPMasqTestSuite) TearDownTest(c *check.C) {
-	err := i.manager.RemoveController("ip-masq-agent")
-	c.Assert(err, check.IsNil)
-
+	i.ipMasqAgent.Stop()
 	os.Remove(i.configFile.Name())
 }
 
@@ -94,10 +119,11 @@ func (i *IPMasqTestSuite) TestUpdate(c *check.C) {
 	c.Assert(err, check.IsNil)
 	time.Sleep(300 * time.Millisecond)
 
-	c.Assert(len(i.ipMasqMap.cidrs), check.Equals, 2)
-	_, ok := i.ipMasqMap.cidrs["1.1.1.1/32"]
+	ipnets := i.ipMasqMap.dumpToSet()
+	c.Assert(len(ipnets), check.Equals, 2)
+	_, ok := ipnets["1.1.1.1/32"]
 	c.Assert(ok, check.Equals, true)
-	_, ok = i.ipMasqMap.cidrs["2.2.0.0/16"]
+	_, ok = ipnets["2.2.0.0/16"]
 	c.Assert(ok, check.Equals, true)
 
 	// Write new config
@@ -107,35 +133,71 @@ func (i *IPMasqTestSuite) TestUpdate(c *check.C) {
 	c.Assert(err, check.IsNil)
 	time.Sleep(300 * time.Millisecond)
 
-	c.Assert(len(i.ipMasqMap.cidrs), check.Equals, 2)
-	_, ok = i.ipMasqMap.cidrs["8.8.0.0/16"]
+	ipnets = i.ipMasqMap.dumpToSet()
+	c.Assert(len(ipnets), check.Equals, 2)
+	_, ok = ipnets["8.8.0.0/16"]
 	c.Assert(ok, check.Equals, true)
-	_, ok = i.ipMasqMap.cidrs["2.2.0.0/16"]
+	_, ok = ipnets["2.2.0.0/16"]
+	c.Assert(ok, check.Equals, true)
+
+	// Write new config in JSON
+	_, err = i.configFile.Seek(0, 0)
+	c.Assert(err, check.IsNil)
+	_, err = i.configFile.WriteString(`{"nonMasqueradeCIDRs": ["8.8.0.0/16", "1.1.2.3/16"]}`)
+	c.Assert(err, check.IsNil)
+	time.Sleep(300 * time.Millisecond)
+
+	ipnets = i.ipMasqMap.dumpToSet()
+	c.Assert(len(ipnets), check.Equals, 2)
+	_, ok = ipnets["8.8.0.0/16"]
+	c.Assert(ok, check.Equals, true)
+	_, ok = ipnets["1.1.0.0/16"]
 	c.Assert(ok, check.Equals, true)
 
 	// Delete file, should remove the CIDRs
 	err = os.Remove(i.configFile.Name())
 	c.Assert(err, check.IsNil)
+	err = i.configFile.Close()
+	c.Assert(err, check.IsNil)
 	time.Sleep(300 * time.Millisecond)
-	c.Assert(len(i.ipMasqMap.cidrs), check.Equals, 0)
+	ipnets = i.ipMasqMap.dumpToSet()
+	c.Assert(len(ipnets), check.Equals, 0)
 }
 
 func (i *IPMasqTestSuite) TestRestore(c *check.C) {
-	err := i.manager.RemoveController("ip-masq-agent")
-	c.Assert(err, check.IsNil)
+	// Stop ip-masq-agent goroutine (we can't use i.ipMasqMap.Stop(), as it stops
+	// the watcher)
+	close(i.ipMasqAgent.stop)
 
 	_, cidr, _ := net.ParseCIDR("3.3.3.0/24")
 	i.ipMasqMap.cidrs[cidr.String()] = *cidr
 	_, cidr, _ = net.ParseCIDR("4.4.0.0/16")
 	i.ipMasqMap.cidrs[cidr.String()] = *cidr
 
-	_, err = i.configFile.WriteString("nonMasqueradeCIDRs:\n- 4.4.0.0/16")
+	_, err := i.configFile.WriteString("nonMasqueradeCIDRs:\n- 4.4.0.0/16")
 	c.Assert(err, check.IsNil)
 
-	start(i.configFile.Name(), 100*time.Millisecond, i.ipMasqMap, i.manager)
+	i.ipMasqAgent.Start()
 	time.Sleep(300 * time.Millisecond)
 
-	c.Assert(len(i.ipMasqMap.cidrs), check.Equals, 1)
-	_, ok := i.ipMasqMap.cidrs["4.4.0.0/16"]
+	ipnets := i.ipMasqMap.dumpToSet()
+	c.Assert(len(ipnets), check.Equals, 1)
+	_, ok := ipnets["4.4.0.0/16"]
+	c.Assert(ok, check.Equals, true)
+
+	// Now stop the goroutine, and also remove the maps. It should bootstrap from
+	// the config
+	close(i.ipMasqAgent.stop)
+	i.ipMasqMap = &ipMasqMapMock{cidrs: map[string]net.IPNet{}}
+	i.ipMasqAgent.ipMasqMap = i.ipMasqMap
+	_, err = i.configFile.Seek(0, 0)
+	c.Assert(err, check.IsNil)
+	_, err = i.configFile.WriteString("nonMasqueradeCIDRs:\n- 3.3.0.0/16")
+	c.Assert(err, check.IsNil)
+	i.ipMasqAgent.Start()
+
+	ipnets = i.ipMasqMap.dumpToSet()
+	c.Assert(len(ipnets), check.Equals, 1)
+	_, ok = ipnets["3.3.0.0/16"]
 	c.Assert(ok, check.Equals, true)
 }
diff --git a/pkg/maps/ctmap/ctmap_privileged_test.go b/pkg/maps/ctmap/ctmap_privileged_test.go
index 41b7b1d72d..499bc4f204 100644
--- a/pkg/maps/ctmap/ctmap_privileged_test.go
+++ b/pkg/maps/ctmap/ctmap_privileged_test.go
@@ -20,9 +20,9 @@ import (
 	"testing"
 	"unsafe"
 
-	"github.com/cilium/cilium/common/types"
 	"github.com/cilium/cilium/pkg/bpf"
 	"github.com/cilium/cilium/pkg/tuple"
+	"github.com/cilium/cilium/pkg/types"
 	"github.com/cilium/cilium/pkg/u8proto"
 
 	. "gopkg.in/check.v1"
diff --git a/pkg/proxy/kafka_test.go b/pkg/proxy/kafka_test.go
index f7d7eaeeea..cafc24d876 100644
--- a/pkg/proxy/kafka_test.go
+++ b/pkg/proxy/kafka_test.go
@@ -24,7 +24,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/completion"
 	"github.com/cilium/cilium/pkg/datapath"
 	"github.com/cilium/cilium/pkg/endpoint"
diff --git a/pkg/testutils/endpoint.go b/pkg/testutils/endpoint.go
index 323b133ad8..5a48693808 100644
--- a/pkg/testutils/endpoint.go
+++ b/pkg/testutils/endpoint.go
@@ -15,7 +15,7 @@
 package testutils
 
 import (
-	"github.com/cilium/cilium/common/addressing"
+	"github.com/cilium/cilium/pkg/addressing"
 	"github.com/cilium/cilium/pkg/identity"
 	"github.com/cilium/cilium/pkg/labels"
 	"github.com/cilium/cilium/pkg/mac"
diff --git a/test/docker-compose.yml b/test/docker-compose.yml
index ad18cc706e..e2059f9ef7 100644
--- a/test/docker-compose.yml
+++ b/test/docker-compose.yml
@@ -15,7 +15,7 @@ services:
     command: "etcd -name etcd0 -advertise-client-urls http://0.0.0.0:4002 -listen-client-urls http://0.0.0.0:4002 -initial-cluster-token etcd-cluster-1 -initial-cluster-state new"
     privileged: true
   base_image:
-    image: "quay.io/cilium/cilium-builder:2020-04-16"
+    image: "quay.io/cilium/cilium-builder:2020-05-06"
     volumes:
       - "./../:/go/src/github.com/cilium/cilium/"
     privileged: true
diff --git a/test/helpers/kubectl.go b/test/helpers/kubectl.go
index 3d3364a4d6..697fe55927 100644
--- a/test/helpers/kubectl.go
+++ b/test/helpers/kubectl.go
@@ -352,6 +352,45 @@ func (kub *Kubectl) WaitForCiliumReadiness() error {
 	}, &TimeoutConfig{Timeout: 4 * time.Minute})
 }
 
+// DeleteResourceInAnyNamespace deletes all objects with the provided name of
+// the specified resource type in all namespaces.
+func (kub *Kubectl) DeleteResourcesInAnyNamespace(resource string, names []string) error {
+	cmd := KubectlCmd + " get " + resource + " --all-namespaces -o json | jq -r '[ .items[].metadata | (.namespace + \"/\" + .name) ]'"
+	res := kub.ExecShort(cmd)
+	if !res.WasSuccessful() {
+		return fmt.Errorf("unable to retrieve %s in all namespaces '%s': %s", resource, cmd, res.OutputPrettyPrint())
+	}
+
+	var allNames []string
+	if err := res.Unmarshal(&allNames); err != nil {
+		return fmt.Errorf("unable to unmarshal string slice '%#v': %s", res.OutputPrettyPrint(), err)
+	}
+
+	namesMap := map[string]struct{}{}
+	for _, name := range names {
+		namesMap[name] = struct{}{}
+	}
+
+	for _, combinedName := range allNames {
+		parts := strings.SplitN(combinedName, "/", 2)
+		if len(parts) != 2 {
+			return fmt.Errorf("The %s idenfifier '%s' is not in the form <namespace>/<name>", resource, combinedName)
+		}
+		namespace, name := parts[0], parts[1]
+		if _, ok := namesMap[name]; ok {
+			ginkgoext.By("Deleting %s %s in namespace %s", resource, name, namespace)
+			cmd = KubectlCmd + " -n " + namespace + " delete " + resource + " " + name
+			res = kub.ExecShort(cmd)
+			if !res.WasSuccessful() {
+				return fmt.Errorf("unable to delete %s %s in namespaces %s with command '%s': %s",
+					resource, name, namespace, cmd, res.OutputPrettyPrint())
+			}
+		}
+	}
+
+	return nil
+}
+
 // DeleteAllInNamespace deletes all namespaces except the ones provided in the
 // exception list
 func (kub *Kubectl) DeleteAllNamespacesExcept(except []string) error {
@@ -2298,8 +2337,8 @@ func (kub *Kubectl) CiliumPolicyAction(namespace, filepath string, action Resour
 				ctx, cancel := context.WithTimeout(context.Background(), ShortCommandTimeout)
 				defer cancel()
 				desiredRevision := revisions[ciliumPod] + 1
-				res := kub.CiliumExecContext(ctx, ciliumPod, fmt.Sprintf("cilium policy wait %d --max-wait-time %f", desiredRevision, ShortCommandTimeout.Seconds()))
-				if !(res.GetExitCode() != 0) {
+				res := kub.CiliumExecContext(ctx, ciliumPod, fmt.Sprintf("cilium policy wait %d --max-wait-time %d", desiredRevision, int(ShortCommandTimeout.Seconds())))
+				if res.GetExitCode() != 0 {
 					kub.Logger().Infof("Failed to wait for policy revision %d on pod %s", desiredRevision, ciliumPod)
 					return false
 				}
diff --git a/test/helpers/manifest.go b/test/helpers/manifest.go
index f713fddc33..9b144a88ab 100644
--- a/test/helpers/manifest.go
+++ b/test/helpers/manifest.go
@@ -38,8 +38,11 @@ type Manifest struct {
 	// a file that contains any number of Deployments, DaemonSets, ...
 	Filename string
 
-	// NumDaemonSet is the number of DaemonSets in the manifest
-	NumDaemonSet int
+	// DaemonSetNames is the list of all daemonset names in the manifest
+	DaemonSetNames []string
+
+	// DeploymentNames is the list of all deployment names in the manifest
+	DeploymentNames []string
 
 	// NumPods is the number of pods expected in the manifest, not counting
 	// any DaemonSets
@@ -48,6 +51,14 @@ type Manifest struct {
 	// LabelSelector is the selector required to select *ALL* pods created
 	// from this manifest
 	LabelSelector string
+
+	// Singleton marks a manifest as singleton. A singleton manifest can be
+	// deployed exactly once into the cluster, regardless of the namespace
+	// the manifest is deployed into. Singletons are required if the
+	// deployment is using HostPorts, NodePorts or other resources which
+	// may conflict if the deployment is scheduled multiple times onto the
+	// same node.
+	Singleton bool
 }
 
 // Deploy deploys the manifest. It will call ginkgoext.Fail() if any aspect of
@@ -61,9 +72,35 @@ func (m Manifest) Deploy(kubectl *Kubectl, namespace string) *Deployment {
 	return deploy
 }
 
+// deleteInAnyNamespace is used to delete all resources of the manifest in all
+// namespaces. This is required to implement singleton manifests. For the most
+// part, this will have no effect as PrepareCluster() will delete any leftover
+// temporary namespaces before starting the tests. This deletion is a safety
+// net for any deployment leaks between tests and in case a test deploys into a
+// non-random namespace.
+func (m Manifest) deleteInAnyNamespace(kubectl *Kubectl) {
+	if len(m.DaemonSetNames) > 0 {
+		if err := kubectl.DeleteResourcesInAnyNamespace("daemonset", m.DaemonSetNames); err != nil {
+			ginkgoext.Failf("Unable to delete existing daemonsets [%s] while deploying singleton manifest: %s",
+				m.DaemonSetNames, err)
+		}
+	}
+
+	if len(m.DeploymentNames) > 0 {
+		if err := kubectl.DeleteResourcesInAnyNamespace("deployment", m.DeploymentNames); err != nil {
+			ginkgoext.Failf("Unable to delete existing deployments [%s] while deploying singleton manifest: %s",
+				m.DeploymentNames, err)
+		}
+	}
+}
+
 func (m Manifest) deploy(kubectl *Kubectl, namespace string) (*Deployment, error) {
 	ginkgoext.By("Deploying %s in namespace %s", m.Filename, namespace)
 
+	if m.Singleton {
+		m.deleteInAnyNamespace(kubectl)
+	}
+
 	numNodes := kubectl.GetNumCiliumNodes()
 	if numNodes == 0 {
 		return nil, fmt.Errorf("No available nodes to deploy")
@@ -100,7 +137,7 @@ type Deployment struct {
 // numExpectedPods returns the number of expected pods the deployment resulted
 // in
 func (d *Deployment) numExpectedPods() int {
-	return (d.numNodes * d.manifest.NumDaemonSet) + d.manifest.NumPods
+	return (d.numNodes * len(d.manifest.DaemonSetNames)) + d.manifest.NumPods
 }
 
 // WaitUntilReady waits until all pods of the deployment are up and in ready
diff --git a/test/k8sT/Identity.go b/test/k8sT/Identity.go
new file mode 100644
index 0000000000..28c6a6fb97
--- /dev/null
+++ b/test/k8sT/Identity.go
@@ -0,0 +1,89 @@
+// Copyright 2020 Authors of Cilium
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package k8sTest
+
+import (
+	"time"
+
+	. "github.com/cilium/cilium/test/ginkgo-ext"
+	"github.com/cilium/cilium/test/helpers"
+
+	. "github.com/onsi/gomega"
+)
+
+var _ = Describe("K8sIdentity", func() {
+	var kubectl *helpers.Kubectl
+	var ciliumFilename string
+
+	BeforeAll(func() {
+		kubectl = helpers.CreateKubectl(helpers.K8s1VMName(), logger)
+		ciliumFilename = helpers.TimestampFilename("cilium.yaml")
+	})
+
+	BeforeEach(func() {
+		kubectl.NodeCleanMetadata()
+	})
+
+	AfterEach(func() {
+		kubectl.DeleteCiliumDS()
+		ExpectAllPodsTerminated(kubectl)
+	})
+
+	AfterFailed(func() {
+		kubectl.CiliumReport(helpers.CiliumNamespace,
+			"cilium endpoint list")
+	})
+
+	AfterAll(func() {
+		DeployCiliumAndDNS(kubectl, ciliumFilename)
+		kubectl.CloseSSHClient()
+	})
+
+	JustAfterEach(func() {
+		blacklist := helpers.GetBadLogMessages()
+		kubectl.ValidateListOfErrorsInLogs(CurrentGinkgoTestDescription().Duration, blacklist)
+	})
+
+	deployCilium := func(options map[string]string) {
+		DeployCiliumOptionsAndDNS(kubectl, ciliumFilename, options)
+
+		_, err := kubectl.CiliumNodesWait()
+		ExpectWithOffset(1, err).Should(BeNil(), "Failure while waiting for k8s nodes to be annotated by Cilium")
+
+		By("Making sure all endpoints are in ready state")
+		err = kubectl.CiliumEndpointWaitReady()
+		ExpectWithOffset(1, err).To(BeNil(), "Failure while waiting for all cilium endpoints to reach ready state")
+	}
+
+	Context("Identity expiration", func() {
+		It("Expiration of CiliumIdentity", func() {
+			deployCilium(map[string]string{
+				"global.endpointGCInterval":       "2s",
+				"global.identityGCInterval":       "2s",
+				"global.identityHeartbeatTimeout": "2s",
+			})
+
+			By("Creating unused CiliumIdentity")
+			dummyIdentity := helpers.ManifestGet(kubectl.BasePath(), "dummy_identity.yaml")
+			kubectl.ApplyDefault(dummyIdentity).ExpectSuccess("Cannot import dummy identity")
+
+			By("Waiting for CiliumIdentity to be garbage collected")
+			err := helpers.RepeatUntilTrue(func() bool {
+				return !kubectl.ExecShort(helpers.KubectlCmd + " get ciliumidentity 99999").WasSuccessful()
+			}, &helpers.TimeoutConfig{Timeout: 2 * time.Minute})
+			Expect(err).Should(BeNil(), "CiliumIdentity did not get garbage collected before timeout")
+		})
+	})
+})
diff --git a/test/k8sT/Services.go b/test/k8sT/Services.go
index 1d1dbf94e7..167b561961 100644
--- a/test/k8sT/Services.go
+++ b/test/k8sT/Services.go
@@ -722,7 +722,7 @@ var _ = Describe("K8sServicesTest", func() {
 			doRequestsFromThirdHost(tftpURL, 10, checkUDP)
 
 			// Make sure all the rest works as expected as well
-			//testNodePort(true)
+			testNodePort(true)
 
 			// Clear CT tables on both Cilium nodes
 			pod, err := kubectl.GetCiliumPodOnNode(helpers.CiliumNamespace, helpers.K8s1)
diff --git a/test/k8sT/manifests.go b/test/k8sT/manifests.go
index 42f50b42e2..8820e490e6 100644
--- a/test/k8sT/manifests.go
+++ b/test/k8sT/manifests.go
@@ -22,9 +22,11 @@ var (
 	deploymentManager = helpers.NewDeploymentManager()
 
 	DemoDaemonSet = helpers.Manifest{
-		Filename:     "demo_ds.yaml",
-		NumDaemonSet: 2,
-		NumPods:      1,
+		Filename:        "demo_ds.yaml",
+		DaemonSetNames:  []string{"testds", "testclient"},
+		DeploymentNames: []string{"test-k8s2"},
+		NumPods:         1,
+		Singleton:       true,
 	}
 
 	NetperfPods = helpers.Manifest{
diff --git a/test/k8sT/manifests/dummy_identity.yaml b/test/k8sT/manifests/dummy_identity.yaml
new file mode 100644
index 0000000000..e243de6977
--- /dev/null
+++ b/test/k8sT/manifests/dummy_identity.yaml
@@ -0,0 +1,14 @@
+apiVersion: cilium.io/v2
+kind: CiliumIdentity
+metadata:
+  labels:
+    io.cilium.k8s.policy.cluster: default
+    io.cilium.k8s.policy.serviceaccount: default
+    io.kubernetes.pod.namespace: default
+    foo: bar
+  name: "99999"
+security-labels:
+  k8s:io.cilium.k8s.policy.cluster: default
+  k8s:io.cilium.k8s.policy.serviceaccount: default
+  k8s:io.kubernetes.pod.namespace: default
+  k8s:foo: bar