Repository: skupperproject/skupper
Pull request #1779 (2024-11-08 20:59:02)

Add option to Get command 
Add option to Get command to specify if getting from runtime or source directory.


Patch:
diff --git a/api/types/crds/skupper_connector_crd.yaml b/api/types/crds/skupper_connector_crd.yaml
index d7dd190d..0e95beff 100644
--- a/api/types/crds/skupper_connector_crd.yaml
+++ b/api/types/crds/skupper_connector_crd.yaml
@@ -25,6 +25,10 @@ spec:
                   type: string
                 tlsCredentials:
                   type: string
+                noClientAuth:
+                  type: boolean
+                verifyHostname:
+                  type: boolean
                 type:
                   type: string
                 includeNotReady:
diff --git a/cmd/bootstrap/PROVIDED_CERTIFICATES.md b/cmd/bootstrap/PROVIDED_CERTIFICATES.md
index 77f2a262..60a0c35e 100644
--- a/cmd/bootstrap/PROVIDED_CERTIFICATES.md
+++ b/cmd/bootstrap/PROVIDED_CERTIFICATES.md
@@ -13,32 +13,32 @@ The certificates generated by Skupper are stored under the namespace home direct
 file system, using the following tree:
 
 ```shell
-certificates/
+runtime/certs/
 ├── ca
 ├── client
 └── server
 ```
 
-As the CA `skupper-site-ca` is generated, it will be stored under the `certificates/ca` path
+As the CA `skupper-site-ca` is generated, it will be stored under the `runtime/certs/ca` path
 as a directory named `skupper-site-ca`. Inside a certificate folder, the following files are
 expected: `tls.key`, `tls.crt` and `ca.crt` (when dealing with a CA, `ca.crt` and `tls.crt`
 have the same content).
 
 If a `RouterAccess` is defined as part of the non kube site definition, Skupper will also
 generate a server certificate that is valid for the provided `RouterAccess.spec.bindHost` and
-for each entry in the  `RouterAccess.spec.subjectAlternativeName` list. The respective certificate
+for each entry in the `RouterAccess.spec.subjectAlternativeName` list. The respective certificate
 is signed by the CA mentioned earlier (skupper-site-ca) and it will be named according to the
 provided `spec.tlsCredentials` field, or when it is omitted, the certificate will be named as the
-RouterAccess and stored as a directory under `certificates/server`.
+RouterAccess and stored as a directory under `runtime/certs/server`.
 
 A set of static links are also created for the provided `RouterAccess.spec.bindHost` and for each
-entry in the  `RouterAccess.spec.subjectAlternativeName` list as separate YAML files.
+entry in the `RouterAccess.spec.subjectAlternativeName` list as separate YAML files.
 
 These static links are composed by a `Link` and a `Secret`, which is basically the client certificate
 signed by `skupper-site-ca`, that will be used by other sites to establish Skupper links. The client
 certificate is also named based on the RouterAccess `spec.tlsCredentials` field or, when it is omitted,
 the certificate will be named as the RouterAccess, prefixed with `client-` and stored under
-`certificates/client`.
+`runtime/certs/client`.
 
 After a site bundle has been produced, it contains the whole site definition that can be installed
 at a remote location. Thus, all certificates and static links are already part of the bundle and no
@@ -49,7 +49,7 @@ new certificate is signed at the moment a bundle is installed.
 You can provide your own certificates to be used by Skupper for site linking.
 
 The provided certificates must be placed in a similar directory tree, as shown before,
-but its path under the namespace home is located at `input/certificates` instead.
+but its path under the namespace home is located at `input/certs` instead.
 
 Certificates placed under this path will be used by Skupper primarily and
 all the other certificates expected by Skupper, will be generated when not provided. 
@@ -86,15 +86,15 @@ provide them at any time, for example:
 
 During "Local site initialization time" or "Site bundle preparation time", Skupper will detect
 that a server certificate has been provided and will inspect it to determine its subject
-alternative names and based on that, a set of static links will be created, allowing those
-links to be distributed to target sites accordingly. For the static links to remain valid,
-the expected client certificate must also be provided.
+alternative names (SAN). A static link file will be created for each SAN, allowing those links
+to be distributed to target sites accordingly. For the static links to remain valid, the expected
+client certificate must also be provided.
 
 If a server certificate is provided at "Site bundle installation time", Skupper will also try to
 determine its subject alternative names using the `openssl` binary (only if available) and it will
-use it to generate the static links for the bundle installation. This way, the set of static links
-available for an installed bundle will be valid for all expected target hostnames and IP addresses
-defined through the server certificate.
+use it to generate the static link files during the bundle installation, or when extracting static links
+from the site bundle. This way, the set of static links available for an installed bundle will be valid for
+all expected target hostnames and IP addresses defined through the server certificate.
 
 Again, if a server certificate is provided, the respective client certificate is also expected
 so that the static links have valid client credentials.
@@ -108,7 +108,7 @@ certificates used for site linking, you can simply create the following structur
 the namespace home of your choice, for example:
 
 ```shell
-${HOME}/.local/share/skupper/namespaces/default/input/certificates/
+${HOME}/.local/share/skupper/namespaces/default/input/certs/
 └── ca
     └── skupper-site-ca
         ├── ca.crt
@@ -137,7 +137,7 @@ field is omitted) is `my-router-access`, then the following structure, for serve
 must be provided under the namespace home of your choice, for example:
 
 ```shell
-${HOME}/.local/share/skupper/namespaces/default/input/certificates/
+${HOME}/.local/share/skupper/namespaces/default/input/certs/
 ├── client
 │   └── client-my-router-access
 │       ├── ca.crt
@@ -166,7 +166,7 @@ If the following domain name is not defined as being the `spec.bindHost` or as p
 link that uses `my.local.server.com` as the target endpoint at:
 
 ```shell
-$HOME/.local/share/skupper/namespaces/default/runtime/link/link-my-router-access-my.local.server.com.yaml
+$HOME/.local/share/skupper/namespaces/default/runtime/links/link-my-router-access-my.local.server.com.yaml
 ```
 
 If the respective server certificates are defined at bundle installation time, Skupper will also inspect
diff --git a/cmd/bootstrap/README.md b/cmd/bootstrap/README.md
index 33244c3e..9c300249 100644
--- a/cmd/bootstrap/README.md
+++ b/cmd/bootstrap/README.md
@@ -2,7 +2,7 @@
 
 In this current phase of the Skupper V2 implementation, non-kubernetes sites
 can be bootstrapped using a locally built binary, that can be produced by running
-`make build-bootstrap`, or using the quay.io/skupper/bootstrap:v2-latest
+`make build-bootstrap`, using the quay.io/skupper/bootstrap:v2-latest
 container image by calling `./cmd/bootstrap/bootstrap.sh` with the appropriate
 flags.
 
@@ -71,7 +71,7 @@ ${HOME}/.local/share/skupper/namespaces when it is not set.
 As the root user, namespaces are stored under: /var/lib/skupper/namespaces.
 
 In case the path (-p) flag is omitted, Skupper will try to process
-custom resources stored at the sources directory of the default namespace,
+custom resources stored at the input/resources directory of the default namespace,
 or from the namespace provided through the namespace (-n) flag.
 
 If the respective namespace already exists and you want to bootstrap it
@@ -158,7 +158,7 @@ information like:
 
 * Location where static links have been defined (when a `RouterAccess` is provided)
 * Location where the site bundle has been saved (if bundle strategy flag `-b` set)
-* Namespace, Site name, Platform, Version and path to the sources
+* Namespace, Site name, Platform, Version and path to the resources
 
 #### Site bundles
 
@@ -257,7 +257,7 @@ The `west` site will be defined using the following CRs:
 
 ##### Site
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Site
 metadata:
   name: west
@@ -266,7 +266,7 @@ metadata:
 ##### Listener
 
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Listener
 metadata:
   name: backend
@@ -279,7 +279,7 @@ spec:
 ##### RouterAccess
 
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: RouterAccess
 metadata:
   name: go-west
@@ -308,7 +308,7 @@ Now that the `west` site has been created, copy the generated link into
 the `east` site local directory. Example:
 
 ```shell
-cp ${HOME}/.local/share/skupper/namespaces/west/runtime/link/link-go-west.yaml ./east/link-go-west.yaml
+cp ${HOME}/.local/share/skupper/namespaces/west/runtime/links/link-go-west-127.0.0.1.yaml ./east/link-go-west.yaml
 ```
 
 #### East
@@ -317,7 +317,7 @@ The `east` site will be defined using the following CRs:
 
 **Site**
 ```yaml
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Site
 metadata:
   name: east
@@ -326,7 +326,7 @@ metadata:
 **Connector**
 ```yaml
 ---
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Connector
 metadata:
   name: backend
@@ -349,7 +349,7 @@ kind: Secret
 metadata:
   name: link-go-west
 ---
-apiVersion: skupper.io/v1alpha1
+apiVersion: skupper.io/v2alpha1
 kind: Link
 metadata:
   name: link-go-west
@@ -381,7 +381,7 @@ in your browser and it should work.
 ### Updating an existing installation
 
 Suppose modifications have been made to the `west` site CRs, directly at the
-namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/input/sources).
+namespace directory (i.e: ${HOME}/.local/share/skupper/namespaces/west/input/resources).
 
 To re-initialize the west site, run:
 
@@ -393,13 +393,6 @@ The command above will reprocess all source CRs from the namespace path and
 restart the related components. The Certificate Authorities (CAs) are preserved,
 therefore eventual incoming links must still work.
 
-In case the CRs are located somewhere else, then a path must also be specified, as
-in the example below:
-
-```shell
-bootstrap -n west -p <path> -f
-```
-
 ### Cleanup
 
 To remove both namespaces, run:
@@ -421,22 +414,22 @@ Skupper nonkube bootstrap (version: main-release-161-g7c5100a2-modified)
 Site "west" has been created (as a distributable bundle)
 Installation bundle available at: /home/user/.local/share/skupper/bundles/skupper-install-west.sh
 Default namespace: default
-Default platform: docker
+Default platform: podman
 ```
 
-#### Extracting the static link
+#### Extracting the static links
 
-To extract the link, run:
+To extract the links, run:
 
 ```shell
 /home/user/.local/share/skupper/bundles/skupper-install-west.sh -d /tmp
 Static links for site "west" have been saved into /tmp/west
 ```
 
-Copy the static Link to the `east` site definition.
+Copy the static link to the `east` site definition.
 
 ```shell
-cp /tmp/west/link-go-west.yaml ./east/link-go-west.yaml
+cp /tmp/west/link-go-west-127.0.0.1.yaml ./east/link-go-west.yaml
 ```
 
 #### Creating the east bundle
diff --git a/cmd/bootstrap/bootstrap.go b/cmd/bootstrap/bootstrap.go
index 3116f9c1..a0b2b803 100644
--- a/cmd/bootstrap/bootstrap.go
+++ b/cmd/bootstrap/bootstrap.go
@@ -49,7 +49,7 @@ ${HOME}/.local/share/skupper/namespaces when it is not set.
 As the root user, namespaces are stored under: /var/lib/skupper/namespaces.
 
 In case the path (-p) flag is omitted, Skupper will try to process
-custom resources stored at the sources directory of the default namespace,
+custom resources stored at the input/resources directory of the default namespace,
 or from the namespace provided through the namespace (-n) flag.
 
 If the respective namespace already exists and you want to bootstrap it
@@ -161,20 +161,20 @@ func main() {
 		// when input path is empty, but a namespace is provided, try to reload an existing site definition
 		if inputSourcesDefined {
 			inputPath = existingPath
-			fmt.Printf("Sources will consumed from namespace %q\n", namespace)
+			fmt.Printf("Resources will consumed from namespace %q\n", namespace)
 		} else {
 			fmt.Printf("Input path has not been provided and namespace %s does not exist\n", namespace)
-			fmt.Printf("No sources found at: %s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
+			fmt.Printf("No resources found at: %s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
 			os.Exit(1)
 		}
 	} else if inputSourcesDefined {
-		fmt.Printf("Input path has been provided, but namespace %s has input sources defined at:\n", namespace)
+		fmt.Printf("Input path has been provided, but namespace %s has input resources defined at:\n", namespace)
 		fmt.Printf("%s\n", path.Join(api.GetHostNamespaceHome(namespace), string(api.InputSiteStatePath)))
 		os.Exit(1)
 	}
 
 	// if namespace already exists, fail if force is not set
-	_, err := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimeSiteStatePath))
+	_, err := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimePath))
 	if !isBundle && err == nil && !*force {
 		fmt.Printf("Namespace already exists: %s\n", namespace)
 		os.Exit(1)
@@ -239,7 +239,7 @@ func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api.
 		nsPlatformLoader := &common.NamespacePlatformLoader{}
 		nsPlatform, err := nsPlatformLoader.Load(namespace)
 		if err != nil {
-			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimeSiteStatePath))
+			_, runtimeStateErr := os.Stat(api.GetInternalOutputPath(namespace, api.RuntimePath))
 			if runtimeStateErr == nil {
 				return nil, fmt.Errorf("unable to determine current platform used in namespace %q", namespace)
 			}
@@ -268,7 +268,7 @@ func bootstrap(inputPath string, namespace string, bundleStrategy string) (*api.
 	targetNamespace := utils.DefaultStr(namespace, "default")
 	if inputPath == sourcesPath {
 		if crNamespace != targetNamespace {
-			return nil, fmt.Errorf("namespace must be %q, but sources are defined using %q", targetNamespace, crNamespace)
+			return nil, fmt.Errorf("namespace must be %q, but resources are defined using %q", targetNamespace, crNamespace)
 		}
 	} else if namespace != "" {
 		siteState.SetNamespace(namespace)
diff --git a/cmd/bootstrap/bootstrap.sh b/cmd/bootstrap/bootstrap.sh
index 07d7aa6e..4f90a11b 100755
--- a/cmd/bootstrap/bootstrap.sh
+++ b/cmd/bootstrap/bootstrap.sh
@@ -107,7 +107,7 @@ create_service() {
         return
     fi
     service_name="skupper-${namespace}.service"
-    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${namespace}/runtime/scripts/${service_name}"
+    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${namespace}/internal/scripts/${service_name}"
     if [ ! -f "${service_file}" ]; then
         echo "SystemD service has not been defined"
         return
diff --git a/cmd/bootstrap/remove.sh b/cmd/bootstrap/remove.sh
index 0874deea..f40e9ef7 100755
--- a/cmd/bootstrap/remove.sh
+++ b/cmd/bootstrap/remove.sh
@@ -26,7 +26,7 @@ usage() {
 }
 
 remove_definition() {
-    platform_file="${namespaces_path}/${namespace}/runtime/state/platform.yaml"
+    platform_file="${namespaces_path}/${namespace}/runtime/platform.yaml"
     if [ -f "${platform_file}" ]; then
         SKUPPER_PLATFORM=$(grep '^platform: ' "${platform_file}" | sed -e 's/.*: //g')
         if [ "${SKUPPER_PLATFORM}" = "podman" ] || [ "${SKUPPER_PLATFORM}" = "docker" ]; then
diff --git a/cmd/config-sync/main.go b/cmd/config-sync/main.go
index a9541c7b..3510a763 100644
--- a/cmd/config-sync/main.go
+++ b/cmd/config-sync/main.go
@@ -12,6 +12,8 @@ import (
 
 	corev1 "k8s.io/api/core/v1"
 
+	iflag "github.com/skupperproject/skupper/internal/flag"
+	"github.com/skupperproject/skupper/internal/kube/adaptor"
 	internalclient "github.com/skupperproject/skupper/internal/kube/client"
 	"github.com/skupperproject/skupper/pkg/kube"
 	"github.com/skupperproject/skupper/pkg/version"
@@ -36,12 +38,23 @@ func SetupSignalHandler() (stopCh <-chan struct{}) {
 	return stop
 }
 
-const SHARED_TLS_DIRECTORY = "/etc/skupper-router-certs"
-
 func main() {
+	flags := flag.NewFlagSet("", flag.ExitOnError)
+
+	var namespace string
+	var kubeconfig string
+	iflag.StringVar(flags, &namespace, "namespace", "NAMESPACE", "", "The Kubernetes namespace scope for the controller")
+	iflag.StringVar(flags, &kubeconfig, "kubeconfig", "KUBECONFIG", "", "A path to the kubeconfig file to use")
+
+	var configDir string
+	var configMapName string
+	iflag.StringVar(flags, &configDir, "config-dir", "SKUPPER_CONFIG_DIR", "/etc/skupper-router-certs", "The directory to which configuration should be saved")
+	iflag.StringVar(flags, &configMapName, "router-config", "SKUPPER_ROUTER_CONFIG", "skupper-router", "The name of the ConfigMap containg the router config")
+
 	// if -version used, report and exit
-	isVersion := flag.Bool("version", false, "Report the version of Config Sync")
-	flag.Parse()
+	isVersion := flags.Bool("version", false, "Report the version of Config Sync")
+	isInit := flags.Bool("init", false, "Downloads configuration and ssl profile artefacts")
+	flags.Parse(os.Args[1:])
 	if *isVersion {
 		fmt.Println(version.Version)
 		os.Exit(0)
@@ -53,11 +66,18 @@ func main() {
 	// set up signals so we handle the first shutdown signal gracefully
 	stopCh := SetupSignalHandler()
 
-	cli, err := internalclient.NewClient("", "", "")
+	cli, err := internalclient.NewClient(namespace, "", kubeconfig)
 	if err != nil {
 		log.Fatal("Error getting van client: ", err.Error())
 	}
 
+	if *isInit {
+		if err := adaptor.InitialiseConfig(cli.GetKubeClient(), cli.GetNamespace(), configDir, configMapName); err != nil {
+			log.Fatal("Error initialising config ", err.Error())
+		}
+		os.Exit(0)
+	}
+
 	log.Println("CONFIG_SYNC: Waiting for Skupper router to be ready")
 	_, err = kube.WaitForPodsSelectorStatus(cli.GetNamespace(), cli.Kube, "skupper.io/component=router", corev1.PodRunning, time.Second*180, time.Second*5)
 	if err != nil {
@@ -65,7 +85,7 @@ func main() {
 	}
 
 	log.Println("CONFIG_SYNC: Starting collector...")
-	go startCollector(cli)
+	go adaptor.StartCollector(cli)
 
 	//start health check
 	http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
@@ -74,16 +94,11 @@ func main() {
 	})
 	go http.ListenAndServe(":9191", nil)
 
-	routerConfigMap := os.Getenv("SKUPPER_CONFIG")
-	if routerConfigMap == "" {
-		routerConfigMap = "skupper-internal" // change defult?
-	}
-
-	configSync := newConfigSync(cli, cli.GetNamespace(), SHARED_TLS_DIRECTORY, routerConfigMap)
+	configSync := adaptor.NewConfigSync(cli, cli.GetNamespace(), configDir, configMapName)
 	log.Println("CONFIG_SYNC: Starting controller loop...")
-	configSync.start(stopCh)
+	configSync.Start(stopCh)
 
 	<-stopCh
 	log.Println("CONFIG_SYNC: Shutting down...")
-	configSync.stop()
+	configSync.Stop()
 }
diff --git a/deployments/helm/crds/skupper_connector_crd.yaml b/deployments/helm/crds/skupper_connector_crd.yaml
index d7dd190d..0e95beff 100644
--- a/deployments/helm/crds/skupper_connector_crd.yaml
+++ b/deployments/helm/crds/skupper_connector_crd.yaml
@@ -25,6 +25,10 @@ spec:
                   type: string
                 tlsCredentials:
                   type: string
+                noClientAuth:
+                  type: boolean
+                verifyHostname:
+                  type: boolean
                 type:
                   type: string
                 includeNotReady:
diff --git a/internal/cmd/skupper/connector/nonkube/connector_status.go b/internal/cmd/skupper/connector/nonkube/connector_status.go
index 7ea8f487..2ad5f1d8 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_status.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_status.go
@@ -35,6 +35,7 @@ func (cmd *CmdConnectorStatus) NewClient(cobraCommand *cobra.Command, args []str
 
 func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
 	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: true}
 	resourceStringValidator := validator.NewResourceStringValidator()
 	outputTypeValidator := validator.NewOptionValidator(common.OutputTypes)
 
@@ -55,7 +56,7 @@ func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
 	}
 	// Validate that there is a connector with this name in the namespace
 	if cmd.connectorName != "" {
-		connector, err := cmd.connectorHandler.Get(cmd.connectorName)
+		connector, err := cmd.connectorHandler.Get(cmd.connectorName, opts)
 		if connector == nil || err != nil {
 			validationErrors = append(validationErrors, fmt.Errorf("connector %s does not exist in namespace %s", cmd.connectorName, cmd.namespace))
 		}
@@ -74,6 +75,7 @@ func (cmd *CmdConnectorStatus) ValidateInput(args []string) []error {
 }
 
 func (cmd *CmdConnectorStatus) Run() error {
+	opts := fs.GetOptions{RuntimeFirst: true}
 	if cmd.connectorName == "" {
 		connectors, err := cmd.connectorHandler.List()
 		if connectors == nil || err != nil {
@@ -103,7 +105,7 @@ func (cmd *CmdConnectorStatus) Run() error {
 			_ = tw.Flush()
 		}
 	} else {
-		connector, err := cmd.connectorHandler.Get(cmd.connectorName)
+		connector, err := cmd.connectorHandler.Get(cmd.connectorName, opts)
 		if connector == nil || err != nil {
 			fmt.Println("No connectors found:")
 			return err
diff --git a/internal/cmd/skupper/connector/nonkube/connector_update.go b/internal/cmd/skupper/connector/nonkube/connector_update.go
index e6044b63..5358950d 100644
--- a/internal/cmd/skupper/connector/nonkube/connector_update.go
+++ b/internal/cmd/skupper/connector/nonkube/connector_update.go
@@ -44,6 +44,7 @@ func (cmd *CmdConnectorUpdate) NewClient(cobraCommand *cobra.Command, args []str
 
 func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 	var validationErrors []error
+	opts := fs.GetOptions{RuntimeFirst: false}
 	resourceStringValidator := validator.NewResourceStringValidator()
 	numberValidator := validator.NewNumberValidator()
 	connectorTypeValidator := validator.NewOptionValidator(common.ConnectorTypes)
@@ -76,7 +77,7 @@ func (cmd *CmdConnectorUpdate) ValidateInput(args []string) []error {
 
 	// Validate that there is already a connector with this name in the namespace
 	if cmd.connectorName != "" {
-		connector, err := cmd.connectorHandler.Update(cmd.connectorName)
+		connector, err := cmd.connectorHandler.Get(cmd.connectorName, opts)
 		if connector == nil || err != nil {
 			validationErrors = append(validationErrors, fmt.Errorf("connector %s must exist in namespace %s to be updated", cmd.connectorName, cmd.namespace))
 		} else {
diff --git a/internal/cmd/skupper/site/nonkube/site_create.go b/internal/cmd/skupper/site/nonkube/site_create.go
index a53438bf..18b879fb 100644
--- a/internal/cmd/skupper/site/nonkube/site_create.go
+++ b/internal/cmd/skupper/site/nonkube/site_create.go
@@ -9,12 +9,15 @@ import (
 	"github.com/skupperproject/skupper/internal/cmd/skupper/common/utils"
 	"github.com/skupperproject/skupper/internal/nonkube/client/fs"
 	"github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
-	"github.com/skupperproject/skupper/pkg/site"
 	"github.com/skupperproject/skupper/pkg/utils/validator"
 	"github.com/spf13/cobra"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
+const (
+	SiteConfigNameKey string = "name"
+)
+
 type CmdSiteCreate struct {
 	siteHandler             *fs.SiteHandler
 	routerAccessHandler     *fs.RouterAccessHandler
@@ -115,7 +118,7 @@ func (cmd *CmdSiteCreate) InputToOptions() {
 	}
 
 	options := make(map[string]string)
-	options[site.SiteConfigNameKey] = cmd.siteName
+	options[SiteConfigNameKey] = cmd.siteName
 
 	cmd.options = options
 	cmd.output = cmd.Flags.Output
diff --git a/cmd/config-sync/collector.go b/internal/kube/adaptor/collector.go
similarity index 98%
rename from cmd/config-sync/collector.go
rename to internal/kube/adaptor/collector.go
index 028cfba3..842fa9d1 100644
--- a/cmd/config-sync/collector.go
+++ b/internal/kube/adaptor/collector.go
@@ -1,4 +1,4 @@
-package main
+package adaptor
 
 import (
 	"context"
@@ -142,7 +142,7 @@ func runLeaderElection(lock *resourcelock.LeaseLock, id string, cli *internalcli
 	})
 }
 
-func startCollector(cli *internalclient.KubeClient) {
+func StartCollector(cli *internalclient.KubeClient) {
 	lockname := types.SiteLeaderLockName
 	namespace := cli.Namespace
 	podname, _ := os.Hostname()
diff --git a/internal/kube/adaptor/config_init.go b/internal/kube/adaptor/config_init.go
new file mode 100644
index 00000000..53be7ee7
--- /dev/null
+++ b/internal/kube/adaptor/config_init.go
@@ -0,0 +1,51 @@
+package adaptor
+
+import (
+	"context"
+	"log"
+	"os"
+	paths "path"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes"
+
+	"github.com/skupperproject/skupper/pkg/qdr"
+)
+
+func InitialiseConfig(client kubernetes.Interface, namespace string, path string, routerConfigMap string) error {
+	ctxt := context.Background()
+	current, err := client.CoreV1().ConfigMaps(namespace).Get(ctxt, routerConfigMap, metav1.GetOptions{})
+	if err != nil {
+		return err
+	}
+
+	config, err := qdr.GetRouterConfigFromConfigMap(current)
+	if err != nil {
+		return err
+	}
+
+	value, err := qdr.MarshalRouterConfig(*config)
+	if err != nil {
+		return err
+	}
+	configFile := paths.Join(path, "skrouterd.json")
+	if err := os.WriteFile(configFile, []byte(value), 0777); err != nil {
+		return err
+	}
+	log.Printf("Router configuration written to %s", configFile)
+
+	profileSyncer := newSslProfileSyncer(path)
+	for _, profile := range config.SslProfiles {
+		target, _ := profileSyncer.get(profile.Name)
+
+		secret, err := client.CoreV1().Secrets(namespace).Get(ctxt, target.name, metav1.GetOptions{})
+		if err != nil {
+			return err
+		}
+		if err := target.sync(secret); err != nil {
+			return err
+		}
+		log.Printf("Resources for SslProfile %s written to %s", profile.Name, target.path)
+	}
+	return nil
+}
diff --git a/cmd/config-sync/config_sync.go b/internal/kube/adaptor/config_sync.go
similarity index 75%
rename from cmd/config-sync/config_sync.go
rename to internal/kube/adaptor/config_sync.go
index 009115e1..f9917c05 100644
--- a/cmd/config-sync/config_sync.go
+++ b/internal/kube/adaptor/config_sync.go
@@ -1,12 +1,9 @@
-package main
+package adaptor
 
 import (
 	"fmt"
 	"log"
-	"os"
-	paths "path"
 	"reflect"
-	"strings"
 
 	corev1 "k8s.io/api/core/v1"
 
@@ -21,26 +18,26 @@ type ConfigSync struct {
 	agentPool       *qdr.AgentPool
 	controller      *kube.Controller
 	namespace       string
-	tracking        map[string]*SyncTarget
+	profileSyncer   *SslProfileSyncer
 	config          *kube.ConfigMapWatcher
 	secrets         *kube.SecretWatcher
 	path            string
 	routerConfigMap string
 }
 
-func newConfigSync(cli internalclient.Clients, namespace string, path string, routerConfigMap string) *ConfigSync {
+func NewConfigSync(cli internalclient.Clients, namespace string, path string, routerConfigMap string) *ConfigSync {
 	configSync := &ConfigSync{
 		agentPool:       qdr.NewAgentPool("amqp://localhost:5672", nil),
 		controller:      kube.NewController("config-sync", cli),
 		namespace:       namespace,
-		tracking:        map[string]*SyncTarget{},
+		profileSyncer:   newSslProfileSyncer(path),
 		path:            path,
 		routerConfigMap: routerConfigMap,
 	}
 	return configSync
 }
 
-func (c *ConfigSync) start(stopCh <-chan struct{}) error {
+func (c *ConfigSync) Start(stopCh <-chan struct{}) error {
 	if err := mkdir(c.path); err != nil {
 		return err
 	}
@@ -58,7 +55,7 @@ func (c *ConfigSync) start(stopCh <-chan struct{}) error {
 	return nil
 }
 
-func (c *ConfigSync) stop() {
+func (c *ConfigSync) Stop() {
 	c.controller.Stop()
 }
 
@@ -66,30 +63,11 @@ func (c *ConfigSync) key(name string) string {
 	return fmt.Sprintf("%s/%s", c.namespace, name)
 }
 
-func (c *ConfigSync) track(name string, path string) (*SyncTarget, bool) {
-	if current, ok := c.tracking[name]; ok {
-		log.Printf("CONFIG_SYNC: Secret %q already being tracked", name)
-		return current, false
-	} else {
-		target := &SyncTarget{
-			name: name,
-			path: path,
-		}
-		c.tracking[name] = target
-		log.Printf("CONFIG_SYNC: Tracking secret %q", name)
-		return target, true
-	}
-}
-
-func (c *ConfigSync) trackSslProfile(profile string) (*SyncTarget, bool) {
-	secret := profile
-	if strings.HasSuffix(profile, "-profile") {
-		secret = strings.TrimSuffix(profile, "-profile")
-	}
-	return c.track(secret, paths.Join(c.path, profile))
+func (c *ConfigSync) trackSslProfile(profile string) (*SslProfile, bool) {
+	return c.profileSyncer.get(profile)
 }
 
-func (c *ConfigSync) sync(target *SyncTarget) error {
+func (c *ConfigSync) sync(target *SslProfile) error {
 	secret, err := c.secrets.Get(c.key(target.name))
 	if err != nil {
 		return fmt.Errorf("CONFIG_SYNC: Error looking up secret for %s: %s", target.name, err)
@@ -110,7 +88,7 @@ func (c *ConfigSync) secretEvent(key string, secret *corev1.Secret) error {
 	if secret == nil {
 		return nil
 	}
-	if current, ok := c.tracking[secret.Name]; ok {
+	if current, ok := c.profileSyncer.bySecretName(secret.Name); ok {
 		if current.secret != nil && reflect.DeepEqual(current.secret.Data, secret.Data) {
 			log.Printf("CONFIG_SYNC: Secret %q already up to date", secret.Name)
 			return nil
@@ -270,9 +248,6 @@ func (c *ConfigSync) syncSslProfilesToRouter(desired map[string]qdr.SslProfile)
 
 func (c *ConfigSync) syncSslProfileCredentialsToDisk(profiles map[string]qdr.SslProfile) error {
 	for _, profile := range profiles {
-		if isExcludedProfile(profile.Name) {
-			continue
-		}
 		if tracker, sync := c.trackSslProfile(profile.Name); sync {
 			if err := c.sync(tracker); err != nil {
 				return fmt.Errorf("Error synchronising secret for profile %s: %s", profile.Name, err)
@@ -281,17 +256,6 @@ func (c *ConfigSync) syncSslProfileCredentialsToDisk(profiles map[string]qdr.Ssl
 	}
 	return nil
 }
-func (c *ConfigSync) trackSslProfiles(config *qdr.RouterConfig, path string) error {
-	for _, profile := range config.SslProfiles {
-		secretName := profile.Name
-
-		if strings.HasSuffix(profile.Name, "-profile") {
-			secretName = strings.TrimSuffix(profile.Name, "-profile")
-		}
-		c.track(secretName, paths.Join(path, profile.Name))
-	}
-	return nil
-}
 
 func (c *ConfigSync) recoverTracking() error {
 	configmap, err := c.config.Get(c.key(c.routerConfigMap))
@@ -310,56 +274,3 @@ func (c *ConfigSync) recoverTracking() error {
 	}
 	return nil
 }
-
-type SyncTarget struct {
-	name   string
-	path   string
-	secret *corev1.Secret
-}
-
-func (s *SyncTarget) sync(secret *corev1.Secret) error {
-	if s.secret != nil && reflect.DeepEqual(s.secret.Data, secret.Data) {
-		return nil
-	}
-	if err := writeSecretToPath(secret, s.path); err != nil {
-		return err
-	}
-	s.secret = secret
-	return nil
-}
-
-func writeSecretToPath(secret *corev1.Secret, path string) error {
-	if err := mkdir(path); err != nil {
-		return err
-	}
-	for key, value := range secret.Data {
-		if err := os.WriteFile(paths.Join(path, key), value, 0777); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func mkdir(path string) error {
-	_, err := os.Stat(path)
-	if os.IsNotExist(err) {
-		err = os.Mkdir(path, 0777)
-		if err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-func excludedProfiles() []string {
-	return []string{"skupper-amqps", "skupper-service-client"}
-}
-
-func isExcludedProfile(name string) bool {
-	for _, v := range excludedProfiles() {
-		if name == v {
-			return true
-		}
-	}
-	return false
-}
diff --git a/internal/kube/adaptor/profile.go b/internal/kube/adaptor/profile.go
new file mode 100644
index 00000000..57a83d8a
--- /dev/null
+++ b/internal/kube/adaptor/profile.go
@@ -0,0 +1,83 @@
+package adaptor
+
+import (
+	"os"
+	paths "path"
+	"reflect"
+	"strings"
+
+	corev1 "k8s.io/api/core/v1"
+)
+
+type SslProfileSyncer struct {
+	profiles map[string]*SslProfile
+	path     string
+}
+
+func newSslProfileSyncer(path string) *SslProfileSyncer {
+	return &SslProfileSyncer{
+		profiles: map[string]*SslProfile{},
+		path:     path,
+	}
+}
+
+func (s SslProfileSyncer) get(profile string) (*SslProfile, bool) {
+	secret := profile
+	if strings.HasSuffix(profile, "-profile") {
+		secret = strings.TrimSuffix(profile, "-profile")
+	}
+	if current, ok := s.profiles[secret]; ok {
+		return current, false
+	}
+	target := &SslProfile{
+		name: secret,
+		path: paths.Join(s.path, profile),
+	}
+	s.profiles[secret] = target
+	return target, true
+}
+
+func (s SslProfileSyncer) bySecretName(secret string) (*SslProfile, bool) {
+	current, ok := s.profiles[secret]
+	return current, ok
+}
+
+type SslProfile struct {
+	name   string
+	path   string
+	secret *corev1.Secret
+}
+
+func (s *SslProfile) sync(secret *corev1.Secret) error {
+	if s.secret != nil && reflect.DeepEqual(s.secret.Data, secret.Data) {
+		return nil
+	}
+	if err := writeSecretToPath(secret, s.path); err != nil {
+		return err
+	}
+	s.secret = secret
+	return nil
+}
+
+func writeSecretToPath(secret *corev1.Secret, path string) error {
+	if err := mkdir(path); err != nil {
+		return err
+	}
+	for key, value := range secret.Data {
+		if err := os.WriteFile(paths.Join(path, key), value, 0777); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+func mkdir(path string) error {
+	_, err := os.Stat(path)
+	if os.IsNotExist(err) {
+		err = os.Mkdir(path, 0777)
+		if err != nil {
+			return err
+		}
+	}
+	return nil
+}
diff --git a/internal/nonkube/bundle/install.sh.template b/internal/nonkube/bundle/install.sh.template
index 833d7abe..f3f12cc1 100644
--- a/internal/nonkube/bundle/install.sh.template
+++ b/internal/nonkube/bundle/install.sh.template
@@ -38,7 +38,7 @@ if [ "${UID}" -eq 0 ]; then
     export USERNS="''"
 fi
 export NAMESPACES_PATH="${SKUPPER_OUTPUT_PATH}/namespaces"
-export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/platform.yaml"
+export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/platform.yaml"
 export USER="${USER:-$(id -un)}"
 SITE_ID="$(hostname -s)-${USER}-$(date +%s)"
 export SITE_ID
@@ -79,7 +79,7 @@ parse_opts() {
                 if [ -n "${OPTARG}" ]; then
                     export NAMESPACE="${OPTARG}"
                 fi
-                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/platform.yaml"
+                export PLATFORM_FILE="${NAMESPACES_PATH}/${NAMESPACE}/runtime/platform.yaml"
                 ;;
             d)
                 export DUMP_TOKENS=true
@@ -125,7 +125,7 @@ create_service() {
     service_name="skupper-${NAMESPACE}.service"
     service_file_suffix="container"
     [ "${SKUPPER_PLATFORM}" = "systemd" ] && service_file_suffix="systemd"
-    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${NAMESPACE}/runtime/scripts/skupper.service.${service_file_suffix}"
+    service_file="${SKUPPER_OUTPUT_PATH}/namespaces/${NAMESPACE}/internal/scripts/skupper.service.${service_file_suffix}"
 
     if [ ! -f "${service_file}" ]; then
         echo "SystemD service has not been defined"
@@ -164,7 +164,7 @@ remove_site() {
         exit 0
     fi
 
-    SITE_PATH="${NAMESPACES_PATH}/${NAMESPACE}/runtime/state/site/"
+    SITE_PATH="${NAMESPACES_PATH}/${NAMESPACE}/runtime/resources/site/"
     if ! grep -qr "name: ${SITE_NAME}\$" "${SITE_PATH}"; then
         echo "Site \"${SITE_NAME}\" is not defined on namespace \"${NAMESPACE}\""
         exit 0
@@ -181,20 +181,20 @@ remove_site() {
 }
 
 dump_tokens() {
-    tokens="$(ls "./${SOURCE_NAMESPACE}/runtime/link/")"
+    tokens="$(ls "./${SOURCE_NAMESPACE}/runtime/links/")"
     if [ -z "${tokens}" ]; then
         echo "No static links defined in this site bundle"
         return
     fi
     token_out_dir="${SKUPPER_TOKEN_OUTPUT_PATH:?}/${SITE_NAME:?}"
     mkdir -p "${token_out_dir}"
-    cp "./${SOURCE_NAMESPACE}/runtime/link/"*.yaml "${token_out_dir}"
+    cp "./${SOURCE_NAMESPACE}/runtime/links/"*.yaml "${token_out_dir}"
     echo "Static links for site \"${SITE_NAME}\" have been saved into ${token_out_dir}"
 }
 
 set_router_access_port() {
-    config_file="${NAMESPACES_PATH}/${NAMESPACE}/config/router/skrouterd.json"
-    python "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/scripts/router_free_port.py" "${config_file}"
+    config_file="${NAMESPACES_PATH}/${NAMESPACE}/runtime/router/skrouterd.json"
+    python "${NAMESPACES_PATH:?}/${NAMESPACE:?}/internal/scripts/router_free_port.py" "${config_file}"
 }
 
 create_sed_script() {
@@ -215,8 +215,8 @@ create_sed_script() {
         echo "s#{{"{{"}}.SiteId{{"}}"}}#${SITE_ID}#g"
         echo "s#{{"{{"}}.NamespacesPath{{"}}"}}#${NAMESPACES_PATH}#g"
         echo "s#{{"{{"}}.RuntimeDir{{"}}"}}#${RUNTIME_DIR}#g"
-        echo "s#{{"{{"}}.SiteScriptPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/runtime/scripts#g"
-        echo "s#{{"{{"}}.SiteConfigPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/config/router#g"
+        echo "s#{{"{{"}}.SiteScriptPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/internal/scripts#g"
+        echo "s#{{"{{"}}.SiteConfigPath{{"}}"}}#${NAMESPACES_PATH}/${NAMESPACE}/runtime/router#g"
         echo "s#{{"{{"}}.RunAs{{"}}"}}#${RUNAS}#g"
         echo "s#{{"{{"}}.UserNamespace{{"}}"}}#${USERNS}#g"
     } >> script.sed
@@ -228,7 +228,7 @@ render_sed_templates() {
 }
 
 show_token_info() {
-    token_path="${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/link"
+    token_path="${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/links"
     [ ! -d "${token_path}" ] && return
     tokens_found=$(find "${token_path}" -type f | wc -l)
     if [ "${tokens_found}" -gt 0 ]; then
@@ -238,27 +238,15 @@ show_token_info() {
 
 create_containers() {
     [ "${SKUPPER_PLATFORM}" = "systemd" ] && return
-    "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime/scripts/containers_create.sh"
+    "${NAMESPACES_PATH:?}/${NAMESPACE:?}/internal/scripts/containers_create.sh"
 }
 
 create_site() {
-    if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime" ]; then
-        echo "Failed: namespace \"${NAMESPACE}\" is already defined"
-        echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}"
-        exit 1
-    fi
-
-    if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources" ]; then
-        echo "Failed: namespace \"${NAMESPACE}\" already contains input sources"
-        echo "Location: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources"
-        exit 1
-    fi
-
     echo "Skupper site bundle installation"
     echo "Namespace : ${NAMESPACE}"
     echo "Site name : ${SITE_NAME}"
     echo "Platform  : ${SKUPPER_PLATFORM}"
-    echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/sources"
+    echo "Definition: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/resources"
     echo "Version   : ${VERSION}"
 
     # Create base directory tree
@@ -301,18 +289,32 @@ sanity_check() {
             exit_error "A required command could not be found: ${cmd}"
         fi
     done
+
+    if ! ${REMOVE} && ! ${DUMP_TOKENS}; then
+        if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/runtime" ]; then
+            echo "Failed: namespace \"${NAMESPACE}\" is already defined"
+            echo "Location: ${NAMESPACES_PATH}/${NAMESPACE}"
+            exit 1
+        fi
+
+        if [ -d "${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/resources" ]; then
+            echo "Failed: namespace \"${NAMESPACE}\" already contains input resources"
+            echo "Location: ${NAMESPACES_PATH:?}/${NAMESPACE:?}/input/resources"
+            exit 1
+        fi
+    fi
 }
 
 list_valid_certificates() {
     base_path="${1}"
     cd "${base_path}"
-    for dir in $(find ./* -type d | cut -c 3-); do
+    for dir in $(find ./* -type d 2> /dev/null | cut -c 3-); do
         [ -f "${dir}/tls.key" ] && [ -f "${dir}/tls.crt" ] && [ -f "${dir}/ca.crt" ] && echo "$dir"
     done
 }
 
 handle_provided_certificates() {
-    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates"
+    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certs"
     [ ! -d "${USER_CERTIFICATES_PATH}" ] && return
     cas=""
     clients=""
@@ -348,15 +350,15 @@ handle_provided_certificates() {
             echo "  - ${server}"
         done
     fi
-    cp -r "${USER_CERTIFICATES_PATH}"/* "./${SOURCE_NAMESPACE}/certificates"
+    cp -r "${USER_CERTIFICATES_PATH}"/* "./${SOURCE_NAMESPACE}/runtime/certs"
     update_static_links
     generate_static_links_for_sans
 }
 
 update_static_links() {
     DIR="./${SOURCE_NAMESPACE}"
-    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certificates"
-    router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f)
+    export USER_CERTIFICATES_PATH="${NAMESPACES_PATH}/${NAMESPACE}/input/certs"
+    router_access_files=$(find "${DIR:?}/internal/snapshot/routerAccesses" -type f)
 
     for ra in ${router_access_files}; do
         ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g')
@@ -381,7 +383,7 @@ update_static_links() {
         tls_crt=$(base64 -w0 "${client_cert_path}/tls.crt")
         tls_key=$(base64 -w0 "${client_cert_path}/tls.key")
         # find all static links generated for the given RouterAccess
-        static_link_files=$(find "${DIR:?}/runtime/link" -type f -name "link-${ra_name}-*")
+        static_link_files=$(find "${DIR:?}/runtime/links" -type f -name "link-${ra_name}-*")
         for link in ${static_link_files}; do
             echo "- Updating client credentials on static link: ${link}"
             sed -i "s/tls.key: .*/tls.key: ${tls_key}/g" "${link}"
@@ -399,7 +401,7 @@ generate_static_links_for_sans() {
         return
     fi
 
-    router_access_files=$(find "${DIR:?}/sources/routerAccesses" -type f)
+    router_access_files=$(find "${DIR:?}/internal/snapshot/routerAccesses" -type f)
 
     for ra in ${router_access_files}; do
         ra_name=$(grep name: "${ra}" | grep -v '\- name:' | awk '{print $NF}' | sed 's/"//g')
@@ -407,17 +409,17 @@ generate_static_links_for_sans() {
         bind_host=$(grep bindHost: "${ra}" | awk '{print $NF}' | sed 's/"//g')
 
         # discovering SANS
-        server_cert="${DIR}/certificates/server/${tls_credential:-${ra_name}}/tls.crt"
+        server_cert="${DIR}/runtime/certs/server/${tls_credential:-${ra_name}}/tls.crt"
         [ ! -f "${server_cert}" ] && continue
         subjects="$(openssl x509 -noout -ext subjectAltName -in "${server_cert}" |grep -v 'Subject Alternative Name:' | \
                     sed 's/ //g;s/,/\n/g' |  awk -F: '{if ($1 == "DNS" || $1 == "IPAddress") print $2}')"
         [ -z "${subjects}" ] && continue
         for subject in ${subjects}; do
-            link_file="${DIR}/runtime/link/link-${ra_name}-${subject}.yaml"
+            link_file="${DIR}/runtime/links/link-${ra_name}-${subject}.yaml"
             [ -f "${link_file}" ] && continue
             echo "- Creating static link for new server subject found: ${subject}"
-            cp "${DIR}/runtime/link/link-${ra_name}-${bind_host:-127.0.0.1}.yaml" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml"
-            sed -i "s/host: .*/host: ${subject}/g" "${DIR}/runtime/link/link-${ra_name}-${subject}.yaml"
+            cp "${DIR}/runtime/links/link-${ra_name}-${bind_host:-127.0.0.1}.yaml" "${DIR}/runtime/links/link-${ra_name}-${subject}.yaml"
+            sed -i "s/host: .*/host: ${subject}/g" "${DIR}/runtime/links/link-${ra_name}-${subject}.yaml"
         done
     done
 }
diff --git a/internal/nonkube/client/fs/connector_handler.go b/internal/nonkube/client/fs/connector_handler.go
index bceaec6c..a1e0c50e 100644
--- a/internal/nonkube/client/fs/connector_handler.go
+++ b/internal/nonkube/client/fs/connector_handler.go
@@ -38,23 +38,33 @@ func (s *ConnectorHandler) Add(resource v2alpha1.Connector) error {
 	return nil
 }
 
-func (s *ConnectorHandler) Get(name string) (*v2alpha1.Connector, error) {
+func (s *ConnectorHandler) Get(name string, opt GetOptions) (*v2alpha1.Connector, error) {
 	var context v2alpha1.Connector
 	fileName := name + ".yaml"
 
-	// First read from runtime directory, where output is found after bootstrap
-	// has run.  If no runtime connectors try and display configured connectors
-	err, file := s.ReadFile(s.pathProvider.GetRuntimeNamespace(), fileName, common.Connectors)
-	if err != nil {
-		fmt.Println("Site not initialized yet")
-		err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
+	if opt.RuntimeFirst == true {
+		// First read from runtime directory, where output is found after bootstrap
+		// has run.  If no runtime connectors try and display configured connectors
+		err, file := s.ReadFile(s.pathProvider.GetRuntimeNamespace(), fileName, common.Connectors)
+		if err != nil {
+			fmt.Println("Site not initialized yet")
+			err, file = s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
+			if err != nil {
+				return nil, err
+			}
+			if err := s.DecodeYaml(file, &context); err != nil {
+				return nil, err
+			}
+		}
+	} else {
+		// read from input directory to get lastest config
+		err, file := s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
 		if err != nil {
 			return nil, err
 		}
-	}
-
-	if err = s.DecodeYaml(file, &context); err != nil {
-		return nil, err
+		if err := s.DecodeYaml(file, &context); err != nil {
+			return nil, err
+		}
 	}
 
 	return &context, nil
@@ -101,20 +111,3 @@ func (s *ConnectorHandler) List() ([]*v2alpha1.Connector, error) {
 	}
 	return connectors, nil
 }
-
-func (s *ConnectorHandler) Update(name string) (*v2alpha1.Connector, error) {
-	var context v2alpha1.Connector
-	fileName := name + ".yaml"
-
-	// read from input directory to get lastest config
-	err, file := s.ReadFile(s.pathProvider.GetNamespace(), fileName, common.Connectors)
-	if err != nil {
-		return nil, err
-	}
-
-	if err = s.DecodeYaml(file, &context); err != nil {
-		return nil, err
-	}
-
-	return &context, nil
-}
diff --git a/internal/nonkube/client/fs/custom_resource_handler.go b/internal/nonkube/client/fs/custom_resource_handler.go
index 22d76d1b..9575d7fa 100644
--- a/internal/nonkube/client/fs/custom_resource_handler.go
+++ b/internal/nonkube/client/fs/custom_resource_handler.go
@@ -11,10 +11,13 @@ import (
 	"sigs.k8s.io/yaml"
 )
 
+type GetOptions struct {
+	RuntimeFirst bool
+}
+
 type CustomResourceHandler[T any] interface {
 	Add(T) error
-	Update(T) error
-	Get(name string) (T, error)
+	Get(name string, opts GetOptions) (T, error)
 	List() ([]T, error)
 	Delete(name string) error
 
diff --git a/internal/nonkube/client/fs/router_access_handler.go b/internal/nonkube/client/fs/router_access_handler.go
index 8877da45..7876cfbb 100644
--- a/internal/nonkube/client/fs/router_access_handler.go
+++ b/internal/nonkube/client/fs/router_access_handler.go
@@ -33,7 +33,8 @@ func (s *RouterAccessHandler) Add(resource v2alpha1.RouterAccess) error {
 
 	return nil
 }
-func (s *RouterAccessHandler) Update(name string) (*v2alpha1.RouterAccess, error) { return nil, nil }
-func (s *RouterAccessHandler) Get(name string) (*v2alpha1.RouterAccess, error)    { return nil, nil }
-func (s *RouterAccessHandler) Delete(name string) error                           { return nil }
-func (s *RouterAccessHandler) List() ([]*v2alpha1.RouterAccess, error)            { return nil, nil }
+func (s *RouterAccessHandler) Get(name string, opts GetOptions) (*v2alpha1.RouterAccess, error) {
+	return nil, nil
+}
+func (s *RouterAccessHandler) Delete(name string) error                { return nil }
+func (s *RouterAccessHandler) List() ([]*v2alpha1.RouterAccess, error) { return nil, nil }
diff --git a/internal/nonkube/client/fs/site_handler.go b/internal/nonkube/client/fs/site_handler.go
index b465081a..a7e3c207 100644
--- a/internal/nonkube/client/fs/site_handler.go
+++ b/internal/nonkube/client/fs/site_handler.go
@@ -34,7 +34,6 @@ func (s *SiteHandler) Add(resource v2alpha1.Site) error {
 	return nil
 }
 
-func (s *SiteHandler) Update(name string) (*v2alpha1.Site, error) { return nil, nil }
-func (s *SiteHandler) Get(name string) (*v2alpha1.Site, error)    { return nil, nil }
-func (s *SiteHandler) Delete(name string) error                   { return nil }
-func (s *SiteHandler) List() ([]*v2alpha1.Site, error)            { return nil, nil }
+func (s *SiteHandler) Get(name string, opts GetOptions) (*v2alpha1.Site, error) { return nil, nil }
+func (s *SiteHandler) Delete(name string) error                                 { return nil }
+func (s *SiteHandler) List() ([]*v2alpha1.Site, error)                          { return nil, nil }
diff --git a/pkg/apis/skupper/v2alpha1/types.go b/pkg/apis/skupper/v2alpha1/types.go
index f476abdf..05147a37 100644
--- a/pkg/apis/skupper/v2alpha1/types.go
+++ b/pkg/apis/skupper/v2alpha1/types.go
@@ -486,6 +486,8 @@ type ConnectorSpec struct {
 	Selector        string            `json:"selector,omitempty"`
 	Port            int               `json:"port"`
 	TlsCredentials  string            `json:"tlsCredentials,omitempty"`
+	NoClientAuth    bool              `json:"noClientAuth,omitempty"`
+	VerifyHostname  bool              `json:"verifyHostname,omitempty"`
 	Type            string            `json:"type,omitempty"`
 	IncludeNotReady bool              `json:"includeNotReady,omitempty"`
 	Settings        map[string]string `json:"settings,omitempty"`
diff --git a/pkg/kube/site/attached_connector.go b/pkg/kube/site/attached_connector.go
index 13adec40..d47a2189 100644
--- a/pkg/kube/site/attached_connector.go
+++ b/pkg/kube/site/attached_connector.go
@@ -22,9 +22,9 @@ type ExtendedBindings struct {
 	logger     *slog.Logger
 }
 
-func NewExtendedBindings(controller *kube.Controller) *ExtendedBindings {
+func NewExtendedBindings(controller *kube.Controller, profilePath string) *ExtendedBindings {
 	return &ExtendedBindings{
-		bindings:   site.NewBindings(),
+		bindings:   site.NewBindings(profilePath),
 		connectors: map[string]*AttachedConnector{},
 		controller: controller,
 		logger: slog.New(slog.Default().Handler()).With(
@@ -74,8 +74,9 @@ func (b *ExtendedBindings) Apply(config *qdr.RouterConfig) bool {
 	for _, connector := range b.connectors {
 		connector.updateBridgeConfig(b.bindings.SiteId, &desired)
 	}
-	//TODO: add/remove SslProfiles as necessary
+	b.bindings.AddSslProfiles(config)
 	config.UpdateBridgeConfig(desired)
+	config.RemoveUnreferencedSslProfiles()
 	return true //TODO: can optimise by indicating if no change was required
 }
 
diff --git a/pkg/kube/site/resources/skupper-router-deployment.yaml b/pkg/kube/site/resources/skupper-router-deployment.yaml
index 2dfbfe0d..81b93fdd 100644
--- a/pkg/kube/site/resources/skupper-router-deployment.yaml
+++ b/pkg/kube/site/resources/skupper-router-deployment.yaml
@@ -34,22 +34,8 @@ spec:
     spec:
       containers:
       - env:
-        - name: APPLICATION_NAME
-          value: skupper-router
-        - name: POD_NAMESPACE
-          valueFrom:
-            fieldRef:
-              apiVersion: v1
-              fieldPath: metadata.namespace
-        - name: POD_IP
-          valueFrom:
-            fieldRef:
-              apiVersion: v1
-              fieldPath: status.podIP
-        - name: QDROUTERD_AUTO_MESH_DISCOVERY
-          value: QUERY
         - name: QDROUTERD_CONF
-          value: /etc/skupper-router/config/skrouterd.json
+          value: /etc/skupper-router-certs/skrouterd.json
         - name: QDROUTERD_CONF_TYPE
           value: json
         - name: SKUPPER_SITE_ID
@@ -93,8 +79,6 @@ spec:
         volumeMounts:
         - mountPath: /etc/skupper-router-certs/skupper-amqps/
           name: skupper-local-server
-        - mountPath: /etc/skupper-router/config/
-          name: router-config
         - mountPath: /etc/skupper-router-certs
           name: skupper-router-certs
       - env:
@@ -107,7 +91,7 @@ spec:
           value: {{ .SiteId }}
         - name: SKUPPER_SITE_NAME
           value: {{ .SiteName }}
-        - name: SKUPPER_CONFIG
+        - name: SKUPPER_ROUTER_CONFIG
           value: {{ .Group }}
         - name: SKUPPER_ROUTER_DEPLOYMENT
           value: {{ .Group }}
@@ -127,15 +111,22 @@ spec:
         volumeMounts:
         - mountPath: /etc/skupper-router-certs
           name: skupper-router-certs
+      initContainers:
+      - env:
+        - name: SKUPPER_ROUTER_CONFIG
+          value: {{ .Group }}
+        image: {{ .ConfigSyncImage.Name }}
+        imagePullPolicy: {{ .ConfigSyncImage.PullPolicy }}
+        name: config-init
+        command: ["/app/config-sync", "-init"]
+        volumeMounts:
+        - mountPath: /etc/skupper-router-certs
+          name: skupper-router-certs
       serviceAccount: {{ .ServiceAccount }}
       volumes:
       - name: skupper-local-server
         secret:
           defaultMode: 420
           secretName: skupper-local-server
-      - configMap:
-          defaultMode: 420
-          name: {{ .Group }}
-        name: router-config
       - emptyDir: {}
         name: skupper-router-certs
diff --git a/pkg/kube/site/site.go b/pkg/kube/site/site.go
index 9d2e4952..42de6bae 100644
--- a/pkg/kube/site/site.go
+++ b/pkg/kube/site/site.go
@@ -47,7 +47,7 @@ type Site struct {
 
 func NewSite(namespace string, controller *kube.Controller, certs certificates.CertificateManager, access SecuredAccessFactory) *Site {
 	return &Site{
-		bindings:   NewExtendedBindings(controller),
+		bindings:   NewExtendedBindings(controller, SSL_PROFILE_PATH),
 		namespace:  namespace,
 		controller: controller,
 		links:      map[string]*site.Link{},
@@ -117,7 +117,20 @@ func (s *Site) reconcile(siteDef *skupperv2alpha1.Site) error {
 				Prefix:       "mc",
 				Distribution: "multicast",
 			})
-			rc.SetNormalListeners(SSL_PROFILE_PATH)
+			rc.AddHealthAndMetricsListener(9090)
+			rc.AddListener(qdr.Listener{
+				Name: "amqp",
+				Host: "localhost",
+				Port: 5672,
+			})
+			rc.AddSslProfile(qdr.ConfigureSslProfile("skupper-local-server", SSL_PROFILE_PATH, true))
+			rc.AddListener(qdr.Listener{
+				Name:             "amqps",
+				Port:             5671,
+				SslProfile:       "skupper-local-server",
+				SaslMechanisms:   "EXTERNAL",
+				AuthenticatePeer: true,
+			})
 			routerConfig = &rc
 		}
 		s.initialised = true
diff --git a/pkg/nonkube/api/environment.go b/pkg/nonkube/api/environment.go
index 50620d4f..66951036 100644
--- a/pkg/nonkube/api/environment.go
+++ b/pkg/nonkube/api/environment.go
@@ -12,19 +12,22 @@ type InternalPath string
 type InternalPathProvider func(namespace string, internalPath InternalPath) string
 
 const (
-	ConfigRouterPath            InternalPath = "config/router"
-	CertificatesCaPath          InternalPath = "certificates/ca"
-	CertificatesClientPath      InternalPath = "certificates/client"
-	CertificatesServerPath      InternalPath = "certificates/server"
-	CertificatesLinkPath        InternalPath = "certificates/link"
-	InputCertificatesCaPath     InternalPath = "input/certificates/ca"
-	InputCertificatesClientPath InternalPath = "input/certificates/client"
-	InputCertificatesServerPath InternalPath = "input/certificates/server"
-	InputSiteStatePath          InternalPath = "input/sources"
-	LoadedSiteStatePath         InternalPath = "sources"
-	RuntimeSiteStatePath        InternalPath = "runtime/state"
-	RuntimeTokenPath            InternalPath = "runtime/link"
-	RuntimeScriptsPath          InternalPath = "runtime/scripts"
+	InputCertificatesBasePath   InternalPath = "input/certs"
+	InputCertificatesCaPath     InternalPath = "input/certs/ca"
+	InputCertificatesClientPath InternalPath = "input/certs/client"
+	InputCertificatesServerPath InternalPath = "input/certs/server"
+	InputSiteStatePath          InternalPath = "input/resources"
+	RouterConfigPath            InternalPath = "runtime/router"
+	CertificatesBasePath        InternalPath = "runtime/certs"
+	CertificatesCaPath          InternalPath = "runtime/certs/ca"
+	CertificatesClientPath      InternalPath = "runtime/certs/client"
+	CertificatesServerPath      InternalPath = "runtime/certs/server"
+	CertificatesLinkPath        InternalPath = "runtime/certs/link"
+	RuntimePath                 InternalPath = "runtime"
+	RuntimeSiteStatePath        InternalPath = "runtime/resources"
+	RuntimeTokenPath            InternalPath = "runtime/links"
+	LoadedSiteStatePath         InternalPath = "internal/snapshot"
+	ScriptsPath                 InternalPath = "internal/scripts"
 )
 
 type IdGetter func() int
diff --git a/pkg/nonkube/api/site_state.go b/pkg/nonkube/api/site_state.go
index 80c11af0..4d26b847 100644
--- a/pkg/nonkube/api/site_state.go
+++ b/pkg/nonkube/api/site_state.go
@@ -239,7 +239,7 @@ func (s *SiteState) linkAccessMap() site.RouterAccessMap {
 func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap {
 	linkMap := site.LinkMap{}
 	for name, link := range s.Links {
-		siteLink := site.NewLink(name, path.Join(sslProfileBasePath, "certificates/link"))
+		siteLink := site.NewLink(name, path.Join(sslProfileBasePath, string(CertificatesLinkPath)))
 		link.SetConfigured(nil)
 		siteLink.Update(link)
 		linkMap[name] = siteLink
@@ -247,8 +247,8 @@ func (s *SiteState) linkMap(sslProfileBasePath string) site.LinkMap {
 	return linkMap
 }
 
-func (s *SiteState) bindings() *site.Bindings {
-	b := site.NewBindings()
+func (s *SiteState) bindings(sslProfileBasePath string) *site.Bindings {
+	b := site.NewBindings(path.Join(sslProfileBasePath, "certificates/bindings"))
 	for name, connector := range s.Connectors {
 		connector.SetConfigured(nil)
 		_ = b.UpdateConnector(name, connector)
@@ -289,11 +289,11 @@ func (s *SiteState) ToRouterConfig(sslProfileBasePath string, platform string) q
 		routerConfig.SiteConfig.Platform = "{{.Platform}}"
 	}
 	// LinkAccess
-	s.linkAccessMap().DesiredConfig(nil, path.Join(sslProfileBasePath, "certificates/server")).Apply(&routerConfig)
+	s.linkAccessMap().DesiredConfig(nil, path.Join(sslProfileBasePath, string(CertificatesServerPath))).Apply(&routerConfig)
 	// Link
 	s.linkMap(sslProfileBasePath).Apply(&routerConfig)
 	// Bindings
-	s.bindings().Apply(&routerConfig)
+	s.bindings(sslProfileBasePath).Apply(&routerConfig)
 	// Log (static for now) TODO use site specific options to configure logging
 	routerConfig.SetLogLevel("ROUTER_CORE", "error+")
 
diff --git a/pkg/nonkube/bundle/site_state_renderer.go b/pkg/nonkube/bundle/site_state_renderer.go
index 0f8e3464..37efe815 100644
--- a/pkg/nonkube/bundle/site_state_renderer.go
+++ b/pkg/nonkube/bundle/site_state_renderer.go
@@ -112,13 +112,13 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		},
 		FileMounts: []container.FileMount{
 			{
-				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", "config/router"),
+				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", string(api.RouterConfigPath)),
 				Destination: "/etc/skupper-router/config",
 				Options:     []string{"z"},
 			},
 			{
-				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", "certificates"),
-				Destination: "/etc/skupper-router/certificates",
+				Source:      path.Join("{{.NamespacesPath}}", "{{.Namespace}}", string(api.CertificatesBasePath)),
+				Destination: "/etc/skupper-router/runtime/certs",
 				Options:     []string{"z"},
 			},
 		},
@@ -143,7 +143,7 @@ func (s *SiteStateRenderer) prepareContainers() error {
 
 func (s *SiteStateRenderer) createContainerScript() error {
 	logger := common.NewLogger()
-	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.RuntimeScriptsPath)
+	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.ScriptsPath)
 	scriptContent := containersToShell(s.containers)
 	logger.Debug("writing containers_create.sh", slog.String("path", scriptsPath))
 	err := os.WriteFile(path.Join(scriptsPath, "containers_create.sh"), scriptContent, 0755)
@@ -198,7 +198,7 @@ func (s *SiteStateRenderer) removeSiteFiles() error {
 
 func (s *SiteStateRenderer) createFreePortScript() error {
 	logger := common.NewLogger()
-	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.RuntimeScriptsPath)
+	scriptsPath := api.GetInternalBundleOutputPath(s.siteState.Site.Namespace, api.ScriptsPath)
 	logger.Debug("writing freeport.sh", slog.String("path", scriptsPath))
 	err := os.WriteFile(path.Join(scriptsPath, "router_free_port.py"), []byte(FreePortScript), 0755)
 	if err != nil {
diff --git a/pkg/nonkube/bundle/systemd.go b/pkg/nonkube/bundle/systemd.go
index 74defad0..9b2200f1 100644
--- a/pkg/nonkube/bundle/systemd.go
+++ b/pkg/nonkube/bundle/systemd.go
@@ -21,7 +21,7 @@ func CreateSystemdServices(siteState *api.SiteState) error {
 		"systemd":   common.SystemdServiceTemplate,
 		"container": common.SystemdContainerServiceTemplate,
 	}
-	scriptsPath := api.GetInternalBundleOutputPath(siteState.Site.Namespace, api.RuntimeScriptsPath)
+	scriptsPath := api.GetInternalBundleOutputPath(siteState.Site.Namespace, api.ScriptsPath)
 	for platform, serviceTemplate := range serviceTemplates {
 		var buf = new(bytes.Buffer)
 		parsedTemplate := template.Must(template.New("service").Parse(serviceTemplate))
diff --git a/pkg/nonkube/common/fs_config_renderer.go b/pkg/nonkube/common/fs_config_renderer.go
index bc20108d..d0745792 100644
--- a/pkg/nonkube/common/fs_config_renderer.go
+++ b/pkg/nonkube/common/fs_config_renderer.go
@@ -21,7 +21,7 @@ import (
 
 var (
 	configurationDirectories = []api.InternalPath{
-		api.ConfigRouterPath,
+		api.RouterConfigPath,
 		api.CertificatesCaPath,
 		api.CertificatesClientPath,
 		api.CertificatesServerPath,
@@ -33,20 +33,27 @@ var (
 		api.LoadedSiteStatePath,
 		api.RuntimeSiteStatePath,
 		api.RuntimeTokenPath,
-		api.RuntimeScriptsPath,
+		api.ScriptsPath,
 	}
 	reloadDirectories = []api.InternalPath{
-		api.ConfigRouterPath,
+		api.RouterConfigPath,
 		api.CertificatesClientPath,
 		api.CertificatesServerPath,
 		api.CertificatesLinkPath,
 		api.RuntimeSiteStatePath,
 		api.RuntimeTokenPath,
-		api.RuntimeScriptsPath,
+		api.ScriptsPath,
 		api.LoadedSiteStatePath,
 	}
 )
 
+type InputPathType string
+
+const (
+	InputPathResources InputPathType = "resources"
+	InputPathCerts     InputPathType = "certs"
+)
+
 const (
 	DefaultSslProfileBasePath = "${SSL_PROFILE_BASE_PATH}"
 )
@@ -118,7 +125,7 @@ func (c *FileSystemConfigurationRenderer) Render(siteState *api.SiteState) error
 	// Saving runtime platform
 	if !c.Bundle {
 		content := fmt.Sprintf("platform: %s\n", c.Platform)
-		platformPath := path.Join(outputPath, string(api.RuntimeSiteStatePath), "platform.yaml")
+		platformPath := path.Join(outputPath, string(api.RuntimePath), "platform.yaml")
 		logger.Debug("writing platform", slog.String("platform", c.Platform), slog.String("path", platformPath))
 		err = os.WriteFile(platformPath, []byte(content), 0644)
 		if err != nil {
@@ -160,13 +167,13 @@ func (c *FileSystemConfigurationRenderer) GetOutputPath(siteState *api.SiteState
 	return defaultOutputPathProvider(siteState.Site.Namespace)
 }
 
-func (c *FileSystemConfigurationRenderer) GetInputPath(siteState *api.SiteState) string {
+func (c *FileSystemConfigurationRenderer) GetInputPath(siteState *api.SiteState, dir InputPathType) string {
 	var customSiteHomeProvider = api.GetCustomSiteHome
 	var defaultOutputPathProvider = api.GetDefaultOutputPath
 	if c.customOutputPath != "" {
-		return path.Join(customSiteHomeProvider(siteState.Site, c.customOutputPath), "input")
+		return path.Join(customSiteHomeProvider(siteState.Site, c.customOutputPath), "input", string(dir))
 	}
-	return path.Join(defaultOutputPathProvider(siteState.Site.Namespace), "input")
+	return path.Join(defaultOutputPathProvider(siteState.Site.Namespace), "input", string(dir))
 }
 
 func (c *FileSystemConfigurationRenderer) MarshalSiteStates(loadedSiteState, runtimeSiteState *api.SiteState) error {
@@ -309,7 +316,7 @@ func (c *FileSystemConfigurationRenderer) createRouterConfig(siteState *api.Site
 		return fmt.Errorf("unable to marshal router config: %v", err)
 	}
 	outputPath := c.GetOutputPath(siteState)
-	routerConfigFileName := path.Join(outputPath, string(api.ConfigRouterPath), "skrouterd.json")
+	routerConfigFileName := path.Join(outputPath, string(api.RouterConfigPath), "skrouterd.json")
 	NewLogger().Debug("Writing router configuration", slog.String("path", routerConfigFileName))
 	err = os.WriteFile(routerConfigFileName, []byte(routerConfigJson), 0644)
 	if err != nil {
@@ -413,7 +420,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S
 			secret = *userSecret
 			fmt.Printf("-> User provided %s certificate found: %s\n", purpose, name)
 		}
-		certPath := path.Join(outputPath, "certificates", purpose, name)
+		certPath := path.Join(outputPath, string(api.CertificatesBasePath), purpose, name)
 		err = writeSecretFiles(certPath, &secret)
 		if err != nil {
 			return err
@@ -426,7 +433,7 @@ func (c *FileSystemConfigurationRenderer) createTlsCertificates(siteState *api.S
 		if !ok {
 			return fmt.Errorf("secret %s not found", secretName)
 		}
-		certPath := path.Join(outputPath, "certificates", "link", secretName+"-profile")
+		certPath := path.Join(outputPath, string(api.CertificatesLinkPath), secretName+"-profile")
 		err = writeSecretFiles(certPath, secret)
 		if err != nil {
 			return err
@@ -477,12 +484,12 @@ func (c *FileSystemConfigurationRenderer) loadClientSecret(siteState *api.SiteSt
 }
 
 func (c *FileSystemConfigurationRenderer) loadCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
-	outputPath := c.GetOutputPath(siteState)
+	outputPath := path.Join(c.GetOutputPath(siteState), string(api.CertificatesBasePath))
 	return c.loadCertAsSecretFrom(outputPath, siteState, purpose, name)
 }
 
 func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
-	userInputPath := c.GetInputPath(siteState)
+	userInputPath := path.Join(c.GetInputPath(siteState, InputPathCerts))
 	secret, err := c.loadCertAsSecretFrom(userInputPath, siteState, purpose, name)
 	if err != nil {
 		if os.IsNotExist(err) {
@@ -507,7 +514,7 @@ func (c *FileSystemConfigurationRenderer) loadUserCertAsSecret(siteState *api.Si
 }
 
 func (c *FileSystemConfigurationRenderer) loadCertAsSecretFrom(basePath string, siteState *api.SiteState, purpose, name string) (*corev1.Secret, error) {
-	certPath := path.Join(basePath, "certificates", purpose, name)
+	certPath := path.Join(basePath, purpose, name)
 	var secret *corev1.Secret
 	certDir, err := os.Open(certPath)
 	if err != nil {
diff --git a/pkg/nonkube/common/platform.go b/pkg/nonkube/common/platform.go
index e998016f..778f40b4 100644
--- a/pkg/nonkube/common/platform.go
+++ b/pkg/nonkube/common/platform.go
@@ -26,7 +26,7 @@ func (s *NamespacePlatformLoader) Load(namespace string) (string, error) {
 		namespace = "default"
 	}
 	pathProvider := s.GetPathProvider()
-	runtimePath := pathProvider(namespace, api.RuntimeSiteStatePath)
+	runtimePath := pathProvider(namespace, api.RuntimePath)
 	platformFile, err := os.ReadFile(path.Join(runtimePath, "platform.yaml"))
 	if err != nil {
 		return "", fmt.Errorf("failed to read platform config file for namespace %s: %w", namespace, err)
diff --git a/pkg/nonkube/common/site_state_loader.go b/pkg/nonkube/common/site_state_loader.go
index a7064322..e2c658d0 100644
--- a/pkg/nonkube/common/site_state_loader.go
+++ b/pkg/nonkube/common/site_state_loader.go
@@ -172,7 +172,7 @@ func LoadIntoSiteState(reader *bufio.Reader, siteState *api.SiteState) error {
 }
 
 func LoadRouterConfig(namespace string) (*qdr.RouterConfig, error) {
-	routerConfigPath := api.GetInternalOutputPath(namespace, api.ConfigRouterPath)
+	routerConfigPath := api.GetInternalOutputPath(namespace, api.RouterConfigPath)
 	routerConfigFile := path.Join(routerConfigPath, "skrouterd.json")
 	routerConfigData, err := os.ReadFile(routerConfigFile)
 	if err != nil {
diff --git a/pkg/nonkube/common/startup.go b/pkg/nonkube/common/startup.go
index 440e7327..92404034 100644
--- a/pkg/nonkube/common/startup.go
+++ b/pkg/nonkube/common/startup.go
@@ -66,7 +66,7 @@ func GetStartupScripts(args StartupScriptsArgs, pathProvider api.InternalPathPro
 	if pathProvider == nil {
 		pathProvider = api.GetInternalOutputPath
 	}
-	scripts.path = pathProvider(args.Namespace, api.RuntimeScriptsPath)
+	scripts.path = pathProvider(args.Namespace, api.ScriptsPath)
 	return scripts, nil
 }
 
diff --git a/pkg/nonkube/common/systemd.go b/pkg/nonkube/common/systemd.go
index 5caa64b3..b50647c8 100644
--- a/pkg/nonkube/common/systemd.go
+++ b/pkg/nonkube/common/systemd.go
@@ -52,8 +52,8 @@ type systemdServiceInfo struct {
 func NewSystemdServiceInfo(siteState *api.SiteState, platform string) (SystemdService, error) {
 	site := siteState.Site
 	siteHomePath := api.GetHostSiteHome(site)
-	siteScriptPath := path.Join(siteHomePath, string(api.RuntimeScriptsPath))
-	siteConfigPath := path.Join(siteHomePath, string(api.ConfigRouterPath))
+	siteScriptPath := path.Join(siteHomePath, string(api.ScriptsPath))
+	siteConfigPath := path.Join(siteHomePath, string(api.RouterConfigPath))
 	namespace := site.Namespace
 	if namespace == "" {
 		namespace = "default"
@@ -126,7 +126,7 @@ func (s *systemdServiceInfo) Create() error {
 
 func (s *systemdServiceInfo) getServiceFile() string {
 	if api.IsRunningInContainer() {
-		return path.Join(api.GetInternalOutputPath(s.Site.Namespace, api.RuntimeScriptsPath), s.GetServiceName())
+		return path.Join(api.GetInternalOutputPath(s.Site.Namespace, api.ScriptsPath), s.GetServiceName())
 	}
 	if s.getUid() == 0 {
 		return path.Join(s.rootSystemdBasePath, s.GetServiceName())
diff --git a/pkg/nonkube/compat/site_state_renderer.go b/pkg/nonkube/compat/site_state_renderer.go
index 25f0f114..cab9b754 100644
--- a/pkg/nonkube/compat/site_state_renderer.go
+++ b/pkg/nonkube/compat/site_state_renderer.go
@@ -212,13 +212,13 @@ func (s *SiteStateRenderer) prepareContainers() error {
 		},
 		FileMounts: []container.FileMount{
 			{
-				Source:      path.Join(siteConfigPath, "config/router"),
+				Source:      path.Join(siteConfigPath, string(api.RouterConfigPath)),
 				Destination: "/etc/skupper-router/config",
 				Options:     []string{"z"},
 			},
 			{
-				Source:      path.Join(siteConfigPath, "certificates"),
-				Destination: "/etc/skupper-router/certificates",
+				Source:      path.Join(siteConfigPath, string(api.CertificatesBasePath)),
+				Destination: "/etc/skupper-router/runtime/certs",
 				Options:     []string{"z"},
 			},
 		},
diff --git a/pkg/qdr/amqp_mgmt.go b/pkg/qdr/amqp_mgmt.go
index 647dd666..4811d9f2 100644
--- a/pkg/qdr/amqp_mgmt.go
+++ b/pkg/qdr/amqp_mgmt.go
@@ -119,7 +119,7 @@ func (r Record) AsRecord(field string) Record {
 }
 
 func asTcpEndpoint(record Record) TcpEndpoint {
-	return TcpEndpoint{
+	endpoint := TcpEndpoint{
 		Name:       record.AsString("name"),
 		Host:       record.AsString("host"),
 		Port:       record.AsString("port"),
@@ -128,6 +128,12 @@ func asTcpEndpoint(record Record) TcpEndpoint {
 		SslProfile: record.AsString("sslProfile"),
 		ProcessID:  record.AsString("processId"),
 	}
+	if value, ok := record["verifyHostname"]; ok {
+		if verify, ok := value.(bool); ok {
+			endpoint.VerifyHostname = &verify
+		}
+	}
+	return endpoint
 }
 
 func asConnection(record Record) Connection {
diff --git a/pkg/qdr/qdr.go b/pkg/qdr/qdr.go
index e762399e..1463e810 100644
--- a/pkg/qdr/qdr.go
+++ b/pkg/qdr/qdr.go
@@ -64,25 +64,9 @@ func InitialConfig(id string, siteId string, version string, edge bool, helloAge
 	return config
 }
 
-func InitialConfigSkupperRouter(id string, siteId string, version string, edge bool, helloAge int, options types.RouterOptions, path string) RouterConfig {
-	routerConfig := InitialConfig(id, siteId, version, edge, helloAge)
-	routerConfig.Metadata.DataConnectionCount = options.DataConnectionCount
-
-	if options.Logging != nil {
-		ConfigureRouterLogging(&routerConfig, options.Logging)
-	}
-	routerConfig.AddAddress(Address{
-		Prefix:       "mc",
-		Distribution: "multicast",
-	})
-	routerConfig.SetListenersForMode(options, path)
-	return routerConfig
-}
-
-func (r *RouterConfig) SetNormalListeners(path string) {
-	r.Listeners = map[string]Listener{}
+func (r *RouterConfig) AddHealthAndMetricsListener(port int32) {
 	r.AddListener(Listener{
-		Port:        9090,
+		Port:        port,
 		Role:        "normal",
 		Http:        true,
 		HttpRootDir: "disabled",
@@ -90,31 +74,6 @@ func (r *RouterConfig) SetNormalListeners(path string) {
 		Healthz:     true,
 		Metrics:     true,
 	})
-	r.AddListener(Listener{
-		Name: "amqp",
-		Host: "localhost",
-		Port: types.AmqpDefaultPort,
-	})
-	r.AddSslProfile(ConfigureSslProfile("skupper-amqps", path, true))
-	r.AddListener(Listener{
-		Name:             "amqps",
-		Port:             types.AmqpsDefaultPort,
-		SslProfile:       "skupper-amqps",
-		SaslMechanisms:   "EXTERNAL",
-		AuthenticatePeer: true,
-	})
-	r.AddSslProfile(ConfigureSslProfile(types.ServiceClientSecret, path, false))
-}
-
-func (r *RouterConfig) SetListenersForMode(options types.RouterOptions, path string) {
-	r.SetNormalListeners(path)
-	if r.Metadata.Mode != ModeEdge {
-		r.AddSslProfile(ConfigureSslProfile(types.InterRouterProfile, path, true))
-		listeners := []Listener{InteriorListener(options), EdgeListener(options)}
-		for _, listener := range listeners {
-			r.AddListener(listener)
-		}
-	}
 }
 
 func NewBridgeConfig() BridgeConfig {
@@ -210,6 +169,17 @@ func (r *RouterConfig) RemoveSslProfile(name string) bool {
 	}
 }
 
+func (r *RouterConfig) RemoveUnreferencedSslProfiles() bool {
+	unreferenced := r.UnreferencedSslProfiles()
+	changed := false
+	for _, profile := range unreferenced {
+		if r.RemoveSslProfile(profile.Name) {
+			changed = true
+		}
+	}
+	return changed
+}
+
 func (r *RouterConfig) UnreferencedSslProfiles() map[string]SslProfile {
 	results := map[string]SslProfile{}
 	for _, profile := range r.SslProfiles {
@@ -824,9 +794,19 @@ func equivalentHost(a string, b string) bool {
 	}
 }
 
+func (a TcpEndpoint) equivalentVerifyHostname(b TcpEndpoint) bool {
+	if a.VerifyHostname == nil {
+		return b.VerifyHostname == nil || *b.VerifyHostname == true
+	}
+	if b.VerifyHostname == nil {
+		return a.VerifyHostname == nil || *a.VerifyHostname == true
+	}
+	return *a.VerifyHostname == *b.VerifyHostname
+}
+
 func (a TcpEndpoint) Equivalent(b TcpEndpoint) bool {
 	if !equivalentHost(a.Host, b.Host) || a.Port != b.Port || a.Address != b.Address ||
-		a.SiteId != b.SiteId || a.ProcessID != b.ProcessID {
+		a.SiteId != b.SiteId || a.ProcessID != b.ProcessID || !a.equivalentVerifyHostname(b) {
 		return false
 	}
 	return true
diff --git a/pkg/site/bindings.go b/pkg/site/bindings.go
index 046c5986..99dcd722 100644
--- a/pkg/site/bindings.go
+++ b/pkg/site/bindings.go
@@ -22,20 +22,22 @@ type ConnectorFunction func(*skupperv2alpha1.Connector) *skupperv2alpha1.Connect
 type ListenerFunction func(*skupperv2alpha1.Listener) *skupperv2alpha1.Listener
 
 type Bindings struct {
-	SiteId     string
-	connectors map[string]*skupperv2alpha1.Connector
-	listeners  map[string]*skupperv2alpha1.Listener
-	handler    BindingEventHandler
-	configure  struct {
+	SiteId      string
+	profilePath string
+	connectors  map[string]*skupperv2alpha1.Connector
+	listeners   map[string]*skupperv2alpha1.Listener
+	handler     BindingEventHandler
+	configure   struct {
 		listener  ListenerConfiguration
 		connector ConnectorConfiguration
 	}
 }
 
-func NewBindings() *Bindings {
+func NewBindings(profilePath string) *Bindings {
 	bindings := &Bindings{
-		connectors: map[string]*skupperv2alpha1.Connector{},
-		listeners:  map[string]*skupperv2alpha1.Listener{},
+		profilePath: profilePath,
+		connectors:  map[string]*skupperv2alpha1.Connector{},
+		listeners:   map[string]*skupperv2alpha1.Listener{},
 	}
 	bindings.configure.listener = UpdateBridgeConfigForListener
 	bindings.configure.connector = UpdateBridgeConfigForConnector
@@ -138,11 +140,11 @@ func (b *Bindings) updateListener(latest *skupperv2alpha1.Listener) qdr.ConfigUp
 	name := latest.ObjectMeta.Name
 	existing, ok := b.listeners[name]
 	b.listeners[name] = latest
-	if b.handler != nil {
-		b.handler.ListenerUpdated(latest)
-	}
 
 	if !ok || !reflect.DeepEqual(existing.Spec, latest.Spec) {
+		if b.handler != nil {
+			b.handler.ListenerUpdated(latest)
+		}
 		return b
 	}
 	return nil
@@ -174,8 +176,41 @@ func (b *Bindings) ToBridgeConfig() qdr.BridgeConfig {
 	return config
 }
 
+func (b *Bindings) AddSslProfiles(config *qdr.RouterConfig) bool {
+	profiles := map[string]qdr.SslProfile{}
+	for _, c := range b.connectors {
+		if c.Spec.TlsCredentials != "" {
+			if c.Spec.NoClientAuth {
+				//if only ca is used, need to qualify the profile to ensure that it does not collide with
+				// use of the same secret where client auth *is* required
+				name := getSslProfileName(c)
+				if _, ok := profiles[name]; !ok {
+					profiles[name] = qdr.ConfigureSslProfile(name, b.profilePath, false)
+				}
+			} else {
+				if _, ok := profiles[c.Spec.TlsCredentials]; !ok {
+					profiles[c.Spec.TlsCredentials] = qdr.ConfigureSslProfile(c.Spec.TlsCredentials, b.profilePath, true)
+				}
+			}
+		}
+	}
+	for _, l := range b.listeners {
+		if _, ok := profiles[l.Spec.TlsCredentials]; l.Spec.TlsCredentials != "" && !ok {
+			profiles[l.Spec.TlsCredentials] = qdr.ConfigureSslProfile(l.Spec.TlsCredentials, b.profilePath, true)
+		}
+	}
+	changed := false
+	for _, profile := range profiles {
+		if config.AddSslProfile(profile) {
+			changed = true
+		}
+	}
+	return changed
+}
+
 func (b *Bindings) Apply(config *qdr.RouterConfig) bool {
-	//TODO: add/remove SslProfiles as necessary
+	b.AddSslProfiles(config)
 	config.UpdateBridgeConfig(b.ToBridgeConfig())
+	config.RemoveUnreferencedSslProfiles()
 	return true //TODO: can optimise by indicating if no change was required
 }
diff --git a/pkg/site/connector.go b/pkg/site/connector.go
index 32e77c24..7c90a7a8 100644
--- a/pkg/site/connector.go
+++ b/pkg/site/connector.go
@@ -21,15 +21,32 @@ func updateBridgeConfigForConnector(siteId string, connector *skupperv2alpha1.Co
 	name := connector.Name + "-" + host
 	if connector.Spec.Type == "tcp" || connector.Spec.Type == "" {
 		config.AddTcpConnector(qdr.TcpEndpoint{
-			Name:       name,
-			SiteId:     siteId,
-			Host:       host,
-			Port:       strconv.Itoa(connector.Spec.Port),
-			Address:    connector.Spec.RoutingKey,
-			SslProfile: connector.Spec.TlsCredentials,
-			ProcessID:  processID,
-			//TODO:
-			//VerifyHostname
+			Name:           name,
+			SiteId:         siteId,
+			Host:           host,
+			Port:           strconv.Itoa(connector.Spec.Port),
+			Address:        connector.Spec.RoutingKey,
+			SslProfile:     getSslProfileName(connector),
+			ProcessID:      processID,
+			VerifyHostname: getVerifyHostname(connector),
 		})
 	}
 }
+
+func getSslProfileName(connector *skupperv2alpha1.Connector) string {
+	if connector.Spec.TlsCredentials == "" {
+		return ""
+	}
+	if connector.Spec.NoClientAuth {
+		return connector.Spec.TlsCredentials + "-profile"
+	}
+	return connector.Spec.TlsCredentials
+}
+
+func getVerifyHostname(connector *skupperv2alpha1.Connector) *bool {
+	if connector.Spec.TlsCredentials == "" {
+		return nil
+	}
+	value := connector.Spec.VerifyHostname
+	return &value
+}
diff --git a/pkg/site/listener.go b/pkg/site/listener.go
index 7f211c61..1a6ea656 100644
--- a/pkg/site/listener.go
+++ b/pkg/site/listener.go
@@ -21,8 +21,6 @@ func UpdateBridgeConfigForListenerWithHostAndPort(siteId string, listener *skupp
 			Port:       strconv.Itoa(port),
 			Address:    listener.Spec.RoutingKey,
 			SslProfile: listener.Spec.TlsCredentials,
-			//TODO:
-			//VerifyHostname
 		})
 	}
 }
diff --git a/pkg/site/siteconfig.go b/pkg/site/siteconfig.go
deleted file mode 100644
index fa53e48e..00000000
--- a/pkg/site/siteconfig.go
+++ /dev/null
@@ -1,861 +0,0 @@
-package site
-
-import (
-	"fmt"
-	"regexp"
-	"strconv"
-	"strings"
-	"time"
-
-	"github.com/skupperproject/skupper/pkg/qdr"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-
-	"github.com/skupperproject/skupper/api/types"
-)
-
-const (
-	// core options
-	SiteConfigNameKey                string = "name"
-	SiteConfigRouterModeKey          string = "router-mode"
-	SiteConfigIngressKey             string = "ingress"
-	SiteConfigIngressAnnotationsKey  string = "ingress-annotations"
-	SiteConfigIngressHostKey         string = "ingress-host"
-	SiteConfigCreateNetworkPolicyKey string = "create-network-policy"
-	SiteConfigRoutersKey             string = "routers"
-	SiteConfigRunAsUserKey           string = "run-as-user"
-	SiteConfigRunAsGroupKey          string = "run-as-group"
-	SiteConfigClusterPermissionsKey  string = "cluster-permissions"
-	SiteConfigServiceAccountKey      string = "service-account"
-
-	// console options
-	SiteConfigConsoleKey               string = "console"
-	SiteConfigConsoleAuthenticationKey string = "console-authentication"
-	SiteConfigConsoleUserKey           string = "console-user"
-	SiteConfigConsolePasswordKey       string = "console-password"
-	SiteConfigConsoleIngressKey        string = "console-ingress"
-	SiteConfigRestAPIKey               string = "rest-api"
-
-	// flow collector options
-	SiteConfigFlowCollectorKey            string = "flow-collector"
-	SiteConfigFlowCollectorRecordTtlKey   string = "flow-collector-record-ttl"
-	SiteConfigFlowCollectorCpuKey         string = "flow-collector-cpu"
-	SiteConfigFlowCollectorMemoryKey      string = "flow-collector-memory"
-	SiteConfigFlowCollectorCpuLimitKey    string = "flow-collector-cpu-limit"
-	SiteConfigFlowCollectorMemoryLimitKey string = "flow-collector-memory-limit"
-
-	// prometheus server options
-	SiteConfigPrometheusExternalServerKey       string = "prometheus-external-server"
-	SiteConfigPrometheusServerAuthenticationKey string = "prometheus-server-authentication"
-	SiteConfigPrometheusServerUserKey           string = "prometheus-server-user"
-	SiteConfigPrometheusServerPasswordKey       string = "prometheus-server-password"
-	SiteConfigPrometheusServerCpuKey            string = "prometheus-server-cpu"
-	SiteConfigPrometheusServerMemoryKey         string = "prometheus-server-memory"
-	SiteConfigPrometheusServerCpuLimitKey       string = "prometheus-server-cpu-limit"
-	SiteConfigPrometheusServerMemoryLimitKey    string = "prometheus-server-memory-limit"
-	SiteConfigPrometheusServerPodAnnotationsKey string = "prometheus-server-pod-annotations"
-
-	// router options
-	SiteConfigRouterConsoleKey             string = "router-console"
-	SiteConfigRouterLoggingKey             string = "router-logging"
-	SiteConfigRouterCpuKey                 string = "router-cpu"
-	SiteConfigRouterMemoryKey              string = "router-memory"
-	SiteConfigRouterCpuLimitKey            string = "router-cpu-limit"
-	SiteConfigRouterMemoryLimitKey         string = "router-memory-limit"
-	SiteConfigRouterAffinityKey            string = "router-pod-affinity"
-	SiteConfigRouterAntiAffinityKey        string = "router-pod-antiaffinity"
-	SiteConfigRouterNodeSelectorKey        string = "router-node-selector"
-	SiteConfigRouterMaxFrameSizeKey        string = "xp-router-max-frame-size"
-	SiteConfigRouterMaxSessionFramesKey    string = "xp-router-max-session-frames"
-	SiteConfigRouterDataConnectionCountKey string = "router-data-connection-count"
-	SiteConfigRouterIngressHostKey         string = "router-ingress-host"
-	SiteConfigRouterServiceAnnotationsKey  string = "router-service-annotations"
-	SiteConfigRouterPodAnnotationsKey      string = "router-pod-annotations"
-	SiteConfigRouterLoadBalancerIp         string = "router-load-balancer-ip"
-	SiteConfigRouterDisableMutualTLS       string = "router-disable-mutual-tls"
-
-	// controller options
-	SiteConfigServiceControllerKey            string = "service-controller"
-	SiteConfigServiceSyncKey                  string = "service-sync"
-	SiteConfigServiceSyncSiteTtlKey           string = "service-sync-site-ttl"
-	SiteConfigControllerCpuKey                string = "controller-cpu"
-	SiteConfigControllerMemoryKey             string = "controller-memory"
-	SiteConfigControllerCpuLimitKey           string = "controller-cpu-limit"
-	SiteConfigControllerMemoryLimitKey        string = "controller-memory-limit"
-	SiteConfigControllerAffinityKey           string = "controller-pod-affinity"
-	SiteConfigControllerAntiAffinityKey       string = "controller-pod-antiaffinity"
-	SiteConfigControllerNodeSelectorKey       string = "controller-node-selector"
-	SiteConfigControllerIngressHostKey        string = "controller-ingress-host"
-	SiteConfigControllerServiceAnnotationsKey string = "controller-service-annotations"
-	SiteConfigControllerPodAnnotationsKey     string = "controller-pod-annotations"
-	SiteConfigControllerLoadBalancerIp        string = "controller-load-balancer-ip"
-
-	// config-sync options
-	SiteConfigConfigSyncCpuKey         string = "config-sync-cpu"
-	SiteConfigConfigSyncMemoryKey      string = "config-sync-memory"
-	SiteConfigConfigSyncCpuLimitKey    string = "config-sync-cpu-limit"
-	SiteConfigConfigSyncMemoryLimitKey string = "config-sync-memory-limit"
-
-	SiteConfigEnableSkupperEventsKey string = "enable-skupper-events"
-
-	//labels:
-	ValidRfc1123Label                = `^(` + ValidRfc1123LabelKey + `)+=(` + ValidRfc1123LabelValue + `)+(,(` + ValidRfc1123LabelKey + `)+=(` + ValidRfc1123LabelValue + `)+)*$`
-	ValidRfc1123LabelKey             = "[a-z0-9]([-._a-z0-9]*[a-z0-9])*"
-	ValidRfc1123LabelValue           = "[a-zA-Z0-9]([-._a-zA-Z0-9]*[a-zA-Z0-9])*"
-	DefaultSkupperExtraLabels string = ""
-)
-
-func WriteSiteConfig(spec types.SiteConfigSpec, namespace string) (*corev1.ConfigMap, error) {
-	var errs []string
-	siteConfig := &corev1.ConfigMap{
-		TypeMeta: metav1.TypeMeta{
-			APIVersion: "v1",
-			Kind:       "ConfigMap",
-		},
-		ObjectMeta: metav1.ObjectMeta{
-			Name:        types.SiteConfigMapName,
-			Annotations: spec.Annotations,
-			Labels:      spec.Labels,
-		},
-		Data: map[string]string{
-			SiteConfigNameKey:                  namespace,
-			SiteConfigRouterModeKey:            string(types.TransportModeInterior),
-			SiteConfigServiceControllerKey:     "true",
-			SiteConfigServiceSyncKey:           "true",
-			SiteConfigConsoleKey:               "false",
-			SiteConfigFlowCollectorKey:         "false",
-			SiteConfigClusterPermissionsKey:    "false",
-			SiteConfigRouterConsoleKey:         "false",
-			SiteConfigRouterLoggingKey:         "",
-			SiteConfigConsoleAuthenticationKey: string(types.ConsoleAuthModeInternal),
-			SiteConfigConsoleUserKey:           "",
-			SiteConfigConsolePasswordKey:       "",
-			SiteConfigIngressKey:               types.IngressLoadBalancerString,
-		},
-	}
-	if spec.SkupperName != "" {
-		siteConfig.Data[SiteConfigNameKey] = spec.SkupperName
-	}
-	if spec.RouterMode != "" {
-		siteConfig.Data[SiteConfigRouterModeKey] = spec.RouterMode
-	}
-	if spec.Routers != 0 {
-		siteConfig.Data[SiteConfigRoutersKey] = strconv.Itoa(spec.Routers)
-	}
-	if !spec.EnableController {
-		siteConfig.Data[SiteConfigServiceControllerKey] = "false"
-	}
-	if !spec.EnableServiceSync {
-		siteConfig.Data[SiteConfigServiceSyncKey] = "false"
-	}
-	if spec.SiteTtl != 0 {
-		siteConfig.Data[SiteConfigServiceSyncSiteTtlKey] = spec.SiteTtl.String()
-	}
-	if spec.EnableConsole {
-		siteConfig.Data[SiteConfigConsoleKey] = "true"
-	}
-	if spec.EnableRestAPI {
-		siteConfig.Data[SiteConfigRestAPIKey] = "true"
-	}
-	if spec.EnableFlowCollector {
-		siteConfig.Data[SiteConfigFlowCollectorKey] = "true"
-	}
-	if spec.AuthMode != "" {
-		siteConfig.Data[SiteConfigConsoleAuthenticationKey] = spec.AuthMode
-	}
-	if spec.User != "" {
-		siteConfig.Data[SiteConfigConsoleUserKey] = spec.User
-	}
-	if spec.Password != "" {
-		siteConfig.Data[SiteConfigConsolePasswordKey] = spec.Password
-	}
-	if spec.Ingress != "" {
-		siteConfig.Data[SiteConfigIngressKey] = spec.Ingress
-	}
-	if len(spec.IngressAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.IngressAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigIngressAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if spec.ConsoleIngress != "" {
-		siteConfig.Data[SiteConfigConsoleIngressKey] = spec.ConsoleIngress
-	}
-	if spec.IngressHost != "" {
-		siteConfig.Data[SiteConfigIngressHostKey] = spec.IngressHost
-	}
-	if spec.CreateNetworkPolicy {
-		siteConfig.Data[SiteConfigCreateNetworkPolicyKey] = "true"
-	}
-	if spec.RunAsUser != 0 {
-		siteConfig.Data[SiteConfigRunAsUserKey] = strconv.FormatInt(spec.RunAsUser, 10)
-	}
-	if spec.RunAsGroup != 0 {
-		siteConfig.Data[SiteConfigRunAsGroupKey] = strconv.FormatInt(spec.RunAsGroup, 10)
-	}
-	if spec.EnableClusterPermissions {
-		siteConfig.Data[SiteConfigClusterPermissionsKey] = "true"
-	}
-	if spec.Router.Logging != nil {
-		siteConfig.Data[SiteConfigRouterLoggingKey] = qdr.RouterLogConfigToString(spec.Router.Logging)
-	}
-	if spec.Router.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.Router.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterCpuKey, spec.Router.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterCpuKey] = spec.Router.Cpu
-		}
-	}
-	if spec.Router.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.Router.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterMemoryKey, spec.Router.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterMemoryKey] = spec.Router.Memory
-		}
-	}
-	if spec.Router.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Router.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterCpuLimitKey, spec.Router.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterCpuLimitKey] = spec.Router.CpuLimit
-		}
-	}
-	if spec.Router.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Router.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigRouterMemoryLimitKey, spec.Router.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigRouterMemoryLimitKey] = spec.Router.MemoryLimit
-		}
-	}
-	if spec.Router.Affinity != "" {
-		siteConfig.Data[SiteConfigRouterAffinityKey] = spec.Router.Affinity
-	}
-	if spec.Router.AntiAffinity != "" {
-		siteConfig.Data[SiteConfigRouterAntiAffinityKey] = spec.Router.AntiAffinity
-	}
-	if spec.Router.NodeSelector != "" {
-		siteConfig.Data[SiteConfigRouterNodeSelectorKey] = spec.Router.NodeSelector
-	}
-	if spec.Router.IngressHost != "" {
-		siteConfig.Data[SiteConfigRouterIngressHostKey] = spec.Router.IngressHost
-	}
-	if spec.Router.MaxFrameSize != types.RouterMaxFrameSizeDefault {
-		siteConfig.Data[SiteConfigRouterMaxFrameSizeKey] = strconv.Itoa(spec.Router.MaxFrameSize)
-	}
-	if spec.Router.MaxSessionFrames != types.RouterMaxSessionFramesDefault {
-		siteConfig.Data[SiteConfigRouterMaxSessionFramesKey] = strconv.Itoa(spec.Router.MaxSessionFrames)
-	}
-	if spec.Router.DataConnectionCount != "" {
-		siteConfig.Data[SiteConfigRouterDataConnectionCountKey] = spec.Router.DataConnectionCount
-	}
-	if len(spec.Router.ServiceAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Router.ServiceAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigRouterServiceAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if len(spec.Router.PodAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Router.PodAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigRouterPodAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if spec.Router.LoadBalancerIp != "" {
-		siteConfig.Data[SiteConfigRouterLoadBalancerIp] = spec.Router.LoadBalancerIp
-	}
-	if spec.Router.DisableMutualTLS {
-		siteConfig.Data[SiteConfigRouterDisableMutualTLS] = "true"
-	}
-	if spec.Controller.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerCpuKey, spec.Controller.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerCpuKey] = spec.Controller.Cpu
-		}
-	}
-	if spec.Controller.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerMemoryKey, spec.Controller.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerMemoryKey] = spec.Controller.Memory
-		}
-	}
-	if spec.Controller.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerCpuLimitKey, spec.Controller.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerCpuLimitKey] = spec.Controller.CpuLimit
-		}
-	}
-	if spec.Controller.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.Controller.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigControllerMemoryLimitKey, spec.Controller.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigControllerMemoryLimitKey] = spec.Controller.MemoryLimit
-		}
-	}
-	if spec.Controller.Affinity != "" {
-		siteConfig.Data[SiteConfigControllerAffinityKey] = spec.Controller.Affinity
-	}
-	if spec.Controller.AntiAffinity != "" {
-		siteConfig.Data[SiteConfigControllerAntiAffinityKey] = spec.Controller.AntiAffinity
-	}
-	if spec.Controller.NodeSelector != "" {
-		siteConfig.Data[SiteConfigControllerNodeSelectorKey] = spec.Controller.NodeSelector
-	}
-	if spec.Controller.IngressHost != "" {
-		siteConfig.Data[SiteConfigControllerIngressHostKey] = spec.Controller.IngressHost
-	}
-	if len(spec.Controller.ServiceAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Controller.ServiceAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigControllerServiceAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if len(spec.Controller.PodAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.Controller.PodAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigControllerPodAnnotationsKey] = strings.Join(annotations, ",")
-	}
-	if spec.Controller.LoadBalancerIp != "" {
-		siteConfig.Data[SiteConfigControllerLoadBalancerIp] = spec.Controller.LoadBalancerIp
-	}
-
-	if spec.ConfigSync.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncCpuKey, spec.ConfigSync.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncCpuKey] = spec.ConfigSync.Cpu
-		}
-	}
-	if spec.ConfigSync.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncMemoryKey, spec.ConfigSync.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncMemoryKey] = spec.ConfigSync.Memory
-		}
-	}
-	if spec.ConfigSync.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncCpuLimitKey, spec.ConfigSync.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncCpuLimitKey] = spec.ConfigSync.CpuLimit
-		}
-	}
-	if spec.ConfigSync.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.ConfigSync.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigConfigSyncMemoryLimitKey, spec.ConfigSync.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigConfigSyncMemoryLimitKey] = spec.ConfigSync.MemoryLimit
-		}
-	}
-
-	if spec.FlowCollector.FlowRecordTtl != 0 {
-		siteConfig.Data[SiteConfigFlowCollectorRecordTtlKey] = spec.FlowCollector.FlowRecordTtl.String()
-	}
-	if spec.FlowCollector.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorCpuKey, spec.FlowCollector.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorCpuKey] = spec.FlowCollector.Cpu
-		}
-	}
-	if spec.FlowCollector.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorMemoryKey, spec.FlowCollector.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorMemoryKey] = spec.FlowCollector.Memory
-		}
-	}
-	if spec.FlowCollector.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorCpuLimitKey, spec.FlowCollector.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorCpuLimitKey] = spec.FlowCollector.CpuLimit
-		}
-	}
-	if spec.FlowCollector.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.FlowCollector.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigFlowCollectorMemoryLimitKey, spec.FlowCollector.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigFlowCollectorMemoryLimitKey] = spec.FlowCollector.MemoryLimit
-		}
-	}
-
-	if spec.EnableSkupperEvents {
-		siteConfig.Data[SiteConfigEnableSkupperEventsKey] = "true"
-	} else {
-		siteConfig.Data[SiteConfigEnableSkupperEventsKey] = "false"
-	}
-
-	if spec.PrometheusServer.ExternalServer != "" {
-		siteConfig.Data[SiteConfigPrometheusExternalServerKey] = spec.PrometheusServer.ExternalServer
-	}
-	if spec.PrometheusServer.AuthMode != "" {
-		siteConfig.Data[SiteConfigPrometheusServerAuthenticationKey] = spec.PrometheusServer.AuthMode
-	}
-	if spec.PrometheusServer.User != "" {
-		siteConfig.Data[SiteConfigPrometheusServerUserKey] = spec.PrometheusServer.User
-	}
-	if spec.PrometheusServer.Password != "" {
-		siteConfig.Data[SiteConfigPrometheusServerPasswordKey] = spec.PrometheusServer.Password
-	}
-	if spec.PrometheusServer.Cpu != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.Cpu); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerCpuKey, spec.PrometheusServer.Cpu, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerCpuKey] = spec.PrometheusServer.Cpu
-		}
-	}
-	if spec.PrometheusServer.Memory != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.Memory); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerMemoryKey, spec.PrometheusServer.Memory, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerMemoryKey] = spec.PrometheusServer.Memory
-		}
-	}
-	if spec.PrometheusServer.CpuLimit != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.CpuLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerCpuLimitKey, spec.PrometheusServer.CpuLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerCpuLimitKey] = spec.PrometheusServer.CpuLimit
-		}
-	}
-	if spec.PrometheusServer.MemoryLimit != "" {
-		if _, err := resource.ParseQuantity(spec.PrometheusServer.MemoryLimit); err != nil {
-			errs = append(errs, fmt.Sprintf("Invalid value for %s %q: %s", SiteConfigPrometheusServerMemoryLimitKey, spec.PrometheusServer.MemoryLimit, err))
-		} else {
-			siteConfig.Data[SiteConfigPrometheusServerMemoryLimitKey] = spec.PrometheusServer.MemoryLimit
-		}
-	}
-	if len(spec.PrometheusServer.PodAnnotations) > 0 {
-		var annotations []string
-		for key, value := range spec.PrometheusServer.PodAnnotations {
-			annotations = append(annotations, key+"="+value)
-		}
-		siteConfig.Data[SiteConfigPrometheusServerPodAnnotationsKey] = strings.Join(annotations, ",")
-	}
-
-	// TODO: allow Replicas to be set through skupper-site configmap?
-	if !spec.SiteControlled {
-		if siteConfig.ObjectMeta.Labels == nil {
-			siteConfig.ObjectMeta.Labels = map[string]string{}
-		}
-		siteConfig.ObjectMeta.Labels[types.SiteControllerIgnore] = "true"
-	}
-	if DefaultSkupperExtraLabels != "" {
-		labelRegex := regexp.MustCompile(ValidRfc1123Label)
-		if labelRegex.MatchString(DefaultSkupperExtraLabels) {
-			s := strings.Split(DefaultSkupperExtraLabels, ",")
-			for _, kv := range s {
-				parts := strings.Split(kv, "=")
-				if len(parts) > 1 {
-					siteConfig.ObjectMeta.Labels[parts[0]] = parts[1]
-				}
-			}
-		}
-	}
-	if len(errs) > 0 {
-		return siteConfig, fmt.Errorf(strings.Join(errs, ", "))
-	}
-	return siteConfig, nil
-}
-
-func ReadSiteConfig(siteConfig *corev1.ConfigMap, defaultIngress string) (*types.SiteConfig, error) {
-	return ReadSiteConfigFrom(&siteConfig.ObjectMeta, &siteConfig.TypeMeta, siteConfig.Data, defaultIngress)
-}
-
-func ReadSiteConfigFrom(siteConfig *metav1.ObjectMeta, typeMeta *metav1.TypeMeta, data map[string]string, defaultIngress string) (*types.SiteConfig, error) {
-	var errs []string
-	var result types.SiteConfig
-	result.Spec.SkupperNamespace = siteConfig.Namespace
-	// TODO: what should the defaults be for name, namespace
-	if skupperName, ok := data[SiteConfigNameKey]; ok {
-		result.Spec.SkupperName = skupperName
-	} else {
-		result.Spec.SkupperName = siteConfig.Namespace
-	}
-	if routerMode, ok := data[SiteConfigRouterModeKey]; ok {
-		result.Spec.RouterMode = routerMode
-	} else {
-		// check for deprecated key
-		if isEdge, ok := data["edge"]; ok {
-			if isEdge == "true" {
-				result.Spec.RouterMode = string(types.TransportModeEdge)
-			} else {
-				result.Spec.RouterMode = string(types.TransportModeInterior)
-			}
-		} else {
-			result.Spec.RouterMode = string(types.TransportModeInterior)
-		}
-	}
-	if routers, ok := data[SiteConfigRoutersKey]; ok {
-		result.Spec.Routers, _ = strconv.Atoi(routers)
-	}
-	if enableController, ok := data[SiteConfigServiceControllerKey]; ok {
-		result.Spec.EnableController, _ = strconv.ParseBool(enableController)
-	} else {
-		result.Spec.EnableController = true
-	}
-	if enableServiceSync, ok := data[SiteConfigServiceSyncKey]; ok {
-		result.Spec.EnableServiceSync, _ = strconv.ParseBool(enableServiceSync)
-	} else {
-		result.Spec.EnableServiceSync = true
-	}
-	if value, ok := data[SiteConfigServiceSyncSiteTtlKey]; ok {
-		ttl, err := time.ParseDuration(value)
-		if err == nil {
-			result.Spec.SiteTtl = ttl
-		}
-	}
-	if enableConsole, ok := data[SiteConfigConsoleKey]; ok {
-		result.Spec.EnableConsole, _ = strconv.ParseBool(enableConsole)
-	} else {
-		result.Spec.EnableConsole = false
-	}
-	if enableFlowCollector, ok := data[SiteConfigFlowCollectorKey]; ok {
-		result.Spec.EnableFlowCollector, _ = strconv.ParseBool(enableFlowCollector)
-	} else {
-		result.Spec.EnableFlowCollector = false
-	}
-	if value, ok := data[SiteConfigFlowCollectorRecordTtlKey]; ok {
-		ttl, err := time.ParseDuration(value)
-		if err == nil {
-			result.Spec.FlowCollector.FlowRecordTtl = ttl
-		}
-	} else {
-		result.Spec.FlowCollector.FlowRecordTtl = types.DefaultFlowTimeoutDuration
-	}
-	if value, ok := data[SiteConfigRestAPIKey]; ok {
-		result.Spec.EnableRestAPI, _ = strconv.ParseBool(value)
-	} else {
-		result.Spec.EnableRestAPI = result.Spec.EnableConsole
-	}
-	if enableClusterPermissions, ok := data[SiteConfigClusterPermissionsKey]; ok {
-		result.Spec.EnableClusterPermissions, _ = strconv.ParseBool(enableClusterPermissions)
-	} else {
-		result.Spec.EnableClusterPermissions = false
-	}
-	if createNetworkPolicy, ok := data[SiteConfigCreateNetworkPolicyKey]; ok {
-		result.Spec.CreateNetworkPolicy, _ = strconv.ParseBool(createNetworkPolicy)
-	} else {
-		result.Spec.CreateNetworkPolicy = false
-	}
-	if authMode, ok := data[SiteConfigConsoleAuthenticationKey]; ok {
-		result.Spec.AuthMode = authMode
-	} else {
-		result.Spec.AuthMode = string(types.ConsoleAuthModeInternal)
-	}
-	if user, ok := data[SiteConfigConsoleUserKey]; ok {
-		result.Spec.User = user
-	} else {
-		result.Spec.User = ""
-	}
-	if password, ok := data[SiteConfigConsolePasswordKey]; ok {
-		result.Spec.Password = password
-	} else {
-		result.Spec.Password = ""
-	}
-	if ingress, ok := data[SiteConfigIngressKey]; ok {
-		result.Spec.Ingress = ingress
-	} else {
-		// check for deprecated key
-		if clusterLocal, ok := data["cluster-local"]; ok && clusterLocal == "true" {
-			result.Spec.Ingress = types.IngressNoneString
-		} else {
-			result.Spec.Ingress = defaultIngress
-		}
-	}
-	if ingressAnnotations, ok := data[SiteConfigIngressAnnotationsKey]; ok {
-		result.Spec.IngressAnnotations = asMap(splitWithEscaping(ingressAnnotations, ',', '\\'))
-	}
-	if consoleIngress, ok := data[SiteConfigConsoleIngressKey]; ok {
-		result.Spec.ConsoleIngress = consoleIngress
-	}
-	if ingressHost, ok := data[SiteConfigIngressHostKey]; ok {
-		result.Spec.IngressHost = ingressHost
-	}
-	if runAsUser, ok := data[SiteConfigRunAsUserKey]; ok {
-		result.Spec.RunAsUser, _ = strconv.ParseInt(runAsUser, 10, 64)
-	}
-	if runAsGroup, ok := data[SiteConfigRunAsGroupKey]; ok {
-		result.Spec.RunAsGroup, _ = strconv.ParseInt(runAsGroup, 10, 64)
-	}
-	// TODO: allow Replicas to be set through skupper-site configmap?
-	if siteConfig.Labels == nil {
-		result.Spec.SiteControlled = true
-	} else if ignore, ok := siteConfig.Labels[types.SiteControllerIgnore]; ok {
-		siteIgnore, _ := strconv.ParseBool(ignore)
-		result.Spec.SiteControlled = !siteIgnore
-	} else {
-		result.Spec.SiteControlled = true
-	}
-	result.Reference.UID = string(siteConfig.UID)
-	result.Reference.Name = siteConfig.Name
-	result.Reference.Kind = typeMeta.Kind
-	result.Reference.APIVersion = typeMeta.APIVersion
-	if routerLogging, ok := data[SiteConfigRouterLoggingKey]; ok && routerLogging != "" {
-		logConf, err := qdr.ParseRouterLogConfig(routerLogging)
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.Logging = logConf
-		}
-	} else {
-		logConf, err := qdr.ParseRouterLogConfig("ROUTER_CORE:error+")
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.Logging = logConf
-		}
-	}
-	if routerCpu, ok := data[SiteConfigRouterCpuKey]; ok && routerCpu != "" {
-		result.Spec.Router.Cpu = routerCpu
-	}
-	if routerMemory, ok := data[SiteConfigRouterMemoryKey]; ok && routerMemory != "" {
-		result.Spec.Router.Memory = routerMemory
-	}
-	if routerCpuLimit, ok := data[SiteConfigRouterCpuLimitKey]; ok && routerCpuLimit != "" {
-		result.Spec.Router.CpuLimit = routerCpuLimit
-	}
-	if routerMemoryLimit, ok := data[SiteConfigRouterMemoryLimitKey]; ok && routerMemoryLimit != "" {
-		result.Spec.Router.MemoryLimit = routerMemoryLimit
-	}
-	if routerNodeSelector, ok := data[SiteConfigRouterNodeSelectorKey]; ok && routerNodeSelector != "" {
-		result.Spec.Router.NodeSelector = routerNodeSelector
-	}
-	if routerAffinity, ok := data[SiteConfigRouterAffinityKey]; ok && routerAffinity != "" {
-		result.Spec.Router.Affinity = routerAffinity
-	}
-	if routerAntiAffinity, ok := data[SiteConfigRouterAntiAffinityKey]; ok && routerAntiAffinity != "" {
-		result.Spec.Router.AntiAffinity = routerAntiAffinity
-	}
-	if routerIngressHost, ok := data[SiteConfigRouterIngressHostKey]; ok && routerIngressHost != "" {
-		result.Spec.Router.IngressHost = routerIngressHost
-	}
-
-	if routerMaxFrameSize, ok := data[SiteConfigRouterMaxFrameSizeKey]; ok && routerMaxFrameSize != "" {
-		val, err := strconv.Atoi(routerMaxFrameSize)
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.MaxFrameSize = val
-		}
-	} else {
-		result.Spec.Router.MaxFrameSize = types.RouterMaxFrameSizeDefault
-	}
-	if routerMaxSessionFrames, ok := data[SiteConfigRouterMaxSessionFramesKey]; ok && routerMaxSessionFrames != "" {
-		val, err := strconv.Atoi(routerMaxSessionFrames)
-		if err != nil {
-			errs = append(errs, err.Error())
-		} else {
-			result.Spec.Router.MaxSessionFrames = val
-		}
-	} else {
-		result.Spec.Router.MaxSessionFrames = types.RouterMaxSessionFramesDefault
-	}
-	if routerDataConnectionCount, ok := data[SiteConfigRouterDataConnectionCountKey]; ok && routerDataConnectionCount != "" {
-		result.Spec.Router.DataConnectionCount = routerDataConnectionCount
-	}
-
-	if routerServiceAnnotations, ok := data[SiteConfigRouterServiceAnnotationsKey]; ok {
-		result.Spec.Router.ServiceAnnotations = asMap(splitWithEscaping(routerServiceAnnotations, ',', '\\'))
-	}
-	if routerPodAnnotations, ok := data[SiteConfigRouterPodAnnotationsKey]; ok {
-		result.Spec.Router.PodAnnotations = asMap(splitWithEscaping(routerPodAnnotations, ',', '\\'))
-	}
-	if routerServiceLoadBalancerIp, ok := data[SiteConfigRouterLoadBalancerIp]; ok {
-		result.Spec.Router.LoadBalancerIp = routerServiceLoadBalancerIp
-	}
-	if value, ok := data[SiteConfigRouterDisableMutualTLS]; ok {
-		result.Spec.Router.DisableMutualTLS, _ = strconv.ParseBool(value)
-	}
-
-	if controllerCpu, ok := data[SiteConfigControllerCpuKey]; ok && controllerCpu != "" {
-		result.Spec.Controller.Cpu = controllerCpu
-	}
-	if controllerMemory, ok := data[SiteConfigControllerMemoryKey]; ok && controllerMemory != "" {
-		result.Spec.Controller.Memory = controllerMemory
-	}
-	if controllerCpuLimit, ok := data[SiteConfigControllerCpuLimitKey]; ok && controllerCpuLimit != "" {
-		result.Spec.Controller.CpuLimit = controllerCpuLimit
-	}
-	if controllerMemoryLimit, ok := data[SiteConfigControllerMemoryLimitKey]; ok && controllerMemoryLimit != "" {
-		result.Spec.Controller.MemoryLimit = controllerMemoryLimit
-	}
-	if controllerNodeSelector, ok := data[SiteConfigControllerNodeSelectorKey]; ok && controllerNodeSelector != "" {
-		result.Spec.Controller.NodeSelector = controllerNodeSelector
-	}
-	if controllerAffinity, ok := data[SiteConfigControllerAffinityKey]; ok && controllerAffinity != "" {
-		result.Spec.Controller.Affinity = controllerAffinity
-	}
-	if controllerAntiAffinity, ok := data[SiteConfigControllerAntiAffinityKey]; ok && controllerAntiAffinity != "" {
-		result.Spec.Controller.AntiAffinity = controllerAntiAffinity
-	}
-	if controllerIngressHost, ok := data[SiteConfigControllerIngressHostKey]; ok && controllerIngressHost != "" {
-		result.Spec.Controller.IngressHost = controllerIngressHost
-	}
-	if controllerServiceAnnotations, ok := data[SiteConfigControllerServiceAnnotationsKey]; ok {
-		result.Spec.Controller.ServiceAnnotations = asMap(splitWithEscaping(controllerServiceAnnotations, ',', '\\'))
-	}
-	if controllerPodAnnotations, ok := data[SiteConfigControllerPodAnnotationsKey]; ok {
-		result.Spec.Controller.PodAnnotations = asMap(splitWithEscaping(controllerPodAnnotations, ',', '\\'))
-	}
-	if controllerServiceLoadBalancerIp, ok := data[SiteConfigControllerLoadBalancerIp]; ok {
-		result.Spec.Controller.LoadBalancerIp = controllerServiceLoadBalancerIp
-	}
-
-	if configSyncCpu, ok := data[SiteConfigConfigSyncCpuKey]; ok && configSyncCpu != "" {
-		result.Spec.ConfigSync.Cpu = configSyncCpu
-	}
-	if configSyncMemory, ok := data[SiteConfigConfigSyncMemoryKey]; ok && configSyncMemory != "" {
-		result.Spec.ConfigSync.Memory = configSyncMemory
-	}
-	if configSyncCpuLimit, ok := data[SiteConfigConfigSyncCpuLimitKey]; ok && configSyncCpuLimit != "" {
-		result.Spec.ConfigSync.CpuLimit = configSyncCpuLimit
-	}
-	if configSyncMemoryLimit, ok := data[SiteConfigConfigSyncMemoryLimitKey]; ok && configSyncMemoryLimit != "" {
-		result.Spec.ConfigSync.MemoryLimit = configSyncMemoryLimit
-	}
-
-	if value, ok := data[SiteConfigEnableSkupperEventsKey]; ok {
-		result.Spec.EnableSkupperEvents, _ = strconv.ParseBool(value)
-	}
-
-	if flowCollectorCpu, ok := data[SiteConfigFlowCollectorCpuKey]; ok && flowCollectorCpu != "" {
-		result.Spec.FlowCollector.Cpu = flowCollectorCpu
-	}
-	if flowCollectorMemory, ok := data[SiteConfigFlowCollectorMemoryKey]; ok && flowCollectorMemory != "" {
-		result.Spec.FlowCollector.Memory = flowCollectorMemory
-	}
-	if flowCollectorCpuLimit, ok := data[SiteConfigFlowCollectorCpuLimitKey]; ok && flowCollectorCpuLimit != "" {
-		result.Spec.FlowCollector.CpuLimit = flowCollectorCpuLimit
-	}
-	if flowCollectorMemoryLimit, ok := data[SiteConfigFlowCollectorMemoryLimitKey]; ok && flowCollectorMemoryLimit != "" {
-		result.Spec.FlowCollector.MemoryLimit = flowCollectorMemoryLimit
-	}
-
-	if externalServer, ok := data[SiteConfigPrometheusExternalServerKey]; ok {
-		result.Spec.PrometheusServer.ExternalServer = externalServer
-	} else {
-		result.Spec.PrometheusServer.ExternalServer = ""
-	}
-	if authMode, ok := data[SiteConfigPrometheusServerAuthenticationKey]; ok {
-		result.Spec.PrometheusServer.AuthMode = authMode
-	} else {
-		result.Spec.PrometheusServer.AuthMode = string(types.PrometheusAuthModeTls)
-	}
-	if user, ok := data[SiteConfigPrometheusServerUserKey]; ok {
-		result.Spec.PrometheusServer.User = user
-	} else {
-		result.Spec.PrometheusServer.User = ""
-	}
-	if password, ok := data[SiteConfigPrometheusServerPasswordKey]; ok {
-		result.Spec.PrometheusServer.Password = password
-	} else {
-		result.Spec.PrometheusServer.Password = ""
-	}
-	if prometheusCpu, ok := data[SiteConfigPrometheusServerCpuKey]; ok && prometheusCpu != "" {
-		result.Spec.PrometheusServer.Cpu = prometheusCpu
-	}
-	if prometheusMemory, ok := data[SiteConfigPrometheusServerMemoryKey]; ok && prometheusMemory != "" {
-		result.Spec.PrometheusServer.Memory = prometheusMemory
-	}
-	if prometheusCpuLimit, ok := data[SiteConfigPrometheusServerCpuLimitKey]; ok && prometheusCpuLimit != "" {
-		result.Spec.PrometheusServer.CpuLimit = prometheusCpuLimit
-	}
-	if prometheusMemoryLimit, ok := data[SiteConfigPrometheusServerMemoryLimitKey]; ok && prometheusMemoryLimit != "" {
-		result.Spec.PrometheusServer.MemoryLimit = prometheusMemoryLimit
-	}
-	if prometheusPodAnnotations, ok := data[SiteConfigPrometheusServerPodAnnotationsKey]; ok {
-		result.Spec.PrometheusServer.PodAnnotations = asMap(splitWithEscaping(prometheusPodAnnotations, ',', '\\'))
-	}
-
-	annotations, labels := GetSiteAnnotationsAndLabels(siteConfig)
-	result.Spec.Annotations = annotations
-	result.Spec.Labels = labels
-	if len(errs) > 0 {
-		return &result, fmt.Errorf(strings.Join(errs, ", "))
-	}
-	return &result, nil
-}
-
-func GetSiteAnnotationsAndLabels(siteConfig *metav1.ObjectMeta) (map[string]string, map[string]string) {
-	annotationExclusions := []string{}
-	labelExclusions := []string{}
-	annotations := map[string]string{}
-	for key, value := range siteConfig.Annotations {
-		if key == types.AnnotationExcludes {
-			annotationExclusions = strings.Split(value, ",")
-		} else if key == types.LabelExcludes {
-			labelExclusions = strings.Split(value, ",")
-		} else {
-			annotations[key] = value
-		}
-	}
-	for _, key := range annotationExclusions {
-		delete(annotations, key)
-	}
-	labels := map[string]string{}
-	for key, value := range siteConfig.Labels {
-		if key != types.SiteControllerIgnore {
-			labels[key] = value
-		}
-	}
-	for _, key := range labelExclusions {
-		delete(labels, key)
-	}
-	return annotations, labels
-}
-
-func UpdateLogging(config types.SiteConfigSpec, configmap *corev1.ConfigMap) bool {
-	latestLogging := qdr.RouterLogConfigToString(config.Router.Logging)
-	if configmap.Data[SiteConfigRouterLoggingKey] != latestLogging {
-		configmap.Data[SiteConfigRouterLoggingKey] = latestLogging
-		return true
-	}
-	return false
-}
-
-func UpdateForCollectorEnabled(configmap *corev1.ConfigMap) {
-	configmap.Data[SiteConfigConsoleKey] = "true"
-	configmap.Data[SiteConfigFlowCollectorKey] = "true"
-}
-
-func splitWithEscaping(s string, separator, escape byte) []string {
-	var token []byte
-	var tokens []string
-	for i := 0; i < len(s); i++ {
-		if s[i] == separator {
-			tokens = append(tokens, strings.TrimSpace(string(token)))
-			token = token[:0]
-		} else if s[i] == escape && i+1 < len(s) {
-			i++
-			token = append(token, s[i])
-		} else {
-			token = append(token, s[i])
-		}
-	}
-	tokens = append(tokens, strings.TrimSpace(string(token)))
-	return tokens
-}
-
-func asMap(entries []string) map[string]string {
-	result := map[string]string{}
-	for _, entry := range entries {
-		parts := strings.Split(entry, "=")
-		if len(parts) > 1 {
-			result[parts[0]] = parts[1]
-		} else {
-			result[parts[0]] = ""
-		}
-	}
-	return result
-}


Test patch:
diff --git a/pkg/kube/site/site_test.go b/pkg/kube/site/site_test.go
index 1d2a3c7f..80d2090a 100644
--- a/pkg/kube/site/site_test.go
+++ b/pkg/kube/site/site_test.go
@@ -1032,7 +1032,7 @@ func newSiteMocks(namespace string, k8sObjects []runtime.Object, skupperObjects
 	controller := kube.NewController("test", client)
 	newSite := &Site{
 		controller: controller,
-		bindings:   NewExtendedBindings(controller),
+		bindings:   NewExtendedBindings(controller, ""),
 		links:      make(map[string]*site1.Link),
 		errors:     make(map[string]string),
 		linkAccess: make(map[string]*skupperv2alpha1.RouterAccess),
@@ -1058,7 +1058,6 @@ func createRouterConfigMock(s *Site) error {
 		Prefix:       "mc",
 		Distribution: "multicast",
 	})
-	rc.SetNormalListeners(SSL_PROFILE_PATH)
 
 	err := s.createRouterConfig(&rc)
 	if err != nil {
diff --git a/pkg/nonkube/api/site_state_test.go b/pkg/nonkube/api/site_state_test.go
index 5a187d63..9c194af5 100644
--- a/pkg/nonkube/api/site_state_test.go
+++ b/pkg/nonkube/api/site_state_test.go
@@ -110,7 +110,7 @@ func TestSiteState_ToRouterConfig(t *testing.T) {
 			}
 			assert.Equal(t, len(rolesFound), 3, "expecting normal, inter-router and edge, found: %s", rolesFound)
 			assert.Equal(t, len(routerConfig.Connectors), 1)
-			assert.Equal(t, len(routerConfig.SslProfiles), 3)
+			assert.Equal(t, len(routerConfig.SslProfiles), 6)
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["link-access-one"].CaCertFile, sslProfileBasePath))
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["link-one-profile"].CaCertFile, sslProfileBasePath))
 			assert.Assert(t, strings.HasPrefix(routerConfig.SslProfiles["local-access-one"].CaCertFile, sslProfileBasePath))
diff --git a/pkg/nonkube/common/fs_config_renderer_test.go b/pkg/nonkube/common/fs_config_renderer_test.go
index 54d1f2e3..519d3ae4 100644
--- a/pkg/nonkube/common/fs_config_renderer_test.go
+++ b/pkg/nonkube/common/fs_config_renderer_test.go
@@ -41,7 +41,7 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 	fsConfigRenderer.customOutputPath = customOutputPath
 	assert.Assert(t, fsConfigRenderer.Render(ss))
 	customOutputPath = fsConfigRenderer.GetOutputPath(ss)
-	for _, dirName := range []string{"certificates", "config", "sources", "runtime"} {
+	for _, dirName := range []string{"input", "runtime", "internal"} {
 		file, err := os.Stat(path.Join(customOutputPath, dirName))
 		assert.Assert(t, err)
 		assert.Assert(t, file.IsDir())
@@ -50,38 +50,38 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 		t.Skipf("The %s environment variable is set to: %s", types.ENV_PLATFORM, envPlatform)
 	}
 	expectedFiles := []string{
-		"config/router/skrouterd.json",
-		"certificates/ca/skupper-site-ca/tls.crt",
-		"certificates/ca/skupper-site-ca/tls.key",
-		"certificates/ca/skupper-service-ca/tls.crt",
-		"certificates/ca/skupper-service-ca/tls.key",
-		"certificates/client/client-link-access-one/tls.crt",
-		"certificates/client/client-link-access-one/tls.key",
-		"certificates/client/client-link-access-one/ca.crt",
-		"certificates/server/link-access-one/tls.crt",
-		"certificates/server/link-access-one/tls.key",
-		"certificates/server/link-access-one/ca.crt",
-		"certificates/server/listener-one-credentials/tls.crt",
-		"certificates/server/listener-one-credentials/tls.key",
-		"certificates/server/listener-one-credentials/ca.crt",
-		"certificates/server/listener-two-credentials/tls.crt",
-		"certificates/server/listener-two-credentials/tls.key",
-		"certificates/server/listener-two-credentials/ca.crt",
-		"certificates/server/connector-one-credentials/tls.key",
-		"certificates/server/connector-one-credentials/ca.crt",
-		"certificates/server/connector-one-credentials/tls.crt",
-		"certificates/link/link-one-profile/ca.crt",
-		"certificates/link/link-one-profile/tls.crt",
-		"certificates/link/link-one-profile/tls.key",
-		"runtime/state/platform.yaml",
-		"runtime/link/link-link-access-one-127.0.0.1.yaml",
+		string(api.RouterConfigPath) + "/skrouterd.json",
+		string(api.CertificatesCaPath) + "/skupper-site-ca/tls.crt",
+		string(api.CertificatesCaPath) + "/skupper-site-ca/tls.key",
+		string(api.CertificatesCaPath) + "/skupper-service-ca/tls.crt",
+		string(api.CertificatesCaPath) + "/skupper-service-ca/tls.key",
+		string(api.CertificatesClientPath) + "/client-link-access-one/tls.crt",
+		string(api.CertificatesClientPath) + "/client-link-access-one/tls.key",
+		string(api.CertificatesClientPath) + "/client-link-access-one/ca.crt",
+		string(api.CertificatesServerPath) + "/link-access-one/tls.crt",
+		string(api.CertificatesServerPath) + "/link-access-one/tls.key",
+		string(api.CertificatesServerPath) + "/link-access-one/ca.crt",
+		string(api.CertificatesServerPath) + "/listener-one-credentials/tls.crt",
+		string(api.CertificatesServerPath) + "/listener-one-credentials/tls.key",
+		string(api.CertificatesServerPath) + "/listener-one-credentials/ca.crt",
+		string(api.CertificatesServerPath) + "/listener-two-credentials/tls.crt",
+		string(api.CertificatesServerPath) + "/listener-two-credentials/tls.key",
+		string(api.CertificatesServerPath) + "/listener-two-credentials/ca.crt",
+		string(api.CertificatesServerPath) + "/connector-one-credentials/tls.key",
+		string(api.CertificatesServerPath) + "/connector-one-credentials/ca.crt",
+		string(api.CertificatesServerPath) + "/connector-one-credentials/tls.crt",
+		string(api.CertificatesLinkPath) + "/link-one-profile/ca.crt",
+		string(api.CertificatesLinkPath) + "/link-one-profile/tls.crt",
+		string(api.CertificatesLinkPath) + "/link-one-profile/tls.key",
+		string(api.RuntimePath) + "/platform.yaml",
+		string(api.RuntimeTokenPath) + "/link-link-access-one-127.0.0.1.yaml",
 	}
 	if !addInputCertificates {
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-localhost.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-localhost.yaml")
 	} else {
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-10.0.0.1.yaml")
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-10.0.0.2.yaml")
-		expectedFiles = append(expectedFiles, "runtime/link/link-link-access-one-fake.domain.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-10.0.0.1.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-10.0.0.2.yaml")
+		expectedFiles = append(expectedFiles, string(api.RuntimeTokenPath)+"/link-link-access-one-fake.domain.yaml")
 	}
 	for _, fileName := range expectedFiles {
 		fs, err := os.Stat(path.Join(customOutputPath, fileName))
@@ -95,12 +95,12 @@ func testFileSystemConfigurationRendererRender(t *testing.T, addInputCertificate
 }
 
 func compareCertificates(t *testing.T, customOutputPath string) {
-	caPath := path.Join(customOutputPath, "certificates/ca/skupper-site-ca")
-	serverPath := path.Join(customOutputPath, "certificates/server/link-access-one")
-	clientPath := path.Join(customOutputPath, "certificates/client/client-link-access-one")
-	inputCaPath := path.Join(customOutputPath, "input/certificates/ca/skupper-site-ca")
-	inputServerPath := path.Join(customOutputPath, "input/certificates/server/link-access-one")
-	inputClientPath := path.Join(customOutputPath, "input/certificates/client/client-link-access-one")
+	caPath := path.Join(customOutputPath, string(api.CertificatesCaPath), "skupper-site-ca")
+	serverPath := path.Join(customOutputPath, string(api.CertificatesServerPath), "link-access-one")
+	clientPath := path.Join(customOutputPath, string(api.CertificatesClientPath), "client-link-access-one")
+	inputCaPath := path.Join(customOutputPath, string(api.InputCertificatesCaPath), "skupper-site-ca")
+	inputServerPath := path.Join(customOutputPath, string(api.InputCertificatesServerPath), "link-access-one")
+	inputClientPath := path.Join(customOutputPath, string(api.InputCertificatesClientPath), "client-link-access-one")
 	pathsToCompare := map[string]string{
 		caPath:     inputCaPath,
 		serverPath: inputServerPath,
@@ -128,9 +128,9 @@ func createInputCertificates(t *testing.T, customOutputPath string) {
 	client := certs.GenerateSecret("fake-client-cert", "fake-client-cert", "", &ca)
 
 	// paths for each provided certificate
-	caPath := path.Join(customOutputPath, "namespaces/default/input/certificates/ca/skupper-site-ca")
-	serverPath := path.Join(customOutputPath, "namespaces/default/input/certificates/server/link-access-one")
-	clientPath := path.Join(customOutputPath, "namespaces/default/input/certificates/client/client-link-access-one")
+	caPath := path.Join(customOutputPath, "namespaces/default", string(api.InputCertificatesCaPath), "skupper-site-ca")
+	serverPath := path.Join(customOutputPath, "namespaces/default", string(api.InputCertificatesServerPath), "link-access-one")
+	clientPath := path.Join(customOutputPath, "namespaces/default", string(api.InputCertificatesClientPath), "client-link-access-one")
 	certsMap := map[string]corev1.Secret{
 		caPath:     ca,
 		serverPath: server,
diff --git a/pkg/nonkube/common/systemd_test.go b/pkg/nonkube/common/systemd_test.go
index c3387f8b..8252aac0 100644
--- a/pkg/nonkube/common/systemd_test.go
+++ b/pkg/nonkube/common/systemd_test.go
@@ -8,6 +8,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/skupperproject/skupper/pkg/nonkube/api"
 	"github.com/skupperproject/skupper/pkg/utils"
 	"gotest.tools/assert"
 )
@@ -35,8 +36,8 @@ func TestSystemdService(t *testing.T) {
 			assert.Assert(t, err)
 			assert.Equal(t, systemdService.GetServiceName(), "skupper-default.service")
 			systemdServiceImpl := systemdService.(*systemdServiceInfo)
-			assert.Equal(t, systemdServiceImpl.SiteScriptPath, path.Join(outputPath, "namespaces/default/runtime/scripts"))
-			assert.Equal(t, systemdServiceImpl.SiteConfigPath, path.Join(outputPath, "namespaces/default/config/router"))
+			assert.Equal(t, systemdServiceImpl.SiteScriptPath, path.Join(outputPath, "namespaces/default", string(api.ScriptsPath)))
+			assert.Equal(t, systemdServiceImpl.SiteConfigPath, path.Join(outputPath, "namespaces/default", string(api.RouterConfigPath)))
 			systemdServiceImpl.command = func(name string, arg ...string) *exec.Cmd {
 				assert.Assert(t, utils.StringSliceContains(arg, "--user") == (uid != 0))
 				t.Logf("mocking command: %s %s", name, strings.Join(arg, " "))
diff --git a/pkg/qdr/qdr_test.go b/pkg/qdr/qdr_test.go
index 7b05be23..d233b7ba 100644
--- a/pkg/qdr/qdr_test.go
+++ b/pkg/qdr/qdr_test.go
@@ -47,35 +47,6 @@ func TestInitialConfig(t *testing.T) {
 	}
 }
 
-func TestInitialConfigSkupperRouter(t *testing.T) {
-	routerOptions := types.RouterOptions{
-		MaxFrameSize:     1518,
-		LoadBalancerIp:   "1.2.3.4",
-		DisableMutualTLS: true,
-		Logging: []types.RouterLogConfig{
-			{
-				Module: "PROTOCOL",
-				Level:  "trace",
-			},
-		},
-	}
-	path := "/tmp/skupper-router"
-	routerConfig := InitialConfigSkupperRouter("foo", "bar", "1.2.3", false, 10, routerOptions, path)
-
-	if routerConfig.Metadata.Id != "foo" {
-		t.Errorf("Invalid id, expected 'foo' got %q", routerConfig.Metadata.Id)
-	}
-	if len(routerConfig.LogConfig) != 1 {
-		t.Errorf("Expect one log entry")
-	}
-	if routerConfig.Addresses["mc"].Distribution != "multicast" {
-		t.Errorf("Expect address to configured as multicast")
-	}
-	if routerConfig.IsEdge() {
-		t.Errorf("Expected interior mode")
-	}
-}
-
 func TestAddRemoveListener(t *testing.T) {
 	config := InitialConfig("foo", "bar", "undefined", true, 3)
 	config.AddListener(Listener{
diff --git a/pkg/site/bindings_test.go b/pkg/site/bindings_test.go
index a7d6c572..5e8476fa 100644
--- a/pkg/site/bindings_test.go
+++ b/pkg/site/bindings_test.go
@@ -10,29 +10,34 @@ import (
 )
 
 func TestBindings_Apply(t *testing.T) {
+	vFalse := false
 	type fields struct {
-		SiteId     string
-		connectors map[string]*skupperv2alpha1.Connector
-		listeners  map[string]*skupperv2alpha1.Listener
+		path                   string
+		SiteId                 string
+		connectors             []*skupperv2alpha1.Connector
+		listeners              []*skupperv2alpha1.Listener
+		connectorConfiguration ConnectorConfiguration
+		listenerConfiguration  ListenerConfiguration
+		connectorFunction      ConnectorFunction
+		listenerFunction       ListenerFunction
 	}
-	type args struct {
-		tcpListeners  map[string]qdr.TcpEndpoint
-		tcpConnectors map[string]qdr.TcpEndpoint
+	type expected struct {
+		tcpListeners  qdr.TcpEndpointMap
+		tcpConnectors qdr.TcpEndpointMap
 		sslProfiles   map[string]qdr.SslProfile
 	}
 	tests := []struct {
-		name   string
-		fields fields
-		args   args
-		want   bool
+		name     string
+		fields   fields
+		config   *qdr.RouterConfig
+		expected expected
 	}{
 		{
 			name: "bind a tcp listener",
 			fields: fields{
-				SiteId:     "site-1",
-				connectors: make(map[string]*skupperv2alpha1.Connector),
-				listeners: map[string]*skupperv2alpha1.Listener{
-					"listener1": &skupperv2alpha1.Listener{
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
 						ObjectMeta: v1.ObjectMeta{
 							Name:      "listener1",
 							Namespace: "test",
@@ -46,21 +51,35 @@ func TestBindings_Apply(t *testing.T) {
 					},
 				},
 			},
-			args: args{
-				tcpListeners:  map[string]qdr.TcpEndpoint{},
-				tcpConnectors: map[string]qdr.TcpEndpoint{},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:    "listener1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "echo:9090",
+						SiteId:  "site-1",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
 				sslProfiles:   map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 		{
 			name: "bind a tcp connector",
 			fields: fields{
 				SiteId: "site-1",
-				connectors: map[string]*skupperv2alpha1.Connector{
-					"connector1": &skupperv2alpha1.Connector{
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
 						ObjectMeta: v1.ObjectMeta{
-							Name:      "listener1",
+							Name:      "connector1",
 							Namespace: "test",
 						},
 						Spec: skupperv2alpha1.ConnectorSpec{
@@ -71,99 +90,486 @@ func TestBindings_Apply(t *testing.T) {
 						},
 					},
 				},
-				listeners: make(map[string]*skupperv2alpha1.Listener),
 			},
-			args: args{
-				tcpListeners:  map[string]qdr.TcpEndpoint{},
-				tcpConnectors: map[string]qdr.TcpEndpoint{},
-				sslProfiles:   map[string]qdr.SslProfile{},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:    "connector1-10.10.10.1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "echo:9090",
+						SiteId:  "site-1",
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 		{
 			name: "unbind a tcp listener",
 			fields: fields{
-				SiteId:     "site-1",
-				connectors: make(map[string]*skupperv2alpha1.Connector),
-				listeners:  make(map[string]*skupperv2alpha1.Listener),
+				SiteId: "site-1",
 			},
-			args: args{
-				tcpListeners: map[string]qdr.TcpEndpoint{
-					"listener1": qdr.TcpEndpoint{
-						Name:   "listener1",
-						Host:   "10.10.10.1",
-						Port:   "9090",
-						SiteId: "site-1",
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners: map[string]qdr.TcpEndpoint{
+						"listener1": qdr.TcpEndpoint{
+							Name:   "listener1",
+							Host:   "10.10.10.1",
+							Port:   "9090",
+							SiteId: "site-1",
+						},
 					},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
 				},
-				tcpConnectors: map[string]qdr.TcpEndpoint{},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners:  qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{},
 				sslProfiles:   map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 		{
 			name: "unbind a tcp connector",
 			fields: fields{
-				SiteId:     "site-1",
-				connectors: make(map[string]*skupperv2alpha1.Connector),
-				listeners:  make(map[string]*skupperv2alpha1.Listener),
+				SiteId: "site-1",
 			},
-			args: args{
-				tcpListeners: map[string]qdr.TcpEndpoint{},
-				tcpConnectors: map[string]qdr.TcpEndpoint{
-					"connector1": qdr.TcpEndpoint{
-						Name:   "connector1",
-						Host:   "10.10.10.1",
-						Port:   "9090",
-						SiteId: "site-1",
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners: map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{
+						"connector1": qdr.TcpEndpoint{
+							Name:   "connector1",
+							Host:   "10.10.10.1",
+							Port:   "9090",
+							SiteId: "site-1",
+						},
+					},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners:  qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles:   map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "bind a tcp listener with ssl enabled",
+			fields: fields{
+				path:   "/etc/foo/",
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey:     "echo:9090",
+							Host:           "10.10.10.1",
+							Port:           9090,
+							Type:           "tcp",
+							TlsCredentials: "my-credentials",
+						},
+					},
+				},
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:       "listener1",
+						Host:       "10.10.10.1",
+						Port:       "9090",
+						Address:    "echo:9090",
+						SiteId:     "site-1",
+						SslProfile: "my-credentials",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles: map[string]qdr.SslProfile{
+					"my-credentials": {
+						Name:           "my-credentials",
+						CertFile:       "/etc/foo/my-credentials/tls.crt",
+						PrivateKeyFile: "/etc/foo/my-credentials/tls.key",
+						CaCertFile:     "/etc/foo/my-credentials/ca.crt",
+					},
+				},
+			},
+		},
+		{
+			name: "bind a tcp connector with ssl enabled",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey:     "echo:9090",
+							Host:           "10.10.10.1",
+							Port:           9090,
+							Type:           "tcp",
+							TlsCredentials: "a-secret",
+						},
+					},
+				},
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:           "connector1-10.10.10.1",
+						Host:           "10.10.10.1",
+						Port:           "9090",
+						Address:        "echo:9090",
+						SiteId:         "site-1",
+						SslProfile:     "a-secret",
+						VerifyHostname: &vFalse,
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{
+					"a-secret": {
+						Name:           "a-secret",
+						CertFile:       "a-secret/tls.crt",
+						PrivateKeyFile: "a-secret/tls.key",
+						CaCertFile:     "a-secret/ca.crt",
+					},
+				},
+			},
+		},
+		{
+			name: "bind a tcp connector with ssl enabled but no client auth",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey:     "echo:9090",
+							Host:           "10.10.10.1",
+							Port:           9090,
+							Type:           "tcp",
+							TlsCredentials: "a-secret",
+							NoClientAuth:   true,
+						},
+					},
+				},
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:           "connector1-10.10.10.1",
+						Host:           "10.10.10.1",
+						Port:           "9090",
+						Address:        "echo:9090",
+						SiteId:         "site-1",
+						SslProfile:     "a-secret-profile",
+						VerifyHostname: &vFalse,
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{
+					"a-secret-profile": {
+						Name:       "a-secret-profile",
+						CaCertFile: "a-secret-profile/ca.crt",
+					},
+				},
+			},
+		},
+		{
+			name: "configure connector for pods",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				connectorConfiguration: getPodConnectorConfiguration([]skupperv2alpha1.PodDetails{
+					{
+						UID: "pod1",
+						IP:  "11.5.6.21",
+					},
+					{
+						UID: "pod2",
+						IP:  "11.5.6.22",
+					},
+				}),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-11.5.6.21": {
+						Name:      "connector1-11.5.6.21",
+						Host:      "11.5.6.21",
+						Port:      "9090",
+						Address:   "echo:9090",
+						SiteId:    "site-1",
+						ProcessID: "pod1",
+					},
+					"connector1-11.5.6.22": {
+						Name:      "connector1-11.5.6.22",
+						Host:      "11.5.6.22",
+						Port:      "9090",
+						Address:   "echo:9090",
+						SiteId:    "site-1",
+						ProcessID: "pod2",
+					},
+				},
+				sslProfiles: map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "listener host and port override",
+			fields: fields{
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listenerConfiguration: getListenerHostPortOverride("my-host", 5678),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:    "listener1",
+						Host:    "my-host",
+						Port:    "5678",
+						Address: "echo:9090",
+						SiteId:  "site-1",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles:   map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "mapped listener",
+			fields: fields{
+				SiteId: "site-1",
+				listeners: []*skupperv2alpha1.Listener{
+					&skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listenerFunction: changeListenerRoutingKey("foo"),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{
+					"listener1": {
+						Name:    "listener1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "foo",
+						SiteId:  "site-1",
+					},
+				},
+				tcpConnectors: qdr.TcpEndpointMap{},
+				sslProfiles:   map[string]qdr.SslProfile{},
+			},
+		},
+		{
+			name: "mapped connector",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: []*skupperv2alpha1.Connector{
+					&skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				connectorFunction: changeConnectorRoutingKey("foo"),
+			},
+			config: &qdr.RouterConfig{
+				Bridges: qdr.BridgeConfig{
+					TcpListeners:  map[string]qdr.TcpEndpoint{},
+					TcpConnectors: map[string]qdr.TcpEndpoint{},
+				},
+				SslProfiles: map[string]qdr.SslProfile{},
+			},
+			expected: expected{
+				tcpListeners: qdr.TcpEndpointMap{},
+				tcpConnectors: qdr.TcpEndpointMap{
+					"connector1-10.10.10.1": {
+						Name:    "connector1-10.10.10.1",
+						Host:    "10.10.10.1",
+						Port:    "9090",
+						Address: "foo",
+						SiteId:  "site-1",
 					},
 				},
 				sslProfiles: map[string]qdr.SslProfile{},
 			},
-			want: true,
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			b := NewBindings()
-			b.SiteId = tt.fields.SiteId
-			b.listeners = tt.fields.listeners
-			b.connectors = tt.fields.connectors
-			// redundant for coverage
-			b.SetListenerConfiguration(b.configure.listener)
-			b.SetConnectorConfiguration(b.configure.connector)
-
-			argsConfig := qdr.InitialConfig("some-id", tt.fields.SiteId, "v2.0", false, 10)
-			argsConfig.Bridges.TcpListeners = tt.args.tcpListeners
-			argsConfig.Bridges.TcpConnectors = tt.args.tcpConnectors
-			argsConfig.SslProfiles = tt.args.sslProfiles
-
-			if got := b.Apply(&argsConfig); got != tt.want {
-				t.Errorf("Bindings.Apply() = %v, want %v", got, tt.want)
+			b := NewBindings(tt.fields.path)
+			b.SetSiteId(tt.fields.SiteId)
+			if tt.fields.listenerConfiguration != nil {
+				b.SetListenerConfiguration(tt.fields.listenerConfiguration)
+			}
+			if tt.fields.connectorConfiguration != nil {
+				b.SetConnectorConfiguration(tt.fields.connectorConfiguration)
+			}
+			for _, listener := range tt.fields.listeners {
+				b.UpdateListener(listener.Name, listener)
+			}
+			for _, connector := range tt.fields.connectors {
+				b.UpdateConnector(connector.Name, connector)
+			}
+			if tt.fields.connectorFunction != nil || tt.fields.listenerFunction != nil {
+				b.Map(tt.fields.connectorFunction, tt.fields.listenerFunction)
 			}
-			assert.Assert(t, len(argsConfig.Bridges.TcpListeners) == len(b.listeners))
-			assert.Assert(t, len(argsConfig.Bridges.TcpConnectors) == len(b.connectors))
+
+			b.Apply(tt.config)
+			assert.DeepEqual(t, tt.config.Bridges.TcpListeners, tt.expected.tcpListeners)
+			assert.DeepEqual(t, tt.config.Bridges.TcpConnectors, tt.expected.tcpConnectors)
+			assert.DeepEqual(t, tt.config.SslProfiles, tt.expected.sslProfiles)
 		})
 	}
 }
 
+func changeConnectorRoutingKey(key string) ConnectorFunction {
+	return func(input *skupperv2alpha1.Connector) *skupperv2alpha1.Connector {
+		copy := *input
+		copy.Spec.RoutingKey = key
+		return &copy
+	}
+}
+
+func changeListenerRoutingKey(key string) ListenerFunction {
+	return func(input *skupperv2alpha1.Listener) *skupperv2alpha1.Listener {
+		copy := *input
+		copy.Spec.RoutingKey = key
+		return &copy
+	}
+}
+
+func getListenerHostPortOverride(host string, port int) ListenerConfiguration {
+	return func(siteId string, listener *skupperv2alpha1.Listener, config *qdr.BridgeConfig) {
+		UpdateBridgeConfigForListenerWithHostAndPort(siteId, listener, host, port, config)
+	}
+}
+
+func getPodConnectorConfiguration(pods []skupperv2alpha1.PodDetails) ConnectorConfiguration {
+	return func(siteId string, connector *skupperv2alpha1.Connector, config *qdr.BridgeConfig) {
+		for _, pod := range pods {
+			UpdateBridgeConfigForConnectorToPod(siteId, connector, pod, config)
+		}
+	}
+}
+
 func TestBindings_UpdateListener(t *testing.T) {
 	type fields struct {
 		SiteId     string
 		connectors map[string]*skupperv2alpha1.Connector
 		listeners  map[string]*skupperv2alpha1.Listener
-		handler    BindingEventHandler
 	}
 	type args struct {
 		name     string
 		listener *skupperv2alpha1.Listener
 	}
+	type expected struct {
+		listener  events
+		connector events
+	}
 	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		want    qdr.ConfigUpdate
-		wantErr bool
+		name     string
+		fields   fields
+		args     args
+		expected expected
 	}{
 		{
 			name: "add listener",
@@ -187,7 +593,11 @@ func TestBindings_UpdateListener(t *testing.T) {
 					},
 				},
 			},
-			wantErr: false,
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1"},
+				},
+			},
 		},
 		{
 			name: "delete listener",
@@ -213,17 +623,125 @@ func TestBindings_UpdateListener(t *testing.T) {
 				name:     "listener1",
 				listener: nil,
 			},
-			wantErr: false,
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1"},
+					Deleted: []string{"listener1"},
+				},
+			},
+		},
+		{
+			name: "delete listener that is not there",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners:  make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name:     "listener1",
+				listener: nil,
+			},
+		},
+		{
+			name: "update listener",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners: map[string]*skupperv2alpha1.Listener{
+					"listener1": &skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+			},
+			args: args{
+				name: "listener1",
+				listener: &skupperv2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "listener1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ListenerSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9999,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1", "listener1"},
+				},
+			},
+		},
+		{
+			name: "unchanged listener",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners: map[string]*skupperv2alpha1.Listener{
+					"listener1": &skupperv2alpha1.Listener{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "listener1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ListenerSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+			},
+			args: args{
+				name: "listener1",
+				listener: &skupperv2alpha1.Listener{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "listener1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ListenerSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9090,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				listener: events{
+					Updated: []string{"listener1"},
+				},
+			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			b := NewBindings()
-			b.SiteId = tt.fields.SiteId
-			b.listeners = tt.fields.listeners
-			b.connectors = tt.fields.connectors
+			b := NewBindings("")
+			b.SetSiteId(tt.fields.SiteId)
+			for _, listener := range tt.fields.listeners {
+				b.UpdateListener(listener.Name, listener)
+			}
+			for _, connector := range tt.fields.connectors {
+				b.UpdateConnector(connector.Name, connector)
+			}
+			handler := &TestBindingEventHandler{}
+			b.SetBindingEventHandler(handler)
 
 			b.UpdateListener(tt.args.name, tt.args.listener)
+			assert.Equal(t, b.GetListener(tt.args.name), tt.args.listener)
+			assert.DeepEqual(t, handler.listener, tt.expected.listener)
+			assert.DeepEqual(t, handler.connector, tt.expected.connector)
 		})
 	}
 }
@@ -233,7 +751,6 @@ func TestBindings_UpdateConnector(t *testing.T) {
 		SiteId     string
 		connectors map[string]*skupperv2alpha1.Connector
 		listeners  map[string]*skupperv2alpha1.Listener
-		handler    BindingEventHandler
 		configure  struct {
 			listener  ListenerConfiguration
 			connector ConnectorConfiguration
@@ -243,12 +760,15 @@ func TestBindings_UpdateConnector(t *testing.T) {
 		name      string
 		connector *skupperv2alpha1.Connector
 	}
+	type expected struct {
+		listener  events
+		connector events
+	}
 	tests := []struct {
-		name    string
-		fields  fields
-		args    args
-		want    qdr.ConfigUpdate
-		wantErr bool
+		name     string
+		fields   fields
+		args     args
+		expected expected
 	}{
 		{
 			name: "add connector",
@@ -272,7 +792,11 @@ func TestBindings_UpdateConnector(t *testing.T) {
 					},
 				},
 			},
-			wantErr: false,
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1"},
+				},
+			},
 		},
 		{
 			name: "delete connector",
@@ -298,17 +822,160 @@ func TestBindings_UpdateConnector(t *testing.T) {
 				name:      "connector1",
 				connector: nil,
 			},
-			wantErr: false,
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1"},
+					Deleted: []string{"connector1"},
+				},
+			},
+		},
+		{
+			name: "delete connector that is not there",
+			fields: fields{
+				SiteId:     "site-1",
+				connectors: make(map[string]*skupperv2alpha1.Connector),
+				listeners:  make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name:      "connector1",
+				connector: nil,
+			},
+		},
+		{
+			name: "update connector",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: map[string]*skupperv2alpha1.Connector{
+					"connector1": &skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listeners: make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name: "connector1",
+				connector: &skupperv2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "connector1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ConnectorSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9999,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1", "connector1"},
+				},
+			},
+		},
+		{
+			name: "add connector that is already there",
+			fields: fields{
+				SiteId: "site-1",
+				connectors: map[string]*skupperv2alpha1.Connector{
+					"connector1": &skupperv2alpha1.Connector{
+						ObjectMeta: v1.ObjectMeta{
+							Name:      "connector1",
+							Namespace: "test",
+						},
+						Spec: skupperv2alpha1.ConnectorSpec{
+							RoutingKey: "echo:9090",
+							Host:       "10.10.10.1",
+							Port:       9090,
+							Type:       "tcp",
+						},
+					},
+				},
+				listeners: make(map[string]*skupperv2alpha1.Listener),
+			},
+			args: args{
+				name: "connector1",
+				connector: &skupperv2alpha1.Connector{
+					ObjectMeta: v1.ObjectMeta{
+						Name:      "connector1",
+						Namespace: "test",
+					},
+					Spec: skupperv2alpha1.ConnectorSpec{
+						RoutingKey: "echo:9090",
+						Host:       "10.10.10.1",
+						Port:       9090,
+						Type:       "tcp",
+					},
+				},
+			},
+			expected: expected{
+				connector: events{
+					Updated: []string{"connector1"},
+				},
+			},
 		},
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			b := NewBindings()
-			b.SiteId = tt.fields.SiteId
-			b.listeners = tt.fields.listeners
-			b.connectors = tt.fields.connectors
+			b := NewBindings("")
+			b.SetSiteId(tt.fields.SiteId)
+			for _, listener := range tt.fields.listeners {
+				b.UpdateListener(listener.Name, listener)
+			}
+			for _, connector := range tt.fields.connectors {
+				b.UpdateConnector(connector.Name, connector)
+			}
+			handler := &TestBindingEventHandler{}
+			b.SetBindingEventHandler(handler)
 
 			b.UpdateConnector(tt.args.name, tt.args.connector)
+			assert.Equal(t, b.GetConnector(tt.args.name), tt.args.connector)
+			assert.DeepEqual(t, handler.listener, tt.expected.listener)
+			assert.DeepEqual(t, handler.connector, tt.expected.connector)
 		})
 	}
 }
+
+type events struct {
+	Updated []string
+	Deleted []string
+}
+
+func (e *events) updated(name string) {
+	e.Updated = append(e.Updated, name)
+}
+
+func (e *events) deleted(name string) {
+	e.Deleted = append(e.Deleted, name)
+}
+
+type TestBindingEventHandler struct {
+	listener  events
+	connector events
+}
+
+func (h *TestBindingEventHandler) ListenerUpdated(listener *skupperv2alpha1.Listener) {
+	h.listener.updated(listener.Name)
+}
+
+func (h *TestBindingEventHandler) ListenerDeleted(listener *skupperv2alpha1.Listener) {
+	h.listener.deleted(listener.Name)
+}
+
+func (h *TestBindingEventHandler) ConnectorUpdated(connector *skupperv2alpha1.Connector) bool {
+	h.connector.updated(connector.Name)
+	return false
+}
+
+func (h *TestBindingEventHandler) ConnectorDeleted(connector *skupperv2alpha1.Connector) {
+	h.connector.deleted(connector.Name)
+}
diff --git a/pkg/site/link_test.go b/pkg/site/link_test.go
index 56f88cbd..f2e2ed79 100644
--- a/pkg/site/link_test.go
+++ b/pkg/site/link_test.go
@@ -4,7 +4,6 @@ import (
 	"fmt"
 	"testing"
 
-	"github.com/skupperproject/skupper/api/types"
 	skupperv2alpha1 "github.com/skupperproject/skupper/pkg/apis/skupper/v2alpha1"
 	"github.com/skupperproject/skupper/pkg/qdr"
 	"gotest.tools/assert"
@@ -15,11 +14,8 @@ func TestLink_Apply(t *testing.T) {
 	id := "router-1"
 	siteId := "site-1"
 	version := "v2.0"
-	isEdge := true
 	notEdge := false
 	helloAge := 10
-	sslPath := ""
-	options := types.RouterOptions{}
 
 	type fields struct {
 		name        string
@@ -43,7 +39,7 @@ func TestLink_Apply(t *testing.T) {
 				definition:  nil,
 			},
 			args: args{
-				current: qdr.InitialConfigSkupperRouter(id, siteId, version, notEdge, helloAge, options, sslPath),
+				current: qdr.InitialConfig(id, siteId, version, notEdge, helloAge),
 			},
 			want: false,
 		},
@@ -61,7 +57,7 @@ func TestLink_Apply(t *testing.T) {
 				},
 			},
 			args: args{
-				current: qdr.InitialConfigSkupperRouter(id, siteId, version, notEdge, helloAge, options, sslPath),
+				current: qdr.InitialConfig(id, siteId, version, notEdge, helloAge),
 			},
 			want: false,
 		},
@@ -87,7 +83,7 @@ func TestLink_Apply(t *testing.T) {
 				},
 			},
 			args: args{
-				current: qdr.InitialConfigSkupperRouter(id, siteId, version, notEdge, helloAge, options, sslPath),
+				current: qdr.InitialConfig(id, siteId, version, notEdge, helloAge),
 			},
 			want: true,
 		},
@@ -113,7 +109,7 @@ func TestLink_Apply(t *testing.T) {
 				},
 			},
 			args: args{
-				current: qdr.InitialConfigSkupperRouter(id, siteId, version, isEdge, helloAge, options, sslPath),
+				current: qdr.InitialConfig(id, siteId, version, true, helloAge),
 			},
 			want: true,
 		},
@@ -136,8 +132,6 @@ func TestLinkMap_Apply(t *testing.T) {
 	//	isEdge := true
 	notEdge := false
 	helloAge := 10
-	sslPath := ""
-	options := types.RouterOptions{}
 
 	type fields struct {
 		name        string
@@ -301,7 +295,7 @@ func TestLinkMap_Apply(t *testing.T) {
 		},
 	}
 	for _, tt := range tests {
-		routerConfig := qdr.InitialConfigSkupperRouter(id, siteId, version, notEdge, helloAge, options, sslPath)
+		routerConfig := qdr.InitialConfig(id, siteId, version, notEdge, helloAge)
 		linkMap := make(LinkMap)
 		t.Run(tt.name, func(t *testing.T) {
 			for i, link := range tt.links {
@@ -426,6 +420,63 @@ func TestLink_Update(t *testing.T) {
 			if got := link.Update(tt.args.definition); got != tt.want {
 				t.Errorf("Link.Update() = %v, want %v", got, tt.want)
 			}
+			assert.Equal(t, link.Definition(), tt.args.definition)
+			assert.DeepEqual(t, link.Definition(), tt.args.definition)
+		})
+	}
+}
+
+func TestRemoveConnector_Apply(t *testing.T) {
+	type fields struct {
+		name string
+	}
+	type args struct {
+		current qdr.RouterConfig
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		args   args
+		want   bool
+	}{
+		{
+			name: "remove connector",
+			fields: fields{
+				name: "link1",
+			},
+			args: args{
+				current: qdr.RouterConfig{
+					Connectors: map[string]qdr.Connector{
+						"link1": {
+							Name:       "link1",
+							SslProfile: "link1-profile",
+						},
+					},
+					SslProfiles: map[string]qdr.SslProfile{
+						"link1-profile": {
+							Name: "link1-profile",
+						},
+					},
+				},
+			},
+			want: true,
+		},
+		{
+			name: "connector does not exist",
+			fields: fields{
+				name: "link1",
+			},
+			args: args{
+				current: qdr.RouterConfig{},
+			},
+			want: false,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := NewRemoveConnector(tt.fields.name).Apply(&tt.args.current); got != tt.want {
+				t.Errorf("Link.Apply() = %v, want %v", got, tt.want)
+			}
 		})
 	}
 }