Repository: skupperproject/skupper
Pull request #1171 (2023-07-11 19:06:30)

Add option to add one edge-host hostAlias to the router
Hi everyone,

In environments where most, if not all, outbound connectivity is blocked, we usually have to has to ask for specific firewall/NAT rules to map an internal IP address and port pair to an external one. When deploying edge Skupper sites in such environments, there are two solutions to still use the `edge-host` domain name that's in the TLS certificate.

The first is to use a local DNS and add a record so that the `edge-host` domain name points to the internal IP. However, this is not always possible because sometime we simply can't create ad hoc DNS servers or add records to the existing DNS.

The second option is to use `/etc/host`. And the suggested way to add entries `/etc/host` with Kubernetes is to use [HostAliases](https://kubernetes.io/docs/tasks/network/customize-hosts-file-for-pods/). However, since Skupper doesn't set HostAliases, until now we have been manually editing the `skupper-router` deployment to add the HostAliases section ourselves. And we love to merge this PR merged to do the same by using the proposed new `router-edge-host-alias` flag.


Pending Issue:
What happens if there are multiple core site? Should we (1) allow a full mapping of edge-hosts and IPs in the config map? or (2) remove the cm flag and add the IP as an annotation of the secret exactly like for the `edge-host`? I prefer the second over the first, but this means that the site-controller should react also to secrets so to patch the router deployment with the new aliases.

nicolacdnll: 100% agree; moving this parameter to the token does make sense but we must document that the router will have to be restarted! Will look into it. Thanks for the feedback.
nicolacdnll: PR updated, here a summary of the changes:

- Removed previous flags
- Included optional flag to force router restart to map inter-router and edge aliases.
- Aliases are now tied to tokens. Aliases from multiple Token are combined together.
- Since updates cause router restarts, updates are only done when the new combined aliases actually differ from the previous ones.


Patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3dff8ce2..dba8ceb8 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -106,7 +106,6 @@ commands:
       - go/install:
           version: "1.19"
       - kube-orb/install-kubectl
-      - run: make generate-client
       - run: make
 
   local_registry_start:
@@ -157,7 +156,10 @@ commands:
         name: vmstat info
         command: vmstat -w -t 10
         background: true
-
+    - run:
+        name: journalctl monitoring
+        command: journalctl -p 7 -b -f
+        background: true
   system_status:
     description: shows some point-in-time system status
     parameters:
@@ -196,6 +198,7 @@ commands:
           command: |
             make build-cmd && sudo install skupper /usr/local/bin
             export PUBLIC_1_INGRESS_HOST=`minikube ip`
+            export SKUPPER_TEST_CLI_MAX_STATUS_ATTEMPTS=10
             go test -tags=integration,podman -timeout=60m -v ./test/integration/... | tee /tmp/results/test-integration.out
       - run:
           name: Run client tests in real cluster
@@ -308,6 +311,10 @@ commands:
             sudo apt-get -qq -y install podman
             podman version
 
+            # Bypassing CircleCI issue with user session (see: https://github.com/containers/podman/issues/16529)
+            mkdir -p ~/.config/containers/containers.conf.d
+            ( echo '[containers]' ; echo 'cgroups = "disabled"' ) > ~/.config/containers/containers.conf.d/podman-circleci-issue.conf
+
             # Starting systemd user service
             systemctl --user start podman.socket
 
@@ -417,7 +424,6 @@ jobs:
                 exit 1
               fi
       - go/mod-download-cached
-      - run: make generate-client
       - run: make
       - run: make package
       - run: make clean
@@ -429,7 +435,6 @@ jobs:
     steps:
       - docker/install-docker
       - checkout
-      - run: make -e generate-client
       - run: make -e docker-build
       - run:
           name: persisting images to workspace
@@ -592,7 +597,6 @@ jobs:
             echo 'export FLOW_COLLECTOR_IMAGE=quay.io/skupper/flow-collector:${CIRCLE_TAG}' >> $BASH_ENV
             echo 'export TEST_IMAGE=quay.io/skupper/skupper-tests:${CIRCLE_TAG}' >> $BASH_ENV
             source $BASH_ENV
-            make -e generate-client
             make -e docker-build
             make -e docker-push
 
@@ -614,6 +618,5 @@ jobs:
             echo 'export FLOW_COLLECTOR_IMAGE=quay.io/skupper/flow-collector:main' >> $BASH_ENV
             echo 'export TEST_IMAGE=quay.io/skupper/skupper-tests:main' >> $BASH_ENV
             source $BASH_ENV
-            make -e generate-client
             make -e docker-build
             make -e docker-push
diff --git a/Makefile b/Makefile
index 0148783e..8b11d8ba 100644
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ TEST_BINARIES_FOLDER := ${PWD}/test/integration/bin
 DOCKER := docker
 LDFLAGS := -X github.com/skupperproject/skupper/pkg/version.Version=${VERSION}
 
-all: build-cmd build-get build-config-sync build-controllers build-tests build-manifest
+all: generate-client build-cmd build-get build-config-sync build-controllers build-tests build-manifest
 
 build-tests:
 	mkdir -p ${TEST_BINARIES_FOLDER}
@@ -20,7 +20,7 @@ build-tests:
 	go test -c -tags=job -v ./test/integration/examples/custom/hipstershop/job -o ${TEST_BINARIES_FOLDER}/grpcclient_test
 	go test -c -tags=job -v ./test/integration/examples/tls_t/job -o ${TEST_BINARIES_FOLDER}/tls_test
 
-build-cmd:
+build-cmd: generate-client
 	go build -ldflags="${LDFLAGS}"  -o skupper ./cmd/skupper
 
 build-get:
@@ -49,7 +49,7 @@ build-manifest:
 docker-build-test-image:
 	${DOCKER} build -t ${TEST_IMAGE} -f Dockerfile.ci-test .
 
-docker-build: docker-build-test-image
+docker-build: generate-client docker-build-test-image
 	${DOCKER} build -t ${SERVICE_CONTROLLER_IMAGE} -f Dockerfile.service-controller .
 	${DOCKER} build -t ${CONTROLLER_PODMAN_IMAGE} -f Dockerfile.controller-podman .
 	${DOCKER} build -t ${SITE_CONTROLLER_IMAGE} -f Dockerfile.site-controller .
diff --git a/api/types/client.go b/api/types/client.go
index 1a88e627..b44c4918 100644
--- a/api/types/client.go
+++ b/api/types/client.go
@@ -188,6 +188,9 @@ func (s *SiteConfigSpec) IsConsoleIngressKubernetes() bool {
 func (s *SiteConfigSpec) IsConsoleIngressNone() bool {
 	return s.getConsoleIngress() == IngressNoneString
 }
+func (s *SiteConfigSpec) IsEdge() bool {
+	return s.RouterMode == string(TransportModeEdge)
+}
 func (s *SiteConfigSpec) getConsoleIngress() string {
 	if s.ConsoleIngress == "" {
 		return s.Ingress
diff --git a/api/types/types.go b/api/types/types.go
index 1991d536..a926ddcc 100644
--- a/api/types/types.go
+++ b/api/types/types.go
@@ -399,6 +399,7 @@ type DeploymentSpec struct {
 	MemoryRequest       *resource.Quantity           `json:"memoryRequest,omitempty"`
 	CpuLimit            *resource.Quantity           `json:"cpuLimit,omitempty"`
 	MemoryLimit         *resource.Quantity           `json:"memoryLimit,omitempty"`
+	HostAliases         []corev1.HostAlias           `json:"hostAliases,omitempty"`
 }
 
 func (s *DeploymentSpec) GetCpuRequest() resource.Quantity {
diff --git a/client/router_create.go b/client/router_create.go
index 256d823a..e303da71 100644
--- a/client/router_create.go
+++ b/client/router_create.go
@@ -5,6 +5,7 @@ import (
 	"context"
 	"fmt"
 	"log"
+	"net"
 	"strconv"
 	"strings"
 	"text/template"
@@ -22,6 +23,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	kubetypes "k8s.io/apimachinery/pkg/types"
 	"k8s.io/apimachinery/pkg/util/intstr"
+	"k8s.io/apimachinery/pkg/util/validation"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/kube"
@@ -37,9 +39,11 @@ func getServingSecretAnnotations(name string) map[string]string {
 }
 
 func OauthProxyContainer(serviceAccount string, servicePort string) *corev1.Container {
+	image := images.GetOauthProxyImageDetails()
 	return &corev1.Container{
-		Image: "openshift/oauth-proxy:latest",
-		Name:  "oauth-proxy",
+		Image:           image.Name,
+		ImagePullPolicy: kube.GetPullPolicy(image.PullPolicy),
+		Name:            "oauth-proxy",
 		Args: []string{
 			"--https-address=:" + strconv.Itoa(int(types.ConsoleOpenShiftOauthServiceTargetPort)),
 			"--provider=openshift",
@@ -714,7 +718,7 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 		fmt.Println("Error configuring router:", err)
 	}
 
-	isEdge := options.RouterMode == string(types.TransportModeEdge)
+	isEdge := options.IsEdge()
 	routerConfig := qdr.InitialConfigSkupperRouter(van.Name+"-${HOSTNAME}", siteId, version.Version, isEdge, 3, options.Router)
 	van.RouterConfig, _ = qdr.MarshalRouterConfig(routerConfig)
 
@@ -1081,6 +1085,63 @@ func (cli *VanClient) GetRouterSpecFromOpts(options types.SiteConfigSpec, siteId
 	return van
 }
 
+func (cli *VanClient) GetRouterHostAliasesSpecFromTokens(ctx context.Context, namespace string) ([]corev1.HostAlias, error) {
+	hostAliasesMap := make(map[string]map[string]bool)
+	secrets, err := cli.KubeClient.CoreV1().Secrets(namespace).List(ctx, metav1.ListOptions{LabelSelector: types.SkupperTypeQualifier + "=" + types.TypeToken})
+	if err != nil {
+		return nil, fmt.Errorf("Failed to retrieve connection token: %w", err)
+	}
+	if len(secrets.Items) == 0 {
+		return nil, nil
+	} else {
+		for _, s := range secrets.Items {
+			if alias, ok := s.ObjectMeta.Annotations["edge-alias"]; ok {
+				if net.ParseIP(alias) == nil {
+					log.Printf("Skipping edge-alias: %s is not a valid textual representation of an IP address\n", alias)
+				} else {
+					name := s.ObjectMeta.Annotations["edge-host"]
+					if msgs := validation.IsDNS1123Subdomain(name); len(msgs) != 0 {
+						log.Printf("Skipping edge-alias: %v\n", msgs)
+					} else {
+						if _, ok := hostAliasesMap[alias][name]; !ok {
+							hostAliasesMap[alias] = make(map[string]bool)
+							hostAliasesMap[alias][name] = true
+						}
+					}
+				}
+			}
+			if alias, ok := s.ObjectMeta.Annotations["inter-router-alias"]; ok {
+				if net.ParseIP(alias) == nil {
+					log.Printf("Skipping inter-router-alias: %s is not a valid textual representation of an IP address", alias)
+				} else {
+					name := s.ObjectMeta.Annotations["inter-router-host"]
+					if msgs := validation.IsDNS1123Subdomain(name); len(msgs) != 0 {
+						log.Printf("Skipping inter-router-alias: %v\n", msgs)
+					} else {
+						if _, ok := hostAliasesMap[alias][name]; !ok {
+							hostAliasesMap[alias] = make(map[string]bool)
+							hostAliasesMap[alias][name] = true
+						}
+					}
+				}
+			}
+		}
+	}
+	hostAliases := []corev1.HostAlias{}
+	for alias, v := range hostAliasesMap {
+		new := corev1.HostAlias{
+			IP:        alias,
+			Hostnames: []string{},
+		}
+		for hn, _ := range v {
+			new.Hostnames = append(new.Hostnames, hn)
+		}
+		hostAliases = append(hostAliases, new)
+	}
+
+	return hostAliases, nil
+}
+
 // RouterCreate instantiates a VAN (router and controller) deployment
 func (cli *VanClient) RouterCreate(ctx context.Context, options types.SiteConfig) error {
 	// todo return error
@@ -1118,6 +1179,11 @@ func (cli *VanClient) RouterCreate(ctx context.Context, options types.SiteConfig
 		ownerRefs = []metav1.OwnerReference{*siteOwnerRef}
 	}
 	var err error
+	hostAliases, err := cli.GetRouterHostAliasesSpecFromTokens(ctx, cli.GetNamespace())
+	if err != nil {
+		return err
+	}
+	van.Transport.HostAliases = hostAliases
 	if options.Spec.AuthMode == string(types.ConsoleAuthModeInternal) {
 		config := `
 pwcheck_method: auxprop
diff --git a/client/router_update.go b/client/router_update.go
index c08ca63b..48f1c9f9 100644
--- a/client/router_update.go
+++ b/client/router_update.go
@@ -10,9 +10,11 @@ import (
 	"os"
 	"os/exec"
 	"reflect"
+	"sort"
 	"strings"
 	"time"
 
+	"github.com/google/go-cmp/cmp"
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/skupperproject/skupper/pkg/version"
 	appsv1 "k8s.io/api/apps/v1"
@@ -1084,6 +1086,56 @@ func (cli *VanClient) RouterUpdateAnnotations(ctx context.Context, settings *cor
 	return updated, nil
 }
 
+func sortHostAliases(hostAliases []corev1.HostAlias) []corev1.HostAlias {
+	sort.Slice(hostAliases, func(i, j int) bool {
+		return hostAliases[i].IP < hostAliases[j].IP
+	})
+
+	for _, hn := range hostAliases {
+		sort.Slice(hn.Hostnames, func(i, j int) bool {
+			return hn.Hostnames[i] < hn.Hostnames[j]
+		})
+	}
+	return hostAliases
+}
+
+func hostAliasesEqual(new, old []corev1.HostAlias) bool {
+	if len(new) != len(old) {
+		return false
+	}
+	return cmp.Equal(sortHostAliases(new), sortHostAliases(old))
+}
+
+func (cli *VanClient) updateHostAliasesOnDeployment(ctx context.Context, namespace string, name string, hostAliases []corev1.HostAlias) (bool, error) {
+	deployment, err := cli.KubeClient.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
+	if err != nil {
+		return false, err
+	}
+
+	if !hostAliasesEqual(hostAliases, deployment.Spec.Template.Spec.HostAliases) {
+		deployment.Spec.Template.Spec.HostAliases = hostAliases
+		_, err = cli.KubeClient.AppsV1().Deployments(namespace).Update(ctx, deployment, metav1.UpdateOptions{})
+		if err != nil {
+			return false, err
+		}
+		return true, nil
+	}
+	return false, nil
+}
+
+func (cli *VanClient) RouterUpdateHostAliases(ctx context.Context, token *corev1.Secret) (bool, error) {
+	hostAliases, err := cli.GetRouterHostAliasesSpecFromTokens(ctx, token.ObjectMeta.Namespace)
+	if err != nil {
+		return false, err
+	}
+
+	updated, err := cli.updateHostAliasesOnDeployment(ctx, token.Namespace, types.TransportDeploymentName, hostAliases)
+	if err != nil {
+		return updated, err
+	}
+	return updated, nil
+}
+
 func (cli *VanClient) RouterRestart(ctx context.Context, namespace string) error {
 	router, err := cli.KubeClient.AppsV1().Deployments(namespace).Get(ctx, types.TransportDeploymentName, metav1.GetOptions{})
 	if err != nil {
diff --git a/client/serviceinterface_update.go b/client/serviceinterface_update.go
index 336cbf19..a0461e77 100644
--- a/client/serviceinterface_update.go
+++ b/client/serviceinterface_update.go
@@ -329,7 +329,9 @@ func removeServiceInterfaceTarget(serviceName string, targetName string, deleteI
 			modified := false
 			targets := []types.ServiceInterfaceTarget{}
 			for _, t := range service.Targets {
-				if (t.Name == targetName || (t.Name == "" && targetName == serviceName)) && (t.Namespace == "" || t.Namespace == namespace) {
+				targetFound := (t.Name == targetName && t.Service == "") && t.Namespace == namespace
+				targetServiceFound := t.Service == targetName && t.Namespace == ""
+				if targetFound || targetServiceFound {
 					modified = true
 				} else {
 					targets = append(targets, t)
diff --git a/cmd/controller-podman/main.go b/cmd/controller-podman/main.go
index 0217ce0e..f961d191 100644
--- a/cmd/controller-podman/main.go
+++ b/cmd/controller-podman/main.go
@@ -35,10 +35,7 @@ func main() {
 	// set up signals so we handle the first shutdown signal gracefully
 	stopCh := SetupSignalHandler()
 
-	tlsConfig, err := certs.GetTlsConfig(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
-	if err != nil {
-		log.Fatal("Error getting tls config", err.Error())
-	}
+	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
 
 	controller, err := podmancontroller.NewControllerPodman(origin, tlsConfig)
 	if err != nil {
diff --git a/cmd/flow-collector/controller.go b/cmd/flow-collector/controller.go
index 595413e5..85524887 100644
--- a/cmd/flow-collector/controller.go
+++ b/cmd/flow-collector/controller.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"crypto/tls"
 	"log"
 	"time"
 
@@ -9,6 +8,7 @@ import (
 
 	"github.com/interconnectedcloud/go-amqp"
 	"github.com/prometheus/client_golang/prometheus"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/flow"
 	"github.com/skupperproject/skupper/pkg/qdr"
 )
@@ -20,7 +20,7 @@ type Controller struct {
 	FlowCollector *flow.FlowCollector
 }
 
-func NewController(origin string, reg prometheus.Registerer, scheme string, host string, port string, tlsConfig *tls.Config, recordTtl time.Duration) (*Controller, error) {
+func NewController(origin string, reg prometheus.Registerer, scheme string, host string, port string, tlsConfig *certs.TlsConfigRetriever, recordTtl time.Duration) (*Controller, error) {
 
 	controller := &Controller{
 		origin:        origin,
diff --git a/cmd/flow-collector/main.go b/cmd/flow-collector/main.go
index 2fc5e875..808026c9 100644
--- a/cmd/flow-collector/main.go
+++ b/cmd/flow-collector/main.go
@@ -208,10 +208,7 @@ func main() {
 		prometheusUrl = os.Getenv("PROMETHEUS_URL")
 	}
 
-	tlsConfig, err := certs.GetTlsConfig(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
-	if err != nil {
-		log.Fatal("Error getting tls config", err.Error())
-	}
+	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
 
 	conn, err := getConnectInfo(types.ControllerConfigPath + "connect.json")
 	if err != nil {
@@ -350,6 +347,7 @@ func main() {
 	addressApi.HandleFunc("/", authenticated(http.HandlerFunc(c.addressHandler))).Name("list")
 	addressApi.HandleFunc("/{id}", authenticated(http.HandlerFunc(c.addressHandler))).Name("item")
 	addressApi.HandleFunc("/{id}/processes", authenticated(http.HandlerFunc(c.addressHandler))).Name("processes")
+	addressApi.HandleFunc("/{id}/processpairs", authenticated(http.HandlerFunc(c.addressHandler))).Name("processpairs")
 	addressApi.HandleFunc("/{id}/flows", authenticated(http.HandlerFunc(c.addressHandler))).Name("flows")
 	addressApi.HandleFunc("/{id}/flowpairs", authenticated(http.HandlerFunc(c.addressHandler))).Name("flowpairs")
 	addressApi.HandleFunc("/{id}/listeners", authenticated(http.HandlerFunc(c.addressHandler))).Name("listeners")
diff --git a/cmd/manifest/manifest.go b/cmd/manifest/manifest.go
index 2719a940..fb332039 100644
--- a/cmd/manifest/manifest.go
+++ b/cmd/manifest/manifest.go
@@ -3,9 +3,10 @@ package main
 import (
 	"encoding/json"
 	"fmt"
+	"os"
+
 	"github.com/skupperproject/skupper/pkg/images"
 	"github.com/spf13/cobra"
-	"os"
 )
 
 type SkupperImage struct {
@@ -54,6 +55,10 @@ func generateManifestFile() error {
 				Name: images.GetPrometheusServerImageName(),
 				SHA:  images.GetSha(images.GetPrometheusServerImageName()),
 			},
+			{
+				Name: images.GetOauthProxyImageName(),
+				SHA:  images.GetSha(images.GetOauthProxyImageName()),
+			},
 		},
 	}
 
diff --git a/cmd/service-controller/console_server.go b/cmd/service-controller/console_server.go
index 51702a90..b3bd2670 100644
--- a/cmd/service-controller/console_server.go
+++ b/cmd/service-controller/console_server.go
@@ -1,7 +1,6 @@
 package main
 
 import (
-	"crypto/tls"
 	"encoding/json"
 	"errors"
 	"fmt"
@@ -20,6 +19,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/data"
 	"github.com/skupperproject/skupper/pkg/event"
 	"github.com/skupperproject/skupper/pkg/qdr"
@@ -40,7 +40,7 @@ type ConsoleServer struct {
 	accessRevoker *AccessRevoker
 }
 
-func newConsoleServer(cli *client.VanClient, config *tls.Config, eventHandler event.EventHandlerInterface) *ConsoleServer {
+func newConsoleServer(cli *client.VanClient, config *certs.TlsConfigRetriever, eventHandler event.EventHandlerInterface) *ConsoleServer {
 	pool := qdr.NewAgentPool("amqps://"+types.QualifiedServiceName(types.LocalTransportServiceName, cli.Namespace)+":5671", config)
 	return &ConsoleServer{
 		agentPool:     pool,
diff --git a/cmd/service-controller/controller.go b/cmd/service-controller/controller.go
index dfaf897d..c5bd9215 100644
--- a/cmd/service-controller/controller.go
+++ b/cmd/service-controller/controller.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"crypto/tls"
 	jsonencoding "encoding/json"
 	"fmt"
 	"log"
@@ -30,6 +29,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/event"
 	"github.com/skupperproject/skupper/pkg/flow"
 	"github.com/skupperproject/skupper/pkg/kube"
@@ -57,7 +57,6 @@ type Controller struct {
 
 	// service_sync state:
 	disableServiceSync bool
-	tlsConfig          *tls.Config
 	amqpClient         *amqp.Client
 	amqpSession        *amqp.Session
 	byOrigin           map[string]map[string]types.ServiceInterface
@@ -116,7 +115,7 @@ func hasOriginalAnnotations(service corev1.Service) bool {
 	return hasOriginalSelector(service) || hasOriginalTargetPort(service) || hasOriginalAssigned(service)
 }
 
-func NewController(cli *client.VanClient, origin string, tlsConfig *tls.Config, disableServiceSync bool) (*Controller, error) {
+func NewController(cli *client.VanClient, origin string, tlsConfig *certs.TlsConfigRetriever, disableServiceSync bool) (*Controller, error) {
 
 	// create informers
 	svcInformer := corev1informer.NewServiceInformer(
@@ -163,7 +162,6 @@ func NewController(cli *client.VanClient, origin string, tlsConfig *tls.Config,
 		vanClient:          cli,
 		policy:             client.NewClusterPolicyValidator(cli),
 		origin:             origin,
-		tlsConfig:          tlsConfig,
 		bridgeDefInformer:  bridgeDefInformer,
 		svcDefInformer:     svcDefInformer,
 		svcInformer:        svcInformer,
@@ -661,7 +659,26 @@ func (c *Controller) updateServiceSync(defs *corev1.ConfigMap) {
 }
 
 func (c *Controller) deleteHeadlessProxy(statefulset *appsv1.StatefulSet) error {
-	return c.vanClient.KubeClient.AppsV1().StatefulSets(c.vanClient.Namespace).Delete(context.TODO(), statefulset.ObjectMeta.Name, metav1.DeleteOptions{})
+	err := c.vanClient.KubeClient.AppsV1().StatefulSets(c.vanClient.Namespace).Delete(context.TODO(), statefulset.ObjectMeta.Name, metav1.DeleteOptions{})
+	if err != nil {
+		c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Error deleting headless proxy %q: %s", statefulset.Name, err))
+		return err
+	}
+	c.eventHandler.RecordNormalEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Deleted headless proxy %q", statefulset.Name))
+	svcName := statefulset.Spec.ServiceName
+	svc, found, err := c.GetService(svcName)
+	if !found {
+		c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Headless proxy service %q not found", svcName))
+	} else if err != nil {
+		c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Error retrieving headless proxy service %q: %s", svcName, err))
+	} else if c.IsOwned(svc) {
+		if err = kube.DeleteService(svcName, c.vanClient.Namespace, c.vanClient.KubeClient); err != nil {
+			c.eventHandler.RecordWarningEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Error deleting headless proxy service %q: %s", statefulset.Name, err))
+			return err
+		}
+		c.eventHandler.RecordNormalEvent(ServiceControllerDeleteEvent, fmt.Sprintf("Deleted headless proxy service %q", statefulset.Name))
+	}
+	return nil
 }
 
 func (c *Controller) ensureHeadlessProxyFor(bindings *service.ServiceBindings, statefulset *appsv1.StatefulSet) error {
diff --git a/cmd/service-controller/main.go b/cmd/service-controller/main.go
index 31c8ce51..5a06712d 100644
--- a/cmd/service-controller/main.go
+++ b/cmd/service-controller/main.go
@@ -67,10 +67,7 @@ func main() {
 		log.Fatal("Error getting van client", err.Error())
 	}
 
-	tlsConfig, err := certs.GetTlsConfig(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
-	if err != nil {
-		log.Fatal("Error getting tls config", err.Error())
-	}
+	tlsConfig := certs.GetTlsConfigRetriever(true, types.ControllerConfigPath+"tls.crt", types.ControllerConfigPath+"tls.key", types.ControllerConfigPath+"ca.crt")
 
 	event.StartDefaultEventStore(stopCh)
 
diff --git a/cmd/service-controller/policy_controller.go b/cmd/service-controller/policy_controller.go
index 984f25fa..e17e8bab 100644
--- a/cmd/service-controller/policy_controller.go
+++ b/cmd/service-controller/policy_controller.go
@@ -354,7 +354,7 @@ func (c *PolicyController) validateExposeStateChanged() {
 			}
 			if !res.Allowed() {
 				// resource is no longer allowed, unbinding
-				event.Recordf(c.name, "[validateExposeStateChanged] exposed resource is no longer authorized - unbinding service %s: %v", service.Address, err)
+				event.Recordf(c.name, "[validateExposeStateChanged] exposed resource is no longer authorized - unbinding target %s/%s for service %s", targetType, target.Name, service.Address)
 				err = c.cli.ServiceInterfaceUnbind(context.Background(), "deployment", target.Name, service.Address, false, svcNamespace)
 				if err != nil {
 					event.Recordf(c.name, "[validateExposeStateChanged] error unbinding service %s: %v", service.Address, err)
diff --git a/cmd/service-controller/site_query.go b/cmd/service-controller/site_query.go
index 3f761cd8..755c5b9d 100644
--- a/cmd/service-controller/site_query.go
+++ b/cmd/service-controller/site_query.go
@@ -2,7 +2,6 @@ package main
 
 import (
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"os"
@@ -15,6 +14,7 @@ import (
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/data"
 	"github.com/skupperproject/skupper/pkg/event"
 	"github.com/skupperproject/skupper/pkg/kube"
@@ -32,17 +32,15 @@ const (
 
 type SiteQueryServer struct {
 	client    *client.VanClient
-	tlsConfig *tls.Config
 	agentPool *qdr.AgentPool
 	server    *qdr.RequestServer
 	iplookup  *IpLookup
 	siteInfo  data.Site
 }
 
-func newSiteQueryServer(cli *client.VanClient, config *tls.Config) *SiteQueryServer {
+func newSiteQueryServer(cli *client.VanClient, config *certs.TlsConfigRetriever) *SiteQueryServer {
 	sqs := SiteQueryServer{
 		client:    cli,
-		tlsConfig: config,
 		agentPool: qdr.NewAgentPool("amqps://"+types.QualifiedServiceName(types.LocalTransportServiceName, cli.Namespace)+":5671", config),
 		iplookup:  NewIpLookup(cli, nil),
 	}
diff --git a/cmd/site-controller/controller.go b/cmd/site-controller/controller.go
index d4c05a93..66ab9a01 100644
--- a/cmd/site-controller/controller.go
+++ b/cmd/site-controller/controller.go
@@ -26,6 +26,7 @@ import (
 type SiteController struct {
 	vanClient            *client.VanClient
 	siteInformer         cache.SharedIndexInformer
+	tokenInformer        cache.SharedIndexInformer
 	tokenRequestInformer cache.SharedIndexInformer
 	workqueue            workqueue.RateLimitingInterface
 }
@@ -52,6 +53,14 @@ func NewSiteController(cli *client.VanClient) (*SiteController, error) {
 			options.FieldSelector = "metadata.name=skupper-site"
 			options.LabelSelector = "!" + types.SiteControllerIgnore
 		}))
+	tokenInformer := corev1informer.NewFilteredSecretInformer(
+		cli.KubeClient,
+		watchNamespace,
+		time.Second*30,
+		cache.Indexers{cache.NamespaceIndex: cache.MetaNamespaceIndexFunc},
+		internalinterfaces.TweakListOptionsFunc(func(options *metav1.ListOptions) {
+			options.LabelSelector = types.TypeTokenQualifier
+		}))
 	tokenRequestInformer := corev1informer.NewFilteredSecretInformer(
 		cli.KubeClient,
 		watchNamespace,
@@ -65,11 +74,13 @@ func NewSiteController(cli *client.VanClient) (*SiteController, error) {
 	controller := &SiteController{
 		vanClient:            cli,
 		siteInformer:         siteInformer,
+		tokenInformer:        tokenInformer,
 		tokenRequestInformer: tokenRequestInformer,
 		workqueue:            workqueue,
 	}
 
 	siteInformer.AddEventHandler(controller.getHandlerFuncs(SiteConfig, configmapResourceVersionTest))
+	tokenInformer.AddEventHandler(controller.getHandlerFuncs(Token, secretResourceVersionTest))
 	tokenRequestInformer.AddEventHandler(controller.getHandlerFuncs(TokenRequest, secretResourceVersionTest))
 	return controller, nil
 }
@@ -110,6 +121,7 @@ func (c *SiteController) Run(stopCh <-chan struct{}) error {
 
 	log.Println("Starting the Skupper site controller informers")
 	go c.siteInformer.Run(stopCh)
+	go c.tokenInformer.Run(stopCh)
 	go c.tokenRequestInformer.Run(stopCh)
 
 	log.Println("Waiting for informer caches to sync")
@@ -175,6 +187,8 @@ func (c *SiteController) dispatchTrigger(trigger trigger) error {
 	switch trigger.category {
 	case SiteConfig:
 		return c.checkSite(trigger.key)
+	case Token:
+		return c.checkToken(trigger.key)
 	case TokenRequest:
 		return c.checkTokenRequest(trigger.key)
 	default:
@@ -199,6 +213,7 @@ func (c *SiteController) enqueueTrigger(obj interface{}, category triggerType) {
 func (c *SiteController) checkAllForSite() {
 	// Now need to check whether there are any token requests already in place
 	log.Println("Checking token requests...")
+	c.checkAllToken()
 	c.checkAllTokenRequests()
 	log.Println("Done.")
 }
@@ -332,6 +347,15 @@ func (c *SiteController) generate(token *corev1.Secret) error {
 	}
 }
 
+func (c *SiteController) checkAllToken() {
+	// can we rely on the cache here?
+	tokens := c.tokenInformer.GetStore().List()
+	for _, t := range tokens {
+		// service from workqueue
+		c.enqueueTrigger(t, Token)
+	}
+}
+
 func (c *SiteController) checkAllTokenRequests() {
 	// can we rely on the cache here?
 	tokens := c.tokenRequestInformer.GetStore().List()
@@ -341,6 +365,32 @@ func (c *SiteController) checkAllTokenRequests() {
 	}
 }
 
+func (c *SiteController) checkToken(key string) error {
+	log.Printf("Handling token for %s", key)
+	obj, exists, err := c.tokenInformer.GetStore().GetByKey(key)
+	if err != nil {
+		log.Println("Error checking connection-token secret: ", err)
+		return err
+	}
+	if !exists {
+		return nil
+	}
+
+	token := obj.(*corev1.Secret)
+	if !c.isTokenValidInSite(token) {
+		log.Println("Cannot handle token, as site not yet initialised")
+		return nil
+	}
+
+	updated, _ := c.vanClient.RouterUpdateHostAliases(context.Background(), token)
+	if updated {
+		log.Printf("Router updated and restarted due to changes in token %s", key)
+	} else {
+		log.Printf("Changes in token %s didn't trigger router update", key)
+	}
+	return nil
+}
+
 func (c *SiteController) checkTokenRequest(key string) error {
 	log.Printf("Handling token request for %s", key)
 	obj, exists, err := c.tokenRequestInformer.GetStore().GetByKey(key)
diff --git a/cmd/skupper/skupper.go b/cmd/skupper/skupper.go
index 8cdceef5..1d35ab31 100644
--- a/cmd/skupper/skupper.go
+++ b/cmd/skupper/skupper.go
@@ -558,6 +558,8 @@ func NewCmdUnexpose(skupperCli SkupperServiceClient) *cobra.Command {
 		RunE:   skupperCli.Unexpose,
 	}
 	cmd.Flags().StringVar(&unexposeAddress, "address", "", "Skupper address the target was exposed as")
+
+	skupperCli.UnexposeFlags(cmd)
 	return cmd
 }
 
diff --git a/cmd/skupper/skupper_kube.go b/cmd/skupper/skupper_kube.go
index 710dab4f..3eb90ba1 100644
--- a/cmd/skupper/skupper_kube.go
+++ b/cmd/skupper/skupper_kube.go
@@ -1,6 +1,8 @@
 package main
 
 import (
+	"fmt"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/spf13/cobra"
 )
@@ -103,3 +105,7 @@ func (s *SkupperKube) NewClient(cmd *cobra.Command, args []string) {
 	}
 	s.Cli = NewClientHandleError(s.Namespace, s.KubeContext, s.KubeConfigPath, exitOnError)
 }
+
+func targetTypeServiceTargetNamespaceError() error {
+	return fmt.Errorf("--target-namespace cannot be used when target type is service (qualify the target service name instead)")
+}
diff --git a/cmd/skupper/skupper_kube_expose.go b/cmd/skupper/skupper_kube_expose.go
index 031262e4..2bd7fe9b 100644
--- a/cmd/skupper/skupper_kube_expose.go
+++ b/cmd/skupper/skupper_kube_expose.go
@@ -84,6 +84,9 @@ func (s *SkupperKubeService) Expose(cmd *cobra.Command, args []string) error {
 	if exposeOpts.PublishNotReadyAddresses && targetType == "service" {
 		return fmt.Errorf("--publish-not-ready-addresses option is only valid for headless services and deployments")
 	}
+	if exposeOpts.Namespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
 
 	addr, err := expose(s.kube.Cli, context.Background(), targetType, targetName, exposeOpts)
 	if err == nil {
@@ -128,6 +131,10 @@ func (s *SkupperKubeService) Unexpose(cmd *cobra.Command, args []string) error {
 	silenceCobra(cmd)
 	targetType, targetName := parseTargetTypeAndName(args)
 
+	if unexposeNamespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
+
 	err := s.kube.Cli.ServiceInterfaceUnbind(context.Background(), targetType, targetName, unexposeAddress, true, unexposeNamespace)
 	if err == nil {
 		fmt.Printf("%s %s unexposed\n", targetType, targetName)
diff --git a/cmd/skupper/skupper_kube_service.go b/cmd/skupper/skupper_kube_service.go
index 1b8e97bc..2147b61a 100644
--- a/cmd/skupper/skupper_kube_service.go
+++ b/cmd/skupper/skupper_kube_service.go
@@ -154,6 +154,10 @@ func (s *SkupperKubeService) Bind(cmd *cobra.Command, args []string) error {
 		return fmt.Errorf("--publish-not-ready-addresses option is only valid for headless services and deployments")
 	}
 
+	if bindOptions.Namespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
+
 	service, err := s.kube.Cli.ServiceInterfaceInspect(context.Background(), args[0])
 
 	if err != nil {
@@ -194,6 +198,10 @@ func (s *SkupperKubeService) Unbind(cmd *cobra.Command, args []string) error {
 
 	targetType, targetName := parseTargetTypeAndName(args[1:])
 
+	if unbindNamespace != "" && targetType == "service" {
+		return targetTypeServiceTargetNamespaceError()
+	}
+
 	err := s.kube.Cli.ServiceInterfaceUnbind(context.Background(), targetType, targetName, args[0], false, unbindNamespace)
 	if err != nil {
 		return fmt.Errorf("%w", err)
diff --git a/pkg/certs/certs.go b/pkg/certs/certs.go
index 294ab550..2266b55f 100644
--- a/pkg/certs/certs.go
+++ b/pkg/certs/certs.go
@@ -86,14 +86,17 @@ func getCAFromSecret(secret *corev1.Secret) CertificateAuthority {
 	}
 }
 
-func generateSecret(name string, subject string, hosts string, ca *CertificateAuthority) corev1.Secret {
+func generateSecret(name string, subject string, hosts string, ca *CertificateAuthority, expiration time.Duration) corev1.Secret {
 	priv, err := rsa.GenerateKey(rand.Reader, 2048)
 	if err != nil {
 		log.Fatalf("failed to generate private key: %s", err)
 	}
 
 	notBefore := time.Now()
-	notAfter := notBefore.Add(5 * 365 * 24 * time.Hour) // TODO: make configurable?
+	if expiration == 0 {
+		expiration = 5 * 365 * 24 * time.Hour
+	}
+	notAfter := notBefore.Add(expiration)
 
 	serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
 	serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
@@ -211,64 +214,16 @@ func CertDataToSecret(name string, certData CertificateData, annotations map[str
 }
 
 func GenerateSecret(name string, subject string, hosts string, ca *corev1.Secret) corev1.Secret {
-	caCert := getCAFromSecret(ca)
-	return generateSecret(name, subject, hosts, &caCert)
-}
-
-func GenerateCASecret(name string, subject string) corev1.Secret {
-	return generateSecret(name, subject, "", nil)
+	return GenerateSecretWithExpiration(name, subject, hosts, 0, ca)
 }
 
-func GenerateCertificateData(name string, subject string, hosts string, caData CertificateData) CertificateData {
-	caSecret := CertDataToSecret("temp", caData, nil)
-	secret := GenerateSecret(name, subject, hosts, &caSecret)
-	return SecretToCertData(secret)
-}
-
-func GenerateCACertificateData(name string, subject string) CertificateData {
-	secret := GenerateCASecret(name, subject)
-	return SecretToCertData(secret)
-}
-
-func PutCertificateData(name string, secretFile string, certData CertificateData, annotations map[string]string) {
-	secret := CertDataToSecret(name, certData, annotations)
-
-	// generate a yaml and save it to the specified path
-	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme, scheme.Scheme)
-	out, err := os.Create(secretFile)
-	if err != nil {
-		log.Fatal("Could not write to file " + secretFile + ": " + err.Error())
-	}
-	err = s.Encode(&secret, out)
-	if err != nil {
-		log.Fatal("Could not write out generated secret: " + err.Error())
-	} else {
-		// TODO: valid token, local cluster? extra
-		fmt.Printf("Connection token written to %s", secretFile)
-		fmt.Println()
-	}
+func GenerateSecretWithExpiration(name string, subject string, hosts string, expiration time.Duration, ca *corev1.Secret) corev1.Secret {
+	caCert := getCAFromSecret(ca)
+	return generateSecret(name, subject, hosts, &caCert, expiration)
 }
 
-func GetSecretContent(secretFile string) (map[string][]byte, error) {
-	yaml, err := ioutil.ReadFile(secretFile)
-	if err != nil {
-		return nil, fmt.Errorf("Could not read connection token: %w", err)
-	}
-	s := json.NewYAMLSerializer(json.DefaultMetaFactory, scheme.Scheme,
-		scheme.Scheme)
-	var secret corev1.Secret
-	_, _, err = s.Decode(yaml, nil, &secret)
-	if err != nil {
-		return nil, fmt.Errorf("Could not parse connection token: %w", err)
-	}
-	content := make(map[string][]byte)
-	for k, v := range secret.Data {
-		content[k] = v
-	}
-	for k, v := range secret.ObjectMeta.Annotations {
-		content[k] = []byte(v)
-	}
-	return content, nil
+func GenerateCASecret(name string, subject string) corev1.Secret {
+	return generateSecret(name, subject, "", nil, 0)
 }
 
 func GenerateSimpleSecret(name string, ca *corev1.Secret) corev1.Secret {
@@ -304,6 +259,14 @@ func GenerateSecretFile(secretFile string, secret *corev1.Secret, localOnly bool
 	return nil
 }
 
+func DecodeCertificate(data []byte) (*x509.Certificate, error) {
+	b, _ := pem.Decode(data)
+	if b == nil {
+		return nil, fmt.Errorf("Could not decode PEM block from data")
+	}
+	return x509.ParseCertificate(b.Bytes)
+}
+
 func GetTlsCrtHostnames(tlscrtData []byte) (subject string, hostnames []string, err error) {
 	b, _ := pem.Decode(tlscrtData)
 	if b == nil {
@@ -320,7 +283,31 @@ func GetTlsCrtHostnames(tlscrtData []byte) (subject string, hostnames []string,
 	return subject, hostnames, nil
 }
 
-func GetTlsConfig(verify bool, cert, key, ca string) (*tls.Config, error) {
+type TlsConfigRetriever struct {
+	Cert   string
+	Key    string
+	Ca     string
+	Verify bool
+}
+
+func (paths *TlsConfigRetriever) GetTlsConfig() (*tls.Config, error) {
+	tlsConfig, err := getTlsConfig(paths.Verify, paths.Cert, paths.Key, paths.Ca)
+	if err != nil {
+		return nil, err
+	}
+	return tlsConfig, nil
+}
+
+func GetTlsConfigRetriever(verify bool, cert, key, ca string) *TlsConfigRetriever {
+	return &TlsConfigRetriever{
+		Cert:   cert,
+		Key:    key,
+		Ca:     ca,
+		Verify: verify,
+	}
+}
+
+func getTlsConfig(verify bool, cert, key, ca string) (*tls.Config, error) {
 	var config tls.Config
 	config.InsecureSkipVerify = true
 	if verify {
diff --git a/pkg/domain/podman/controller/controller.go b/pkg/domain/podman/controller/controller.go
index c59a2ba0..00c61dcb 100644
--- a/pkg/domain/podman/controller/controller.go
+++ b/pkg/domain/podman/controller/controller.go
@@ -1,13 +1,13 @@
 package controller
 
 import (
-	"crypto/tls"
 	"fmt"
 	"log"
 	"time"
 
 	"github.com/skupperproject/skupper/api/types"
 	clientpodman "github.com/skupperproject/skupper/client/podman"
+	"github.com/skupperproject/skupper/pkg/certs"
 	"github.com/skupperproject/skupper/pkg/domain/podman"
 	"github.com/skupperproject/skupper/pkg/flow"
 	"github.com/skupperproject/skupper/pkg/qdr"
@@ -19,14 +19,14 @@ import (
 type ControllerPodman struct {
 	cli               *clientpodman.PodmanRestClient
 	cfg               *podman.Config
-	tlsConfig         *tls.Config
+	tlsConfig         *certs.TlsConfigRetriever
 	origin            string
 	containerInformer *clientpodman.ContainerInformer
 	servicesWatcher   *ServiceTargetWatcher
 	site              *podman.Site
 }
 
-func NewControllerPodman(origin string, tlsConfig *tls.Config) (*ControllerPodman, error) {
+func NewControllerPodman(origin string, tlsConfig *certs.TlsConfigRetriever) (*ControllerPodman, error) {
 	cfg, err := podman.NewPodmanConfigFileHandler().GetConfig()
 	if err != nil {
 		return nil, fmt.Errorf("error reading podman site config - %s", err)
diff --git a/pkg/domain/podman/router.go b/pkg/domain/podman/router.go
index 89e58033..62aa0158 100644
--- a/pkg/domain/podman/router.go
+++ b/pkg/domain/podman/router.go
@@ -72,7 +72,12 @@ func (r *RouterEntityManager) QueryConnections(routerId string, edge bool) ([]qd
 	var connections []qdr.Connection
 	err = json.Unmarshal([]byte(data), &connections)
 	if err != nil {
-		return nil, fmt.Errorf("error retrieving connections - %w", err)
+		if r.isInvalidResponseFromStaleRouter(data) {
+			fmt.Printf("Warning: unable to retrieve connections from router %q, as it is no longer available", routerId)
+			fmt.Println()
+		} else {
+			return nil, fmt.Errorf("error retrieving connections - %w - output: %q", err, data)
+		}
 	}
 	return connections, nil
 }
@@ -106,7 +111,10 @@ func (r *RouterEntityManager) QueryAllRouters() ([]qdr.Router, error) {
 		var records []qdr.Record
 		err = json.Unmarshal([]byte(rJson), &records)
 		if err != nil {
-			return nil, fmt.Errorf("error decoding router info from %s - %w", routerToQuery, err)
+			if r.isInvalidResponseFromStaleRouter(rJson) {
+				continue
+			}
+			return nil, fmt.Errorf("error decoding router info from %s - %w - %s", routerToQuery, err, rJson)
 		}
 		router.Site = qdr.GetSiteMetadata(records[0].AsString("metadata"))
 
@@ -145,6 +153,14 @@ func (r *RouterEntityManager) QueryAllRouters() ([]qdr.Router, error) {
 	return routersTmp, nil
 }
 
+// isInvalidResponseFromStaleRouter returns true if response is not a valid JSON
+// message returned by the management API, because the given router is stale as it
+// is still showing up as an existing router.node entity, but the connection is
+// no longer active.
+func (r *RouterEntityManager) isInvalidResponseFromStaleRouter(jsonOutput string) bool {
+	return strings.Contains(jsonOutput, "SendException: RELEASED") || strings.Contains(jsonOutput, "Timeout: ")
+}
+
 func (r *RouterEntityManager) QueryRouterNodes() ([]qdr.RouterNode, error) {
 	var routerNodes []qdr.RouterNode
 	// Retrieving all connections
diff --git a/pkg/flow/collector.go b/pkg/flow/collector.go
index 3375deb8..14c704f1 100644
--- a/pkg/flow/collector.go
+++ b/pkg/flow/collector.go
@@ -169,6 +169,7 @@ type FlowCollector struct {
 	Links                   map[string]*LinkRecord
 	Listeners               map[string]*ListenerRecord
 	Connectors              map[string]*ConnectorRecord
+	recentConnectors        map[string]*ConnectorRecord
 	Flows                   map[string]*FlowRecord
 	FlowPairs               map[string]*FlowPairRecord
 	FlowAggregates          map[string]*FlowAggregateRecord
@@ -214,6 +215,7 @@ func NewFlowCollector(origin string, reg prometheus.Registerer, connectionFactor
 		Links:                   make(map[string]*LinkRecord),
 		Listeners:               make(map[string]*ListenerRecord),
 		Connectors:              make(map[string]*ConnectorRecord),
+		recentConnectors:        make(map[string]*ConnectorRecord),
 		Flows:                   make(map[string]*FlowRecord),
 		FlowPairs:               make(map[string]*FlowPairRecord),
 		FlowAggregates:          make(map[string]*FlowAggregateRecord),
diff --git a/pkg/flow/flow_mem_driver.go b/pkg/flow/flow_mem_driver.go
index 7709bf35..e67eb1c8 100644
--- a/pkg/flow/flow_mem_driver.go
+++ b/pkg/flow/flow_mem_driver.go
@@ -4,6 +4,7 @@ import (
 	"encoding/json"
 	"fmt"
 	"log"
+	"net"
 	"strings"
 	"time"
 
@@ -240,11 +241,15 @@ func (fc *FlowCollector) getFlowPlace(flow *FlowRecord) FlowPlace {
 		return clientSide
 	} else if _, ok := fc.Connectors[flow.Parent]; ok {
 		return serverSide
+	} else if _, ok := fc.recentConnectors[flow.Parent]; ok {
+		return serverSide
 	} else if l4Flow, ok := fc.Flows[flow.Parent]; ok {
 		if _, ok := fc.Listeners[l4Flow.Parent]; ok {
 			return clientSide
 		} else if _, ok := fc.Connectors[l4Flow.Parent]; ok {
 			return serverSide
+		} else if _, ok := fc.recentConnectors[l4Flow.Parent]; ok {
+			return serverSide
 		}
 	}
 	return unknown
@@ -300,7 +305,7 @@ func (fc *FlowCollector) annotateFlowTrace(flow *FlowRecord) *string {
 }
 
 func (fc *FlowCollector) linkFlowPair(flow *FlowRecord) (*FlowPairRecord, bool) {
-	var sourceSiteId, destSiteId string = "", ""
+	var sourceSiteId, destSiteId, sourceSiteName, destSiteName string = "", "", "sourceSite", "destSite"
 	var sourceFlow, destFlow *FlowRecord = nil, nil
 	var ok bool
 
@@ -330,14 +335,20 @@ func (fc *FlowCollector) linkFlowPair(flow *FlowRecord) (*FlowPairRecord, bool)
 	}
 
 	sourceSiteId = fc.getRecordSiteId(*sourceFlow)
+	if sourceSite, ok := fc.Sites[sourceSiteId]; ok {
+		sourceSiteName = *sourceSite.Name
+	}
 	destSiteId = fc.getRecordSiteId(*destFlow)
-	fwdLabels["sourceSite"] = sourceSiteId
-	fwdLabels["destSite"] = destSiteId
+	if destSite, ok := fc.Sites[destSiteId]; ok {
+		destSiteName = *destSite.Name
+	}
+	fwdLabels["sourceSite"] = sourceSiteName + "@_@" + sourceSiteId
+	fwdLabels["destSite"] = destSiteName + "@_@" + destSiteId
 	fwdLabels["sourceProcess"] = *sourceFlow.ProcessName
 	fwdLabels["destProcess"] = *destFlow.ProcessName
 	delete(fwdLabels, "process")
-	revLabels["sourceSite"] = destSiteId
-	revLabels["destSite"] = sourceSiteId
+	revLabels["sourceSite"] = destSiteName + "@_@" + destSiteId
+	revLabels["destSite"] = sourceSiteName + "@_@" + sourceSiteId
 	revLabels["sourceProcess"] = *destFlow.ProcessName
 	revLabels["destProcess"] = *sourceFlow.ProcessName
 	delete(revLabels, "process")
@@ -354,6 +365,9 @@ func (fc *FlowCollector) linkFlowPair(flow *FlowRecord) (*FlowPairRecord, bool)
 		ForwardFlow:       sourceFlow,
 		CounterFlow:       destFlow,
 	}
+	if sourceFlow.EndTime != 0 {
+		fp.Duration = sourceFlow.EndTime - sourceFlow.StartTime
+	}
 	if sourceSite, ok := fc.Sites[sourceSiteId]; ok {
 		fp.SourceSiteName = sourceSite.Name
 	} else {
@@ -453,7 +467,6 @@ func (fc *FlowCollector) addRecord(record interface{}) error {
 }
 
 func (fc *FlowCollector) deleteRecord(record interface{}) error {
-	// TODO: log the record
 	if record == nil {
 		return fmt.Errorf("No record to delete")
 	}
@@ -481,6 +494,8 @@ func (fc *FlowCollector) deleteRecord(record interface{}) error {
 		}
 	case *ConnectorRecord:
 		if connector, ok := record.(*ConnectorRecord); ok {
+			// keep around for flows that will be terminated
+			fc.recentConnectors[connector.Identity] = connector
 			delete(fc.Connectors, connector.Identity)
 		}
 	case *FlowRecord:
@@ -741,8 +756,8 @@ func (fc *FlowCollector) updateRecord(record interface{}) error {
 			} else {
 				if connector.EndTime > 0 {
 					current.EndTime = connector.EndTime
-					if current.process != nil {
-						if process, ok := fc.Processes[*current.process]; ok {
+					if current.ProcessId != nil {
+						if process, ok := fc.Processes[*current.ProcessId]; ok {
 							process.connector = nil
 							process.ProcessBinding = &Unbound
 						}
@@ -901,6 +916,7 @@ func (fc *FlowCollector) updateRecord(record interface{}) error {
 					if fc.getFlowPlace(current) == clientSide {
 						if flowpair, ok := fc.FlowPairs["fp-"+current.Identity]; ok {
 							flowpair.EndTime = current.EndTime
+							flowpair.Duration = flowpair.EndTime - flowpair.StartTime
 						}
 					}
 				}
@@ -1322,8 +1338,8 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 		case "process":
 			if id, ok := vars["id"]; ok {
 				if connector, ok := fc.Connectors[id]; ok {
-					if connector.process != nil {
-						if process, ok := fc.Processes[*connector.process]; ok {
+					if connector.ProcessId != nil {
+						if process, ok := fc.Processes[*connector.ProcessId]; ok {
 							p.Count = 1
 							p.Results = *process
 						}
@@ -1345,10 +1361,10 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			retrieveError = sortAndSlice(addresses, &p, queryParams)
 		case "item":
 			if id, ok := vars["id"]; ok {
-				if addr, ok := fc.VanAddresses[id]; ok {
-					fc.getAddressMetrics(addr)
+				if address, ok := fc.VanAddresses[id]; ok {
+					fc.getAddressMetrics(address)
 					p.Count = 1
-					p.Results = addr
+					p.Results = address
 				}
 			}
 		case "flows":
@@ -1433,8 +1449,8 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			if id, ok := vars["id"]; ok {
 				if vanaddr, ok := fc.VanAddresses[id]; ok {
 					for _, connector := range fc.Connectors {
-						if *connector.Address == vanaddr.Name && connector.process != nil {
-							if process, ok := fc.Processes[*connector.process]; ok {
+						if *connector.Address == vanaddr.Name && connector.ProcessId != nil {
+							if process, ok := fc.Processes[*connector.ProcessId]; ok {
 								if filterRecord(*process, queryParams) && process.Base.TimeRangeValid(queryParams) {
 									unique[process.Identity] = process
 								}
@@ -1448,6 +1464,27 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			}
 			p.TotalCount = len(unique)
 			retrieveError = sortAndSlice(processes, &p, queryParams)
+		case "processpairs":
+			processPairs := []FlowAggregateRecord{}
+			if id, ok := vars["id"]; ok {
+				if vanaddr, ok := fc.VanAddresses[id]; ok {
+					for _, connector := range fc.Connectors {
+						if *connector.Address == vanaddr.Name && connector.ProcessId != nil {
+							for _, aggregate := range fc.FlowAggregates {
+								if aggregate.PairType == recordNames[Process] {
+									if *connector.ProcessId == *aggregate.DestinationId {
+										p.TotalCount++
+										if filterRecord(*aggregate, queryParams) {
+											processPairs = append(processPairs, *aggregate)
+										}
+									}
+								}
+							}
+						}
+					}
+				}
+			}
+			retrieveError = sortAndSlice(processPairs, &p, queryParams)
 		case "listeners":
 			listeners := []ListenerRecord{}
 			if id, ok := vars["id"]; ok {
@@ -1515,7 +1552,7 @@ func (fc *FlowCollector) retrieve(request ApiRequest) (*string, error) {
 			if id, ok := vars["id"]; ok {
 				if _, ok := fc.Processes[id]; ok {
 					for _, connector := range fc.Connectors {
-						if connector.process != nil && *connector.process == id {
+						if connector.ProcessId != nil && *connector.ProcessId == id {
 							for _, address := range fc.VanAddresses {
 								if *connector.Address == address.Name {
 									if filterRecord(*address, queryParams) {
@@ -1928,7 +1965,7 @@ func (fc *FlowCollector) setupFlowMetrics(va *VanAddressRecord, flow *FlowRecord
 		return fmt.Errorf("Metric label missing source site key")
 	}
 	if key.sourceProcess, ok = metricLabel["sourceProcess"]; !ok {
-		return fmt.Errorf("Metric label soruce process key")
+		return fmt.Errorf("Metric label missing source process key")
 	}
 	if key.destSite, ok = metricLabel["destSite"]; !ok {
 		return fmt.Errorf("Metric label missing dest site key")
@@ -2033,8 +2070,9 @@ func (fc *FlowCollector) reconcileRecords() error {
 			}
 			if flow.SourceHost != nil {
 				if connector, ok := fc.Connectors[flow.Parent]; ok {
-					if connector.process != nil && connector.AddressId != nil {
-						flow.Process = connector.process
+					flow.Place = serverSide
+					if connector.ProcessId != nil && connector.AddressId != nil {
+						flow.Process = connector.ProcessId
 						if process, ok := fc.Processes[*flow.Process]; ok {
 							flow.ProcessName = process.Name
 						}
@@ -2045,6 +2083,7 @@ func (fc *FlowCollector) reconcileRecords() error {
 						}
 					}
 				} else if listener, ok := fc.Listeners[flow.Parent]; ok {
+					flow.Place = clientSide
 					if listener.AddressId != nil {
 						found := false
 						for _, process := range fc.Processes {
@@ -2106,12 +2145,21 @@ func (fc *FlowCollector) reconcileRecords() error {
 				delete(fc.connectorsToReconcile, connId)
 			} else if connector.DestHost != nil {
 				siteId := fc.getRecordSiteId(*connector)
+				var matchHost *string
 				found := false
+				if net.ParseIP(*connector.DestHost) == nil {
+					addrs, err := net.LookupHost(*connector.DestHost)
+					if err == nil && len(addrs) > 0 {
+						matchHost = &addrs[0]
+					}
+				} else {
+					matchHost = connector.DestHost
+				}
 				for _, process := range fc.Processes {
 					if siteId == process.Parent {
 						if process.SourceHost != nil {
-							if *connector.DestHost == *process.SourceHost {
-								connector.process = &process.Identity
+							if *matchHost == *process.SourceHost {
+								connector.ProcessId = &process.Identity
 								process.connector = &connector.Identity
 								process.ProcessBinding = &Bound
 								log.Printf("COLLECTOR: Connector %s/%s associated to process %s\n", connector.Identity, *connector.Address, *process.Name)
@@ -2136,7 +2184,7 @@ func (fc *FlowCollector) reconcileRecords() error {
 						for _, process := range fc.Processes {
 							if process.Name != nil && *process.Name == processName {
 								log.Printf("COLLECTOR: Associating connector %s to external process %s\n", connector.Identity, processName)
-								connector.process = &process.Identity
+								connector.ProcessId = &process.Identity
 								delete(fc.connectorsToReconcile, connId)
 								break
 							}
@@ -2238,8 +2286,8 @@ func (fc *FlowCollector) reconcileRecords() error {
 		}
 	}
 
+	t := time.Now()
 	for _, source := range fc.eventSources {
-		t := time.Now()
 		diff := uint64(t.UnixNano())/uint64(time.Microsecond) - source.EventSourceRecord.LastHeard
 		if diff > 30*oneSecond {
 			log.Printf("COLLECTOR: Purging event source %s of type %s \n", source.Beacon.Identity, source.Beacon.SourceType)
@@ -2247,6 +2295,14 @@ func (fc *FlowCollector) reconcileRecords() error {
 		}
 	}
 
+	// recentConnectors for flows after the fact
+	for _, connector := range fc.recentConnectors {
+		diff := uint64(t.UnixNano())/uint64(time.Microsecond) - connector.EndTime
+		if diff > 120*oneSecond {
+			delete(fc.recentConnectors, connector.Identity)
+		}
+	}
+
 	return nil
 }
 
diff --git a/pkg/flow/record.go b/pkg/flow/record.go
index 0066497a..6ac887b2 100644
--- a/pkg/flow/record.go
+++ b/pkg/flow/record.go
@@ -283,7 +283,7 @@ type ConnectorRecord struct {
 	FlowRateL4  *uint64 `json:"flowRateL4,omitempty"`
 	FlowCountL7 *uint64 `json:"flowCountL7,omitempty"`
 	FlowRateL7  *uint64 `json:"flowRateL7,omitempty"`
-	process     *string
+	ProcessId   *string `json:"processId,omitempty"`
 	AddressId   *string `json:"addressId,omitempty"`
 }
 
@@ -367,6 +367,7 @@ type FlowRecord struct {
 // Note a flowpair does not have a defined parent relationship through Base
 type FlowPairRecord struct {
 	Base
+	Duration                uint64      `json:"duration"`
 	Protocol                *string     `json:"protocol,omitempty"`
 	SourceSiteId            string      `json:"sourceSiteId,omitempty"`
 	SourceSiteName          *string     `json:"sourceSiteName,omitempty"`
@@ -661,7 +662,7 @@ func matchFieldValue(x interface{}, y string) bool {
 	if x != nil && y != "" {
 		switch x.(type) {
 		case string:
-			return x.(string) == y
+			return strings.HasPrefix(x.(string), y)
 		case uint64:
 			i, err := strconv.ParseInt(y, 10, 64)
 			if err == nil {
diff --git a/pkg/images/image_utils.go b/pkg/images/image_utils.go
index eadde1af..990f3aa9 100644
--- a/pkg/images/image_utils.go
+++ b/pkg/images/image_utils.go
@@ -17,14 +17,17 @@ const (
 	ConfigSyncImageEnvKey             string = "SKUPPER_CONFIG_SYNC_IMAGE"
 	FlowCollectorImageEnvKey          string = "SKUPPER_FLOW_COLLECTOR_IMAGE"
 	PrometheusServerImageEnvKey       string = "PROMETHEUS_SERVER_IMAGE"
+	OauthProxyImageEnvKey             string = "OAUTH_PROXY_IMAGE"
 	RouterPullPolicyEnvKey            string = "QDROUTERD_IMAGE_PULL_POLICY"
 	ServiceControllerPullPolicyEnvKey string = "SKUPPER_SERVICE_CONTROLLER_IMAGE_PULL_POLICY"
 	ControllerPodmanPullPolicyEnvKey  string = "SKUPPER_CONTROLLER_PODMAN_IMAGE_PULL_POLICY"
 	ConfigSyncPullPolicyEnvKey        string = "SKUPPER_CONFIG_SYNC_IMAGE_PULL_POLICY"
 	FlowCollectorPullPolicyEnvKey     string = "SKUPPER_FLOW_COLLECTOR_IMAGE_PULL_POLICY"
 	PrometheusServerPullPolicyEnvKey  string = "PROMETHEUS_SERVER_IMAGE_PULL_POLICY"
+	OauthProxyPullPolicyEnvKey        string = "OAUTH_PROXY_IMAGE_PULL_POLICY"
 	SkupperImageRegistryEnvKey        string = "SKUPPER_IMAGE_REGISTRY"
 	PrometheusImageRegistryEnvKey     string = "PROMETHEUS_IMAGE_REGISTRY"
+	OauthProxyRegistryEnvKey          string = "OAUTH_PROXY_IMAGE_REGISTRY"
 )
 
 func getPullPolicy(key string) string {
@@ -220,3 +223,32 @@ func GetSha(imageName string) string {
 
 	return digest
 }
+
+func GetOauthProxyImageName() string {
+	image := os.Getenv(OauthProxyImageEnvKey)
+	if image == "" {
+		imageRegistry := GetOauthProxyImageRegistry()
+		return strings.Join([]string{imageRegistry, OauthProxyImageName}, "/")
+	} else {
+		return image
+	}
+}
+
+func GetOauthProxyImagePullPolicy() string {
+	return getPullPolicy(OauthProxyPullPolicyEnvKey)
+}
+
+func GetOauthProxyImageDetails() types.ImageDetails {
+	return types.ImageDetails{
+		Name:       GetOauthProxyImageName(),
+		PullPolicy: GetOauthProxyImagePullPolicy(),
+	}
+}
+
+func GetOauthProxyImageRegistry() string {
+	imageRegistry := os.Getenv(OauthProxyRegistryEnvKey)
+	if imageRegistry == "" {
+		return OauthProxyImageRegistry
+	}
+	return imageRegistry
+}
diff --git a/pkg/images/images.go b/pkg/images/images.go
index 1c2c8555..f6a13424 100644
--- a/pkg/images/images.go
+++ b/pkg/images/images.go
@@ -10,4 +10,6 @@ const (
 	SiteControllerImageName    string = "site-controller:main"
 	PrometheusImageRegistry    string = "quay.io/prometheus"
 	PrometheusServerImageName  string = "prometheus:v2.42.0"
+	OauthProxyImageRegistry    string = "quay.io/openshift"
+	OauthProxyImageName        string = "origin-oauth-proxy:4.14.0"
 )
diff --git a/pkg/kube/deployments.go b/pkg/kube/deployments.go
index 360100da..c0380c69 100644
--- a/pkg/kube/deployments.go
+++ b/pkg/kube/deployments.go
@@ -3,6 +3,7 @@ package kube
 import (
 	"context"
 	"fmt"
+	"os"
 	"time"
 
 	"github.com/skupperproject/skupper/pkg/qdr"
@@ -129,7 +130,10 @@ func NewProxyStatefulSet(image types.ImageDetails, serviceInterface types.Servic
 
 	replicas := int32(serviceInterface.Headless.Size)
 	labels := map[string]string{
-		"internal.skupper.io/type": "proxy",
+		"internal.skupper.io/type":  "proxy",
+		"app.kubernetes.io/name":    "skupper-router",
+		"app.kubernetes.io/part-of": "skupper",
+		"skupper.io/component":      "router",
 	}
 	if len(transportDep.ObjectMeta.Labels) > 0 {
 		for k, v := range transportDep.ObjectMeta.Labels {
@@ -168,6 +172,9 @@ func NewProxyStatefulSet(image types.ImageDetails, serviceInterface types.Servic
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: map[string]string{
 						"internal.skupper.io/service": serviceInterface.Address,
+						"app.kubernetes.io/name":      "skupper-router",
+						"app.kubernetes.io/part-of":   "skupper",
+						"skupper.io/component":        "router",
 					},
 				},
 				Spec: corev1.PodSpec{
@@ -187,7 +194,11 @@ func NewProxyStatefulSet(image types.ImageDetails, serviceInterface types.Servic
 									Value: "json",
 								},
 								{
-									Name: "NAMESPACE",
+									Name:  "SKUPPER_SITE_ID",
+									Value: os.Getenv("SKUPPER_SITE_ID"),
+								},
+								{
+									Name: "POD_NAMESPACE",
 									ValueFrom: &corev1.EnvVarSource{
 										FieldRef: &corev1.ObjectFieldSelector{
 											FieldPath: "metadata.namespace",
@@ -477,6 +488,7 @@ func NewTransportDeployment(van *types.RouterSpec, ownerRef *metav1.OwnerReferen
 						Containers: []corev1.Container{
 							ContainerForTransport(van.Transport),
 						},
+						HostAliases: van.Transport.HostAliases,
 					},
 				},
 			},
diff --git a/pkg/kube/misc.go b/pkg/kube/misc.go
index c08b7bc5..72111f92 100644
--- a/pkg/kube/misc.go
+++ b/pkg/kube/misc.go
@@ -17,12 +17,13 @@ package kube
 import (
 	"context"
 	"fmt"
-	"github.com/openshift/client-go/apps/clientset/versioned"
-	"k8s.io/client-go/kubernetes"
 	"reflect"
 	"strconv"
 	"strings"
 
+	"github.com/openshift/client-go/apps/clientset/versioned"
+	"k8s.io/client-go/kubernetes"
+
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/pkg/utils"
 	corev1 "k8s.io/api/core/v1"
@@ -69,14 +70,13 @@ func GetServiceInterfaceTarget(targetType string, targetName string, deducePort
 	} else if targetType == "pods" {
 		return nil, fmt.Errorf("VAN service interfaces for pods not yet implemented")
 	} else if targetType == "service" {
-		var svcName, svcNamespace string = GetServiceName(targetName, namespace)
-		var formattedName = fmt.Sprintf("%s.%s", svcName, svcNamespace)
 		target := types.ServiceInterfaceTarget{
-			Name:      formattedName,
-			Service:   formattedName,
-			Namespace: namespace,
+			Name:    targetName,
+			Service: targetName,
+			// decided to keep namespace empty when target type is a service
+			Namespace: "",
 		}
-		if deducePort {
+		if deducePort && !strings.Contains(targetName, ".") {
 			ports, err := GetPortsForServiceTarget(targetName, namespace, cli)
 			if err != nil {
 				return nil, err
diff --git a/pkg/qdr/amqp_mgmt.go b/pkg/qdr/amqp_mgmt.go
index ad6bbdc5..71cb8ab4 100644
--- a/pkg/qdr/amqp_mgmt.go
+++ b/pkg/qdr/amqp_mgmt.go
@@ -2,7 +2,6 @@ package qdr
 
 import (
 	"context"
-	"crypto/tls"
 	"encoding/json"
 	"fmt"
 	"log"
@@ -190,11 +189,11 @@ func (node *RouterNode) AsRouter() *Router {
 
 type AgentPool struct {
 	url    string
-	config *tls.Config
+	config TlsConfigRetriever
 	pool   chan *Agent
 }
 
-func NewAgentPool(url string, config *tls.Config) *AgentPool {
+func NewAgentPool(url string, config TlsConfigRetriever) *AgentPool {
 	return &AgentPool{
 		url:    url,
 		config: config,
@@ -223,7 +222,7 @@ func (p *AgentPool) Put(a *Agent) {
 	}
 }
 
-func Connect(url string, config *tls.Config) (*Agent, error) {
+func Connect(url string, config TlsConfigRetriever) (*Agent, error) {
 	factory := ConnectionFactory{
 		url:    url,
 		config: config,
diff --git a/pkg/qdr/messaging.go b/pkg/qdr/messaging.go
index 137469da..df52e69a 100644
--- a/pkg/qdr/messaging.go
+++ b/pkg/qdr/messaging.go
@@ -9,16 +9,24 @@ import (
 	"github.com/skupperproject/skupper/pkg/messaging"
 )
 
+type TlsConfigRetriever interface {
+	GetTlsConfig() (*tls.Config, error)
+}
+
 type ConnectionFactory struct {
 	url    string
-	config *tls.Config
+	config TlsConfigRetriever
 }
 
 func (f *ConnectionFactory) Connect() (messaging.Connection, error) {
 	if f.config == nil {
 		return dial(f.url, amqp.ConnMaxFrameSize(4294967295))
 	} else {
-		return dial(f.url, amqp.ConnSASLExternal(), amqp.ConnMaxFrameSize(4294967295), amqp.ConnTLSConfig(f.config))
+		tlsConfig, err := f.config.GetTlsConfig()
+		if err != nil {
+			return nil, err
+		}
+		return dial(f.url, amqp.ConnSASLExternal(), amqp.ConnMaxFrameSize(4294967295), amqp.ConnTLSConfig(tlsConfig))
 	}
 }
 
@@ -39,7 +47,7 @@ func (f *ConnectionFactory) Url() string {
 	return f.url
 }
 
-func NewConnectionFactory(url string, config *tls.Config) *ConnectionFactory {
+func NewConnectionFactory(url string, config TlsConfigRetriever) *ConnectionFactory {
 	return &ConnectionFactory{
 		url:    url,
 		config: config,


Test patch:
diff --git a/client/serviceinterface_update_test.go b/client/serviceinterface_update_test.go
index 7b9edbd5..4672b066 100644
--- a/client/serviceinterface_update_test.go
+++ b/client/serviceinterface_update_test.go
@@ -130,7 +130,7 @@ var httpDeployment *appsv1.Deployment = &appsv1.Deployment{
 				Containers: []corev1.Container{
 					{
 						Name:            "nginx",
-						Image:           "nginxinc/nginx-unprivileged",
+						Image:           "quay.io/skupper/nginx-unprivileged",
 						ImagePullPolicy: corev1.PullIfNotPresent,
 						Ports: []corev1.ContainerPort{
 							{
diff --git a/cmd/skupper/skupper_cluster_test.go b/cmd/skupper/skupper_cluster_test.go
index 8408a3eb..0f50b8e5 100644
--- a/cmd/skupper/skupper_cluster_test.go
+++ b/cmd/skupper/skupper_cluster_test.go
@@ -1606,6 +1606,9 @@ func TestDebugDumpWithCluster(t *testing.T) {
 }
 
 func TestNetworkStatusWithCluster(t *testing.T) {
+	if os.Getenv("USER") == "circleci" {
+		t.Skipf("Test is temporarily disabled")
+	}
 	testcases := []testCase{
 		{
 			doc:             "network-status-test1",
diff --git a/pkg/domain/podman/main_test.go b/pkg/domain/podman/main_test.go
index 9128927c..78abfd00 100644
--- a/pkg/domain/podman/main_test.go
+++ b/pkg/domain/podman/main_test.go
@@ -32,7 +32,7 @@ var (
 const (
 	ENV_PODMAN_ENDPOINT = "SKUPPER_TEST_PODMAN_ENDPOINT"
 	NS                  = "podman-system-test"
-	NGINX_IMAGE         = "docker.io/nginxinc/nginx-unprivileged:stable-alpine"
+	NGINX_IMAGE         = "quay.io/skupper/nginx-unprivileged:stable-alpine"
 )
 
 // newBasicSite returns a new instance of a basic Site instance
diff --git a/pkg/flow/flow_mem_driver_test.go b/pkg/flow/flow_mem_driver_test.go
index 22d6e6b3..1a1381e1 100644
--- a/pkg/flow/flow_mem_driver_test.go
+++ b/pkg/flow/flow_mem_driver_test.go
@@ -180,9 +180,9 @@ func TestRecordGraph(t *testing.T) {
 				Parent:    "router:0",
 				StartTime: uint64(time.Now().UnixNano()) / uint64(time.Microsecond),
 			},
-			Address:  &address3,
-			Protocol: &protocol,
-			process:  &connectorProcess1,
+			Address:   &address3,
+			Protocol:  &protocol,
+			ProcessId: &connectorProcess1,
 		},
 		{
 			Base: Base{
@@ -200,9 +200,9 @@ func TestRecordGraph(t *testing.T) {
 				Parent:    "router:1",
 				StartTime: uint64(time.Now().UnixNano()) / uint64(time.Microsecond),
 			},
-			Protocol: &protocol,
-			Address:  &address4,
-			process:  &connectorProcess2,
+			Protocol:  &protocol,
+			Address:   &address4,
+			ProcessId: &connectorProcess2,
 		},
 	}
 	flows := []FlowRecord{
diff --git a/pkg/flow/record_test.go b/pkg/flow/record_test.go
index 8f9a4107..9564f970 100644
--- a/pkg/flow/record_test.go
+++ b/pkg/flow/record_test.go
@@ -676,6 +676,18 @@ func TestFilterFieldsRecord(t *testing.T) {
 			filterField: map[string]string{"sourceSiteName": "public1"},
 			result:      true,
 		},
+		{
+			filterField: map[string]string{"sourceSiteName": "pub"},
+			result:      true,
+		},
+		{
+			filterField: map[string]string{"sourceSiteName": "ic1"},
+			result:      false,
+		},
+		{
+			filterField: map[string]string{"sourceSiteName": "xpublic1"},
+			result:      false,
+		},
 		{
 			filterField: map[string]string{"forwardFlow.SourceHost": "10.20.30.40", "sourceSiteName": "public1"},
 			result:      true,
diff --git a/pkg/qdr/amqp_mgmt_mock_test.go b/pkg/qdr/amqp_mgmt_mock_test.go
index 0081766d..bc186660 100644
--- a/pkg/qdr/amqp_mgmt_mock_test.go
+++ b/pkg/qdr/amqp_mgmt_mock_test.go
@@ -5,7 +5,6 @@ import (
 	"reflect"
 	"testing"
 
-	"crypto/tls"
 	"encoding/json"
 
 	"gotest.tools/assert"
@@ -103,7 +102,7 @@ func TestQDR(t *testing.T) {
 	assert.Assert(t, router2.Id == router_node.Id && router2.Edge == false, "RouterNode.AsRouter failure: Id |%s|  Edge: %t.", router2.Id, router2.Edge)
 
 	// NewAgentPool ------------------------------------
-	agentPool := NewAgentPool("my_url", &tls.Config{})
+	agentPool := NewAgentPool("my_url", nil)
 	assert.Assert(t, agentPool.url == "my_url", "NewAgentPool failure")
 
 	// Get ------------------------------------
diff --git a/test/images/Dockerfile.nghttp2 b/test/images/Dockerfile.nghttp2
new file mode 100644
index 00000000..6b69a2ff
--- /dev/null
+++ b/test/images/Dockerfile.nghttp2
@@ -0,0 +1,9 @@
+FROM alpine:latest
+
+LABEL description="A supporting test image containing nghttp2 (client, server and load tester), for Skupper testing.  By default, this image does not start any nghttp2 project; the user needs to overwrite CMD with the command they want"
+LABEL maintainer="Skupper project"
+LABEL origin=https://github.com/skupperproject/skupper/blob/main/test/images
+
+RUN apk add --no-cache nghttp2
+
+CMD ["ls"]
diff --git a/test/images/Makefile b/test/images/Makefile
new file mode 100644
index 00000000..acc5dfbb
--- /dev/null
+++ b/test/images/Makefile
@@ -0,0 +1,180 @@
+# This make file provides three operations:
+#
+# - Copy a list of images (hardcoded below) to a main repository (by default,
+#   quay.io/skupper)
+# - Generate and push images to that same repository, based on Dockerfiles
+# - Copy both lists of images above (external and generated) to another,
+#   configurable repository
+#
+# See the README.md for the rationale of each of them.
+#
+# The basic operation is to copy or generate-and-push a single image into
+# the MAIN_REPO:
+#
+#   make docker.io/svagi/nghttp2
+#   make nghttp2
+#
+# The possible values are those listed on EXTERNAL_IMAGES and GENERATED_IMAGES.
+#
+# You can also only build an image (and not push it).  Just add _build to the
+# image name listed in GENERATED_IMAGES:
+#
+#   make nghttp2_build
+#
+# Alternativelly, one can build/push or copy all generated, all external, or
+# all of them:
+#
+#   make external_images
+#   make generated_images
+#   make everything
+#
+# Notice that the Makefile itself will not check whether the copy or build/push
+# is necessary; the underlying tools (podman and skopeo) may do that, but they
+# may still consume Dockerhub pull requests in the process.
+#
+# As with any Makefile, you can configure the execution by setting variables
+# on the make invokation:
+#
+#   make MAIN_REPO=quay.io/dhashimo docker.io/ssorj/quiver
+#
+# The command above will copy docker.io/ssorj/quiver into the quay repository
+# quay.io/dhashimo, instead of the default MAIN_REPO
+#
+#   make MAIN_REPO=quay.io/dhashimo everything
+#
+# Similarly, the command above will copy and build/generate everything, but
+# into the specified MAIN_REPO.
+#
+# The last operation is to copy images from MAIN_REPO to another repository,
+# which needs to be specified.
+#
+#   make COPY_REPO=192.168.0.1:5000/internal copy
+#
+# The command above will copy all images (external and generated, as listed
+# below) from the MAIN_REPO into the 'internal' repository within the
+# registry running at 192.168.0.1:5000.
+#
+# Individual items can also be specified.  For that, just add '_copy' to
+# their main target names:
+#
+#   make COPY_REPO=192.168.0.1:5000/internal nghttp2_copy
+#   make COPY_REPO=192.168.0.1:5000/internal docker.io/ssorj/quiver_copy
+#
+# This Makefile is intended to be executed manually, with the login of
+# individual users, and not by a robot account on a CI.
+#
+# ATTENTION: Remember that images copied for the first time into quay may
+#            be created as 'private' images, and need to be changed into
+#            'public' before they can be used by CI and other users.
+
+# --- configuration ---
+
+# tools
+SHELL = /bin/sh
+PODMAN = podman
+SKOPEO = skopeo
+
+# tool options
+
+# The options below create or transform the images into the docker format.
+# That is required for their use with Openshift 3.11
+FORMAT_OPTIONS = --format docker
+TRANSFORM_OPTIONS = --format v2s1
+
+# Repositories
+MAIN_REPO = quay.io/skupper
+COPY_REPO = localhost:5000/local
+
+# This is the list of external images that will be copied to $(MAIN_REPO).  If
+# the image specifies a tag, make sure to escape the colon with a backslash
+# (such as in docker.io/library/mongo\:5.0).  Otherwise, you'll get an error
+# like `multiple target patterns.  Stop.`
+EXTERNAL_IMAGES := \
+	docker.io/nginxinc/nginx-unprivileged \
+	docker.io/ssorj/quiver \
+	docker.io/library/postgres \
+	docker.io/library/redis \
+	docker.io/maistra/examples-bookinfo-productpage-v1\:0.12.0 \
+	docker.io/maistra/examples-bookinfo-ratings-v1\:0.12.0 \
+	docker.io/maistra/examples-bookinfo-details-v1\:0.12.0 \
+	docker.io/maistra/examples-bookinfo-reviews-v3\:0.12.0 \
+	docker.io/library/mongo\:5.0 \
+	docker.io/nginxinc/nginx-unprivileged\:stable-alpine
+
+# These are the images that are generated by this Makefile (as opposed to
+# created elsewhere and just copied here).  To add a new image, simply create
+# Dockerfile.IMAGENAME and add the IMAGENAME below.
+GENERATED_IMAGES := \
+	nghttp2
+
+# --- end of configuration ---
+
+
+# These are the build target names, such as nghttp2_build, generated
+# automatically from GENERATED_IMAGES.  Do not overwrite this
+generated_build := $(patsubst %,%_build,$(GENERATED_IMAGES))
+
+# The copy target names, to be used with the copy operation
+generated_copy := $(patsubst %,%_copy,$(GENERATED_IMAGES))
+external_copy   := $(patsubst %,%_copy,$(EXTERNAL_IMAGES))
+
+# We don't want someone to just run `make` and start copying stuff
+# around, so instead the default operation is to just give some
+# hint on how to use it.
+all:
+	@echo "Use 'make everything' for building and copying everything to the MAIN_REPO".
+	@echo
+	@echo "MAIN_REPO=$(MAIN_REPO)"
+	@echo
+	@echo Normal use, however, is to select individual targets.
+	@echo Check Makefile contents for documentation.
+
+# For a generated image, we simply have a Dockerfile named after it and call
+# `podman build` on it, tagging it on the MAIN_REPO
+$(generated_build): TARGET = $(patsubst %_build,%,$@)
+$(generated_build):
+	$(PODMAN) build $(FORMAT_OPTIONS) --file Dockerfile.$(TARGET) --tag $(MAIN_REPO)/$(TARGET)
+
+# This is generic the push target, for images built here (GENERATED_IMAGES)
+# Each image depends on its respective _build target
+%: %_build
+	$(PODMAN) push $(MAIN_REPO)/$@
+
+# Shortcut targets
+external_images: $(EXTERNAL_IMAGES)
+generated_images: $(GENERATED_IMAGES)
+everything: external_images generated_images
+
+# This is the main target for the external images; it copies them from their
+# original locations _into_ MAIN_REPO.  Do not confuse this for the `copy` operation
+# below, that copies _from_ MAIN_REPO into COPY_REPO.
+#
+# The first line prepares a target-local TARGET variable, which contains only
+# the last part of the URL
+$(EXTERNAL_IMAGES): TARGET = $(shell echo "$@" | sed s_.*/__ )
+$(EXTERNAL_IMAGES):
+	$(SKOPEO) copy $(TRANSFORM_OPTIONS) \
+		docker://$@ \
+		docker://$(MAIN_REPO)/$(TARGET)
+
+#
+# Copy operation
+#
+copy: $(generated_copy) $(external_copy)
+
+# The targets for external and generated images are basically the same; the
+# only difference is the way the original/main target needs to be manipulated
+# to generate the image name+tag (saved in the target-local variable TARGET).
+
+$(generated_copy): TARGET = $(patsubst %_copy,%,$@)
+$(generated_copy):
+	$(SKOPEO) copy $(TRANSFORM_OPTIONS) \
+		docker://$(MAIN_REPO)/$(TARGET) \
+		docker://$(COPY_REPO)/$(TARGET)
+
+# Remove _copy, but also everything till the last /
+$(external_copy): TARGET = $(shell echo "$@" | sed -e "s/_copy$$//" -e 's_.*/__' )
+$(external_copy):
+	$(SKOPEO) copy $(TRANSFORM_OPTIONS) \
+		docker://$(MAIN_REPO)/$(TARGET) \
+		docker://$(COPY_REPO)/$(TARGET)
diff --git a/test/images/README.md b/test/images/README.md
new file mode 100644
index 00000000..7e52f780
--- /dev/null
+++ b/test/images/README.md
@@ -0,0 +1,37 @@
+# Images for external dependencies
+
+The Makefile on this directory allows for the manipulation of container images
+pertaining to external test dependencies, such as Mongo or quiver.
+
+It contains three operations: the first exists to avoid the Dockerhub pull
+limit, and also to allow for testing on disconnected clusters.  It simply
+copies the images from their original locations into our Quay repo.
+
+The second operation was created in response to an image used by our tests that
+stopped getting updates, while the actual project continued to evolve.  It
+builds images from Dockerfile and pushes them into our Quay repo.
+
+Finally, the third one also allows for the running of tests on disconnected
+clusters, which cannot access the original locations of these dependencies to
+download them (as they have no external Internet access).  The operation allows
+for copying the list of images from our Quay repo into another repo.
+
+This allows for the test dependencies to be copied to the disconnected
+cluster's local repository, from where the cluster can access them.  Additional
+configuration will be required on the cluster (such as creating an MCO to
+re-route the image pull requests), but that's out of the scope of this
+document.
+
+Note that this is not a full repo copy; it's restricted to the list of images
+contained on the Makefile.  Skupper images (such as the router or controller)
+are not copied by this operation.
+
+See the Makefile contents for information on how to execute the different
+operations.
+
+
+# skupper-test image
+
+Note that the `skupper-test` image used by Skupper integration is part of
+Skupper's own test code, so it's build by the main Makefile at ../.., and not
+here.
diff --git a/test/integration/acceptance/annotation/annotated_resource.go b/test/integration/acceptance/annotation/annotated_resource.go
index 94fb7002..d01dd163 100644
--- a/test/integration/acceptance/annotation/annotated_resource.go
+++ b/test/integration/acceptance/annotation/annotated_resource.go
@@ -250,7 +250,7 @@ func createDeployment(cluster *client.VanClient, annotations map[string]string)
 				},
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
-						{Name: "nginx", Image: "nginxinc/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
+						{Name: "nginx", Image: "quay.io/skupper/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
 					},
 					RestartPolicy: corev1.RestartPolicyAlways,
 				},
@@ -325,7 +325,7 @@ func createStatefulSet(cluster *client.VanClient, annotations map[string]string)
 				},
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
-						{Name: "nginx", Image: "nginxinc/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
+						{Name: "nginx", Image: "quay.io/skupper/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
 					},
 					RestartPolicy: corev1.RestartPolicyAlways,
 				},
@@ -373,7 +373,7 @@ func createDaemonSet(cluster *client.VanClient, annotations map[string]string) (
 				},
 				Spec: corev1.PodSpec{
 					Containers: []corev1.Container{
-						{Name: "nginx", Image: "nginxinc/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
+						{Name: "nginx", Image: "quay.io/skupper/nginx-unprivileged:stable-alpine", Ports: []corev1.ContainerPort{{Name: "web", ContainerPort: 8080}}, ImagePullPolicy: corev1.PullIfNotPresent},
 					},
 					RestartPolicy: corev1.RestartPolicyAlways,
 				},
diff --git a/test/integration/acceptance/custom/headless/headless.go b/test/integration/acceptance/custom/headless/headless.go
index 1191b784..ccfaf745 100644
--- a/test/integration/acceptance/custom/headless/headless.go
+++ b/test/integration/acceptance/custom/headless/headless.go
@@ -26,7 +26,7 @@ const (
 	appPort     = "8080"
 	appPortInt  = 8080
 	appPortName = "web"
-	appImage    = "nginxinc/nginx-unprivileged:stable-alpine"
+	appImage    = "quay.io/skupper/nginx-unprivileged:stable-alpine"
 	pod0Name    = appName + "-0"
 	pod1Name    = appName + "-1"
 )
diff --git a/test/integration/examples/custom/helloworld/helloworld_podman_test.go b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
index 5e20620b..4c60325e 100644
--- a/test/integration/examples/custom/helloworld/helloworld_podman_test.go
+++ b/test/integration/examples/custom/helloworld/helloworld_podman_test.go
@@ -3,10 +3,14 @@
 package helloworld
 
 import (
+	"bytes"
 	"fmt"
 	"log"
 	"os"
+	"os/exec"
+	"strings"
 	"testing"
+	"time"
 
 	"github.com/skupperproject/skupper/api/types"
 	"github.com/skupperproject/skupper/client/container"
@@ -34,6 +38,9 @@ const (
 // It uses the kubernetes cluster as a public cluster and a podman site
 // as the private one.
 func TestHelloWorldCLIOnPodman(t *testing.T) {
+	stopCh := make(chan interface{}, 1)
+	go restartOnLinkDisconnected(stopCh, t)
+	defer close(stopCh)
 
 	// First, validate if skupper binary is in the PATH, or skip test
 	log.Printf("Running 'skupper --help' to determine if skupper binary is available")
@@ -58,7 +65,8 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 	assert.Assert(t, err)
 
 	// Validating if podman site exists
-	prvSiteHandler, _ := podman.NewSitePodmanHandler("")
+	prvSiteHandler, err := podman.NewSitePodmanHandler("")
+	assert.Assert(t, err)
 	prvSite, _ := prvSiteHandler.Get()
 	assert.Assert(t, prvSite == nil, "podman site already exists")
 
@@ -157,7 +165,7 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 						RouterMode:          "interior",
 						EnableConsole:       true,
 						EnableFlowCollector: true,
-						RunAsUser:           "1000",
+						RunAsUser:           getRunAsUserOrDefault("1000", pub),
 						RunAsGroup:          "2000",
 					},
 					// skupper status - verify initialized as interior
@@ -172,7 +180,7 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 					// skupper init - interior mode
 					&cli.InitTester{
 						Ingress:             "none",
-						RouterLogging:       "trace",
+						RouterLogging:       "info",
 						RouterMode:          "interior",
 						ConsoleAuth:         "internal",
 						ConsoleUser:         "internal",
@@ -674,6 +682,35 @@ func TestHelloWorldCLIOnPodman(t *testing.T) {
 
 }
 
+// restartOnLinkDisconnected On CircleCI external network connectivity
+// is being lost this routine will restart the skupper-podman user service
+// to bypass this issue.
+// TODO: this can be removed once CI is fixed
+func restartOnLinkDisconnected(stopCh chan interface{}, t *testing.T) {
+	if os.Getenv("USER") != "circleci" {
+		return
+	}
+	// Preparing the command to run
+	tick := time.NewTicker(time.Second * 5)
+	defer tick.Stop()
+	for {
+		select {
+		case <-tick.C:
+			stdout := new(bytes.Buffer)
+			cmd := exec.Command("skupper", "--platform=podman", "link", "status")
+			cmd.Stdout = stdout
+			_ = cmd.Run()
+			if strings.Contains(stdout.String(), " not connected") {
+				t.Logf("[Link Disconnected] - Restarting skupper-podman (user) service")
+				restartCmd := exec.Command("systemctl", "--user", "restart", "skupper-podman")
+				_ = restartCmd.Run()
+			}
+		case <-stopCh:
+			return
+		}
+	}
+}
+
 // deployPodmanResources runs the hello-world-backend as a podman container
 func deployPodmanResources() error {
 	podmanCli, err := clientpodman.NewPodmanClient("", "")
diff --git a/test/integration/examples/custom/helloworld/helloworld_test.go b/test/integration/examples/custom/helloworld/helloworld_test.go
index 15e858a3..db98a8d0 100644
--- a/test/integration/examples/custom/helloworld/helloworld_test.go
+++ b/test/integration/examples/custom/helloworld/helloworld_test.go
@@ -7,8 +7,6 @@ import (
 	"context"
 	"log"
 	"os"
-	"strconv"
-	"strings"
 	"testing"
 
 	"github.com/skupperproject/skupper/api/types"
@@ -82,29 +80,6 @@ func TestHelloWorldCLI(t *testing.T) {
 	// deploying frontend and backend services
 	assert.Assert(t, deployResources(pub, prv))
 
-	var runAsUser = "1000"
-
-	// OpenShift requires container user IDs to exist within a range; we try to satisfy it here.
-	namespace, err := pub.VanClient.KubeClient.CoreV1().Namespaces().Get(context.Background(), pub.Namespace, metav1.GetOptions{})
-	if err != nil {
-		log.Printf("Unable to get namespace %q; using pre-defined runAsUser value %v", pub.Namespace, runAsUser)
-		// We do not fail here; we just try the test with the pre-defined value
-	} else {
-		// On this block, we just ignore any errors and use the pre-existing value
-		ns_annotations := namespace.GetAnnotations()
-		if users, ok := ns_annotations["openshift.io/sa.scc.uid-range"]; ok {
-			log.Printf("OpenShift UID range annotation found: %q", users)
-			// format is like 1000860000/10000, where the first number is the
-			// range start, and the second its length
-			split_users := strings.Split(users, "/")
-			if split_users[0] != "" {
-				if _, err := strconv.Atoi(split_users[0]); err == nil {
-					runAsUser = split_users[0]
-				}
-			}
-		}
-	}
-
 	// These test scenarios allow defining a set of skupper cli
 	// commands to be executed as a workflow, against specific
 	// clusters. Each execution is validated accordingly by its
@@ -129,7 +104,7 @@ func TestHelloWorldCLI(t *testing.T) {
 						RouterMode:          "interior",
 						EnableConsole:       true,
 						EnableFlowCollector: true,
-						RunAsUser:           runAsUser,
+						RunAsUser:           getRunAsUserOrDefault("1000", pub),
 						RunAsGroup:          "2000",
 					},
 					// skupper status - verify initialized as interior
diff --git a/test/integration/examples/custom/helloworld/main_test.go b/test/integration/examples/custom/helloworld/main_test.go
index ae7e9dc6..cea68ae7 100644
--- a/test/integration/examples/custom/helloworld/main_test.go
+++ b/test/integration/examples/custom/helloworld/main_test.go
@@ -1,10 +1,15 @@
 package helloworld
 
 import (
+	"context"
+	"log"
 	"os"
+	"strconv"
+	"strings"
 	"testing"
 
 	"github.com/skupperproject/skupper/test/utils/base"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 // TestMain initializes flag parsing
@@ -12,3 +17,37 @@ func TestMain(m *testing.M) {
 	base.ParseFlags()
 	os.Exit(m.Run())
 }
+
+// Receives a default value, that will be overridden when running on Openshift.
+// Returns the value to be used.
+//
+// OpenShift requires RunAsUser to be configured according to annotations
+// present on the namespace, lest it will fail with SCC errors.
+//
+// If any errors found while tring to determine the correct user, this function
+// will ignore them and simply use the default value.
+func getRunAsUserOrDefault(runAsUser string, cctx *base.ClusterContext) string {
+	// OpenShift requires container user IDs to exist within a range; we try to satisfy it here.
+	namespace, err := cctx.VanClient.KubeClient.CoreV1().Namespaces().Get(context.Background(), cctx.Namespace, metav1.GetOptions{})
+	if err != nil {
+		log.Printf("Unable to get namespace %q; using pre-defined runAsUser value %v", cctx.Namespace, runAsUser)
+	} else {
+		ns_annotations := namespace.GetAnnotations()
+		if users, ok := ns_annotations["openshift.io/sa.scc.uid-range"]; ok {
+			log.Printf("OpenShift UID range annotation found: %q", users)
+			// format is like 1000860000/10000, where the first number is the
+			// range start, and the second its length
+			split_users := strings.Split(users, "/")
+			if split_users[0] != "" {
+				if _, err := strconv.Atoi(split_users[0]); err == nil {
+					runAsUser = split_users[0]
+				} else {
+					log.Printf("Failed to parse openshift uid-range annotation: using default value %v", runAsUser)
+				}
+			} else {
+				log.Printf("openshift uid-range annotation is empty, which is unexpected: using default value %v", runAsUser)
+			}
+		} // if annotation not found, we're not on Openshift, and we can use the default value.
+	}
+	return runAsUser
+}
diff --git a/test/integration/examples/http/http.go b/test/integration/examples/http/http.go
index 1d5a1625..8fe4d259 100644
--- a/test/integration/examples/http/http.go
+++ b/test/integration/examples/http/http.go
@@ -6,8 +6,10 @@ import (
 	"regexp"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/skupperproject/skupper/api/types"
+	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/test/utils/base"
 	"github.com/skupperproject/skupper/test/utils/constants"
 	"github.com/skupperproject/skupper/test/utils/k8s"
@@ -16,6 +18,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
 	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -91,7 +94,7 @@ var nginxDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nginx1",
-						Image:           "nginxinc/nginx-unprivileged:stable-alpine",
+						Image:           "quay.io/skupper/nginx-unprivileged:stable-alpine",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -142,7 +145,7 @@ var nghttp2Dep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -204,7 +207,7 @@ var nghttp2TlsDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -266,7 +269,7 @@ var nghttp2TcpTlsDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tcptls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -328,7 +331,7 @@ var nghttp1TlsDep = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp1tls",
-						Image:           "nginxinc/nginx-unprivileged:stable-alpine",
+						Image:           "quay.io/skupper/nginx-unprivileged:stable-alpine",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -389,7 +392,7 @@ var nghttp2TlsDepWithCertFiles = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -461,7 +464,7 @@ var nghttp2tcpTlsDepWithCertFiles = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp2tcptls",
-						Image:           "docker.io/svagi/nghttp2",
+						Image:           "quay.io/skupper/nghttp2",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -572,7 +575,7 @@ var nghttp1TlsDepWithCertFiles = &appsv1.Deployment{
 				Containers: []apiv1.Container{
 					{
 						Name:            "nghttp1tls",
-						Image:           "nginxinc/nginx-unprivileged:stable-alpine",
+						Image:           "quay.io/skupper/nginx-unprivileged:stable-alpine",
 						ImagePullPolicy: apiv1.PullIfNotPresent,
 						Ports: []apiv1.ContainerPort{
 							{
@@ -608,7 +611,7 @@ var h2loadJob = &batchv1.Job{
 				Containers: []apiv1.Container{
 					{
 						Name:  "h2load",
-						Image: "docker.io/svagi/nghttp2",
+						Image: "quay.io/skupper/nghttp2",
 						// Command: []string{"h2load", "-n10", "-c10", "-m10", "http://nghttp2:8443"},
 						Command: []string{"h2load", "-n1000", "-c1", "-m1", "http://nghttp2:8443"},
 						Env: []apiv1.EnvVar{
@@ -650,6 +653,73 @@ var h1HeyBaseJob = &batchv1.Job{
 	},
 }
 
+// This is required to avoid test flakiness while #1208 is not fixed.  It will wait for the
+// secret to be created up to a maximum time.
+//
+// It returns nothing and ignores any errors that occur, leaving any inconsistencies to be
+// detected later on by other code
+func waitSecret(cctx *base.ClusterContext, secretName string) {
+	// The check below is required while https://github.com/skupperproject/skupper/issues/1208
+	// is not fixed; both the ServiceInterfaceCreate above and ServiceInterfaceBind below may
+	// create the secret, and sometimes they race on it, making the test flaky.
+	err := utils.Retry(
+		time.Second*3,
+		20,
+		func() (bool, error) {
+			_, err := cctx.VanClient.KubeClient.CoreV1().Secrets(cctx.Namespace).Get(
+				context.Background(),
+				secretName,
+				metav1.GetOptions{},
+			)
+			if err == nil {
+				return true, nil
+			}
+			if errors.IsNotFound(err) {
+				fmt.Printf("TLS Secret %q not yet created.  See #1208\n", secretName)
+				return false, nil
+			}
+			return false, err
+		},
+	)
+	if err != nil {
+		fmt.Printf("Secret retrieval failed: %v\n", err)
+		// We do not stop for this, as this test is just to avoid flakiness on the
+		// test; if whatever failed above is reason for an actual test failure,
+		// it will fail on the continuation of the actual test, below
+	}
+}
+
+// Updating a deployment may cause Skupper to trigger an asynchronous operation that updates
+// the configmap skupper-internal.  That may happen at the same time as another synchronous
+// operation down the road, causing it to fail (see #1153).
+//
+// This function executes the requested operation, but it first gets the ResourceVersion of
+// the configmap, and then waits until it changes, or a maximum wait time is reached.
+func updateDeployment(cctx *base.ClusterContext, ctx context.Context, deploy *appsv1.Deployment) error {
+	orig, err := cctx.VanClient.KubeClient.CoreV1().ConfigMaps(cctx.Namespace).Get(ctx, "skupper-internal", metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+	_, err = cctx.VanClient.KubeClient.AppsV1().Deployments(cctx.Namespace).Update(ctx, deploy, metav1.UpdateOptions{})
+	if err != nil {
+		return err
+	}
+	err = utils.Retry(
+		time.Millisecond*600,
+		100,
+		func() (bool, error) {
+			cm, err := cctx.VanClient.KubeClient.CoreV1().ConfigMaps(cctx.Namespace).Get(ctx, "skupper-internal", metav1.GetOptions{})
+			if err != nil {
+				fmt.Printf("Failed getting config map skupper-internal; ignoring (%s)\n", err)
+			}
+			changed := cm.ResourceVersion != orig.ResourceVersion
+			return changed, nil
+		},
+	)
+
+	return err
+}
+
 func runHeyTesWithParameter(t *testing.T, cluster *base.ClusterContext, numOfWorkers string, durationOfTests string, jobName string, targetURL string) {
 
 	waitJob := func(cc *base.ClusterContext, jobName string) {
@@ -866,6 +936,8 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 	// Create the deployment for HTTP1 with TLS enabled
 	createDeploymentInPrivateSite(nghttp1TlsDep)
 
+	fmt.Println("Creating and binding skupper service interfaces...")
+
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &service)
 	assert.Assert(t, err)
 
@@ -881,21 +953,25 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &http2TlsService)
 	assert.Assert(t, err)
 
+	waitSecret(prv1Cluster, http2TlsService.TlsCredentials)
+
 	err = prv1Cluster.VanClient.ServiceInterfaceBind(ctx, &http2TlsService, "deployment", "nghttp2tls", map[int]int{}, "")
 	assert.Assert(t, err)
 
 	//update tls service with cert files
-	_, err = prv1Cluster.VanClient.KubeClient.AppsV1().Deployments(prv1Cluster.Namespace).Update(ctx, nghttp2TlsDepWithCertFiles, metav1.UpdateOptions{})
+	err = updateDeployment(prv1Cluster, ctx, nghttp2TlsDepWithCertFiles)
 	assert.Assert(t, err)
 
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &http2TcpTlsService)
 	assert.Assert(t, err)
 
+	waitSecret(prv1Cluster, http2TcpTlsService.TlsCredentials)
+
 	err = prv1Cluster.VanClient.ServiceInterfaceBind(ctx, &http2TcpTlsService, "deployment", "nghttp2tcptls", map[int]int{}, "")
 	assert.Assert(t, err)
 
 	//update tls service with cert files
-	_, err = prv1Cluster.VanClient.KubeClient.AppsV1().Deployments(prv1Cluster.Namespace).Update(context.TODO(), nghttp2tcpTlsDepWithCertFiles, metav1.UpdateOptions{})
+	err = updateDeployment(prv1Cluster, ctx, nghttp2tcpTlsDepWithCertFiles)
 	assert.Assert(t, err)
 
 	_, err = prv1Cluster.VanClient.KubeClient.CoreV1().ConfigMaps(prv1Cluster.Namespace).Create(context.TODO(), nghttp1TlsConfigMap, metav1.CreateOptions{})
@@ -904,11 +980,13 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 	err = prv1Cluster.VanClient.ServiceInterfaceCreate(ctx, &http1TlsService)
 	assert.Assert(t, err)
 
+	waitSecret(prv1Cluster, http1TlsService.TlsCredentials)
+
 	err = prv1Cluster.VanClient.ServiceInterfaceBind(ctx, &http1TlsService, "deployment", "nghttp1tls", map[int]int{}, "")
 	assert.Assert(t, err)
 
 	//update tls service with cert files
-	_, err = prv1Cluster.VanClient.KubeClient.AppsV1().Deployments(prv1Cluster.Namespace).Update(context.TODO(), nghttp1TlsDepWithCertFiles, metav1.UpdateOptions{})
+	err = updateDeployment(prv1Cluster, ctx, nghttp1TlsDepWithCertFiles)
 	assert.Assert(t, err)
 
 	http21service := types.ServiceInterface{
diff --git a/test/integration/examples/tls_t/job/tls_job_test.go b/test/integration/examples/tls_t/job/tls_job_test.go
index 19d81d34..adf9b88f 100644
--- a/test/integration/examples/tls_t/job/tls_job_test.go
+++ b/test/integration/examples/tls_t/job/tls_job_test.go
@@ -1,4 +1,5 @@
-//+build job
+//go:build job
+// +build job
 
 package job
 
@@ -13,14 +14,14 @@ import (
 
 func TestTlsJob(t *testing.T) {
 
+	m := regexp.MustCompile("[ /]")
 	for _, test := range tls_t.Tests {
 		testName := fmt.Sprintf("server-%v-client-%v", test.Server.Options, strings.Join(test.Client.Options, "-"))
-		m, _ := regexp.Compile("[ /]")
 		testName = m.ReplaceAllLiteralString(testName, "-")
 
 		t.Run(testName, func(t *testing.T) {
 			addr := fmt.Sprintf("ssl-server:%v", test.Server.Port)
-			result := tls_t.SendReceive(addr, test.Client.Options, test.Seek)
+			result := tls_t.SendReceive(addr, test.Client.Options, test.Seek, testName)
 			t.Logf("Success expected: %t; result: %v", test.Success, result)
 			if (result == nil) != test.Success {
 				t.Errorf("failed: client options: %v, server options: %v, result: %v", test.Client.Options, test.Server.Options, result)
diff --git a/test/integration/examples/tls_t/tls_t.go b/test/integration/examples/tls_t/tls_t.go
index c636fda1..7ed5ef5e 100644
--- a/test/integration/examples/tls_t/tls_t.go
+++ b/test/integration/examples/tls_t/tls_t.go
@@ -20,10 +20,12 @@ import (
 	"log"
 	"os"
 	"os/exec"
+	"regexp"
 	"strings"
 	"testing"
 	"time"
 
+	"github.com/skupperproject/skupper/pkg/utils"
 	"github.com/skupperproject/skupper/test/utils/base"
 	"github.com/skupperproject/skupper/test/utils/constants"
 	"github.com/skupperproject/skupper/test/utils/k8s"
@@ -34,6 +36,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 
 	apiv1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -489,14 +492,41 @@ func setup(ctx context.Context, t *testing.T, r base.ClusterTestRunner) {
 		fmt.Printf(" * %s (%d replicas)\n", d.Name, *d.Spec.Replicas)
 	}
 
+	// The check below is required while https://github.com/skupperproject/skupper/issues/1208
+	// is not fixed; both the ServiceInterfaceCreate above and ServiceInterfaceBind below may
+	// create the secret, and sometimes they race on it, making the test flaky.
+	err = utils.Retry(
+		time.Second*3,
+		20,
+		func() (bool, error) {
+			_, err := pub1Cluster.VanClient.KubeClient.CoreV1().Secrets(pub1Cluster.Namespace).Get(
+				context.Background(),
+				"skupper-tls-ssl-server",
+				metav1.GetOptions{},
+			)
+			if err == nil {
+				return true, nil
+			}
+			if errors.IsNotFound(err) {
+				fmt.Println("TLS Secret not yet created.  See #1208")
+				return false, nil
+			}
+			return false, err
+		},
+	)
+	if err != nil {
+		fmt.Printf("Secret retrieval failed: %v\n", err)
+		// We do not stop for this, as this test is just to avoid flakiness on the
+		// test; if whatever failed above is reason for an actual test failure,
+		// it will fail on the continuation of the actual test, below
+	}
+
 	err = pub1Cluster.VanClient.ServiceInterfaceBind(ctx, &service, "deployment", "ssl-server", map[int]int{}, "")
 	assert.Assert(t, err)
 }
 
 func runTests(t *testing.T, r base.ClusterTestRunner) {
 
-	endTime := time.Now().Add(constants.ImagePullingAndResourceCreationTimeout)
-
 	pub1Cluster, err := r.GetPublicContext(1)
 	assert.Assert(t, err)
 
@@ -522,24 +552,26 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 	_, err = k8s.CreateTestJobWithSecret(prv1Cluster.Namespace, prv1Cluster.VanClient.KubeClient, jobName, jobCmd, "skupper-tls-ssl-server")
 	assert.Assert(t, err)
 
-	endTime = time.Now().Add(constants.ImagePullingAndResourceCreationTimeout)
+	endTime := time.Now().Add(constants.ImagePullingAndResourceCreationTimeout)
+
+	slashN := regexp.MustCompile("\n")
 
 	job, err := k8s.WaitForJob(pub1Cluster.Namespace, pub1Cluster.VanClient.KubeClient, jobName, endTime.Sub(time.Now()))
-	if err != nil || job.Status.Succeeded != 1 {
+	if err != nil || job.Status.Succeeded != 1 || testing.Verbose() {
 		logs, _ := k8s.GetJobsLogs(pub1Cluster.Namespace, pub1Cluster.VanClient.KubeClient, jobName, true)
-		log.Printf("%s job output: %s", jobName, logs)
+		log.Printf("%s pub job output: \n%s", jobName, slashN.ReplaceAllString(logs, "\n\t"))
+		pub1Cluster.KubectlExec("logs --prefix=true job/" + jobName)
 	}
 	assert.Assert(t, err)
-	pub1Cluster.KubectlExec("logs job/" + jobName)
 	k8s.AssertJob(t, job)
 
 	job, err = k8s.WaitForJob(prv1Cluster.Namespace, prv1Cluster.VanClient.KubeClient, jobName, endTime.Sub(time.Now()))
-	if err != nil || job.Status.Succeeded != 1 {
+	if err != nil || job.Status.Succeeded != 1 || testing.Verbose() {
 		logs, _ := k8s.GetJobsLogs(prv1Cluster.Namespace, prv1Cluster.VanClient.KubeClient, jobName, true)
-		log.Printf("%s job output: %s", jobName, logs)
+		log.Printf("%s prv job output: \n%s", jobName, slashN.ReplaceAllString(logs, "\n\t"))
+		prv1Cluster.KubectlExec("logs --prefix=true job/" + jobName)
 	}
 	assert.Assert(t, err)
-	prv1Cluster.KubectlExec("logs job/" + jobName)
 	k8s.AssertJob(t, job)
 
 }
@@ -552,8 +584,8 @@ func runTests(t *testing.T, r base.ClusterTestRunner) {
 // complete.
 // 'seek', if given, is a string to be searched for on the initial flush.  If
 // given and not found, return an error.
-func flushStdOut(outputCh <-chan string, seek string) error {
-	log.Printf("Flushing stdout")
+func flushStdOut(outputCh <-chan string, seek string, name string) error {
+	log.Printf("%v: Flushing stdout", name)
 
 	var found bool
 
@@ -562,27 +594,36 @@ func flushStdOut(outputCh <-chan string, seek string) error {
 	}
 
 	var count int
+	var timeoutCh <-chan time.Time
+	// Wait up to 5 seconds for the first line...
+	firstReadTimeout := time.After(time.Second * 5)
 
 outer:
 	for {
 		count++
-		timeoutCh := time.After(time.Second)
 		var line string
 		var ok bool
 
 		select {
+		case <-firstReadTimeout:
+			return fmt.Errorf("no command output before initial timeout")
 		case line, ok = <-outputCh:
+			// If any lines read, ignore initial timeout; we'll do a per-line max 1 second below
+			firstReadTimeout = nil
 		case <-timeoutCh:
-			log.Printf("Flush complete")
+			// We read at least one line, and the inter-line timeout expired.  Let's call it done
+			log.Printf("%v: Flush complete", name)
 			break outer
 		}
 		if !ok {
 			return fmt.Errorf("command output finished during initial stdout flush")
 		}
-		log.Printf("  %v", line)
+		log.Printf("%v:  %v", name, line)
 		if strings.Contains(line, seek) {
 			found = true
 		}
+		// If we got to this point, wait up to one second for the next line
+		timeoutCh = time.After(time.Second)
 	}
 
 	if !found {
@@ -600,9 +641,9 @@ outer:
 // s_client can be given in `options`.  If provided, the initial
 // output will be searched for the `seek` string.  If that is not
 // found, the function fails.
-func SendReceive(addr string, options []string, seek string) error {
+func SendReceive(addr string, options []string, seek string, name string) error {
 	defer func() {
-		log.Println("SendReceive completed")
+		log.Printf("%v: SendReceive completed", name)
 	}()
 	cmdArgs := []string{
 		"s_client",
@@ -631,8 +672,8 @@ func SendReceive(addr string, options []string, seek string) error {
 
 		strEcho := "Halo\n"
 
-		log.Println("Starting openssl s_client...")
-		log.Printf("Executing command openssl %v", cmdArgs)
+		log.Printf("%v: Starting openssl s_client...", name)
+		log.Printf("%v: Executing command openssl %v", name, cmdArgs)
 		cmd := exec.Command("openssl", cmdArgs...)
 		// We're not processing stderr, so just send it to the main stderr;
 		// we'll get it on the logs, then.
@@ -656,14 +697,14 @@ func SendReceive(addr string, options []string, seek string) error {
 			doneCh <- fmt.Errorf("error starting command: %w", err)
 		}
 		defer func() {
-			log.Printf("Closing stdin pipe...")
+			log.Printf("%v: Closing stdin pipe...", name)
 			pipeIn.Close()
-			log.Printf("Waiting for the command to complete...")
+			log.Printf("%v: Waiting for the command to complete...", name)
 			err := cmd.Wait()
 			if err != nil {
-				log.Printf("After wait, the command returned an error: %v", err)
+				log.Printf("%v: After wait, the command returned an error: %v", name, err)
 			}
-			log.Printf("...done")
+			log.Printf("%v: ...done", name)
 		}()
 
 		pReader := bufio.NewReader(pipeOut)
@@ -673,37 +714,37 @@ func SendReceive(addr string, options []string, seek string) error {
 				line, err := pReader.ReadString('\n')
 				outputCh <- line
 				if err == io.EOF {
-					log.Printf("Read EOF")
+					log.Printf("%v: Read EOF", name)
 					close(outputCh)
 					return
 				}
 				if err != nil {
-					log.Printf("non-EOF error reading command output: %v", err)
+					log.Printf("%v: non-EOF error reading command output: %v", name, err)
 					close(outputCh)
 					return
 				}
 			}
 		}()
 
-		err = flushStdOut(outputCh, seek)
+		err = flushStdOut(outputCh, seek, name)
 		if err != nil {
 			doneCh <- err
 			return
 		}
 
-		log.Println("Sending data")
+		log.Printf("%v: Sending data", name)
 		_, err = pipeIn.Write([]byte(strEcho))
 		if err != nil {
 			doneCh <- fmt.Errorf("write to server failed: %w", err)
 			return
 		}
 
-		log.Println("Receiving reply")
+		log.Printf("%v: Receiving reply", name)
 
 		reply := <-outputCh
 
-		log.Printf("Sent to server = %q", strEcho)
-		log.Printf("Reply from server = %q", string(reply))
+		log.Printf("%v: Sent to server = %q", name, strEcho)
+		log.Printf("%v: Reply from server = %q", name, string(reply))
 
 		if len(reply) == len(strEcho) {
 			// We're using openssl s_server -rev, so we have to revert
@@ -713,12 +754,12 @@ func SendReceive(addr string, options []string, seek string) error {
 				r := reply[i_r]
 
 				if r != c {
-					doneCh <- fmt.Errorf("response from server different than expected: %s (%d/%q != %d/%q)", string(reply), i_r, r, i_c, c)
+					doneCh <- fmt.Errorf("response from server different than expected: %q (%d/%q != %d/%q)", string(reply), i_r, r, i_c, c)
 				}
 			}
 		} else {
 
-			doneCh <- fmt.Errorf("response length from server different than expected: %s", string(reply))
+			doneCh <- fmt.Errorf("response length from server different than expected: %q", string(reply))
 		}
 
 		doneCh <- nil
@@ -731,9 +772,11 @@ func SendReceive(addr string, options []string, seek string) error {
 	case err = <-doneCh:
 	case <-timeoutCh:
 		err = fmt.Errorf("timed out waiting for SendReceive function to finish")
+		// If we got a timeout, we renew the timer for the stdout flushing below
+		timeoutCh = time.After(time.Minute / 2)
 	}
 
-	log.Print("Flushing stdout after test")
+	log.Printf("%v: Flushing stdout after test", name)
 	// Otherwise, output gets mixed with the next test
 	for {
 		select {
@@ -742,12 +785,14 @@ func SendReceive(addr string, options []string, seek string) error {
 				// We're done here, so we just return whatever err we got before
 				return err
 			}
-			log.Printf("  %v", line)
+			log.Printf("%v:  %v", name, line)
 		case <-timeoutCh:
 			if err != nil {
-				log.Printf("timed out waiting for SendReceive, but received an error befor: %v", err)
+				return fmt.Errorf("%v: timed out on post-test stdout flush, but received an error before: %w", name, err)
 			}
-			return fmt.Errorf("timed out waiting for SendReceive function to finish")
+			// We do not consider this as an error, as it is post-test, but worthy to note
+			log.Printf("%v: timed out on post-test stdout flush", name)
+			return nil
 		}
 	}
 }
diff --git a/test/integration/performance/amqp_test.go b/test/integration/performance/amqp_test.go
index b98fe22e..d89e7300 100644
--- a/test/integration/performance/amqp_test.go
+++ b/test/integration/performance/amqp_test.go
@@ -177,7 +177,7 @@ func getAmqpClientInfo(settings *amqpSettings) *common.ClientInfo {
 
 func getAmqpJobs(settings *amqpSettings) []common.JobInfo {
 	var jobs []common.JobInfo
-	image := "ssorj/quiver"
+	image := "quay.io/ssorj/quiver"
 	jobName := fmt.Sprintf("quiver")
 	labels := map[string]string{"job": jobName}
 	job := &batchv1.Job{
diff --git a/test/integration/performance/http_test.go b/test/integration/performance/http_test.go
index 43370925..271f7aea 100644
--- a/test/integration/performance/http_test.go
+++ b/test/integration/performance/http_test.go
@@ -426,7 +426,7 @@ func getHttpServerInfo(settings *httpSettings) *common.ServerInfo {
 
 func getHttpDeployment() *appsv1.Deployment {
 	dep, _ := k8s.NewDeployment("http-server", "", k8s.DeploymentOpts{
-		Image:  "nginxinc/nginx-unprivileged:stable-alpine",
+		Image:  "quay.io/skupper/nginx-unprivileged:stable-alpine",
 		Labels: map[string]string{"app": "http-server"},
 	})
 	return dep
diff --git a/test/integration/performance/postgres_test.go b/test/integration/performance/postgres_test.go
index 457ed2ff..1d877f93 100644
--- a/test/integration/performance/postgres_test.go
+++ b/test/integration/performance/postgres_test.go
@@ -187,7 +187,7 @@ func getPostgresClientInfo(settings *postgresSettings) *common.ClientInfo {
 
 func getPostgresJobs(settings *postgresSettings) []common.JobInfo {
 	var jobs []common.JobInfo
-	image := "postgres"
+	image := "quay.io/skupper/postgres"
 	for _, clients := range settings.clients {
 		jobName := fmt.Sprintf("pgbench-clients-%d", clients)
 		labels := map[string]string{"job": jobName}
diff --git a/test/integration/performance/redis_test.go b/test/integration/performance/redis_test.go
index 90c4ddb6..199a7d6d 100644
--- a/test/integration/performance/redis_test.go
+++ b/test/integration/performance/redis_test.go
@@ -218,7 +218,7 @@ func getRedisJobs(settings *redisSettings) []common.JobInfo {
 	var jobs []common.JobInfo
 	var testCount int
 	var found bool
-	image := "redis"
+	image := "quay.io/skupper/redis"
 	for _, clients := range settings.clients {
 		testNameCount := map[string]int{}
 		for _, testName := range settings.tests {
@@ -283,7 +283,7 @@ func getRedisServerInfo(settings *redisSettings) *common.ServerInfo {
 
 func getRedisDeployment() *appsv1.Deployment {
 	dep, _ := k8s.NewDeployment("redis-server", "", k8s.DeploymentOpts{
-		Image:  "redis",
+		Image:  "quay.io/skupper/redis",
 		Labels: map[string]string{"app": "redis-server"},
 	})
 	return dep
diff --git a/test/integration/performance/redis_tls_test.go b/test/integration/performance/redis_tls_test.go
index bf11d6dd..665a6dac 100644
--- a/test/integration/performance/redis_tls_test.go
+++ b/test/integration/performance/redis_tls_test.go
@@ -108,7 +108,7 @@ func getRedisTlsJobs(settings *redisSettings) []common.JobInfo {
 	var jobs []common.JobInfo
 	var testCount int
 	var found bool
-	image := "redis"
+	image := "quay.io/skupper/redis"
 	for _, clients := range settings.clients {
 		testNameCount := map[string]int{}
 		for _, testName := range settings.tests {
@@ -215,7 +215,7 @@ func getRedisTlsDeployment() *appsv1.Deployment {
 		"redis-server-tls",
 		"",
 		k8s.DeploymentOpts{
-			Image:  "redis",
+			Image:  "quay.io/skupper/redis",
 			Labels: map[string]string{"app": "redis-server-tls"},
 			Args: []string{
 				"redis-server",
diff --git a/test/utils/base/cluster_test_runner.go b/test/utils/base/cluster_test_runner.go
index fe7797b3..3ac03067 100644
--- a/test/utils/base/cluster_test_runner.go
+++ b/test/utils/base/cluster_test_runner.go
@@ -249,6 +249,7 @@ func ConnectSimplePublicPrivate(ctx context.Context, r *ClusterTestRunnerBase) e
 	if err != nil {
 		return fmt.Errorf("error creating pub1 router: %w", err)
 	}
+	WaitSkupperRunning(pub1Cluster)
 
 	secretFile := "/tmp/" + r.Needs.NamespaceId + "_public_secret.yaml"
 	err = pub1Cluster.VanClient.ConnectorTokenCreateFile(ctx, types.DefaultVanName, secretFile)
@@ -267,6 +268,7 @@ func ConnectSimplePublicPrivate(ctx context.Context, r *ClusterTestRunnerBase) e
 	if err != nil {
 		return fmt.Errorf("error creating prv1 router: %w", err)
 	}
+	WaitSkupperRunning(prv1Cluster)
 
 	var connectorCreateOpts types.ConnectorCreateOptions = types.ConnectorCreateOptions{
 		SkupperNamespace: prv1Cluster.Namespace,
diff --git a/test/utils/skupper/cli/expose.go b/test/utils/skupper/cli/expose.go
index ab425ab5..d1b911a3 100644
--- a/test/utils/skupper/cli/expose.go
+++ b/test/utils/skupper/cli/expose.go
@@ -174,9 +174,6 @@ func (e *ExposeTester) validateTargets(platform types.Platform, svc types.Servic
 	found := false
 	for _, target := range svc.Targets {
 		expectedTargetName := e.TargetName
-		if e.TargetType == "service" {
-			expectedTargetName += "." + target.Namespace
-		}
 		if platform.IsKubernetes() {
 			if target.Name == expectedTargetName {
 				found = true
diff --git a/test/utils/skupper/cli/service/status.go b/test/utils/skupper/cli/service/status.go
index 7db95071..5b046ef0 100644
--- a/test/utils/skupper/cli/service/status.go
+++ b/test/utils/skupper/cli/service/status.go
@@ -332,7 +332,8 @@ func (s *StatusTester) parseBindings(stdout string) (ifaces []*types.ServiceInte
 			}
 			// Then we get the second item, that should be the name
 			pieces = strings.Split(strings.Trim(pieces[1], " "), " ")
-			if len(pieces) != 3 {
+			// namespace is not displayed for target type service
+			if len(pieces) < 2 || len(pieces) > 3 {
 				err = fmt.Errorf("Parsing failed due to unexpected target output format on line %v: %v", line, text)
 				return
 			}