Repository: EnMasseProject/enmasse
Pull request #2476 (2019-03-19 14:14:30)

Use generated inter-service passwords
This change generated inter service passwords, unless provided.


Patch:
diff --git a/.checkstyle/checkstyle.xml b/.checkstyle/checkstyle.xml
index 129ff4e03..12ac97a11 100644
--- a/.checkstyle/checkstyle.xml
+++ b/.checkstyle/checkstyle.xml
@@ -10,7 +10,13 @@
         <property name="fileExtensions" value="java"/>
     </module>
     <module name="SuppressionFilter">
-      <property name="file" value="${checkstyle.suppressions.file}" />
+        <property name="file" value="${checkstyle.suppressions.file}" />
+    </module>
+    <module name="TreeWalker">
+        <module name="FallThrough">
+            <property name="reliefPattern" value="FALL-THROUGH"/>
+        </module>
+        <module name="StringLiteralEquality"/>
     </module>
 </module>
 
diff --git a/.gitignore b/.gitignore
index 281213241..a576adb84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,7 @@
 **/.project
 **/.classpath
 .factorypath
+**/.checkstyle
 
 # VS Code
 .vscode
@@ -31,4 +32,4 @@ go/
 
 # Documentation specific
 documentation/html
-scripts/swagger2markup.jar
\ No newline at end of file
+scripts/swagger2markup.jar
diff --git a/.jenkins/Jenkinsfile-pr b/.jenkins/Jenkinsfile-pr
index ca244c171..ef1377f16 100644
--- a/.jenkins/Jenkinsfile-pr
+++ b/.jenkins/Jenkinsfile-pr
@@ -9,6 +9,8 @@ pipeline {
     parameters {
         //execute all tests as default for each new Pull request
         string(name: 'TEST_CASE', defaultValue: '', description: 'maven parameter for executing specific tests')
+        string(name: 'TEST_PROFILE', defaultValue: 'systemtests-pr', description: 'maven profile for tests')
+        string(name: 'UPGRADE_FROM', defaultValue: '0.26.2', description: 'base version which is upgraded')
     }
     options {
         timeout(time: 10, unit: 'HOURS')
@@ -49,8 +51,22 @@ pipeline {
                 script {
                     if (env.ghprbCommentBody.contains('testcase=')) {
                         env.TEST_CASE = env.ghprbCommentBody.split('testcase=')[1].split(/\s/)[0]
+                    } else {
+                        env.TEST_CASE = params.TEST_CASE
+                    }
+                    if (env.ghprbCommentBody.contains('profile=')) {
+                        env.TEST_PROFILE = env.ghprbCommentBody.split('profile=')[1].split(/\s/)[0]
+                    } else {
+                        env.TEST_PROFILE = params.TEST_PROFILE
+                    }
+                    if (env.ghprbCommentBody.contains('upgrade_from=')) {
+                        env.UPGRADE_FROM = env.ghprbCommentBody.split('upgrade_from=')[1].split(/\s/)[0]
+                    } else {
+                        env.UPGRADE_FROM = params.UPGRADE_FROM
                     }
                     echo "TEST_CASE: ${env.TEST_CASE}"
+                    echo "TEST_PROFILE: ${env.TEST_PROFILE}"
+                    echo "UPGRADE_FROM: ${env.UPGRADE_FROM}"
                 }
             }
         }
@@ -78,7 +94,129 @@ pipeline {
                 sh 'make docker_push'
             }
         }
-        stage('install enmasse') {
+
+        //----------------------------------------------------------------------------------------------------
+        // upgrade PR build
+        //----------------------------------------------------------------------------------------------------
+        stage('[upgrade] download released version') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            steps {
+                sh "git checkout -- agent && git checkout tags/${env.UPGRADE_FROM}"
+                sh "sh ./systemtests/scripts/download_released_enmasse.sh ${env.UPGRADE_FROM}"
+            }
+        }
+        stage('[upgrade] deploy enmasse') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            steps {
+                withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
+                    script {
+                        lib.installEnmasse(env.UPGRADE_FROM, false, false, false)
+                    }
+                }
+            }
+        }
+        stage('[upgrade] run systemtests before upgrade') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            environment {
+                USE_DUMMY_ADDRESS = true
+                SKIP_CLEANUP = true
+                SYSTEMTESTS_UPGRADED = false
+            }
+            steps {
+                withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
+                    script {
+                        lib.runSystemtests(env.CORES_DIR, 'systemtests-upgrade', 'upgrade.**')
+                    }
+                }
+            }
+        }
+        stage('[upgrade] upgrade enmasse') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            environment {
+                TAG = "${BUILD_TAG}"
+            }
+            steps {
+                withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
+                    script {
+                        sh "git checkout ${sha1}"
+                        lib.installEnmasse(env.TAG, true, true, true)
+                    }
+                }
+            }
+        }
+        stage('[upgrade] run systemtests after upgrade') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            environment {
+                USE_DUMMY_ADDRESS = true
+                SKIP_CLEANUP = true
+                SYSTEMTESTS_UPGRADED = true
+            }
+            steps {
+                withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
+                    script {
+                        lib.runSystemtests(env.CORES_DIR, 'systemtests-upgrade', 'upgrade.**')
+                    }
+                }
+            }
+        }
+        stage('[upgrade] download released version again') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            steps {
+                sh "sh ./systemtests/scripts/download_released_enmasse.sh ${env.UPGRADE_FROM}"
+            }
+        }
+        stage('[upgrade] downgrade enmasse') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            environment {
+                TAG = "${env.UPGRADE_FROM}"
+            }
+            steps {
+                withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
+                    script {
+                        sh "git checkout tags/${env.UPGRADE_FROM}"
+                        lib.installEnmasse(env.UPGRADE_FROM, true, true, false)
+                    }
+                }
+            }
+        }
+        stage('[upgrade] run systemtests after downgrade') {
+            when {
+                expression { env.TEST_PROFILE == 'systemtests-upgrade' }
+            }
+            environment {
+                USE_DUMMY_ADDRESS = true
+                SKIP_CLEANUP = false
+                SYSTEMTESTS_UPGRADED = true
+            }
+            steps {
+                withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
+                    script {
+                        lib.runSystemtests(env.CORES_DIR, 'systemtests-upgrade', 'upgrade.**')
+                    }
+                }
+            }
+        }
+        //----------------------------------------------------------------------------------------------------
+        // standard PR build
+        //----------------------------------------------------------------------------------------------------
+        stage('[PR] install enmasse') {
+            when {
+                expression { env.TEST_PROFILE != 'systemtests-upgrade' }
+            }
             steps {
                 withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
                     script {
@@ -87,11 +225,14 @@ pipeline {
                 }
             }
         }
-        stage('system tests') {
+        stage('[PR] system tests') {
+            when {
+                expression { env.TEST_PROFILE != 'systemtests-upgrade' }
+            }
             steps {
                 withCredentials([string(credentialsId: 'openshift-host', variable: 'OPENSHIFT_URL'), usernamePassword(credentialsId: 'openshift-credentials', passwordVariable: 'OPENSHIFT_PASSWD', usernameVariable: 'OPENSHIFT_USER')]) {
                     script {
-                        lib.runSystemtests(env.CORES_DIR, 'systemtests-pr', env.TEST_CASE)
+                        lib.runSystemtests(env.CORES_DIR, env.TEST_PROFILE, env.TEST_CASE)
                     }
                 }
             }
diff --git a/HACKING.md b/HACKING.md
index cf189d603..affe05b7c 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -10,7 +10,6 @@ To build EnMasse, you need
    * [GNU Make](https://www.gnu.org/software/make/)
    * [Asciidoctor](https://asciidoctor.org/) >= 1.5.7
    * [Go](https://golang.org/) > 1.10.0
-   * [go2xunit](https://github.com/tebeka/go2xunit) >= v1.4.10
 
 *Note*: On OSX, make sure you have [Coreutils](https://www.gnu.org/software/coreutils/) installed, e.g. `brew install coreutils`
 
@@ -167,16 +166,7 @@ connect with development tool to the forwarded port on localhost
 #### Go unit tests
 
 Go unit tests output can be converted into xUnit compatible format by a tool named `go2xunit`. This is
-being used by the build to have a combined test result of Java and Go parts.
-
-By default, when you run tests, `go test` will executed, but doesn't fail the build. Afterwards `go2xunit`
-will be executed to convert the output, and this may be fail the build when one of the unit tests failed.
-
-`go2xunit` is a pre-requisite of the build system, which has to be provided. However you can disable the
-conversion process by setting the make variable `GO2XUNIT` to an empty string. In this case the build will
-still execute `go test`, but skip the conversion.
-
-You may also set the variable `GO2XUNIT` to an absolute location of the binary.
+being used by the build to have a combined test result of Java and Go parts.  You can enable the conversion process by setting the make variable `GO2XUNIT` to the go2xunit binary. In this case the build will execute `go test` and convert the results.
 
 #### Environment variables
 
diff --git a/Makefile b/Makefile
index 2f8b0d142..82630d6b7 100644
--- a/Makefile
+++ b/Makefile
@@ -1,17 +1,36 @@
 TOPDIR          := $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST))))
 include $(TOPDIR)/Makefile.env.mk
 
-BUILD_DIRS       = none-authservice
-DOCKER_DIRS      = agent topic-forwarder artemis broker-plugin api-server address-space-controller standard-controller keycloak-plugin router router-metrics mqtt-gateway mqtt-lwt service-broker iot/iot-tenant-service iot/iot-auth-service iot/iot-device-registry iot/iot-http-adapter iot/iot-mqtt-adapter
-FULL_BUILD       = true
-
-GO_DIRS          = \
+GO_DIRS = \
 	enmasse-controller-manager \
 	iot/qdr-proxy-configurator \
 	iot/iot-gc \
 
+DOCKER_DIRS = \
+	agent \
+	topic-forwarder \
+	artemis \
+	broker-plugin \
+	api-server \
+	address-space-controller \
+	none-authservice \
+	standard-controller \
+	keycloak-plugin \
+	router \
+	router-metrics \
+	mqtt-gateway \
+	mqtt-lwt \
+	service-broker \
+	iot/iot-tenant-service \
+	iot/iot-auth-service \
+	iot/iot-device-registry \
+	iot/iot-http-adapter \
+	iot/iot-mqtt-adapter \
+
+FULL_BUILD       = true
+
+
 DOCKER_TARGETS   = docker_build docker_tag docker_push clean
-BUILD_TARGETS    = init build test package $(DOCKER_TARGETS) coverage
 INSTALLDIR       = $(CURDIR)/templates/install
 SKIP_TESTS      ?= false
 
@@ -25,14 +44,7 @@ ifneq ($(strip $(PROJECT_DISPLAY_NAME)),)
 	MAVEN_ARGS+="-Dapplication.display.name=$(PROJECT_DISPLAY_NAME)"
 endif
 
-all: init build_java docker_build templates
-
-test: test_go
-
-ifeq ($(SKIP_TESTS),true)
-else
-all: test_go
-endif
+all: build_java build_go docker_build templates
 
 templates: docu_html
 	$(MAKE) -C templates
@@ -40,10 +52,16 @@ templates: docu_html
 build_java:
 	$(IMAGE_ENV) mvn package -q -B $(MAVEN_ARGS)
 
-build_go: $(GO_DIRS)
-	for i in $?; do $(MAKE) -C $$i build; done
+build_go: $(GO_DIRS) test_go
 
+$(GO_DIRS): $(GOPRJ)
+	$(MAKE) FULL_BUILD=$(FULL_BUILD) -C $@ $(MAKECMDGOALS)
+
+ifeq ($(SKIP_TESTS),true)
+test_go:
+else
 test_go: test_go_vet test_go_run
+endif
 
 test_go_vet:
 	cd $(GOPRJ) && go tool vet cmd pkg
@@ -66,8 +84,6 @@ buildpush:
 	$(MAKE) docker_tag
 	$(MAKE) docker_push
 
-$(GO_DIRS): $(GOPRJ)
-
 $(GOPRJ):
 	mkdir -p $(dir $(GOPRJ))
 	ln -s $(TOPDIR) $(GOPRJ)
@@ -93,17 +109,10 @@ java_coverage:
 	mvn test -Pcoverage -B $(MAVEN_ARGS)
 	mvn jacoco:report-aggregate
 
-$(BUILD_TARGETS): $(BUILD_DIRS)
-$(BUILD_DIRS):
-	$(MAKE) FULL_BUILD=$(FULL_BUILD) -C $@ $(MAKECMDGOALS)
-
 $(DOCKER_TARGETS): $(DOCKER_DIRS) $(GO_DIRS)
 $(DOCKER_DIRS):
 	$(MAKE) FULL_BUILD=$(FULL_BUILD) -C $@ $(MAKECMDGOALS)
 
-$(GO_DIRS): $(GOPRJ)
-	$(MAKE) -C $@ $(MAKECMDGOALS)
-
 systemtests:
 	make -C systemtests
 
@@ -132,5 +141,5 @@ docu_check:
 docu_clean: docu_htmlclean
 	rm scripts/swagger2markup.jar
 
-.PHONY: test_go_vet test_go_plain
-.PHONY: all $(BUILD_TARGETS) $(GO_DIRS) $(DOCKER_TARGETS) $(BUILD_DIRS) $(DOCKER_DIRS) build_java test_go systemtests clean_java docu_html docu_swagger docu_htmlclean docu_check
+.PHONY: test_go_vet test_go_plain build_go
+.PHONY: all $(GO_DIRS) $(DOCKER_TARGETS) $(DOCKER_DIRS) build_java test_go systemtests clean_java docu_html docu_swagger docu_htmlclean docu_check
diff --git a/Makefile.common b/Makefile.common
index cbf10499c..b8301d9b4 100644
--- a/Makefile.common
+++ b/Makefile.common
@@ -5,6 +5,9 @@ MOCHA_FILE      ?= build/test-results/test/TEST-$(PROJECT_NAME).xml
 SKIP_TESTS      ?= false
 UNAME_S := $(shell uname -s)
 LN=ln
+BUILD_GOOS=linux
+BUILD_GOARCH=amd64
+
 
 ifeq ($(UNAME_S),Darwin)
 	LN = gln
diff --git a/Makefile.env.mk b/Makefile.env.mk
index dbcd1c5cc..afff66318 100644
--- a/Makefile.env.mk
+++ b/Makefile.env.mk
@@ -9,8 +9,6 @@ VERSION         ?= $(shell grep "release.version" $(TOPDIR)/pom.properties| cut
 MAVEN_VERSION   ?= $(shell grep "maven.version" $(TOPDIR)/pom.properties| cut -d'=' -f2)
 TAG             ?= latest
 
-GO2XUNIT        ?= go2xunit
-
 # Go settings
 GOPATH          := $(abspath $(TOPDIR))/go
 GOPRJ           := $(GOPATH)/src/github.com/enmasseproject/enmasse
diff --git a/Makefile.go.mk b/Makefile.go.mk
new file mode 100644
index 000000000..111d17474
--- /dev/null
+++ b/Makefile.go.mk
@@ -0,0 +1,16 @@
+TOPDIR=$(dir $(lastword $(MAKEFILE_LIST)))
+include $(TOPDIR)/Makefile.common
+
+build/$(CMD):
+	cd $(GOPRJ)/cmd/$(@F) && GOOS=$(BUILD_GOOS) GOARCH=$(BUILD_GOARCH) go build -o $(abspath $@) .
+
+build: build/$(CMD)
+
+build_go: build
+
+package:
+	@echo "Nothing to package"
+
+test:
+
+.PHONY: build/$(CMD) build
diff --git a/api-model/src/main/java/io/enmasse/address/model/AddressPlanStatus.java b/api-model/src/main/java/io/enmasse/address/model/AddressPlanStatus.java
new file mode 100644
index 000000000..d90ef82f9
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/address/model/AddressPlanStatus.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.address.model;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import io.enmasse.admin.model.AddressPlan;
+import io.enmasse.admin.model.v1.AbstractWithAdditionalProperties;
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.BuildableReference;
+import io.sundr.builder.annotations.Inline;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        refs= {@BuildableReference(AbstractWithAdditionalProperties.class)},
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"
+        )
+)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class AddressPlanStatus extends AbstractWithAdditionalProperties {
+    private String name;
+    private Map<String, Double> resources;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public Map<String, Double> getResources() {
+        return resources;
+    }
+
+    public void setResources(Map<String, Double> resources) {
+        this.resources = new HashMap<>(resources);
+    }
+
+    @Override
+    public String toString() {
+        return "AddressPlanStatus{" +
+                "name='" + name + '\'' +
+                ", resources=" + resources +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        AddressPlanStatus that = (AddressPlanStatus) o;
+        return Objects.equals(name, that.name) &&
+                Objects.equals(resources, that.resources);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(name, resources);
+    }
+
+    public static AddressPlanStatus fromAddressPlan(AddressPlan addressPlan) {
+        AddressPlanStatus planStatus = new AddressPlanStatus();
+        planStatus.setName(addressPlan.getMetadata().getName());
+        planStatus.setResources(addressPlan.getResources());
+        return planStatus;
+    }
+}
diff --git a/api-model/src/main/java/io/enmasse/address/model/AddressResolver.java b/api-model/src/main/java/io/enmasse/address/model/AddressResolver.java
index d6332c920..933903ffb 100644
--- a/api-model/src/main/java/io/enmasse/address/model/AddressResolver.java
+++ b/api-model/src/main/java/io/enmasse/address/model/AddressResolver.java
@@ -24,13 +24,22 @@ public class AddressResolver {
         return getType(address).findAddressPlan(address.getSpec().getPlan());
     }
 
+    public AddressPlan getDesiredPlan(Address address) {
+        return getType(address)
+                .findAddressPlan(address.getSpec().getPlan()).orElseThrow(() -> new UnresolvedAddressException("Unknown address plan " + address.getSpec().getPlan() + " for address type " + address.getSpec().getType()));
+    }
+
     public AddressPlan getPlan(AddressType addressType, String plan) {
         return addressType.findAddressPlan(plan).orElseThrow(() -> new UnresolvedAddressException("Unknown address plan " + plan + " for address type " + addressType.getName()));
     }
 
     public AddressPlan getPlan(AddressType addressType, Address address) {
-        return addressType.findAddressPlan(address.getAnnotation(AnnotationKeys.APPLIED_PLAN))
-                .orElse(addressType.findAddressPlan(address.getSpec().getPlan()).orElseThrow(() -> new UnresolvedAddressException("Unknown address plan " + address.getSpec().getPlan() + " for address type " + address.getSpec().getType())));
+        Optional<AddressPlan> found = Optional.empty();
+        if (address.getStatus().getPlanStatus() != null) {
+            found = addressType.findAddressPlan(address.getStatus().getPlanStatus().getName());
+        }
+        return found.orElse(addressType.findAddressPlan(address.getAnnotation(AnnotationKeys.APPLIED_PLAN))
+                .orElse(addressType.findAddressPlan(address.getSpec().getPlan()).orElseThrow(() -> new UnresolvedAddressException("Unknown address plan " + address.getSpec().getPlan() + " for address type " + address.getSpec().getType()))));
     }
 
     public AddressType getType(Address address) {
diff --git a/api-model/src/main/java/io/enmasse/address/model/AddressSpaceStatus.java b/api-model/src/main/java/io/enmasse/address/model/AddressSpaceStatus.java
index f64108db4..a6126d1d0 100644
--- a/api-model/src/main/java/io/enmasse/address/model/AddressSpaceStatus.java
+++ b/api-model/src/main/java/io/enmasse/address/model/AddressSpaceStatus.java
@@ -113,7 +113,6 @@ public class AddressSpaceStatus {
         return Objects.hash(ready, endpointStatuses, messages);
     }
 
-
     @Override
     public String toString() {
         return new StringBuilder()
diff --git a/api-model/src/main/java/io/enmasse/address/model/Status.java b/api-model/src/main/java/io/enmasse/address/model/Status.java
index ff831fe53..0dd156d6a 100644
--- a/api-model/src/main/java/io/enmasse/address/model/Status.java
+++ b/api-model/src/main/java/io/enmasse/address/model/Status.java
@@ -11,6 +11,7 @@ import javax.validation.Valid;
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
+import io.enmasse.admin.model.v1.AbstractWithAdditionalProperties;
 import io.enmasse.common.model.AbstractHasMetadata;
 import io.fabric8.kubernetes.api.model.Doneable;
 import io.sundr.builder.annotations.Buildable;
@@ -24,7 +25,7 @@ import io.sundr.builder.annotations.Inline;
         editableEnabled = false,
         generateBuilderPackage = false,
         builderPackage = "io.fabric8.kubernetes.api.builder",
-        refs= {@BuildableReference(AbstractHasMetadata.class)},
+        refs= {@BuildableReference(AbstractWithAdditionalProperties.class)},
         inline = @Inline(
                 type = Doneable.class,
                 prefix = "Doneable",
@@ -32,13 +33,14 @@ import io.sundr.builder.annotations.Inline;
                 )
         )
 @JsonInclude(JsonInclude.Include.NON_NULL)
-public class Status {
+public class Status extends AbstractWithAdditionalProperties {
 
     @JsonProperty("isReady")
     private boolean ready = false;
     private Phase phase = Phase.Pending;
     private List<String> messages = new ArrayList<>();
     private List<@Valid BrokerStatus> brokerStatuses = new ArrayList<>();
+    private AddressPlanStatus planStatus;
 
     public Status() {
     }
@@ -117,12 +119,13 @@ public class Status {
         return ready == status.ready &&
                 phase == status.phase &&
                 Objects.equals(messages, status.messages) &&
-                Objects.equals(brokerStatuses, status.brokerStatuses);
+                Objects.equals(brokerStatuses, status.brokerStatuses) &&
+                Objects.equals(planStatus, status.planStatus);
     }
 
     @Override
     public int hashCode() {
-        return Objects.hash(ready, phase, messages, brokerStatuses);
+        return Objects.hash(ready, phase, messages, brokerStatuses, planStatus);
     }
 
 
@@ -133,6 +136,7 @@ public class Status {
                 .append(",").append("phase=").append(phase)
                 .append(",").append("messages=").append(messages)
                 .append(",").append("brokerStatuses=").append(brokerStatuses)
+                .append(",").append("planStatus=").append(planStatus)
                 .append("}")
                 .toString();
     }
@@ -140,4 +144,12 @@ public class Status {
     public void addAllBrokerStatuses(List<BrokerStatus> toAdd) {
         brokerStatuses.addAll(toAdd);
     }
+
+    public AddressPlanStatus getPlanStatus() {
+        return planStatus;
+    }
+
+    public void setPlanStatus(AddressPlanStatus planStatus) {
+        this.planStatus = planStatus;
+    }
 }
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java
index 3f20716ae..49282ca4e 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/DownstreamStrategy.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018, EnMasse authors.
+ * Copyright 2019, EnMasse authors.
  * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
  */
 package io.enmasse.iot.model.v1;
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java
index 3a3535623..f5261bcbe 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ExternalDownstreamStrategy.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018, EnMasse authors.
+ * Copyright 2019, EnMasse authors.
  * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
  */
 
@@ -26,7 +26,6 @@ import io.sundr.builder.annotations.Inline;
                 value = "done"
                 )
         )
-//FIXME: implement missing fields and remove ignore annotation
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(NON_NULL)
 public class ExternalDownstreamStrategy {
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ImageOverride.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ImageOverride.java
new file mode 100644
index 000000000..e06620b9f
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ImageOverride.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"
+                )
+        )
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class ImageOverride {
+
+    private String name;
+    private String imagePullPolicy;
+
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getImagePullPolicy() {
+        return imagePullPolicy;
+    }
+
+    public void setImagePullPolicy(String imagePullPolicy) {
+        this.imagePullPolicy = imagePullPolicy;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/InterServiceCertificates.java b/api-model/src/main/java/io/enmasse/iot/model/v1/InterServiceCertificates.java
new file mode 100644
index 000000000..f49a04e32
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/InterServiceCertificates.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"
+                )
+        )
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class InterServiceCertificates {
+
+    private SecretCertificatesStrategy secretCertificatesStrategy;
+    private ServiceCAStrategy serviceCAStrategy;
+
+    public SecretCertificatesStrategy getSecretCertificatesStrategy() {
+        return secretCertificatesStrategy;
+    }
+
+    public void setSecretCertificatesStrategy(SecretCertificatesStrategy secretCertificatesStrategy) {
+        this.secretCertificatesStrategy = secretCertificatesStrategy;
+    }
+
+    public ServiceCAStrategy getServiceCAStrategy() {
+        return serviceCAStrategy;
+    }
+
+    public void setServiceCAStrategy(ServiceCAStrategy serviceCAStrategy) {
+        this.serviceCAStrategy = serviceCAStrategy;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfig.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfig.java
new file mode 100644
index 000000000..d7eada49a
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfig.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+import io.enmasse.common.model.AbstractHasMetadata;
+import io.enmasse.common.model.DefaultCustomResource;
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.BuildableReference;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        refs = {@BuildableReference(AbstractHasMetadata.class)},
+        inline = @Inline(
+                        type = Doneable.class,
+                        prefix = "Doneable",
+                        value = "done"))
+@DefaultCustomResource
+@SuppressWarnings("serial")
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class IoTConfig extends AbstractHasMetadata<IoTConfig> {
+
+    public static final String KIND = "IoTConfig";
+
+    private IoTConfigSpec spec;
+    private IoTConfigStatus status;
+
+    public IoTConfig() {
+        super(KIND, IoTCrd.API_VERSION);
+    }
+
+    public IoTConfigSpec getSpec() {
+        return spec;
+    }
+
+    public void setSpec(IoTConfigSpec spec) {
+        this.spec = spec;
+    }
+
+    public IoTConfigStatus getStatus() {
+        return status;
+    }
+
+    public void setStatus(IoTConfigStatus status) {
+        this.status = status;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfigSpec.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfigSpec.java
new file mode 100644
index 000000000..7e9c0b3cc
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfigSpec.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"
+                )
+        )
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class IoTConfigSpec {
+
+    private Boolean enableDefaultRoutes;
+    private Map<String, ImageOverride> imageOverrides;
+    private InterServiceCertificates interServiceCertificates;
+
+    public Boolean getEnableDefaultRoutes() {
+        return enableDefaultRoutes;
+    }
+
+    public void setEnableDefaultRoutes(Boolean enableDefaultRoutes) {
+        this.enableDefaultRoutes = enableDefaultRoutes;
+    }
+
+    public Map<String, ImageOverride> getImageOverrides() {
+        return imageOverrides;
+    }
+
+    public void setImageOverrides(Map<String, ImageOverride> imageOverrides) {
+        this.imageOverrides = imageOverrides;
+    }
+
+    public InterServiceCertificates getInterServiceCertificates() {
+        return interServiceCertificates;
+    }
+
+    public void setInterServiceCertificates(InterServiceCertificates interServiceCertificates) {
+        this.interServiceCertificates = interServiceCertificates;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfigStatus.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfigStatus.java
new file mode 100644
index 000000000..70dec25ea
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTConfigStatus.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"
+                )
+        )
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class IoTConfigStatus {
+
+    private boolean initialized = false;
+    private String state;
+    private String authenticationServicePSK;
+
+    public boolean isInitialized() {
+        return initialized;
+    }
+
+    public void setInitialized(boolean initialized) {
+        this.initialized = initialized;
+    }
+
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    public String getAuthenticationServicePSK() {
+        return authenticationServicePSK;
+    }
+
+    public void setAuthenticationServicePSK(String authenticationServicePSK) {
+        this.authenticationServicePSK = authenticationServicePSK;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTCrd.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTCrd.java
index 98def8f64..4f4c50d2e 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTCrd.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTCrd.java
@@ -23,6 +23,7 @@ public class IoTCrd {
     public static void registerCustomCrds() {
         KubernetesDeserializer.registerCustomKind(API_VERSION, IoTProject.KIND, IoTProject.class);
         KubernetesDeserializer.registerCustomKind(API_VERSION, IoTProjectList.KIND, IoTProjectList.class);
+        KubernetesDeserializer.registerCustomKind(API_VERSION, IoTConfig.KIND, IoTConfig.class);
     }
 
     public static CustomResourceDefinition project() {
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java
index 337806c7a..9503a358c 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProject.java
@@ -24,7 +24,6 @@ import io.sundr.builder.annotations.Inline;
                                 value = "done"))
 @DefaultCustomResource
 @SuppressWarnings("serial")
-//FIXME: implement missing fields and remove ignore annotation
 @JsonIgnoreProperties(ignoreUnknown = true)
 public class IoTProject extends AbstractHasMetadata<IoTProject> {
 
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java
index 7f9821708..f0d0d9435 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectSpec.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018, EnMasse authors.
+ * Copyright 2019, EnMasse authors.
  * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
  */
 package io.enmasse.iot.model.v1;
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java
index 9b24f8007..f09ea206a 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/IoTProjectStatus.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018, EnMasse authors.
+ * Copyright 2019, EnMasse authors.
  * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
  */
 package io.enmasse.iot.model.v1;
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java
index ec418abce..d67bf9a75 100644
--- a/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ManagedDownstreamStrategy.java
@@ -1,5 +1,5 @@
 /*
- * Copyright 2018, EnMasse authors.
+ * Copyright 2019, EnMasse authors.
  * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
  */
 package io.enmasse.iot.model.v1;
@@ -23,7 +23,6 @@ import io.sundr.builder.annotations.Inline;
                 value = "done"
                 )
         )
-//FIXME: implement missing fields and remove ignore annotation
 @JsonIgnoreProperties(ignoreUnknown = true)
 @JsonInclude(NON_NULL)
 public class ManagedDownstreamStrategy {
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/SecretCertificatesStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/SecretCertificatesStrategy.java
new file mode 100644
index 000000000..685f93d18
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/SecretCertificatesStrategy.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import java.util.Map;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import io.fabric8.kubernetes.api.model.Doneable;
+import io.sundr.builder.annotations.Buildable;
+import io.sundr.builder.annotations.Inline;
+
+@Buildable(
+        editableEnabled = false,
+        generateBuilderPackage = false,
+        builderPackage = "io.fabric8.kubernetes.api.builder",
+        inline = @Inline(
+                type = Doneable.class,
+                prefix = "Doneable",
+                value = "done"
+                )
+        )
+@JsonIgnoreProperties(ignoreUnknown = true)
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class SecretCertificatesStrategy {
+
+    private String caSecretName;
+    private Map<String, String> serviceSecretNames;
+
+    public String getCaSecretName() {
+        return caSecretName;
+    }
+
+    public void setCaSecretName(String caSecretName) {
+        this.caSecretName = caSecretName;
+    }
+
+    public Map<String, String> getServiceSecretNames() {
+        return serviceSecretNames;
+    }
+
+    public void setServiceSecretNames(Map<String, String> serviceSecretNames) {
+        this.serviceSecretNames = serviceSecretNames;
+    }
+
+}
diff --git a/api-model/src/main/java/io/enmasse/iot/model/v1/ServiceCAStrategy.java b/api-model/src/main/java/io/enmasse/iot/model/v1/ServiceCAStrategy.java
new file mode 100644
index 000000000..063026e6e
--- /dev/null
+++ b/api-model/src/main/java/io/enmasse/iot/model/v1/ServiceCAStrategy.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class ServiceCAStrategy {
+
+}
diff --git a/documentation/modules/con-brokered-topic.adoc b/documentation/modules/con-brokered-topic.adoc
index 1ceccd809..6a3ee88ae 100644
--- a/documentation/modules/con-brokered-topic.adoc
+++ b/documentation/modules/con-brokered-topic.adoc
@@ -24,4 +24,7 @@ So, for example:
 
 * `a/+/b` matches `a/foo/b` and `a/bar/b`, but would not match `a/foo/bar`
 
+== Known issue with creating a subscriber on a hierarchical topic
+
+A known issue exists where creating a subscriber on a hierarchical topic in {ProductName} causes the broker to instead create it as a competing consumer (handling the address like a queue rather than a topic). For more information about the specific workaround for your client, see the applicable client example section in link:{BookUrlBase}{BaseProductVersion}{BookNameUrl}#assembly-connecting-applications-messaging[Connecting applications to {ProductName}].
 
diff --git a/documentation/modules/con-standard-topic.adoc b/documentation/modules/con-standard-topic.adoc
index 5362803fb..82e740bf3 100644
--- a/documentation/modules/con-standard-topic.adoc
+++ b/documentation/modules/con-standard-topic.adoc
@@ -27,3 +27,7 @@ So, for example:
 
 In the standard address space, the first level must always be a defined topic address; that is, `#` and `+` are not valid as the first characters of a subscribing address.
 
+== Known issue with creating a subscriber on a hierarchical topic
+
+A known issue exists where creating a subscriber on a hierarchical topic in {ProductName} causes the broker to instead create it as a competing consumer (handling the address like a queue rather than a topic). For more information about the specific workaround for your client, see the applicable client example section in link:{BookUrlBase}{BaseProductVersion}{BookNameUrl}#assembly-connecting-applications-messaging[Connecting applications to {ProductName}].
+
diff --git a/documentation/modules/ref-cpp-example.adoc b/documentation/modules/ref-cpp-example.adoc
index 771873a0a..7002f3a0d 100644
--- a/documentation/modules/ref-cpp-example.adoc
+++ b/documentation/modules/ref-cpp-example.adoc
@@ -71,3 +71,28 @@ int main(int argc, char **argv) {
 }
 ----
 
+== Known issue with creating a subscriber on a hierarchical topic
+
+A known issue exists where creating a subscriber on a hierarchical topic in {ProductName} causes the broker to instead create it as a competing consumer (handling the address like a queue rather than a topic).
+
+The workaround involves setting the capability `"topic"` in the source.
+
+.Procedure
+
+* In the `topic_receive.cpp` file, edit the code so that it is similar to what is shown in this example:
+
+[source,cpp,options="nowrap",subs="+quotes,attributes"]
+----
+void on_container_start(proton::container& cont) override {
+        proton::connection conn = cont.connect(conn_url_);
+        proton::receiver_options opts {};
+        proton::source_options sopts {};
+
+        sopts.capabilities(std::vector<proton::symbol> { "topic" });
+        opts.source(sopts);
+
+        conn.open_receiver(address_, opts);
+    }
+----
+
+
diff --git a/documentation/modules/ref-python-example.adoc b/documentation/modules/ref-python-example.adoc
index 7d319c3e4..bccda5bdc 100644
--- a/documentation/modules/ref-python-example.adoc
+++ b/documentation/modules/ref-python-example.adoc
@@ -37,3 +37,28 @@ class HelloWorld(MessagingHandler):
 Container(HelloWorld("amqps://_messaging-route-hostname_:443", "myqueue")).run()
 ----
 
+== Known issue with creating a subscriber on a hierarchical topic
+
+A known issue exists where creating a subscriber on a hierarchical topic in {ProductName} causes the broker to instead create it as a competing consumer (handling the address like a queue rather than a topic).
+
+The workaround for this issue involves setting the capability `"topic"` in the source.
+
+.Procedure
+
+. In the `simple_recv.py` file, modify the `from proton.reactor import Container` to add the `ReceiverOption`:
+
+[source,python,options="nowrap",subs="+quotes,attributes"]
+----
+class CapabilityOptions(ReceiverOption):
+    def apply(self, receiver):
+        receiver.source.capabilities.put_object(symbol("topic"))
+----
+
+. Modify the following line to add `options=CapabilityOptions()`:
+
+[source,python,options="nowrap",subs="+quotes,attributes"]
+----
+def on_start(self, event):
+        event.container.create_receiver(conn, self.address, options=CapabilityOptions())
+----
+
diff --git a/enmasse-controller-manager/Dockerfile b/enmasse-controller-manager/Dockerfile
index 39f9e2df1..141f071e5 100644
--- a/enmasse-controller-manager/Dockerfile
+++ b/enmasse-controller-manager/Dockerfile
@@ -7,7 +7,8 @@ FROM centos:7
 
 ARG version
 ARG commit
-ENV VERSION=${version} COMMIT=${commit} ENMASSE_IMAGE_MAP_FILE=/etc/operatorImageMap.yaml
+ARG maven_version
+ENV VERSION=${version} MAVEN_VERSION=${maven_version} COMMIT=${commit} ENMASSE_IMAGE_MAP_FILE=/etc/operatorImageMap.yaml
 
 ADD build/operatorImageMap.yaml /etc/operatorImageMap.yaml
 ADD build/enmasse-controller-manager /
diff --git a/enmasse-controller-manager/Makefile b/enmasse-controller-manager/Makefile
index 7b187b84a..1775734ed 100644
--- a/enmasse-controller-manager/Makefile
+++ b/enmasse-controller-manager/Makefile
@@ -1,15 +1,8 @@
-include ../Makefile.common
-
 CMD=enmasse-controller-manager
-
-build/$(CMD):
-	cd $(GOPRJ)/cmd/$(@F) && go build -o $(abspath $@) .
+include ../Makefile.go.mk
 
 build: build/operatorImageMap.yaml build/$(CMD)
 
-package:
-	@echo "Nothing to package"
-
 build/operatorImageMap.yaml:
 	echo "" > "$@"
 	
@@ -20,10 +13,9 @@ build/operatorImageMap.yaml:
 	echo "iot-mqtt-adapter: $(IOT_MQTT_ADAPTER_IMAGE)" >> "$@"
 	echo "iot-tenant-service: $(IOT_TENANT_SERVICE_IMAGE)" >> "$@"
 	echo "qdr-proxy-configurator: $(QDR_PROXY_CONFIGURATOR_IMAGE)" >> "$@"
+	echo "qdrouterd-base: $(QDROUTERD_BASE_IMAGE)" >> "$@"
 	echo "none-authservice: $(NONE_AUTHSERVICE_IMAGE)" >> "$@"
 	echo "keycloak: $(KEYCLOAK_IMAGE)" >> "$@"
 	echo "keycloak-plugin: $(KEYCLOAK_PLUGIN_IMAGE)" >> "$@"
 
-test:
-
-.PHONY: build/$(CMD) build/operatorImageMap.yaml
+.PHONY: build/operatorImageMap.yaml
diff --git a/iot/examples/README.md b/iot/examples/README.md
index 10b462505..ffeb0df72 100644
--- a/iot/examples/README.md
+++ b/iot/examples/README.md
@@ -18,6 +18,7 @@
   oc apply -f templates/build/enmasse-latest/install/components/enmasse-controller-manager
   
   oc apply -f templates/build/enmasse-latest/install/preview-bundles/iot
+  oc apply -f iot/examples/iot-config.yaml
   ```
 
 * Wait for the EnMasse installation to succeed. The following command should show all pods to be ready:
diff --git a/iot/iot-gc/Dockerfile b/iot/iot-gc/Dockerfile
index 521d5bb1c..a12c57fe2 100644
--- a/iot/iot-gc/Dockerfile
+++ b/iot/iot-gc/Dockerfile
@@ -6,8 +6,9 @@
 FROM enmasseproject/qdrouterd-base:1.5-rc2
 
 ARG version
+ARG maven_version
 ARG commit
-ENV VERSION=${version} COMMIT=${commit}
+ENV VERSION=${version} MAVEN_VERSION=${maven_version} COMMIT=${commit}
 
 ADD build/iot-gc /
 
diff --git a/iot/iot-gc/Makefile b/iot/iot-gc/Makefile
index 66b3f5aa6..08c572068 100644
--- a/iot/iot-gc/Makefile
+++ b/iot/iot-gc/Makefile
@@ -1,15 +1,2 @@
-include ../../Makefile.common
-
 CMD=iot-gc
-
-build/$(CMD):
-	cd $(GOPRJ)/cmd/$(@F) && go build -o $(abspath $@) .
-
-build: build/$(CMD)
-
-package:
-	@echo "Nothing to package"
-
-test:
-
-.PHONY: build/$(CMD)
\ No newline at end of file
+include ../../Makefile.go.mk
diff --git a/iot/qdr-proxy-configurator/Dockerfile b/iot/qdr-proxy-configurator/Dockerfile
index 68231198e..a4eaeff0a 100644
--- a/iot/qdr-proxy-configurator/Dockerfile
+++ b/iot/qdr-proxy-configurator/Dockerfile
@@ -6,8 +6,9 @@
 FROM enmasseproject/qdrouterd-base:1.5-rc2
 
 ARG version
+ARG maven_version
 ARG commit
-ENV VERSION=${version} COMMIT=${commit}
+ENV VERSION=${version} MAVEN_VERSION=${maven_version} COMMIT=${commit}
 
 ADD build/qdr-proxy-configurator /
 
diff --git a/iot/qdr-proxy-configurator/Makefile b/iot/qdr-proxy-configurator/Makefile
index 8b9d29109..5c271fd43 100644
--- a/iot/qdr-proxy-configurator/Makefile
+++ b/iot/qdr-proxy-configurator/Makefile
@@ -1,15 +1,2 @@
-include ../../Makefile.common
-
 CMD=qdr-proxy-configurator
-
-build/$(CMD):
-	cd $(GOPRJ)/cmd/$(@F) && go build -o $(abspath $@) .
-
-build: build/$(CMD)
-
-package:
-	@echo "Nothing to package"
-
-test:
-
-.PHONY: build/$(CMD)
\ No newline at end of file
+include ../../Makefile.go.mk
diff --git a/keycloak-plugin/sasl-plugin/src/main/java/io/enmasse/keycloak/spi/ScramSaslAuthenticator.java b/keycloak-plugin/sasl-plugin/src/main/java/io/enmasse/keycloak/spi/ScramSaslAuthenticator.java
index d08c40f9a..90df22fef 100644
--- a/keycloak-plugin/sasl-plugin/src/main/java/io/enmasse/keycloak/spi/ScramSaslAuthenticator.java
+++ b/keycloak-plugin/sasl-plugin/src/main/java/io/enmasse/keycloak/spi/ScramSaslAuthenticator.java
@@ -385,6 +385,7 @@ class ScramSaslAuthenticator implements SaslServerMechanism.Instance
                     challenge = new byte[0];
                     break;
                 }
+              //$FALL-THROUGH$
             default:
                 throw new IllegalArgumentException("No response expected in state " + state);
 
diff --git a/none-authservice/Dockerfile b/none-authservice/Dockerfile
index cab7b963d..d1e629b5c 100644
--- a/none-authservice/Dockerfile
+++ b/none-authservice/Dockerfile
@@ -1,14 +1,10 @@
-FROM enmasseproject/nodejs-base:6-1
+FROM enmasseproject/java-base:11-0
 
-RUN mkdir -p /opt/app-root/src/
-RUN cd /opt/app-root/src/
-RUN ["/bin/bash", "-c", "npm install debug && npm install rhea"]
 ARG version
 ARG maven_version
 ARG commit
 ENV VERSION=${version} COMMIT=${commit} MAVEN_VERSION=${maven_version}
 
-ADD build/none-authservice-${MAVEN_VERSION}.tgz /opt/app-root/src/
-EXPOSE 5671
+ADD target/none-authservice-${MAVEN_VERSION}.jar /none-authservice.jar
 
-CMD ["node", "/opt/app-root/src/none-authservice.js"]
+CMD ["/opt/run-java/launch_java.sh", "/none-authservice.jar"]
diff --git a/none-authservice/Makefile b/none-authservice/Makefile
index 3e933513a..27e8bca79 100644
--- a/none-authservice/Makefile
+++ b/none-authservice/Makefile
@@ -1,11 +1,1 @@
-include ../Makefile.common
-
-build:
-	@echo "Nothing to build"
-
-package:
-	tar -czf build/none-authservice-$(MAVEN_VERSION).tgz *.js
-
-test:
-
-coverage:
+include ../Makefile.java.mk
diff --git a/none-authservice/none-authservice.js b/none-authservice/none-authservice.js
deleted file mode 100644
index e2ef17385..000000000
--- a/none-authservice/none-authservice.js
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Copyright 2017 Red Hat Inc.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *     http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-var container = require('rhea');
-var fs = require('fs');
-var path = require('path');
-var cert_dir = (process.env.CERT_DIR !== undefined) ? process.env.CERT_DIR : "/opt/none-authservice/cert";
-
-function authenticate(username, password) {
-    console.log('Authenticating as ' + username);
-    return true;
-}
-container.sasl_server_mechanisms.enable_plain(authenticate);
-container.sasl_server_mechanisms.enable_anonymous();
-var server = container.listen({
-    port: process.env.LISTENPORT,
-    require_sasl: true,
-    transport: 'tls',
-    key: fs.readFileSync(path.resolve(cert_dir, 'tls.key')),
-    cert: fs.readFileSync(path.resolve(cert_dir, 'tls.crt'))
-});
-
-console.log('Listening on port ' + process.env.LISTENPORT);
-container.on('connection_open', function (context) {
-    var authenticatedIdentity = { 'sub' : context.connection.sasl_transport.username || 'anonymous' };
-    var groups = [ "manage" ];
-    var properties = context.connection.local.open.properties || {};
-    properties["authenticated-identity"] = authenticatedIdentity;
-    properties["groups"] = groups;
-    context.connection.local.open.properties = properties;
-    context.connection.close();
-});
-container.on('disconnected', function (context) {
-});
diff --git a/none-authservice/pom.xml b/none-authservice/pom.xml
new file mode 100644
index 000000000..4ba277d08
--- /dev/null
+++ b/none-authservice/pom.xml
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
+  <parent>
+    <groupId>io.enmasse</groupId>
+    <artifactId>enmasse</artifactId>
+    <version>0.27-SNAPSHOT</version>
+  </parent>
+  <modelVersion>4.0.0</modelVersion>
+  <artifactId>none-authservice</artifactId>
+  <dependencies>
+    <dependency>
+      <groupId>io.vertx</groupId>
+      <artifactId>vertx-proton</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-api</artifactId>
+      <scope>compile</scope>
+    </dependency>
+    <dependency>
+      <groupId>ch.qos.logback</groupId>
+      <artifactId>logback-classic</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+            <configuration>
+              <transformers>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
+                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                  <mainClass>io.enmasse.authservice.none.NoneAuthService</mainClass>
+                </transformer>
+              </transformers>
+              <filters>
+                <filter>
+                  <artifact>*:*</artifact>
+                  <excludes>
+                    <exclude>META-INF/*.SF</exclude>
+                    <exclude>META-INF/*.DSA</exclude>
+                    <exclude>META-INF/*.RSA</exclude>
+                  </excludes>
+                </filter>
+              </filters>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/none-authservice/src/main/java/io/enmasse/authservice/none/NoneAuthService.java b/none-authservice/src/main/java/io/enmasse/authservice/none/NoneAuthService.java
new file mode 100644
index 000000000..910e0464c
--- /dev/null
+++ b/none-authservice/src/main/java/io/enmasse/authservice/none/NoneAuthService.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.authservice.none;
+
+import io.vertx.core.Vertx;
+import io.vertx.core.net.PemKeyCertOptions;
+import io.vertx.proton.ProtonConnection;
+import io.vertx.proton.ProtonServer;
+import io.vertx.proton.ProtonServerOptions;
+import org.apache.qpid.proton.amqp.Symbol;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+public class NoneAuthService {
+    private static final Logger log = LoggerFactory.getLogger(NoneAuthService.class);
+    private static final Symbol AUTHENTICATED_IDENTITY = Symbol.getSymbol("authenticated-identity");
+    private static final Symbol GROUPS = Symbol.getSymbol("groups");
+
+    public static void main(String[] args) {
+        Map<String, String> env = System.getenv();
+        String certDir = env.getOrDefault("CERT_DIR", "/opt/none-authservice/cert");
+        int listenPort = Integer.parseInt(env.getOrDefault("LISTENPORT", "5671"));
+
+        Vertx vertx = Vertx.vertx();
+
+        ProtonServerOptions options = new ProtonServerOptions();
+        options.setSsl(true);
+        options.setPemKeyCertOptions(new PemKeyCertOptions()
+                .setCertPath(new File(certDir, "tls.crt").getAbsolutePath())
+                .setKeyPath(new File(certDir, "tls.key").getAbsolutePath()));
+
+        ProtonServer server = ProtonServer.create(vertx, options);
+        server.connectHandler(NoneAuthService::connectHandler);
+        server.saslAuthenticatorFactory(NoneAuthServiceAuthenticator::new);
+        server.listen(listenPort, "0.0.0.0", result -> {
+            if (result.succeeded()) {
+                log.info("Listening on {}", listenPort);
+            } else {
+                log.error("Error listening on {}", listenPort, result.cause());
+            }
+        });
+    }
+
+    private static void connectHandler(ProtonConnection connection) {
+        connection.setContainer("none-authservice");
+        connection.openHandler(conn -> {
+            Map<Symbol, Object> properties = new HashMap<>();
+
+            properties.put(AUTHENTICATED_IDENTITY, Collections.singletonMap("sub", "anonymous"));
+            properties.put(GROUPS, Collections.singletonList("manage"));
+
+            connection.setProperties(properties);
+            connection.open();
+            connection.close();
+
+        }).closeHandler(conn -> {
+            connection.close();
+            connection.disconnect();
+        }).disconnectHandler(protonConnection -> {
+            connection.disconnect();
+        });
+    }
+}
diff --git a/none-authservice/src/main/java/io/enmasse/authservice/none/NoneAuthServiceAuthenticator.java b/none-authservice/src/main/java/io/enmasse/authservice/none/NoneAuthServiceAuthenticator.java
new file mode 100644
index 000000000..08d7efbb4
--- /dev/null
+++ b/none-authservice/src/main/java/io/enmasse/authservice/none/NoneAuthServiceAuthenticator.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.authservice.none;
+
+import io.vertx.core.Handler;
+import io.vertx.core.net.NetSocket;
+import io.vertx.proton.ProtonConnection;
+import io.vertx.proton.sasl.ProtonSaslAuthenticator;
+import org.apache.qpid.proton.engine.Sasl;
+import org.apache.qpid.proton.engine.Transport;
+
+public class NoneAuthServiceAuthenticator implements ProtonSaslAuthenticator {
+    private static final String MECH_ANONYMOUS = "ANONYMOUS";
+    private static final String MECH_PLAIN = "PLAIN";
+
+    private Sasl sasl;
+
+    @Override
+    public void init(NetSocket netSocket, ProtonConnection protonConnection, Transport transport) {
+
+        transport.setInitialRemoteMaxFrameSize(1024*1024);
+        this.sasl = transport.sasl();
+        sasl.server();
+        sasl.allowSkip(false);
+        sasl.setMechanisms(MECH_ANONYMOUS, MECH_PLAIN);
+    }
+
+    @Override
+    public void process(Handler<Boolean> handler) {
+        String[] remoteMechanisms = sasl.getRemoteMechanisms();
+        boolean done = false;
+        if (!done && remoteMechanisms.length > 0) {
+            String chosen = remoteMechanisms[0];
+            byte[] response;
+            if (sasl.pending() > 0) {
+                response = new byte[sasl.pending()];
+                sasl.recv(response, 0, response.length);
+            }
+            done = true;
+            if (MECH_ANONYMOUS.equals(chosen) || MECH_PLAIN.equals(chosen)) {
+                sasl.done(Sasl.SaslOutcome.PN_SASL_OK);
+            } else {
+                sasl.done(Sasl.SaslOutcome.PN_SASL_SYS);
+            }
+        }
+        handler.handle(done);
+    }
+
+    @Override
+    public boolean succeeded() {
+        return true;
+    }
+}
diff --git a/none-authservice/src/main/resources/logback.xml b/none-authservice/src/main/resources/logback.xml
new file mode 100644
index 000000000..50d086292
--- /dev/null
+++ b/none-authservice/src/main/resources/logback.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE xml>
+<configuration>
+
+  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
+    <encoder>
+      <pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSS'Z',GMT} %-5p [%c{0}] %m%n</pattern>
+    </encoder>
+  </appender>
+
+  <root level="${LOG_LEVEL:-info}">
+    <appender-ref ref="STDOUT" />
+  </root>
+
+  <logger name="io.vertx.core.net.impl.NetServerImpl" level="OFF"/>
+
+</configuration>
diff --git a/pkg/apis/iot/v1alpha1/types.go b/pkg/apis/iot/v1alpha1/types.go
index 86f632e97..803162aea 100644
--- a/pkg/apis/iot/v1alpha1/types.go
+++ b/pkg/apis/iot/v1alpha1/types.go
@@ -155,7 +155,12 @@ type IoTConfigStatus struct {
 	Initialized bool   `json:"initialized"`
 	State       string `json:"state"`
 
-	AuthenticationServicePSK *string `json:"authenticationServicePSK"`
+	AuthenticationServicePSK *string                  `json:"authenticationServicePSK"`
+	Adapters                 map[string]AdapterStatus `json:"adapters,omitempty"`
+}
+
+type AdapterStatus struct {
+	InterServicePassword string `json:"interServicePassword"`
 }
 
 const (
diff --git a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
index eb7db10b3..8fc03e883 100644
--- a/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
+++ b/pkg/apis/iot/v1alpha1/zz_generated.deepcopy.go
@@ -47,6 +47,22 @@ func (in *AdapterEndpointConfig) DeepCopy() *AdapterEndpointConfig {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *AdapterStatus) DeepCopyInto(out *AdapterStatus) {
+	*out = *in
+	return
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdapterStatus.
+func (in *AdapterStatus) DeepCopy() *AdapterStatus {
+	if in == nil {
+		return nil
+	}
+	out := new(AdapterStatus)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *AdaptersConfig) DeepCopyInto(out *AdaptersConfig) {
 	*out = *in
@@ -285,6 +301,13 @@ func (in *IoTConfigStatus) DeepCopyInto(out *IoTConfigStatus) {
 		*out = new(string)
 		**out = **in
 	}
+	if in.Adapters != nil {
+		in, out := &in.Adapters, &out.Adapters
+		*out = make(map[string]AdapterStatus, len(*in))
+		for key, val := range *in {
+			(*out)[key] = val
+		}
+	}
 	return
 }
 
diff --git a/pkg/controller/iotconfig/auth.go b/pkg/controller/iotconfig/auth.go
new file mode 100644
index 000000000..53e718a7a
--- /dev/null
+++ b/pkg/controller/iotconfig/auth.go
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package iotconfig
+
+import (
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+	"github.com/enmasseproject/enmasse/pkg/util"
+)
+
+func ensureAdapterAuthCredentials(status map[string]iotv1alpha1.AdapterStatus) error {
+
+	for k, v := range status {
+		if v.InterServicePassword == "" {
+			pwd, err := util.GeneratePassword(64)
+			if err != nil {
+				return err
+			}
+			v.InterServicePassword = pwd
+			status[k] = v
+		}
+	}
+
+	return nil
+}
+
+// Ensure that we have exactly the require entries
+func ensureAdapterStatus(currentStatus map[string]iotv1alpha1.AdapterStatus, names ...string) map[string]iotv1alpha1.AdapterStatus {
+
+	var result = make(map[string]iotv1alpha1.AdapterStatus)
+
+	// prefill
+
+	for _, k := range names {
+		result[k] = iotv1alpha1.AdapterStatus{}
+	}
+
+	if currentStatus == nil {
+		return result
+	}
+
+	// overwrite with existing
+
+	for k, v := range currentStatus {
+		// only overwrite if it is existing
+		if _, ok := result[k]; ok {
+			result[k] = v
+		}
+	}
+
+	return result
+
+}
diff --git a/pkg/controller/iotconfig/auth_service.go b/pkg/controller/iotconfig/auth_service.go
index acd876b58..e522b72ee 100644
--- a/pkg/controller/iotconfig/auth_service.go
+++ b/pkg/controller/iotconfig/auth_service.go
@@ -7,6 +7,7 @@ package iotconfig
 
 import (
 	"context"
+	"encoding/json"
 
 	"k8s.io/apimachinery/pkg/util/intstr"
 
@@ -135,6 +136,19 @@ func (r *ReconcileIoTConfig) reconcileAuthServiceConfigMap(config *iotv1alpha1.I
 
 	install.ApplyDefaultLabels(&configMap.ObjectMeta, "iot", configMap.Name)
 
+	// JSON encode passwords
+
+	httpPassword, err := json.Marshal(config.Status.Adapters["http"].InterServicePassword)
+	if err != nil {
+		return err
+	}
+	mqttPassword, err := json.Marshal(config.Status.Adapters["mqtt"].InterServicePassword)
+	if err != nil {
+		return err
+	}
+
+	// create config map data
+
 	if configMap.Data == nil {
 		configMap.Data = make(map[string]string)
 	}
@@ -193,26 +207,21 @@ hono:
 				"activities":["READ","WRITE"]
 			},
 			{
-				"operation":"tenant/*:*",
+				"operation":"tenant:get",
 				"activities":["EXECUTE"]
 			}
 		]
 	},
 	"users":{
-		"amqp-adapter@HONO":{
-			"mechanism":"PLAIN",
-			"password":"amqp-secret",
-			"authorities":["hono-component","protocol-adapter"]
-		},
 		"http-adapter@HONO":{
 			"mechanism":"PLAIN",
-			"password":"http-secret",
-			"authorities":["hono-component","protocol-adapter"]
+			"password":` + string(httpPassword) + `,
+			"authorities":["protocol-adapter"]
 		},
 		"mqtt-adapter@HONO":{
 			"mechanism":"PLAIN",
-			"password":"mqtt-secret",
-			"authorities":["hono-component","protocol-adapter"]
+			"password":` + string(mqttPassword) + `,
+			"authorities":["protocol-adapter"]
 		},
 		"device-registry":{
 			"mechanism":"EXTERNAL",
diff --git a/pkg/controller/iotconfig/http_adapter.go b/pkg/controller/iotconfig/http_adapter.go
index f9254d6dd..418913e71 100644
--- a/pkg/controller/iotconfig/http_adapter.go
+++ b/pkg/controller/iotconfig/http_adapter.go
@@ -94,7 +94,7 @@ func (r *ReconcileIoTConfig) reconcileHttpAdapterDeployment(config *iotv1alpha1.
 			{Name: "HONO_HTTP_NATIVE_TLS_REQUIRED", Value: "false"},
 		}
 
-		AppendHonoAdapterEnvs(container, "http-adapter@HONO", "http-secret")
+		AppendHonoAdapterEnvs(container, "http-adapter@HONO", config.Status.Adapters["http"].InterServicePassword)
 
 		if err := AppendTrustStores(config, container, []string{
 			"HONO_CREDENTIALS_TRUST_STORE_PATH",
diff --git a/pkg/controller/iotconfig/iotconfig_controller.go b/pkg/controller/iotconfig/iotconfig_controller.go
index cd9dcdcc4..8a89443f0 100644
--- a/pkg/controller/iotconfig/iotconfig_controller.go
+++ b/pkg/controller/iotconfig/iotconfig_controller.go
@@ -39,16 +39,14 @@ func Add(mgr manager.Manager) error {
 	return add(mgr, newReconciler(
 		mgr,
 		util.GetEnvOrDefault("ENMASSE_IOT_CONFIG_NAME", "default"),
-		util.GetEnvOrDefault("ENMASSE_IOT_CONFIG_NAMESPACE", "enmasse-infra"),
 	))
 }
 
-func newReconciler(mgr manager.Manager, configName string, configNamespace string) *ReconcileIoTConfig {
+func newReconciler(mgr manager.Manager, configName string) *ReconcileIoTConfig {
 	return &ReconcileIoTConfig{
-		client:          mgr.GetClient(),
-		scheme:          mgr.GetScheme(),
-		configName:      configName,
-		configNamespace: configNamespace,
+		client:     mgr.GetClient(),
+		scheme:     mgr.GetScheme(),
+		configName: configName,
 	}
 }
 
@@ -111,9 +109,8 @@ type ReconcileIoTConfig struct {
 	scheme *runtime.Scheme
 
 	// The name of the configuration we are watching
-	// we are watching only one config, in one namespace
-	configName      string
-	configNamespace string
+	// we are watching only one config, in our own namespace
+	configName string
 }
 
 // Reconcile
@@ -145,21 +142,14 @@ func (r *ReconcileIoTConfig) Reconcile(request reconcile.Request) (reconcile.Res
 		return reconcile.Result{}, err
 	}
 
-	if config.Name != r.configName || config.Namespace != r.configNamespace {
+	if config.Name != r.configName {
 		return r.failWrongConfigName(ctx, config)
 	}
 
 	rc := &recon.ReconcileContext{}
 
 	rc.ProcessSimple(func() error {
-		if config.Status.AuthenticationServicePSK == nil {
-			s, err := util.GeneratePassword(128)
-			if err != nil {
-				return err
-			}
-			config.Status.AuthenticationServicePSK = &s
-		}
-		return nil
+		return r.processGeneratedCredentials(ctx, config)
 	})
 	rc.Process(func() (reconcile.Result, error) {
 		return r.processQdrProxyConfig(ctx, config)
@@ -363,3 +353,30 @@ func (r *ReconcileIoTConfig) processRoute(ctx context.Context, name string, conf
 
 	return nil
 }
+
+func (r *ReconcileIoTConfig) processGeneratedCredentials(ctx context.Context, config *iotv1alpha1.IoTConfig) error {
+
+	// generate auth service PSK
+
+	if config.Status.AuthenticationServicePSK == nil {
+		s, err := util.GeneratePassword(128)
+		if err != nil {
+			return err
+		}
+		config.Status.AuthenticationServicePSK = &s
+	}
+
+	// ensure we have all adapter status entries we want
+
+	config.Status.Adapters = ensureAdapterStatus(config.Status.Adapters, "mqtt", "http")
+
+	// generate adapter users
+
+	if err := ensureAdapterAuthCredentials(config.Status.Adapters); err != nil {
+		return err
+	}
+
+	// return
+
+	return nil
+}
diff --git a/pkg/controller/iotconfig/mqtt_adapter.go b/pkg/controller/iotconfig/mqtt_adapter.go
index 7698ac528..873b97b5b 100644
--- a/pkg/controller/iotconfig/mqtt_adapter.go
+++ b/pkg/controller/iotconfig/mqtt_adapter.go
@@ -95,7 +95,7 @@ func (r *ReconcileIoTConfig) reconcileMqttAdapterDeployment(config *iotv1alpha1.
 			{Name: "HONO_MQTT_NATIVE_TLS_REQUIRED", Value: "false"},
 		}
 
-		AppendHonoAdapterEnvs(container, "mqtt-adapter@HONO", "mqtt-secret")
+		AppendHonoAdapterEnvs(container, "mqtt-adapter@HONO", config.Status.Adapters["mqtt"].InterServicePassword)
 
 		if err := AppendTrustStores(config, container, []string{
 			"HONO_CREDENTIALS_TRUST_STORE_PATH",
diff --git a/pom.xml b/pom.xml
index ff069120d..ca241941c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -42,7 +42,7 @@
         <maven.compiler.target>11</maven.compiler.target>
         <fabric8.kubernetes-client.version>4.1.3</fabric8.kubernetes-client.version>
         <sundrio.version>0.16.4</sundrio.version>
-        <vertx.version>3.6.2</vertx.version>
+        <vertx.version>3.6.3</vertx.version>
         <slf4j.version>1.7.21</slf4j.version>
         <logback.version>1.0.13</logback.version>
         <hibernate.validator.version>6.0.10.Final</hibernate.validator.version>
@@ -115,6 +115,7 @@
         <module>discovery-lib</module>
         <module>topic-forwarder</module>
         <module>mqtt-gateway</module>
+        <module>none-authservice</module>
         <module>mqtt-lwt</module>
         <module>standard-controller</module>
         <module>address-space-controller</module>
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
index 239a1deb7..45a915684 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressController.java
@@ -121,7 +121,7 @@ public class AddressController implements Watcher<Address> {
 
         long calculatedUsage = System.nanoTime();
         Set<Address> pendingAddresses = filterBy(addressSet, address -> address.getStatus().getPhase().equals(Pending) ||
-                    AddressProvisioner.hasPlansChanged(address));
+                    AddressProvisioner.hasPlansChanged(addressResolver, address));
 
         Map<String, Map<String, UsageInfo>> neededMap = provisioner.checkQuota(usageMap, pendingAddresses, addressSet);
 
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java
index 3ea9ac7c1..42ad70241 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/AddressProvisioner.java
@@ -157,7 +157,9 @@ public class AddressProvisioner {
                 Map<String, Map<String, UsageInfo>> neededMap = checkQuotaForAddress(limits, newUsageMap, address, all);
                 if (neededMap != null) {
                     newUsageMap = neededMap;
+                    AddressPlan addressPlan = addressResolver.getDesiredPlan(address);
                     address.getStatus().setPhase(Phase.Configuring);
+                    address.getStatus().setPlanStatus(AddressPlanStatus.fromAddressPlan(addressPlan));
                     address.putAnnotation(AnnotationKeys.APPLIED_PLAN, address.getSpec().getPlan());
                 } else {
                     if (previousBrokerId != null) {
@@ -275,8 +277,7 @@ public class AddressProvisioner {
     }
 
     private Map<String, Map<String, UsageInfo>> checkQuotaForAddress(Map<String, Double> limits, Map<String, Map<String, UsageInfo>> usage, Address address, Set<Address> addressSet) {
-        AddressType addressType = addressResolver.getType(address);
-        AddressPlan addressPlan = addressResolver.getPlan(addressType, address.getSpec().getPlan());
+        AddressPlan addressPlan = addressResolver.getDesiredPlan(address);
 
         Map<String, Map<String, UsageInfo>> needed = copyUsageMap(usage);
 
@@ -308,7 +309,7 @@ public class AddressProvisioner {
                         }
                     }
                 } else {
-                    if (hasPlansChanged(address)) {
+                    if (hasPlansChanged(addressPlan, address)) {
                         address.removeAnnotation(AnnotationKeys.CLUSTER_ID);
                         address.removeAnnotation(AnnotationKeys.BROKER_ID);
                     }
@@ -330,7 +331,7 @@ public class AddressProvisioner {
 
                     updateBrokerStatus(address, Collections.singletonList(brokerStatus));
 
-                    brokers.sort(Comparator.comparingDouble(BrokerInfo::getCredit));
+                    brokers.sort(Comparator.comparing(BrokerInfo::getBrokerId));
 
                     for (BrokerInfo brokerInfo : brokers) {
                         if (brokerInfo.getCredit() + resourceRequest.getValue() < 1) {
@@ -365,8 +366,14 @@ public class AddressProvisioner {
         return needed;
     }
 
-    static boolean hasPlansChanged(Address address) {
-        return !address.getSpec().getPlan().equals(address.getAnnotation(AnnotationKeys.APPLIED_PLAN));
+    static boolean hasPlansChanged(AddressResolver addressResolver, Address address) {
+        AddressPlan addressPlan = addressResolver.getDesiredPlan(address);
+        return hasPlansChanged(addressPlan, address);
+    }
+
+    static boolean hasPlansChanged(AddressPlan addressPlan, Address address) {
+        return !AddressPlanStatus.fromAddressPlan(addressPlan).equals(address.getStatus().getPlanStatus()) ||
+                !address.getSpec().getPlan().equals(address.getAnnotation(AnnotationKeys.APPLIED_PLAN));
     }
 
     static int sumTotalNeeded(Map<String, Map<String, UsageInfo>> usageMap) {
@@ -447,7 +454,11 @@ public class AddressProvisioner {
             String resourceName = entry.getKey();
             if ("router".equals(resourceName)) {
                 int totalNeeded = sumNeeded(entry.getValue());
-                router.setNewReplicas(Math.max(totalNeeded, router.getInfraConfig() != null ? router.getInfraConfig().getSpec().getRouter().getMinReplicas() : 0));
+                int infraConfigMin = Optional.ofNullable(router.getInfraConfig())
+                        .flatMap(infraConfig -> Optional.ofNullable(router.getInfraConfig().getSpec().getRouter()))
+                        .flatMap(routerSpec -> Optional.ofNullable(routerSpec.getMinReplicas()))
+                        .orElse(1);
+                router.setNewReplicas(Math.max(totalNeeded, infraConfigMin));
             } else if ("broker".equals(resourceName)) {
                 // Provision pooled broker
                 int needPooled = sumNeededMatching(entry.getValue(), pooledPattern);
@@ -504,7 +515,7 @@ public class AddressProvisioner {
             }
         }
 
-        brokers.sort(Comparator.comparingDouble(BrokerInfo::getCredit));
+        brokers.sort(Comparator.comparing(BrokerInfo::getBrokerId));
 
         for (BrokerInfo brokerInfo : brokers) {
             if (brokerInfo.getCredit() + credit < 1) {
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatus.java b/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatus.java
index c2df3858f..ad7bdbd19 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatus.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatus.java
@@ -61,11 +61,13 @@ class RouterStatus {
         int ok = 0;
 
         for (List<String> linkRoute : linkRoutes) {
-            String prefix = linkRoute.get(0);
+            if (linkRoute.size() > 0) {
+                String prefix = linkRoute.get(0);
 
-            // Pooled topics have active link routes
-            if (prefix.equals(address.getSpec().getAddress())) {
-                ok++;
+                // Pooled topics have active link routes
+                if (prefix.equals(address.getSpec().getAddress())) {
+                    ok++;
+                }
             }
         }
 
@@ -83,12 +85,14 @@ class RouterStatus {
         for (RouterStatus routerStatus : routerStatusList) {
 
             for (List<String> autoLink : routerStatus.autoLinks) {
-                String addr = autoLink.get(0);
-                String dir = autoLink.get(2);
-                String operStatus = autoLink.get(3);
-
-                if (addr.equals(address.getSpec().getAddress()) && operStatus.equals("active")) {
-                    active.add(dir);
+                if (autoLink.size() > 3) {
+                    String addr = autoLink.get(0);
+                    String dir = autoLink.get(2);
+                    String operStatus = autoLink.get(3);
+
+                    if (addr.equals(address.getSpec().getAddress()) && operStatus.equals("active")) {
+                        active.add(dir);
+                    }
                 }
             }
         }
@@ -108,14 +112,16 @@ class RouterStatus {
         for (RouterStatus routerStatus : routerStatusList) {
 
             for (List<String> linkRoute : routerStatus.linkRoutes) {
-                String addr = linkRoute.get(0);
-                @SuppressWarnings("unused")
-                String containerId = linkRoute.get(1);
-                String dir = linkRoute.get(2);
-                String operStatus = linkRoute.get(3);
-
-                if (addr.equals(address.getSpec().getAddress()) && operStatus.equals("active")) {
-                    active.add(dir);
+                if (linkRoute.size() > 3) {
+                    String addr = linkRoute.get(0);
+                    @SuppressWarnings("unused")
+                    String containerId = linkRoute.get(1);
+                    String dir = linkRoute.get(2);
+                    String operStatus = linkRoute.get(3);
+
+                    if (addr.equals(address.getSpec().getAddress()) && operStatus.equals("active")) {
+                        active.add(dir);
+                    }
                 }
             }
         }
diff --git a/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatusCollector.java b/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatusCollector.java
index 0f726974d..1785c81fc 100644
--- a/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatusCollector.java
+++ b/standard-controller/src/main/java/io/enmasse/controller/standard/RouterStatusCollector.java
@@ -88,7 +88,10 @@ class RouterStatusCollector {
     private static List<String> filterOnAttribute(List<List<String>> list, int attrNum) {
         List<String> filtered = new ArrayList<>();
         for (List<String> entry : list) {
-            filtered.add(entry.get(attrNum));
+            String filteredValue = entry.get(attrNum);
+            if (filteredValue != null) {
+                filtered.add(filteredValue);
+            }
         }
         return filtered;
     }
@@ -107,10 +110,19 @@ class RouterStatusCollector {
 
         Message response = client.request(message, 10, TimeUnit.SECONDS);
         AmqpValue value = (AmqpValue) response.getBody();
+        if (value == null) {
+            throw new IllegalArgumentException("Unexpected null body");
+        }
         Map<?,?> values = (Map<?,?>) value.getValue();
+        if (values == null) {
+            throw new IllegalArgumentException("Unexpected null body value");
+        }
 
         @SuppressWarnings("unchecked")
         List<List<String>> results = (List<List<String>>) values.get("results");
+        if (results == null) {
+            throw new IllegalArgumentException("Unexpected null results list");
+        }
         return results;
     }
 }
diff --git a/templates/iot/api/010-CustomResourceDefinition-iot-config.yaml b/templates/crds/iotconfigs.crd.yaml
similarity index 100%
rename from templates/iot/api/010-CustomResourceDefinition-iot-config.yaml
rename to templates/crds/iotconfigs.crd.yaml
diff --git a/templates/csv/enmasse.clusterserviceversion.yaml b/templates/csv/enmasse.clusterserviceversion.yaml
index 2fd4af40a..7eb11c76e 100644
--- a/templates/csv/enmasse.clusterserviceversion.yaml
+++ b/templates/csv/enmasse.clusterserviceversion.yaml
@@ -9,10 +9,10 @@ metadata:
     categories: "Streaming & Messaging"
     certified: "false"
     description: EnMasse provides a self-service messaging platform with a uniform interface to manage different messaging infrastructure.
-    containerImage: ${ADDRESS_SPACE_CONTROLLER_IMAGE}
+    containerImage: ${ENMASSE_CONTROLLER_MANAGER_IMAGE}
     createdAt: 2019-02-19T00:00:00Z
     support: EnMasse
-    alm-examples: '[{"apiVersion":"admin.enmasse.io/v1beta1","kind":"StandardInfraConfig","metadata":{"name":"my-standard-infra"},"spec":{"version":"${ENMASSE_VERSION}","broker":{"resources":{"storage":"5Gi"},"addressFullPolicy":"DROP"},"router":{"linkCapacity":50}}},{"apiVersion":"admin.enmasse.io/v1beta1","kind":"BrokeredInfraConfig","metadata":{"name":"my-brokered-infra"},"spec":{"version":"${ENMASSE_VERSION}","broker":{"resources":{"memory":"4Gi"}}}},{"apiVersion":"admin.enmasse.io/v1beta2","kind":"AddressPlan","metadata":{"name":"small-standard-queue"},"spec":{"addressType":"queue","shortDescription":"Small Queue","resources":{"router":0.01,"broker":0.1}}},{"apiVersion":"admin.enmasse.io/v1beta2","kind":"AddressSpacePlan","metadata":{"name":"standard-small"},"spec":{"addressSpaceType":"standard","infraConfigRef":"my-standard-infra","shortDescription":"Small Address Space Plan","resourceLimits":{"router":1,"broker":2,"aggregate":2},"addressPlans":["small-standard-queue"]}]'
+    alm-examples: '[{"apiVersion":"admin.enmasse.io/v1beta1","kind":"StandardInfraConfig","metadata":{"name":"my-standard-infra"},"spec":{"version":"${ENMASSE_VERSION}","broker":{"resources":{"memory":"1Gi","storage":"5Gi"},"addressFullPolicy":"BLOCK"},"router":{"linkCapacity":50,"resources":{"memory":"512Mi"}}}},{"apiVersion":"admin.enmasse.io/v1beta1","kind":"BrokeredInfraConfig","metadata":{"name":"my-brokered-infra"},"spec":{"version":"${ENMASSE_VERSION}","broker":{"resources":{"memory":"4Gi"}}}},{"apiVersion":"admin.enmasse.io/v1beta2","kind":"AddressPlan","metadata":{"name":"small-standard-queue"},"spec":{"addressType":"queue","shortDescription":"Small Queue","resources":{"router":0.01,"broker":0.1}}},{"apiVersion":"admin.enmasse.io/v1beta2","kind":"AddressSpacePlan","metadata":{"name":"standard-small"},"spec":{"addressSpaceType":"standard","infraConfigRef":"my-standard-infra","shortDescription":"Small Address Space Plan","resourceLimits":{"router":1,"broker":2,"aggregate":2},"addressPlans":["small-standard-queue"]}},{"apiVersion":"admin.enmasse.io/v1beta1","kind":"AuthenticationService","metadata":{"name":"none-authservice"},"spec":{"type":"none"}}]'
 spec:
   maturity: beta
   displayName: EnMasse
@@ -22,11 +22,11 @@ spec:
 
     ### Supported features
 
-    * ** Built-in authentication and authorization ** - Use the built-in or plug in your own authentication service for authentication and authorization of messaging clients.
+    * **Built-in authentication and authorization** - Use the built-in or plug in your own authentication service for authentication and authorization of messaging clients.
 
-    * ** Self-service messaging for applications ** - The service admin deploys and manages the messaging infrastructure, while applications can request messaging resources without caring about the messaging infrastructure.
+    * **Self-service messaging for applications** - The service admin deploys and manages the messaging infrastructure, while applications can request messaging resources without caring about the messaging infrastructure.
 
-    * ** Supports a wide variety of messaging patterns ** - Choose between JMS-style messaging with strict guarantees, or messaging that supports a larger number of connections and higher throughput.
+    * **Supports a wide variety of messaging patterns** - Choose between JMS-style messaging with strict guarantees, or messaging that supports a larger number of connections and higher throughput.
 
     ### Documentation
     
@@ -35,6 +35,7 @@ spec:
     ### Getting help
 
     * [EnMasse mailing list](https://www.redhat.com/mailman/listinfo/enmasse)
+
     * [#enmasse IRC channel on Freenode](https://webchat.freenode.net/?randomnick=1&channels=enmasse&uio=d4)
 
     ### Contributing
@@ -42,8 +43,10 @@ spec:
     If you've got some great ideas and use cases for EnMasse, we would love to hear them!
 
     * Raise issues on [GitHub](https://github.com/EnMasseProject/enmasse/issues).
+
     * Read the [Hacking guide](https://github.com/EnMasseProject/enmasse/blob/master/HACKING.md) for details on how to build EnMasse.
-    * Fix issues by opening Pull Requests
+
+    * Fix issues and open Pull Requests
 
     ### License
 
@@ -65,7 +68,7 @@ spec:
       app: enmasse
   links:
   - name: Documentation
-    url: http://enmasse.io/documentation
+    url: https://enmasse.io/documentation
   - name: GitHub
     url: https://github.com/EnMasseProject/enmasse
 
@@ -83,6 +86,20 @@ spec:
     strategy: deployment
     spec:
       permissions:
+      - serviceAccountName: enmasse-operator
+        rules:
+        - apiGroups: [ "apps" ]
+          resources: [ "deployments" ]
+          verbs: [ "get", "list", "watch", "update", "create", "patch", "delete" ]
+        - apiGroups: [ "" ]
+          resources: [ "configmaps", "secrets", "persistentvolumeclaims", "services" ]
+          verbs: [ "get", "list", "watch", "update", "create", "patch", "delete" ]
+        - apiGroups: [ "", "route.openshift.io" ]
+          resources: [ "routes", "routes/custom-host", "routes/status"]
+          verbs: [ "create", "update", "patch", "get", "list", "watch", "delete" ]
+        - apiGroups: [ "admin.enmasse.io" ]
+          resources: [ "authenticationservices", "authenticationservices/finalizers" ]
+          verbs: [ "get", "list", "watch", "update", "create", "patch" ]
       - serviceAccountName: address-space-controller
         rules:
         - apiGroups: [ "admin.enmasse.io" ]
@@ -141,6 +158,21 @@ spec:
           resources: [ "addressspaceplans", "addressplans", "standardinfraconfigs", "brokeredinfraconfigs", "authenticationservices" ]
           verbs: [ "get", "list", "watch" ]
       clusterPermissions:
+      - serviceAccountName: standard-authservice
+        rules:
+        - apiGroups: [ "authentication.k8s.io" ]
+          resources: [ "tokenreviews" ]
+          verbs: [ "create" ]
+      - serviceAccountName: address-space-controller
+        rules:
+        - apiGroups: [ "", "user.openshift.io" ]
+          resources: [ "users" ]
+          verbs: [ "get" ]
+      - serviceAccountName: enmasse-operator
+        rules:
+        - apiGroups: [ "oauth.openshift.io" ]
+          resources: [ "oauthclients" ]
+          verbs: [ "create", "get", "list", "watch" ]
       - serviceAccountName: api-server
         rules:
         - apiGroups: [ "authentication.k8s.io" ]
@@ -180,7 +212,7 @@ spec:
               containers:
               - env:
                 - name: JAVA_OPTS
-                  value: -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps
+                  value: -verbose:gc -Xlog:gc*
                 - name: CERT_DIR
                   value: /api-server-cert
                 - name: ENABLE_RBAC
@@ -252,7 +284,7 @@ spec:
               containers:
               - env:
                 - name: JAVA_OPTS
-                  value: -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps
+                  value: -verbose:gc -Xlog:gc*
                 - name: CERT_DIR
                   value: /api-server-cert
                 - name: ENABLE_RBAC
@@ -298,6 +330,54 @@ spec:
                   name: apiservice-cert
                   readOnly: true
               serviceAccountName: api-server
+      - name: enmasse-operator
+        spec:
+          replicas: 1
+          selector:
+            matchLabels:
+              app: enmasse
+              name: enmasse-operator
+          template:
+            metadata:
+              labels:
+                app: enmasse
+                name: enmasse-operator
+            spec:
+              affinity:
+                nodeAffinity:
+                  preferredDuringSchedulingIgnoredDuringExecution:
+                    - weight: 1
+                      preference:
+                        matchExpressions:
+                          - key: node-role.enmasse.io/operator-infra
+                            operator: In
+                            values:
+                              - "true"
+              serviceAccountName: enmasse-operator
+              containers:
+              - name: controller
+                image: ${ENMASSE_CONTROLLER_MANAGER_IMAGE}
+                imagePullPolicy: ${IMAGE_PULL_POLICY}
+                env:
+                - name: POD_NAME
+                  valueFrom:
+                    fieldRef:
+                      fieldPath: metadata.name
+                - name: NAMESPACE
+                  valueFrom:
+                    fieldRef:
+                      fieldPath: metadata.namespace
+                - name: OPERATOR_NAME
+                  value: "enmasse-operator"
+                - name: ENMASSE_DEFAULT_PULL_POLICY
+                  value: "${IMAGE_PULL_POLICY}"
+                - name: CONTROLLER_DISABLE_ALL
+                  value: "true"
+                - name: CONTROLLER_ENABLE_AUTHENTICATION_SERVICE
+                  value: "true"
+                resources:
+                  limits:
+                    memory: 128Mi
       - name: address-space-controller
         spec:
           replicas: 1
@@ -475,35 +555,15 @@ spec:
   customresourcedefinitions:
     owned:
       - group: admin.enmasse.io
-        version: v1alpha1
+        version: v1beta1
         kind: AuthenticationService
         name: authenticationservices.admin.enmasse.io
         displayName: Authentication Service
         description: Authentication service configuration available to address spaces.
         specDescriptors:
-          - description: The hostname of the authentication service.
-            displayName: Hostname
-            path: host
-            x-descriptors:
-              - 'urn:alm:descriptor:com.tectonic.ui:label'
-          - description: The port of the authentication service
-            displayName: Port
-            path: port
-            x-descriptors:
-              - 'urn:alm:descriptor:com.tectonic.ui:label'
-          - description: The realm to use for the authentication service.
-            displayName: Realm
-            path: realm
-            x-descriptors:
-              - 'urn:alm:descriptor:com.tectonic.ui:label'
-          - description: The secret containing the CA certificate of the authentication service.
-            displayName: CA Secret Name
-            path: caCertSecretName
-            x-descriptors:
-              - 'urn:alm:descriptor:com.tectonic.ui:label'
-          - description: The secret containing the client certificate to use for authenticating to the authentication service.
-            displayName: Client Secret Name
-            path: clientCertSecretName
+          - description: The type of authentication service
+            displayName: Type
+            path: type
             x-descriptors:
               - 'urn:alm:descriptor:com.tectonic.ui:label'
       - group: admin.enmasse.io
@@ -649,3 +709,9 @@ spec:
             path: resources.router
             x-descriptors:
               - 'urn:alm:descriptor:com.tectonic.ui:label'
+      - group: iot.enmasse.io
+        version: v1alpha1
+        kind: IotConfig
+        name: iotconfigs.iot.enmasse.io
+        displayName: IoT Config
+        description: IoT Infrastructure Configuration
diff --git a/templates/enmasse-operator/020-Role-enmasse-operator.yaml b/templates/enmasse-operator/020-Role-enmasse-operator.yaml
index cbb3d7654..4c6c3123b 100644
--- a/templates/enmasse-operator/020-Role-enmasse-operator.yaml
+++ b/templates/enmasse-operator/020-Role-enmasse-operator.yaml
@@ -17,3 +17,6 @@ rules:
   - apiGroups: [ "admin.enmasse.io" ]
     resources: [ "authenticationservices", "authenticationservices/finalizers" ]
     verbs: [ "get", "list", "watch", "update", "create", "patch" ]
+  - apiGroups: [ "iot.enmasse.io" ]
+    resources: [ "iotconfigs", "iotconfigs/finalizers" ]
+    verbs: [ "get", "list", "watch", "update", "create", "patch" ]
diff --git a/templates/enmasse-operator/050-Deployment-enmasse-operator.yaml b/templates/enmasse-operator/050-Deployment-enmasse-operator.yaml
index af38a24a9..0f6709257 100644
--- a/templates/enmasse-operator/050-Deployment-enmasse-operator.yaml
+++ b/templates/enmasse-operator/050-Deployment-enmasse-operator.yaml
@@ -17,6 +17,16 @@ spec:
         app: enmasse
         name: enmasse-operator
     spec:
+      affinity:
+        nodeAffinity:
+          preferredDuringSchedulingIgnoredDuringExecution:
+            - weight: 1
+              preference:
+                matchExpressions:
+                  - key: node-role.enmasse.io/operator-infra
+                    operator: In
+                    values:
+                      - "true"
       serviceAccountName: enmasse-operator
       containers:
       - name: controller
@@ -37,6 +47,8 @@ spec:
           value: "${IMAGE_PULL_POLICY}"
         - name: CONTROLLER_DISABLE_ALL
           value: "true"
+        - name: CONTROLLER_ENABLE_IOT_CONFIG
+          value: "true"
         - name: CONTROLLER_ENABLE_AUTHENTICATION_SERVICE
           value: "true"
         resources:


Test patch:
diff --git a/api-model/src/test/java/io/enmasse/iot/model/v1/IoTConfigTest.java b/api-model/src/test/java/io/enmasse/iot/model/v1/IoTConfigTest.java
new file mode 100644
index 000000000..ddc33167c
--- /dev/null
+++ b/api-model/src/test/java/io/enmasse/iot/model/v1/IoTConfigTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2018, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.iot.model.v1;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+public class IoTConfigTest {
+
+    @Test
+    void testCreateConfig() {
+        IoTConfig config = new IoTConfigBuilder()
+                .withNewMetadata()
+                .withName("config")
+                .endMetadata()
+                .withNewSpec()
+                .withNewInterServiceCertificates()
+                .withNewSecretCertificatesStrategy()
+                .withCaSecretName("ca-secret-name")
+                .withServiceSecretNames(Collections.singletonMap("iot", "value"))
+                .endSecretCertificatesStrategy()
+                .endInterServiceCertificates()
+                .withImageOverrides(Collections.singletonMap("image1",
+                        new ImageOverrideBuilder()
+                            .withName("img")
+                            .withImagePullPolicy("Always")
+                            .build()))
+                .endSpec()
+                .build();
+
+        assertEquals("config", config.getMetadata().getName());
+        assertNotNull(config.getSpec().getInterServiceCertificates());
+        assertNotNull(config.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy());
+        assertEquals("ca-secret-name",
+                config.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy().getCaSecretName());
+        assertEquals(Collections.singletonMap("iot", "value"),
+                config.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy().getServiceSecretNames());
+        assertNotNull(config.getSpec().getImageOverrides());
+        ImageOverride image = config.getSpec().getImageOverrides().get("image1");
+        assertNotNull(image);
+        assertEquals("img", image.getName());
+        assertEquals("Always", image.getImagePullPolicy());
+        assertNull(config.getSpec().getEnableDefaultRoutes());
+    }
+
+    @Test
+    public void testParse() throws IOException {
+        final ObjectMapper mapper = new ObjectMapper();
+
+        final URL url = IoTProjectTest.class.getResource("resources/iot-config.json");
+        final IoTConfig value = mapper.readValue(url, IoTConfig.class);
+
+        assertThat(value, notNullValue());
+
+        assertThat(value.getMetadata(), notNullValue());
+        assertThat(value.getMetadata().getName(), is("default"));
+
+        assertThat(value.getSpec(), notNullValue());
+
+        assertThat(value.getSpec().getEnableDefaultRoutes(), notNullValue());
+        assertThat(value.getSpec().getEnableDefaultRoutes(), is(true));
+
+        assertThat(value.getSpec().getImageOverrides(), notNullValue());
+        assertThat(value.getSpec().getImageOverrides().get("image1"), notNullValue());
+        assertThat(value.getSpec().getImageOverrides().get("image2"), notNullValue());
+        assertThat(value.getSpec().getImageOverrides().get("image1").getName(), is("image1-name"));
+        assertThat(value.getSpec().getImageOverrides().get("image1").getImagePullPolicy(), is("Always"));
+
+        assertThat(value.getSpec().getInterServiceCertificates(), notNullValue());
+        assertThat(value.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy(), notNullValue());
+        assertThat(value.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy().getCaSecretName(), is("ca-name"));
+        assertThat(value.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy().getServiceSecretNames(), notNullValue());
+        assertThat(value.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy().getServiceSecretNames().get("secret1"), is("value1"));
+        assertThat(value.getSpec().getInterServiceCertificates().getSecretCertificatesStrategy().getServiceSecretNames().get("secret2"), is("value2"));
+
+        assertThat(value.getStatus(), notNullValue());
+        assertThat(value.getStatus().isInitialized(), is(true));
+        assertThat(value.getStatus().getState(), is("ok"));
+        assertThat(value.getStatus().getAuthenticationServicePSK(), is("psk"));
+
+
+    }
+
+}
diff --git a/api-model/src/test/resources/io/enmasse/iot/model/v1/resources/iot-config.json b/api-model/src/test/resources/io/enmasse/iot/model/v1/resources/iot-config.json
new file mode 100644
index 000000000..6d89f2fac
--- /dev/null
+++ b/api-model/src/test/resources/io/enmasse/iot/model/v1/resources/iot-config.json
@@ -0,0 +1,34 @@
+{
+	"apiVersion": "iot.enmasse.io/v1alpha1",
+	"kind": "IoTConfig",
+	"metadata": {
+		"name": "default"
+	},
+	"spec": {
+		"enableDefaultRoutes": true,
+		"imageOverrides": {
+			"image1": {
+				"name": "image1-name",
+				"imagePullPolicy": "Always"
+			},
+			"image2": {
+				"name": "image2-name",
+				"imagePullPolicy": "Always"
+			}
+		},
+		"interServiceCertificates": {
+			"secretCertificatesStrategy": {
+				"caSecretName": "ca-name",
+				"serviceSecretNames": {
+					"secret1": "value1",
+					"secret2": "value2"
+				}
+			}
+		}
+	},
+	"status": {
+		"initialized": true,
+		"state": "ok",
+		"authenticationServicePSK": "psk"
+	}
+}
\ No newline at end of file
diff --git a/pkg/controller/iotconfig/auth_test.go b/pkg/controller/iotconfig/auth_test.go
new file mode 100644
index 000000000..a54db65b2
--- /dev/null
+++ b/pkg/controller/iotconfig/auth_test.go
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+
+package iotconfig
+
+import (
+	"testing"
+
+	iotv1alpha1 "github.com/enmasseproject/enmasse/pkg/apis/iot/v1alpha1"
+)
+
+func TestAdapterStatus(t *testing.T) {
+
+	as := make(map[string]iotv1alpha1.AdapterStatus)
+
+	as["foo"] = iotv1alpha1.AdapterStatus{}
+	as["bar"] = iotv1alpha1.AdapterStatus{}
+	as["boo"] = iotv1alpha1.AdapterStatus{
+		InterServicePassword: "foobar",
+	}
+
+	as = ensureAdapterStatus(as, "foo", "baz", "boo")
+
+	err := ensureAdapterAuthCredentials(as)
+	if err != nil {
+		t.Fatal("ensureAdapterAuthCredentials failed: ", err)
+		return
+	}
+
+	if len(as) != 3 {
+		t.Fatalf("Length must be 3, but was %d", len(as))
+		return
+	}
+
+	if as["foo"].InterServicePassword == "" {
+		t.Error("InterServicePassword for 'foo' is not set")
+	}
+	if as["baz"].InterServicePassword == "" {
+		t.Error("InterServicePassword for 'baz' is not set")
+	}
+	if as["boo"].InterServicePassword != "foobar" {
+		t.Error("InterServicePassword for 'boo' has changed")
+	}
+}
diff --git a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
index 4c4eca3f5..4bc7ce4da 100644
--- a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
+++ b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressControllerTest.java
@@ -5,12 +5,7 @@
 
 package io.enmasse.controller.standard;
 
-import io.enmasse.address.model.Address;
-import io.enmasse.address.model.AddressBuilder;
-import io.enmasse.address.model.BrokerState;
-import io.enmasse.address.model.BrokerStatus;
-import io.enmasse.address.model.Phase;
-import io.enmasse.address.model.Status;
+import io.enmasse.address.model.*;
 import io.enmasse.config.AnnotationKeys;
 import io.enmasse.k8s.api.AddressApi;
 import io.enmasse.k8s.api.EventLogger;
@@ -41,6 +36,7 @@ public class AddressControllerTest {
     private BrokerSetGenerator mockGenerator;
     private int id = 0;
     private BrokerIdGenerator idGenerator = () -> String.valueOf(id++);
+    private StandardControllerSchema standardControllerSchema = new StandardControllerSchema();
 
     @BeforeEach
     public void setUp() throws IOException {
@@ -50,7 +46,6 @@ public class AddressControllerTest {
         mockApi = mock(AddressApi.class);
         mockClient = mock(OpenShiftClient.class);
         EventLogger eventLogger = mock(EventLogger.class);
-        StandardControllerSchema standardControllerSchema = new StandardControllerSchema();
         when(mockHelper.getRouterCluster()).thenReturn(new RouterCluster("qdrouterd", 1, null));
         StandardControllerOptions options = new StandardControllerOptions();
         options.setAddressSpace("me1");
@@ -83,6 +78,10 @@ public class AddressControllerTest {
                 .withNewStatus()
                 .withReady(true)
                 .withPhase(Phase.Active)
+                .withPlanStatus(AddressPlanStatus.fromAddressPlan(standardControllerSchema.getType().findAddressType("queue").get().findAddressPlan("small-queue").get()))
+                .editOrNewPlanStatus()
+                .withName("small-queue")
+                .endPlanStatus()
                 .endStatus()
 
                 .build();
@@ -106,6 +105,8 @@ public class AddressControllerTest {
                 .withNewStatus()
                 .withReady(false)
                 .withPhase(Phase.Terminating)
+                .withPlanStatus(AddressPlanStatus.fromAddressPlan(standardControllerSchema.getType().findAddressType("queue").get().findAddressPlan("small-queue").get()))
+
                 .endStatus()
 
                 .build();
diff --git a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
index 583cbecd9..b0f659f34 100644
--- a/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
+++ b/standard-controller/src/test/java/io/enmasse/controller/standard/AddressProvisionerTest.java
@@ -44,22 +44,28 @@ public class AddressProvisionerTest {
         kubernetes = mock(Kubernetes.class);
     }
 
-    private AddressProvisioner createProvisioner() {
-        StandardControllerSchema standardControllerSchema = new StandardControllerSchema();
-        AddressResolver resolver = new AddressResolver(standardControllerSchema.getType());
-        AddressSpaceResolver addressSpaceResolver = new AddressSpaceResolver(standardControllerSchema.getSchema());
-        EventLogger logger = mock(EventLogger.class);
-
-        return new AddressProvisioner(addressSpaceResolver, resolver, standardControllerSchema.getPlan(), generator, kubernetes, logger, "1234", idGenerator);
-    }
-
-    private AddressProvisioner createProvisioner(List<ResourceAllowance> resourceAllowances) {
-        StandardControllerSchema standardControllerSchema = new StandardControllerSchema(resourceAllowances);
-        AddressResolver resolver = new AddressResolver(standardControllerSchema.getType());
-        AddressSpaceResolver addressSpaceResolver = new AddressSpaceResolver(standardControllerSchema.getSchema());
+    private class ProvisionerTestFixture {
+        StandardControllerSchema standardControllerSchema;
+        AddressResolver resolver;
+        AddressSpaceResolver addressSpaceResolver;
         EventLogger logger = mock(EventLogger.class);
+        AddressProvisioner addressProvisioner;
+
+        public ProvisionerTestFixture() {
+            standardControllerSchema = new StandardControllerSchema();
+            resolver = new AddressResolver(standardControllerSchema.getType());
+            addressSpaceResolver = new AddressSpaceResolver(standardControllerSchema.getSchema());
+            logger = mock(EventLogger.class);
+            addressProvisioner = new AddressProvisioner(addressSpaceResolver, resolver, standardControllerSchema.getPlan(), generator, kubernetes, logger, "1234", idGenerator);
+        }
 
-        return new AddressProvisioner(addressSpaceResolver, resolver, standardControllerSchema.getPlan(), generator, kubernetes, logger, "1234", idGenerator);
+        public ProvisionerTestFixture(List<ResourceAllowance> resourceAllowances) {
+            standardControllerSchema = new StandardControllerSchema(resourceAllowances);
+            resolver = new AddressResolver(standardControllerSchema.getType());
+            addressSpaceResolver = new AddressSpaceResolver(standardControllerSchema.getSchema());
+            logger = mock(EventLogger.class);
+            addressProvisioner = new AddressProvisioner(addressSpaceResolver, resolver, standardControllerSchema.getPlan(), generator, kubernetes, logger, "1234", idGenerator);
+        }
     }
 
     @Test
@@ -78,7 +84,7 @@ public class AddressProvisionerTest {
                 .endSpec()
 
                 .build());
-        AddressProvisioner provisioner = createProvisioner();
+        AddressProvisioner provisioner = new ProvisionerTestFixture().addressProvisioner;
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         assertThat(usageMap.size(), is(1));
@@ -153,7 +159,7 @@ public class AddressProvisionerTest {
             annotations.put(AnnotationKeys.BROKER_ID, "broker-1234-1-0");
         }));
 
-        AddressProvisioner provisioner = createProvisioner();
+        AddressProvisioner provisioner = new ProvisionerTestFixture().addressProvisioner;
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         Address largeQueue = createQueue("q4", "xlarge-queue");
@@ -177,7 +183,7 @@ public class AddressProvisionerTest {
         }
 
 
-        AddressProvisioner provisioner = createProvisioner();
+        AddressProvisioner provisioner = new ProvisionerTestFixture().addressProvisioner;
 
         Map<String, Map<String, UsageInfo>> usageMap = new HashMap<>();
         Map<String, Map<String, UsageInfo>> provisionMap = provisioner.checkQuota(usageMap, new LinkedHashSet<>(addresses.values()), new LinkedHashSet<>(addresses.values()));
@@ -199,7 +205,7 @@ public class AddressProvisionerTest {
         addresses.add(createAddress("q1", "queue", "small-queue", annotations -> {annotations.put(AnnotationKeys.BROKER_ID, "broker-1234-0");}));
 
 
-        AddressProvisioner provisioner = createProvisioner();
+        AddressProvisioner provisioner = new ProvisionerTestFixture().addressProvisioner;
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         Address queue = createAddress("q2", "queue", "small-queue");
@@ -228,7 +234,7 @@ public class AddressProvisionerTest {
         addresses.add(createAddress("q2", "queue", "small-queue", annotations -> annotations.put(AnnotationKeys.CLUSTER_ID, "broker-1234-0")));
         id = 1;
 
-        AddressProvisioner provisioner = createProvisioner();
+        AddressProvisioner provisioner = new ProvisionerTestFixture().addressProvisioner;
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         Address queue = createAddress("q3", "queue", "small-queue");
@@ -248,10 +254,10 @@ public class AddressProvisionerTest {
 
     @Test
     public void testProvisionColocated() {
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 2)));
+                new ResourceAllowance("aggregate", 2))).addressProvisioner;
 
         Set<Address> addressSet = Sets.newSet(
                 createQueue("q9", "pooled-queue-tiny"),
@@ -356,10 +362,10 @@ public class AddressProvisionerTest {
         Set<Address> addresses = new HashSet<>();
         addresses.add(createAddress("a1", "anycast", "small-anycast"));
 
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 3),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 4)));
+                new ResourceAllowance("aggregate", 4))).addressProvisioner;
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         Address q1 = createQueue("q1", "xlarge-queue");
@@ -384,10 +390,10 @@ public class AddressProvisionerTest {
     @Test
     public void testUpgradeFromNoAppliedPlan() throws Exception {
 
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 3),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 4)));
+                new ResourceAllowance("aggregate", 4))).addressProvisioner;
 
         Set<Address> addresses = new HashSet<>();
         Address q1 = createQueue("q1", "xlarge-queue");
@@ -408,10 +414,10 @@ public class AddressProvisionerTest {
     @Test
     public void testSwitchShardedAddressPlan() throws Exception {
 
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 3),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 4)));
+                new ResourceAllowance("aggregate", 4))).addressProvisioner;
 
         Address q1 = createQueue("q1", "large-queue");
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(Collections.emptySet());
@@ -436,10 +442,10 @@ public class AddressProvisionerTest {
     @Test
     public void testSwitchPooledToShardedQuotaCheck() throws Exception {
 
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 1),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 4)));
+                new ResourceAllowance("aggregate", 4))).addressProvisioner;
 
         Address q1 = createQueue("q1", "small-queue");
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(Collections.emptySet());
@@ -465,10 +471,10 @@ public class AddressProvisionerTest {
     @Test
     public void testSwitchShardedToShardedQuotaCheck() throws Exception {
 
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 4)));
+                new ResourceAllowance("aggregate", 4))).addressProvisioner;
 
         Address q1 = createQueue("q1", "large-queue");
         Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(Collections.emptySet());
@@ -496,10 +502,11 @@ public class AddressProvisionerTest {
         final Set<Address> addresses = new HashSet<>();
         addresses.add(createAddress("a1", "anycast", "small-anycast"));
 
-        final AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        ProvisionerTestFixture fixture = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 3),
                 new ResourceAllowance("router", 1),
                 new ResourceAllowance("aggregate", 4)));
+        AddressProvisioner provisioner = fixture.addressProvisioner;
         final Map<String, Map<String, UsageInfo>> usageMap = provisioner.checkUsage(addresses);
 
         final String manualClusterId = "foobar";
@@ -508,6 +515,7 @@ public class AddressProvisionerTest {
             annotations.put(AnnotationKeys.CLUSTER_ID, manualClusterId);
             annotations.put(AnnotationKeys.APPLIED_PLAN, "xlarge-queue");
         });
+        q.getStatus().setPlanStatus(AddressPlanStatus.fromAddressPlan(fixture.standardControllerSchema.getType().findAddressType("queue").get().findAddressPlan("xlarge-queue").get()));
 
         final Map<String, Map<String, UsageInfo>> neededMap = provisioner.checkQuota(usageMap, singleton(q), singleton(q));
 
@@ -532,10 +540,10 @@ public class AddressProvisionerTest {
         }
 
 
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 0),
                 new ResourceAllowance("router", 100000),
-                new ResourceAllowance("aggregate", 100000)));
+                new ResourceAllowance("aggregate", 100000))).addressProvisioner;
 
         Map<String, Map<String, UsageInfo>> usageMap = new HashMap<>();
         Map<String, Map<String, UsageInfo>> neededMap = provisioner.checkQuota(usageMap, addresses, addresses);
@@ -548,10 +556,10 @@ public class AddressProvisionerTest {
 
     @Test
     public void testDurableSubscriptionsColocated() {
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 3)));
+                new ResourceAllowance("aggregate", 3))).addressProvisioner;
 
         Set<Address> addressSet = Sets.newSet(
                 createAddress("t1", "topic", "small-topic"),
@@ -575,10 +583,10 @@ public class AddressProvisionerTest {
 
     @Test
     public void testDurableSubscriptionsColocatedStaysOnTopicBroker() {
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 3)));
+                new ResourceAllowance("aggregate", 3))).addressProvisioner;
 
         Set<Address> addressSet = Sets.newSet(
                 createAddress("t1", "topic", "small-topic"),
@@ -623,10 +631,10 @@ public class AddressProvisionerTest {
 
     @Test
     public void testDurableSubscriptionsSharded() throws Exception {
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 3)));
+                new ResourceAllowance("aggregate", 3))).addressProvisioner;
 
         Address t1 = createAddress("t1", "topic", "xlarge-topic");
         Address t2 = createAddress("t2", "topic", "xlarge-topic");
@@ -657,10 +665,10 @@ public class AddressProvisionerTest {
 
     @Test
     public void testLargeSubscription() throws Exception {
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 3)));
+                new ResourceAllowance("aggregate", 3))).addressProvisioner;
 
         Address t1 = createAddress("t1", "topic", "xlarge-topic");
         Address s1 = createSubscription("s1", "t1", "large-subscription");
@@ -687,10 +695,10 @@ public class AddressProvisionerTest {
 
     @Test
     public void testDurableSubscriptionsShardedStaysOnTopicBroker() {
-        AddressProvisioner provisioner = createProvisioner(Arrays.asList(
+        AddressProvisioner provisioner = new ProvisionerTestFixture(Arrays.asList(
                 new ResourceAllowance("broker", 2),
                 new ResourceAllowance("router", 1),
-                new ResourceAllowance("aggregate", 3)));
+                new ResourceAllowance("aggregate", 3))).addressProvisioner;
 
         Address t1 = createAddress("t1", "topic", "small-topic");
         Address t2 = createAddress("t2", "topic", "small-topic");
diff --git a/systemtests/scripts/run_test_kubernetes.sh b/systemtests/scripts/run_test_kubernetes.sh
index 675aa3800..8636f824d 100755
--- a/systemtests/scripts/run_test_kubernetes.sh
+++ b/systemtests/scripts/run_test_kubernetes.sh
@@ -32,14 +32,6 @@ mkdir -p api-server-cert/
 openssl req -new -x509 -batch -nodes -days 11000 -subj "/O=io.enmasse/CN=api-server.${KUBERNETES_NAMESPACE}.svc.cluster.local" -out api-server-cert/tls.crt -keyout api-server-cert/tls.key
 kubectl create secret tls api-server-cert --cert=api-server-cert/tls.crt --key=api-server-cert/tls.key
 
-mkdir -p none-authservice-cert/
-openssl req -new -x509 -batch -nodes -days 11000 -subj "/O=io.enmasse/CN=none-authservice.${KUBERNETES_NAMESPACE}.svc.cluster.local" -out none-authservice-cert/tls.crt -keyout none-authservice-cert/tls.key
-kubectl create secret tls none-authservice-cert --cert=none-authservice-cert/tls.crt --key=none-authservice-cert/tls.key
-
-mkdir -p standard-authservice-cert/
-openssl req -new -x509 -batch -nodes -days 11000 -subj "/O=io.enmasse/CN=standard-authservice.${KUBERNETES_NAMESPACE}.svc.cluster.local" -out standard-authservice-cert/tls.crt -keyout standard-authservice-cert/tls.key
-kubectl create secret tls standard-authservice-cert --cert=standard-authservice-cert/tls.crt --key=standard-authservice-cert/tls.key
-
 sed -i "s/enmasse-infra/${KUBERNETES_NAMESPACE}/" ${ENMASSE_DIR}/install/*/*/*.yaml
 kubectl create -f ${ENMASSE_DIR}/install/bundles/enmasse
 kubectl create -f ${ENMASSE_DIR}/install/components/example-plans
@@ -65,6 +57,7 @@ EOF
 
 if [ "$DEPLOY_IOT" = "true" ]; then
     echo "Deploying IoT components"
+    sed -i "s/enmasse-infra/${KUBERNETES_NAMESPACE}/" ${ENMASSE_DIR}/install/*/*/*/*.yaml
 
     "${BASE_DIR}/iot/examples/k8s-tls/create"
     NAMESPACE="${KUBERNETES_NAMESPACE}" PREFIX="systemtests-" "${BASE_DIR}/iot/examples/k8s-tls/deploy"
diff --git a/systemtests/scripts/test_func.sh b/systemtests/scripts/test_func.sh
index 824d1bb5c..d30fde95a 100755
--- a/systemtests/scripts/test_func.sh
+++ b/systemtests/scripts/test_func.sh
@@ -386,18 +386,23 @@ function get_oc_args() {
 }
 
 function is_upgraded() {
-    IMAGE=$1
+    IMAGE=${1}
     TAG=${TAG:-"latest"}
 
-    IMAGE_TAG=$(echo $IMAGE | cut -f 2 -d ':')
+    IMAGE_TAG=${IMAGE##*:}
     TEMPLATES=$(find ${CURDIR}/../../templates/build/enmasse-${TAG} -name '*.yaml' -exec cat {} \; | grep "image")
     if [[ "${TEMPLATES}" == *"${IMAGE}"* ]]; then
         echo "true"
     else
-        if [[ "${IMAGE_TAG}" == "${TAG}" ]]; then
+        OPERATOR_TEMPLATES=$(cat ${CURDIR}/../../enmasse-controller-manager/build/operatorImageMap.yaml)
+        if [[ "${OPERATOR_TEMPLATES}" == *"${IMAGE}"* ]]; then
             echo "true"
         else
-            echo "false"
+            if [[ "${IMAGE_TAG}" == "${TAG}" ]]; then
+                echo "true"
+            else
+                echo "false"
+            fi
         fi
     fi
 }
diff --git a/systemtests/scripts/wait_until_up.sh b/systemtests/scripts/wait_until_up.sh
index 914e8d17b..631be772b 100755
--- a/systemtests/scripts/wait_until_up.sh
+++ b/systemtests/scripts/wait_until_up.sh
@@ -27,12 +27,15 @@ function waitingContainersReady {
     pods_id=$($CMD get pods -n ${NAMESPACE} | awk 'NR >1 {print $1}')
     for pod_id in ${pods_id}
     do
-        ready=$($CMD get -o json pod -n ${NAMESPACE}  ${pod_id} -o jsonpath={.status.containerStatuses[0].ready})
+        ready=$($CMD get -o json pod -n ${NAMESPACE} ${pod_id} -o jsonpath={.status.containerStatuses[0].ready})
         if [[ "${UPGRADED}" == "true" ]]; then
             image=$($CMD get pod ${pod_id} -o jsonpath={.spec.containers[*].image})
             initImage=$($CMD get pod ${pod_id} -o jsonpath={.spec.initContainers[*].image})
             imageUpgraded=$(is_upgraded ${image})
             initImageUpgraded=$(is_upgraded ${initImage})
+            echo "-----------------------------------------------------"
+            echo "image upgraded: ${imageUpgraded}"
+            echo "init image upgraded: ${initImageUpgraded}"
             if [[ "${imageUpgraded}" == "true" ]] && [[ "${initImageUpgraded}" == "true" ]]; then
                 info "Pod ${pod_id} is upgraded to ${image}, initImage: ${initImage}"
             else
@@ -67,6 +70,7 @@ do
     fi
     num_running=`${CMD} get pods -n ${NAMESPACE}| grep -v deploy | grep -c Running`
     if [[ "$num_running" -eq "$EXPECTED_PODS" ]]; then
+        echo "==================================================================================="
         waitingContainersReady ${NAMESPACE}
         if [[ $? -gt 0 ]]
         then
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/CertBundle.java b/systemtests/src/main/java/io/enmasse/systemtest/CertBundle.java
new file mode 100644
index 000000000..9e75a1439
--- /dev/null
+++ b/systemtests/src/main/java/io/enmasse/systemtest/CertBundle.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.systemtest;
+
+import java.io.File;
+
+public class CertBundle {
+
+    private File caCert;
+    private File crtFile;
+    private String key;
+    private String cert;
+
+    public CertBundle(File caCert, File crtFile, String key, String cert) {
+        this.caCert = caCert;
+        this.crtFile = crtFile;
+        this.key = key;
+        this.cert = cert;
+    }
+
+    public File getCaCert() {
+        return caCert;
+    }
+
+    public File getCrtFile() {
+        return crtFile;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getCert() {
+        return cert;
+    }
+
+}
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/CertProvider.java b/systemtests/src/main/java/io/enmasse/systemtest/CertProvider.java
new file mode 100644
index 000000000..6d8806292
--- /dev/null
+++ b/systemtests/src/main/java/io/enmasse/systemtest/CertProvider.java
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2019, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.systemtest;
+
+public enum CertProvider {
+    certBundle,
+    selfsigned,
+    openshift,
+    wildcard
+}
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/GlobalLogCollector.java b/systemtests/src/main/java/io/enmasse/systemtest/GlobalLogCollector.java
index 315c38830..257bfa347 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/GlobalLogCollector.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/GlobalLogCollector.java
@@ -88,6 +88,21 @@ public class GlobalLogCollector {
         });
     }
 
+    public void collectLogsOfPodsByLabels(String namespace, Map<String, String> labels) {
+        log.info("Store logs from all pods in namespace '{}' matching labels {}", namespace, labels);
+        kubernetes.getLogsByLables(namespace, labels).forEach((podName, podLogs) -> {
+            try {
+                Path podLog = resolveLogFile(namespace + "." + podName + ".log");
+                log.info("log of '{}' pod will be archived with path: '{}'", podName, podLog);
+                try (BufferedWriter bf = Files.newBufferedWriter(podLog)) {
+                    bf.write(podLogs);
+                }
+            } catch (IOException e) {
+                throw new UncheckedIOException(e);
+            }
+        });
+    }
+
     public void collectEvents() throws IOException {
         long timestamp = System.currentTimeMillis();
         log.info("Collecting events in {}", namespace);
@@ -126,7 +141,11 @@ public class GlobalLogCollector {
         allArgs.add("127.0.0.1:55671");
         allArgs.addAll(Arrays.asList(args));
 
-        String output = kubernetes.runOnPod(pod, "router", allArgs.toArray(new String[0]));
+        String output = KubeCMDClient.runOnPod(
+                pod.getMetadata().getNamespace(),
+                pod.getMetadata().getName(),
+                Optional.of("router"),
+                allArgs.toArray(new String[0])).getStdOut();
         try {
             Path routerAutoLinks = resolveLogFile(pod.getMetadata().getName() + filesuffix);
             log.info("router info '{}' pod will be archived with path: '{}'", pod.getMetadata().getName(), routerAutoLinks);
@@ -146,7 +165,11 @@ public class GlobalLogCollector {
     }
 
     private void collectJmap(Pod pod) {
-        kubernetes.runOnPod(pod, "api-server", "jmap", "-dump:live,format=b,file=/tmp/dump.bin", "1");
+        KubeCMDClient.runOnPod(
+                pod.getMetadata().getNamespace(),
+                pod.getMetadata().getName(),
+                Optional.empty(),
+                "api-server", "jmap", "-dump:live,format=b,file=/tmp/dump.bin", "1");
         try {
             Path jmapLog = resolveLogFile(pod.getMetadata().getName() + ".dump." + Instant.now().toString().replace(":", "_") + ".bin");
             KubeCMDClient.copyPodContent(pod.getMetadata().getName(), "/tmp/dump.bin", jmapLog.toAbsolutePath().toString());
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/Kubernetes.java b/systemtests/src/main/java/io/enmasse/systemtest/Kubernetes.java
index bfeee32f4..a2b8b62e8 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/Kubernetes.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/Kubernetes.java
@@ -103,8 +103,8 @@ public abstract class Kubernetes {
         }
     }
 
-    public HashMap<String, String> getLogsOfTerminatedPods(String namespace) {
-        HashMap<String, String> terminatedPodsLogs = new HashMap<>();
+    public Map<String, String> getLogsOfTerminatedPods(String namespace) {
+        Map<String, String> terminatedPodsLogs = new HashMap<>();
         try {
             client.pods().inNamespace(namespace).list().getItems().forEach(pod -> {
                 pod.getStatus().getContainerStatuses().forEach(containerStatus -> {
@@ -128,6 +128,23 @@ public abstract class Kubernetes {
         return terminatedPodsLogs;
     }
 
+    public Map<String, String> getLogsByLables(String namespace, Map<String,String> labels) {
+        Map<String, String> logs = new HashMap<>();
+        try {
+            client.pods().inNamespace(namespace).withLabels(labels).list().getItems().stream()
+                .forEach(pod->{
+                    logs.put(pod.getMetadata().getName(),
+                            client.pods().inNamespace(namespace)
+                                .withName(pod.getMetadata().getName())
+                                .getLog());
+                });
+        } catch (Exception e) {
+            log.error("Error getting logs by labels.");
+            e.printStackTrace();
+        }
+        return logs;
+    }
+
     public void setDeploymentReplicas(String name, int numReplicas) {
         client.apps().deployments().inNamespace(globalNamespace).withName(name).scale(numReplicas, true);
     }
@@ -539,17 +556,15 @@ public abstract class Kubernetes {
                     @Override
                     public void onFailure(Throwable throwable, Response response) {
                         data.completeExceptionally(throwable);
-
                     }
-
                     @Override
                     public void onClose(int i, String s) {
                         data.complete(baos.toString());
                     }
                 }).exec(command)) {
-            return data.get(1, TimeUnit.MINUTES);
+            return data.get(10, TimeUnit.SECONDS);
         } catch (Exception e) {
-            log.warn("Exception running command {} on pod: {}", command, e.getMessage());
+            log.warn("Exception running command {} on pod: {} with exception {}", command, pod.getMetadata().getName(), e);
             return "";
         }
     }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/SystemtestsKubernetesApps.java b/systemtests/src/main/java/io/enmasse/systemtest/SystemtestsKubernetesApps.java
index 14e720707..2872d00d6 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/SystemtestsKubernetesApps.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/SystemtestsKubernetesApps.java
@@ -22,6 +22,7 @@ public class SystemtestsKubernetesApps {
     public static final String SELENIUM_CHROME = "selenium-chrome";
     public static final String SELENIUM_PROJECT = "selenium";
     public static final String SELENIUM_CONFIG_MAP = "rhea-configmap";
+    public static final String OPENSHIFT_CERT_VALIDATOR = "openshift-cert-validator";
 
     public static void deployMessagingClientApp(String namespace, Kubernetes kubeClient) throws Exception {
         kubeClient.createServiceFromResource(namespace, getSystemtestsServiceResource(MESSAGING_CLIENTS, 4242));
@@ -38,6 +39,24 @@ public class SystemtestsKubernetesApps {
         }
     }
 
+    public static void deployOpenshiftCertValidator(String namespace, Kubernetes kubeClient) throws Exception {
+        if (!kubeClient.namespaceExists(namespace)) {
+            kubeClient.createNamespace(namespace);
+        }
+        kubeClient.createServiceFromResource(namespace, getSystemtestsServiceResource(OPENSHIFT_CERT_VALIDATOR, 8080));
+        kubeClient.createDeploymentFromResource(namespace, getOpenshiftCertValidatorDeploymentResource());
+        kubeClient.createIngressFromResource(namespace, getSystemtestsIngressResource(OPENSHIFT_CERT_VALIDATOR, 8080));
+        Thread.sleep(5000);
+    }
+
+    public static void deleteOpenshiftCertValidator(String namespace, Kubernetes kubeClient) {
+        if (kubeClient.deploymentExists(namespace, OPENSHIFT_CERT_VALIDATOR)) {
+            kubeClient.deleteDeployment(namespace, OPENSHIFT_CERT_VALIDATOR);
+            kubeClient.deleteService(namespace, OPENSHIFT_CERT_VALIDATOR);
+            kubeClient.deleteIngress(namespace, OPENSHIFT_CERT_VALIDATOR);
+        }
+    }
+
     public static void deployFirefoxSeleniumApp(String namespace, Kubernetes kubeClient) throws Exception {
         if (!kubeClient.namespaceExists(namespace)) {
             kubeClient.createNamespace(namespace);
@@ -91,6 +110,10 @@ public class SystemtestsKubernetesApps {
         return new Endpoint(kubeClient.getIngressHost(SELENIUM_PROJECT, SELENIUM_CHROME), 80);
     }
 
+    public static Endpoint getOpenshiftCertValidatorEndpoint(String namespace, Kubernetes kubeClient) {
+        return new Endpoint(kubeClient.getIngressHost(namespace, OPENSHIFT_CERT_VALIDATOR), 80);
+    }
+
     private static Deployment getSeleniumNodeDeploymentResource(String appName, String imageName) {
         return new DeploymentBuilder()
                 .withNewMetadata()
@@ -224,8 +247,8 @@ public class SystemtestsKubernetesApps {
     private static ConfigMap getRheaConfigMap() throws Exception {
         File rheaHtml = new File("src/main/resources/rhea.html");
         File rheaJs = new File("client_executable/rhea/dist/rhea.js");
-        String htmlContent = new String(Files.readAllBytes(rheaHtml.toPath()), Charset.forName("UTF-8"));
-        String jsContent = new String(Files.readAllBytes(rheaJs.toPath()), Charset.forName("UTF-8"));
+        String htmlContent = Files.readString(rheaHtml.toPath());
+        String jsContent = Files.readString(rheaJs.toPath());
 
         return new ConfigMapBuilder()
                 .withNewMetadata()
@@ -235,4 +258,39 @@ public class SystemtestsKubernetesApps {
                 .addToData("rhea.js", jsContent)
                 .build();
     }
+
+    private static Deployment getOpenshiftCertValidatorDeploymentResource() {
+        return new DeploymentBuilder()
+                .withNewMetadata()
+                .withName(OPENSHIFT_CERT_VALIDATOR)
+                .endMetadata()
+                .withNewSpec()
+                .withNewSelector()
+                .addToMatchLabels("app", OPENSHIFT_CERT_VALIDATOR)
+                .endSelector()
+                .withReplicas(1)
+                .withNewTemplate()
+                .withNewMetadata()
+                .addToLabels("app", OPENSHIFT_CERT_VALIDATOR)
+                .endMetadata()
+                .withNewSpec()
+                .addNewContainer()
+                .withName(OPENSHIFT_CERT_VALIDATOR)
+                .withImage("famargon/openshift-cert-validator:latest")
+                .addNewPort()
+                .withContainerPort(8080)
+                .endPort()
+                .withNewLivenessProbe()
+                .withNewTcpSocket()
+                .withNewPort(8080)
+                .endTcpSocket()
+                .withInitialDelaySeconds(10)
+                .withPeriodSeconds(5)
+                .endLivenessProbe()
+                .endContainer()
+                .endSpec()
+                .endTemplate()
+                .endSpec()
+                .build();
+    }
 }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/apiclients/OpenshiftCertValidatorApiClient.java b/systemtests/src/main/java/io/enmasse/systemtest/apiclients/OpenshiftCertValidatorApiClient.java
new file mode 100644
index 000000000..6347d34fc
--- /dev/null
+++ b/systemtests/src/main/java/io/enmasse/systemtest/apiclients/OpenshiftCertValidatorApiClient.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2018, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.systemtest.apiclients;
+
+import java.net.HttpURLConnection;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import io.enmasse.systemtest.Endpoint;
+import io.enmasse.systemtest.Kubernetes;
+import io.vertx.core.json.JsonObject;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+import io.vertx.ext.web.codec.BodyCodec;
+
+public class OpenshiftCertValidatorApiClient extends ApiClient {
+
+
+    public OpenshiftCertValidatorApiClient(Kubernetes kubernetes, Endpoint endpoint) {
+        super(kubernetes, () -> endpoint, "");
+    }
+
+    @Override
+    protected String apiClientName() {
+        return "openshift-cert-validator";
+    }
+
+    @Override
+    protected void connect() {
+        this.client = WebClient.create(vertx, new WebClientOptions()
+                .setSsl(false)
+                .setTrustAll(true)
+                .setVerifyHost(false));
+    }
+
+    public JsonObject test(JsonObject request) throws InterruptedException, ExecutionException, TimeoutException {
+        CompletableFuture<JsonObject> responsePromise = new CompletableFuture<>();
+        client.get(endpoint.getPort(), endpoint.getHost(), "")
+                .as(BodyCodec.jsonObject())
+                .timeout(120000)
+                .sendJson(request,
+                        ar -> responseHandler(ar, responsePromise, HttpURLConnection.HTTP_OK, "Error testing openshift-cert-validator"));
+        return responsePromise.get(150000, TimeUnit.SECONDS);
+
+    }
+
+}
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java b/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java
index 2495e2d4d..241d95bbf 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/cmdclients/KubeCMDClient.java
@@ -217,6 +217,15 @@ public class KubeCMDClient extends CmdClient {
         return execute(deleteCmd, DEFAULT_SYNC_TIMEOUT, true);
     }
 
+    public static ExecutionResultData runOnPod(String namespace, String podName, Optional<String> container, String... args) {
+        List<String> runCmd = new ArrayList<>();
+        String[] base = container.map(s -> new String[]{CMD, "exec", podName, "-n", namespace, "--container", s, "--"})
+                .orElseGet(() -> new String[]{CMD, "exec", podName, "-n", namespace, "--"});
+        Collections.addAll(runCmd, base);
+        Collections.addAll(runCmd, args);
+        return execute(runCmd, DEFAULT_SYNC_TIMEOUT, false);
+    }
+
     public static ExecutionResultData runQDstat(String podName, String... args) {
         List<String> runCmd = new ArrayList<>();
         String[] base = new String[]{CMD, "exec", podName, "--", "qdstat"};
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/mqtt/MqttClientFactory.java b/systemtests/src/main/java/io/enmasse/systemtest/mqtt/MqttClientFactory.java
index 9dfb1c0dc..6442dbdfb 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/mqtt/MqttClientFactory.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/mqtt/MqttClientFactory.java
@@ -46,7 +46,7 @@ public class MqttClientFactory {
         this.password = credentials.getPassword();
     }
 
-    private SSLContext tryGetSSLContext(final String... protocols) throws NoSuchAlgorithmException {
+    public SSLContext tryGetSSLContext(final String... protocols) throws NoSuchAlgorithmException {
         for (String protocol : protocols) {
             try {
                 return SSLContext.getInstance(protocol);
@@ -102,12 +102,17 @@ public class MqttClientFactory {
             String externalEndpointName = AddressSpaceUtils.getExternalEndpointName(addressSpace, "mqtt");
             mqttEndpoint = kubernetes.getExternalEndpoint(externalEndpointName + "-" + AddressSpaceUtils.getAddressSpaceInfraUuid(addressSpace));
         }
-        SSLContext sslContext = tryGetSSLContext("TLSv1.2", "TLSv1.1", "TLS", "TLSv1");
-        sslContext.init(null, new X509TrustManager[]{new MyX509TrustManager()}, new SecureRandom());
 
-        SSLSocketFactory sslSocketFactory = new SNISettingSSLSocketFactory(sslContext.getSocketFactory(), mqttEndpoint.getHost());
+        if(options.getSocketFactory()==null) {
+            SSLContext sslContext = tryGetSSLContext("TLSv1.2", "TLSv1.1", "TLS", "TLSv1");
+            sslContext.init(null, new X509TrustManager[]{new MyX509TrustManager()}, new SecureRandom());
 
-        options.setSocketFactory(sslSocketFactory);
+            SSLSocketFactory sslSocketFactory = new SNISettingSSLSocketFactory(sslContext.getSocketFactory(), mqttEndpoint.getHost());
+
+            options.setSocketFactory(sslSocketFactory);
+        }else if(options.getSocketFactory() instanceof SSLSocketFactory) {
+            options.setSocketFactory(new SNISettingSSLSocketFactory((SSLSocketFactory)options.getSocketFactory(), mqttEndpoint.getHost()));
+        }
 
         if (!TestUtils.resolvable(mqttEndpoint)) {
             mqttEndpoint = new Endpoint("localhost", 443);
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/selenium/SeleniumManagement.java b/systemtests/src/main/java/io/enmasse/systemtest/selenium/SeleniumManagement.java
index e5a6fc55d..bc1a724cc 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/selenium/SeleniumManagement.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/selenium/SeleniumManagement.java
@@ -61,13 +61,22 @@ public class SeleniumManagement {
 
     public static void restartSeleniumApp() throws Exception {
         List<String> beforeRestart = Kubernetes.getInstance().listPods(SystemtestsKubernetesApps.SELENIUM_PROJECT).stream().map(pod -> pod.getMetadata().getName()).collect(Collectors.toList());
-        SystemtestsKubernetesApps.deleteSeleniumPod(SystemtestsKubernetesApps.SELENIUM_PROJECT, Kubernetes.getInstance());
-        TestUtils.waitUntilCondition("Selenium pods ready", () -> {
-                    List<String> current = TestUtils.listReadyPods(Kubernetes.getInstance(), SystemtestsKubernetesApps.SELENIUM_PROJECT).stream().map(pod -> pod.getMetadata().getName()).collect(Collectors.toList());
-                    current.removeAll(beforeRestart);
-                    log.info("Following pods are in ready state {}", current);
-                    return current.size() == beforeRestart.size();
-                },
-                new TimeoutBudget(1, TimeUnit.MINUTES));
+        int attempts = 5;
+        for (int i = 1; i <= attempts; i++) {
+            SystemtestsKubernetesApps.deleteSeleniumPod(SystemtestsKubernetesApps.SELENIUM_PROJECT, Kubernetes.getInstance());
+            try {
+                TestUtils.waitUntilCondition("Selenium pods ready", () -> {
+                            List<String> current = TestUtils.listReadyPods(Kubernetes.getInstance(),
+                                    SystemtestsKubernetesApps.SELENIUM_PROJECT).stream().map(pod -> pod.getMetadata().getName()).collect(Collectors.toList());
+                            current.removeAll(beforeRestart);
+                            log.info("Following pods are in ready state {}", current);
+                            return current.size() == beforeRestart.size();
+                        },
+                        new TimeoutBudget(1, TimeUnit.MINUTES));
+                break;
+            } catch (Exception ex) {
+                log.warn("Selenium application was not redeployed correctly, try it again {}/{}", i, attempts);
+            }
+        }
     }
 }
diff --git a/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java b/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java
index c67d22d98..bcde51540 100644
--- a/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java
+++ b/systemtests/src/main/java/io/enmasse/systemtest/utils/TestUtils.java
@@ -19,7 +19,6 @@ import io.fabric8.kubernetes.api.model.Pod;
 import io.vertx.core.VertxException;
 import io.vertx.core.json.JsonArray;
 import io.vertx.core.json.JsonObject;
-import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.function.ThrowingSupplier;
 import org.openqa.selenium.Capabilities;
 import org.openqa.selenium.chrome.ChromeOptions;
@@ -319,7 +318,7 @@ public class TestUtils {
         boolean isReady = false;
         JsonObject annotations = address.getJsonObject("metadata").getJsonObject("annotations");
         if (annotations != null) {
-            String appliedPlan = annotations.getString("enmasse.io/applied-plan");
+            String appliedPlan = address.getJsonObject("status").getJsonObject("planStatus").getString("name");
             String actualPlan = address.getJsonObject("spec").getString("plan");
             isReady = actualPlan.equals(appliedPlan);
         }
@@ -606,7 +605,7 @@ public class TestUtils {
             log.debug("next iteration, remaining time: {}", budget.timeLeft());
             Thread.sleep(2000);
         }
-        Assertions.fail(String.format("Expected: '%s' in content, but was: '%s'", expected, actual));
+        throw new IllegalStateException(String.format("Expected: '%s' in content, but was: '%s'", expected, actual));
     }
 
     public static void waitUntilCondition(final String forWhat, final BooleanSupplier condition, final TimeoutBudget budget) throws Exception {
@@ -622,7 +621,7 @@ public class TestUtils {
             log.debug("next iteration, remaining time: {}", budget.timeLeft());
             Thread.sleep(2000);
         }
-        Assertions.fail("Failed to wait for: " + forWhat);
+        throw new IllegalStateException("Failed to wait for: " + forWhat);
     }
 
     public static void waitForChangedResourceVersion(final TimeoutBudget budget, final AddressApiClient client, final String name, final String currentResourceVersion) throws Exception {
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/bases/TestBase.java b/systemtests/src/test/java/io/enmasse/systemtest/bases/TestBase.java
index d8adb8334..d2f92c357 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/bases/TestBase.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/bases/TestBase.java
@@ -579,14 +579,19 @@ public abstract class TestBase implements ITestBase, ITestSeparator {
     }
 
     protected String getConsoleRoute(AddressSpace addressSpace) {
+        Endpoint consoleEndpoint = getConsoleEndpoint(addressSpace);
+        String consoleRoute = String.format("https://%s", consoleEndpoint.toString());
+        log.info(consoleRoute);
+        return consoleRoute;
+    }
+
+    protected Endpoint getConsoleEndpoint(AddressSpace addressSpace) {
         Endpoint consoleEndpoint = AddressSpaceUtils.getEndpointByServiceName(addressSpace, "console");
         if (consoleEndpoint == null) {
             String externalEndpointName = AddressSpaceUtils.getExternalEndpointName(addressSpace, "console");
             consoleEndpoint = kubernetes.getExternalEndpoint(externalEndpointName);
         }
-        String consoleRoute = String.format("https://%s", consoleEndpoint.toString());
-        log.info(consoleRoute);
-        return consoleRoute;
+        return consoleEndpoint;
     }
 
     /**
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java b/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java
index 91f473b32..c233a659e 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/common/api/ApiServerTest.java
@@ -53,7 +53,7 @@ import static org.hamcrest.MatcherAssert.assertThat;
 import static org.junit.jupiter.api.Assertions.*;
 
 @Tag(isolated)
-class ApiServerTest extends TestBase {
+public class ApiServerTest extends TestBase {
     private static Logger log = CustomLogger.getLogger();
     private static final PlansProvider plansProvider = new PlansProvider(kubernetes);
 
@@ -191,7 +191,7 @@ class ApiServerTest extends TestBase {
         }
     }
 
-    private static void simpleMQTTSendReceive(Address dest, IMqttClient client, int msgCount) throws Exception {
+    public static void simpleMQTTSendReceive(Address dest, IMqttClient client, int msgCount) throws Exception {
         List<MqttMessage> messages = IntStream.range(0, msgCount).boxed().map(i -> {
             MqttMessage m = new MqttMessage();
             m.setPayload(String.format("mqtt-simple-send-receive-%s", i).getBytes(StandardCharsets.UTF_8));
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/common/certs/CertProviderTest.java b/systemtests/src/test/java/io/enmasse/systemtest/common/certs/CertProviderTest.java
new file mode 100644
index 000000000..d2619261b
--- /dev/null
+++ b/systemtests/src/test/java/io/enmasse/systemtest/common/certs/CertProviderTest.java
@@ -0,0 +1,421 @@
+/*
+ * Copyright 2018, EnMasse authors.
+ * License: Apache License 2.0 (see the file LICENSE or http://apache.org/licenses/LICENSE-2.0.html).
+ */
+package io.enmasse.systemtest.common.certs;
+
+import static io.enmasse.systemtest.TestTag.isolated;
+import static org.junit.jupiter.api.Assertions.fail;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManagerFactory;
+
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.FilenameUtils;
+import org.eclipse.paho.client.mqttv3.IMqttClient;
+import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Tag;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
+import org.slf4j.Logger;
+
+import io.enmasse.address.model.Address;
+import io.enmasse.address.model.AddressSpace;
+import io.enmasse.address.model.AuthenticationServiceType;
+import io.enmasse.address.model.CertSpec;
+import io.enmasse.address.model.CertSpecBuilder;
+import io.enmasse.address.model.EndpointSpec;
+import io.enmasse.address.model.ExposeSpecBuilder;
+import io.enmasse.address.model.ExposeType;
+import io.enmasse.address.model.TlsTermination;
+import io.enmasse.systemtest.AddressSpaceType;
+import io.enmasse.systemtest.CertBundle;
+import io.enmasse.systemtest.CertProvider;
+import io.enmasse.systemtest.CustomLogger;
+import io.enmasse.systemtest.DestinationPlan;
+import io.enmasse.systemtest.Endpoint;
+import io.enmasse.systemtest.Environment;
+import io.enmasse.systemtest.SystemtestsKubernetesApps;
+import io.enmasse.systemtest.TimeoutBudget;
+import io.enmasse.systemtest.UserCredentials;
+import io.enmasse.systemtest.VertxFactory;
+import io.enmasse.systemtest.amqp.AmqpClient;
+import io.enmasse.systemtest.apiclients.OpenshiftCertValidatorApiClient;
+import io.enmasse.systemtest.bases.TestBase;
+import io.enmasse.systemtest.common.api.ApiServerTest;
+import io.enmasse.systemtest.executor.Executor;
+import io.enmasse.systemtest.standard.QueueTest;
+import io.enmasse.systemtest.utils.AddressSpaceUtils;
+import io.enmasse.systemtest.utils.AddressUtils;
+import io.vertx.core.buffer.Buffer;
+import io.vertx.core.json.JsonObject;
+import io.vertx.core.net.PemTrustOptions;
+import io.vertx.ext.web.client.WebClient;
+import io.vertx.ext.web.client.WebClientOptions;
+
+@Tag(isolated)
+class CertProviderTest extends TestBase {
+
+    private static Logger log = CustomLogger.getLogger();
+    private static String ENDPOINT_PREFIX = "test-endpoint-";
+
+    private List<File> createdFiles;
+
+    private AddressSpace addressSpace;
+    private UserCredentials user;
+    private Address queue;
+    private Address topic;
+
+    @BeforeEach
+    void setUp() {
+        createdFiles = new ArrayList<>();
+    }
+
+    @AfterEach
+    void tearDown() {
+        createdFiles.forEach(FileUtils::deleteQuietly);
+    }
+
+    @Test
+    void testSelfSigned() throws Exception {
+
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.selfsigned.name())
+                .build());
+
+        String caCert = new String(Base64.getDecoder().decode(getAddressSpace(addressSpace.getMetadata().getName()).getStatus().getCaCert()));
+
+        AmqpClient amqpClient = amqpClientFactory.createQueueClient(addressSpace);
+        amqpClient.getConnectOptions().setCredentials(user).setCert(caCert);
+
+        MqttConnectOptions mqttOptions = new MqttConnectOptions();
+        mqttOptions.setSocketFactory(getSocketFactory(new ByteArrayInputStream(caCert.getBytes())));
+        mqttOptions.setUserName(user.getUsername());
+        mqttOptions.setPassword(user.getPassword().toCharArray());
+        IMqttClient mqttClient = mqttClientFactory.build()
+                .addressSpace(addressSpace)
+                .mqttConnectionOptions(mqttOptions).create();
+
+        testCertProvider(amqpClient, mqttClient);
+    }
+
+    @Test
+    @Disabled("Disabled due to #2427")
+    void testConsoleSelfSignedTrustEndpointCert() throws Exception {
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.selfsigned.name())
+                .build(), false);
+
+        String caCert = new String(Base64.getDecoder().decode(getAddressSpace(addressSpace.getMetadata().getName()).getStatus().getCaCert()));
+
+        WebClient webClient = WebClient.create(VertxFactory.create(), new WebClientOptions()
+                .setSsl(true)
+                .setTrustAll(false)
+                .setPemTrustOptions(new PemTrustOptions()
+                          .addCertValue(Buffer.buffer(caCert)))
+                .setVerifyHost(false));
+
+        testConsole(webClient);
+    }
+
+    @Test
+    @Disabled("Disabled due to #2427")
+    void testConsoleSelfSignedTrustCaCert() throws Exception {
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.selfsigned.name())
+                .build(), false);
+
+        String endpointCert = new String(Base64.getDecoder().decode(
+                getAddressSpace(addressSpace.getMetadata().getName())
+                        .getSpec()
+                        .getEndpoints()
+                        .stream()
+                        .filter(e->e.getService().equals("console"))
+                        .findFirst()
+                        .get()
+                        .getCert()
+                        .getTlsCert()));
+
+        WebClient webClient = WebClient.create(VertxFactory.create(), new WebClientOptions()
+                .setSsl(true)
+                .setTrustAll(false)
+                .setPemTrustOptions(new PemTrustOptions()
+                          .addCertValue(Buffer.buffer(endpointCert)))
+                .setVerifyHost(false));
+
+        testConsole(webClient);
+    }
+
+    @Test
+    void testCertBundle() throws Exception {
+        CertBundle certBundle = createCertBundle();
+
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.certBundle.name())
+                .withTlsKey(certBundle.getKey())
+                .withTlsCert(certBundle.getCert())
+                .build());
+
+        AmqpClient amqpClient = amqpClientFactory.createQueueClient(addressSpace);
+        amqpClient.getConnectOptions()
+            .setCredentials(user)
+            .getProtonClientOptions()
+                .setSsl(true)
+                .setHostnameVerificationAlgorithm("")
+                .setPemTrustOptions(new PemTrustOptions()
+                        .addCertPath(certBundle.getCaCert().getAbsolutePath()))
+                .setTrustAll(false);
+
+        MqttConnectOptions mqttOptions = new MqttConnectOptions();
+        mqttOptions.setSocketFactory(getSocketFactory(new FileInputStream(certBundle.getCaCert())));
+        mqttOptions.setUserName(user.getUsername());
+        mqttOptions.setPassword(user.getPassword().toCharArray());
+        IMqttClient mqttClient = mqttClientFactory.build()
+                .addressSpace(addressSpace)
+                .mqttConnectionOptions(mqttOptions).create();
+
+        testCertProvider(amqpClient, mqttClient);
+
+    }
+
+    @Test
+    @Disabled("Disabled due to #2427")
+    void testConsoleCertBundleTrustEndpointCert() throws Exception {
+        CertBundle certBundle = createCertBundle();
+
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.certBundle.name())
+                .withTlsKey(certBundle.getKey())
+                .withTlsCert(certBundle.getCert())
+                .build(), false);
+
+        WebClient webClient = WebClient.create(VertxFactory.create(), new WebClientOptions()
+                .setSsl(true)
+                .setTrustAll(false)
+                .setPemTrustOptions(new PemTrustOptions()
+                        .addCertPath(certBundle.getCrtFile().getAbsolutePath()))
+                .setVerifyHost(false));
+
+        testConsole(webClient);
+    }
+
+    @Test
+    @Disabled("Disabled due to #2427")
+    void testConsoleCertBundleTrustCaCert() throws Exception {
+        CertBundle certBundle = createCertBundle();
+
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.certBundle.name())
+                .withTlsKey(certBundle.getKey())
+                .withTlsCert(certBundle.getCert())
+                .build(), false);
+
+        WebClient webClient = WebClient.create(VertxFactory.create(), new WebClientOptions()
+                .setSsl(true)
+                .setTrustAll(false)
+                .setPemTrustOptions(new PemTrustOptions()
+                        .addCertPath(certBundle.getCaCert().getAbsolutePath()))
+                .setVerifyHost(false));
+
+        testConsole(webClient);
+    }
+
+    @Test
+    @DisabledIfEnvironmentVariable(named = Environment.useMinikubeEnv, matches = "true")
+    void testOpenshiftCertProvider() throws Exception {
+        createTestEnv(new CertSpecBuilder()
+                .withProvider(CertProvider.openshift.name())
+                .build(),
+                false);
+        String appNamespace = "certificate-validator-ns";
+        boolean testSucceeded = false;
+        try {
+            SystemtestsKubernetesApps.deployOpenshiftCertValidator(appNamespace, kubernetes);
+            OpenshiftCertValidatorApiClient client = new OpenshiftCertValidatorApiClient(kubernetes, SystemtestsKubernetesApps.getOpenshiftCertValidatorEndpoint(appNamespace, kubernetes));
+
+            JsonObject request = new JsonObject();
+            request.put("username", user.getUsername());
+            request.put("password", user.getPassword());
+
+            Endpoint messagingEndpoint = kubernetes.getEndpoint("messaging-"+AddressSpaceUtils.getAddressSpaceInfraUuid(addressSpace), environment.namespace(), "amqps");
+            request.put("messagingHost", messagingEndpoint.getHost());
+            request.put("messagingPort", messagingEndpoint.getPort());
+
+            Endpoint mqttEndpoint = kubernetes.getEndpoint("mqtt-"+AddressSpaceUtils.getAddressSpaceInfraUuid(addressSpace), environment.namespace(), "secure-mqtt");
+            request.put("mqttHost", mqttEndpoint.getHost());
+            request.put("mqttPort", Integer.toString(mqttEndpoint.getPort()));
+
+            Endpoint consoleEndpoint = kubernetes.getEndpoint("console-"+AddressSpaceUtils.getAddressSpaceInfraUuid(addressSpace), environment.namespace(), "https");
+            request.put("consoleHost", consoleEndpoint.getHost());
+            request.put("consolePort", consoleEndpoint.getPort());
+
+            TimeoutBudget timeout = new TimeoutBudget(5, TimeUnit.MINUTES);
+            Exception lastException = null;
+            while (!timeout.timeoutExpired()) {
+                try {
+                    log.info("Making request to openshift-cert-validator {}", request);
+                    JsonObject response = client.test(request);
+                    if (response.containsKey("error")) {
+                        fail("Error testing openshift provider " + response.getString("error"));
+                    } else {
+                        testSucceeded = true;
+                        return;
+                    }
+                } catch (Exception e) {
+                    lastException = e;
+                }
+                log.debug("next iteration, remaining time: {}", timeout.timeLeft());
+                Thread.sleep(5000);
+            }
+            log.error("Timeout expired");
+            if (lastException != null) {
+                throw lastException;
+            }
+        } finally {
+            if (!testSucceeded) {
+                logCollector.collectLogsOfPodsByLabels(appNamespace,
+                        Collections.singletonMap("app", SystemtestsKubernetesApps.OPENSHIFT_CERT_VALIDATOR));
+            }
+            SystemtestsKubernetesApps.deleteOpenshiftCertValidator(appNamespace, kubernetes);
+        }
+    }
+
+    private void createTestEnv(CertSpec endpointCert) throws Exception {
+        createTestEnv(endpointCert, true);
+    }
+
+    private void createTestEnv(CertSpec endpointCert, boolean createAddresses) throws Exception {
+        addressSpace = AddressSpaceUtils.createAddressSpaceObject("cert-provider-addr-space", AddressSpaceType.STANDARD, AuthenticationServiceType.STANDARD);
+        addressSpace.getSpec().setEndpoints(Arrays.asList(
+                createEndpointSpec("messaging", "amqps", endpointCert),
+                createEndpointSpec("console", "https", endpointCert),
+                createEndpointSpec("mqtt", "secure-mqtt", endpointCert)));
+        createAddressSpace(addressSpace);
+
+        user = new UserCredentials("user1", "password1");
+        createUser(addressSpace, user);
+
+        if (createAddresses) {
+            queue = AddressUtils.createQueueAddressObject("test-queue", DestinationPlan.STANDARD_SMALL_QUEUE);
+            topic = AddressUtils.createTopicAddressObject("mytopic", DestinationPlan.STANDARD_LARGE_TOPIC);
+            setAddresses(addressSpace, queue, topic);
+        }
+    }
+
+    private EndpointSpec createEndpointSpec(String service, String servicePort, CertSpec endpointCert) {
+        return new EndpointSpec(ENDPOINT_PREFIX + service,
+                service,
+                new ExposeSpecBuilder()
+                .withRouteServicePort(servicePort)
+                .withType(ExposeType.route)
+                .withRouteTlsTermination(TlsTermination.passthrough)
+                .build(),
+                endpointCert);
+    }
+
+    private void testCertProvider(AmqpClient amqpClient, IMqttClient mqttClient) throws Exception {
+        QueueTest.runQueueTest(amqpClient, queue);
+        mqttClient.connect();
+        ApiServerTest.simpleMQTTSendReceive(topic, mqttClient, 3);
+        mqttClient.disconnect();
+    }
+
+    private void testConsole(WebClient webClient) throws Exception {
+        Endpoint consoleEndpoint = getConsoleEndpoint(addressSpace);
+        CompletableFuture<Optional<Throwable>> promise = new CompletableFuture<>();
+        webClient.get(consoleEndpoint.getPort(), consoleEndpoint.getHost(), "").ssl(true).send(ar -> {
+            log.info("get console " + ar.toString());
+            if (ar.succeeded()) {
+                promise.complete(Optional.empty());
+            } else {
+                log.info("Exception in get console", ar.cause());
+                promise.complete(Optional.of(ar.cause()));
+            }
+        });
+
+        Optional<Throwable> optError = promise.get();
+        if (optError.isPresent()) {
+            fail(optError.get());
+        }
+    }
+
+    private void createSelfSignedCert(File cert, File key) throws Exception {
+        new Executor().execute(Arrays.asList("openssl", "req", "-new", "-days", "11000", "-x509", "-batch", "-nodes",
+                "-out", cert.getAbsolutePath(), "-keyout", key.getAbsolutePath()));
+    }
+
+    public void createCsr(File keyFile, File csrFile) throws Exception {
+        String subjString = "/O=enmasse-systemtests";
+        new Executor().execute(Arrays.asList("openssl", "req", "-new", "-batch", "-nodes", "-keyout",
+                keyFile.getAbsolutePath(), "-subj", subjString, "-out", csrFile.getAbsolutePath()));
+    }
+
+    public File signCsr(File caKey, File caCert, File csrKey, File csrCsr) throws Exception {
+        File crtFile = createTempFile(FilenameUtils.removeExtension(csrKey.getName()), "crt");
+        new Executor().execute(Arrays.asList("openssl", "x509", "-req", "-days", "11000", "-in",
+                csrCsr.getAbsolutePath(), "-CA", caCert.getAbsolutePath(), "-CAkey", caKey.getAbsolutePath(),
+                "-CAcreateserial", "-out", crtFile.getAbsolutePath()));
+        return crtFile;
+    }
+
+    private File createTempFile(String prefix, String suffix) throws IOException {
+        File file = File.createTempFile(prefix, suffix);
+        createdFiles.add(file);
+        return file;
+    }
+
+    private SSLSocketFactory getSocketFactory(InputStream caCrtFile) throws Exception {
+        CertificateFactory cf = CertificateFactory.getInstance("X.509");
+
+        X509Certificate caCert = (X509Certificate) cf.generateCertificate(caCrtFile);
+
+        KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
+        caKs.load(null, null);
+        caKs.setCertificateEntry("ca-certificate", caCert);
+        TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
+        tmf.init(caKs);
+
+        SSLContext context = mqttClientFactory.tryGetSSLContext("TLSv1.2", "TLSv1.1", "TLS", "TLSv1");
+        context.init(null, tmf.getTrustManagers(), new SecureRandom());
+
+        return context.getSocketFactory();
+    }
+
+    private CertBundle createCertBundle() throws Exception {
+        File caCert = createTempFile("certAuthority", "crt");
+        File caKey = createTempFile("certAuthority", "key");
+        createSelfSignedCert(caCert, caKey);
+        String randomName = UUID.randomUUID().toString();
+        File keyFile = createTempFile(randomName, "key");
+        File csrFile = createTempFile(randomName, "csr");
+        createCsr(keyFile, csrFile);
+        File crtFile = signCsr(caKey, caCert, keyFile, csrFile);
+        String key = Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(keyFile));
+        String cert = Base64.getEncoder().encodeToString(FileUtils.readFileToByteArray(crtFile));
+        return new CertBundle(caCert, crtFile, key, cert);
+    }
+
+}
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java b/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java
index 18b37c6ca..1dda952de 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/iot/SimpleK8sDeployTest.java
@@ -8,22 +8,27 @@ package io.enmasse.systemtest.iot;
 import static io.enmasse.systemtest.TestTag.sharedIot;
 import static io.enmasse.systemtest.TestTag.smoke;
 
+import java.io.File;
 import java.nio.file.Paths;
 import java.time.Duration;
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Map;
 
-import io.enmasse.systemtest.utils.TestUtils;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Tag;
 import org.junit.jupiter.api.Test;
 
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import io.enmasse.iot.model.v1.IoTConfig;
+import io.enmasse.iot.model.v1.IoTConfigBuilder;
 import io.enmasse.systemtest.Environment;
 import io.enmasse.systemtest.Kubernetes;
 import io.enmasse.systemtest.TimeoutBudget;
 import io.enmasse.systemtest.cmdclients.KubeCMDClient;
+import io.enmasse.systemtest.utils.TestUtils;
 
 @Tag(sharedIot)
 @Tag(smoke)
@@ -47,15 +52,43 @@ public class SimpleK8sDeployTest {
         IOT_LABELS.put("component", "iot");
     }
 
+    private static File configTempFile;
     private Kubernetes client = Kubernetes.getInstance();
 
     @BeforeAll
     protected static void setup () throws Exception {
-        KubeCMDClient.createFromFile(NAMESPACE, Paths.get("templates/iot/k8s-systemtests-config.json"));
+        Map<String, String> secrets = new HashMap<>();
+        secrets.put("iot-auth-service-tls", "systemtests-iot-auth-service-tls");
+        secrets.put("iot-tenant-service-tls", "systemtests-iot-tenant-service-tls");
+        secrets.put("iot-device-registry-tls", "systemtests-iot-device-registry-tls");
+        secrets.put("iot-http-adapter-tls", "systemtests-iot-http-adapter-tls");
+        secrets.put("iot-mqtt-adapter-tls", "systemtests-iot-mqtt-adapter-tls");
+
+        IoTConfig config = new IoTConfigBuilder()
+                .withNewMetadata()
+                .withName("default")
+                .endMetadata()
+                .withNewSpec()
+                .withNewInterServiceCertificates()
+                .withNewSecretCertificatesStrategy()
+                .withCaSecretName("systemtests-iot-service-ca")
+                .withServiceSecretNames(secrets)
+                .endSecretCertificatesStrategy()
+                .endInterServiceCertificates()
+                .endSpec()
+                .build();
+
+        configTempFile = File.createTempFile("iot-config", "json");
+        new ObjectMapper().writeValue(configTempFile, config);
+
+        KubeCMDClient.createFromFile(NAMESPACE, Paths.get(configTempFile.toURI()));
     }
 
     @AfterAll
     protected static void cleanup() throws Exception {
+        if(configTempFile!=null && configTempFile.exists()) {
+            configTempFile.delete();
+        }
         KubeCMDClient.deleteIoTConfig(NAMESPACE, "default");
     }
 
diff --git a/systemtests/src/test/java/io/enmasse/systemtest/standard/infra/InfraTest.java b/systemtests/src/test/java/io/enmasse/systemtest/standard/infra/InfraTest.java
index a85caf34e..dcf4dd2f5 100644
--- a/systemtests/src/test/java/io/enmasse/systemtest/standard/infra/InfraTest.java
+++ b/systemtests/src/test/java/io/enmasse/systemtest/standard/infra/InfraTest.java
@@ -39,7 +39,7 @@ class InfraTest extends InfraTestBase implements ITestBaseStandard {
         testInfra = PlanUtils.createStandardInfraConfigObject("test-infra-1",
                 PlanUtils.createStandardBrokerResourceObject("512Mi", "1Gi"),
                 PlanUtils.createStandardAdminResourceObject("512Mi"),
-                PlanUtils.createStandardRouterResourceObject("512Mi"),
+                PlanUtils.createStandardRouterResourceObject("256Mi"),
                 environment.enmasseVersion());
         plansProvider.createInfraConfig(testInfra);
 
@@ -56,7 +56,7 @@ class InfraTest extends InfraTestBase implements ITestBaseStandard {
                         new ResourceAllowance("broker", 3.0),
                         new ResourceAllowance("router", 3.0),
                         new ResourceAllowance("aggregate", 5.0)),
-                Arrays.asList(exampleAddressPlan));
+                Collections.singletonList(exampleAddressPlan));
 
         plansProvider.createAddressSpacePlan(exampleSpacePlan);
 
@@ -66,7 +66,7 @@ class InfraTest extends InfraTestBase implements ITestBaseStandard {
 
         setAddresses(exampleAddressSpace, AddressUtils.createTopicAddressObject("example-queue", exampleAddressPlan.getMetadata().getName()));
 
-        assertInfra("512Mi", Optional.of("1Gi"), 2, "256Mi", "512Mi");
+        assertInfra("512Mi", Optional.of("1Gi"), 1, "256Mi", "512Mi");
 
     }
 
diff --git a/systemtests/templates/iot/k8s-systemtests-config.json b/systemtests/templates/iot/k8s-systemtests-config.json
deleted file mode 100644
index 10bcbee87..000000000
--- a/systemtests/templates/iot/k8s-systemtests-config.json
+++ /dev/null
@@ -1,21 +0,0 @@
-{
-  "apiVersion": "iot.enmasse.io/v1alpha1",
-  "kind": "IoTConfig",
-  "metadata": {
-    "name": "default"
-  },
-  "spec": {
-    "interServiceCertificates": {
-      "secretCertificatesStrategy": {
-        "caSecretName": "systemtests-iot-service-ca",
-        "serviceSecretNames": {
-          "iot-auth-service-tls": "systemtests-iot-auth-service-tls",
-          "iot-tenant-service-tls": "systemtests-iot-tenant-service-tls",
-          "iot-device-registry-tls": "systemtests-iot-device-registry-tls",
-          "iot-http-adapter-tls": "systemtests-iot-http-adapter-tls",
-          "iot-mqtt-adapter-tls": "systemtests-iot-mqtt-adapter-tls"
-        }
-      }
-    }
-  }
-}
\ No newline at end of file