diff --git a/class/defaults.yml b/class/defaults.yml
index 3ec61b8..161039d 100644
--- a/class/defaults.yml
+++ b/class/defaults.yml
@@ -1,5 +1,11 @@
 parameters:
   openshift4_console:
+    images:
+      oc:
+        registry: quay.io
+        repository: appuio/oc
+        tag: v4.15
+
     namespace: openshift-console
     namespace_annotations:
       openshift.io/node-selector: ''
diff --git a/component/main.jsonnet b/component/main.jsonnet
index 3a7b223..b627f31 100644
--- a/component/main.jsonnet
+++ b/component/main.jsonnet
@@ -172,48 +172,6 @@ local consoleRoutePatch =
 
 local tls = import 'tls.libsonnet';
 
-// If we deploy cert-manager Certificates, we annotate namespace
-// openshift-config with the `kyvernoAnnotation` defined in `tls.libsonnet`
-// through a ResourceLocker patch. This triggers the the Kyverno policy to
-// copy the cert-manager TLS secrets into namespace openshift-config.
-//
-// We add the ResourceLocker patch to ArgoCD sync-wave 5, so it's guaranteed
-// to be applied in the cluster after the certificate has been issued and
-// before the custom openshift console route config is applied.
-//
-// NOTE: Due to the current implementation of the resource locker component
-// library this prevents other components from also providing ResourceLocker
-// patches for the `openshift-config` namespace.
-local openshiftConfigNsAnnotationPatch =
-  local needsPatch = hostname != null && std.length(tls.certs) > 0;
-  if needsPatch then
-    local target = kube.Namespace('openshift-config');
-    local patch = {
-      metadata: {
-        annotations: tls.kyvernoAnnotation,
-      },
-    };
-    [
-      if obj.kind == 'Patch' then
-        obj {
-          metadata+: {
-            annotations+: {
-              // Annotate namespace openshift-config before we configure the
-              // route certificate, see patch above
-              'argocd.argoproj.io/sync-wave': '5',
-            },
-          },
-        }
-      else
-        obj
-      for obj in
-        po.Patch(
-          target,
-          patch,
-          patchstrategy='application/merge-patch+json'
-        )
-    ];
-
 {
   '00_namespace': kube.Namespace(params.namespace) {
     metadata+: {
@@ -247,6 +205,4 @@ local openshiftConfigNsAnnotationPatch =
     faviconRoute,
   [if consoleRoutePatch != null then '20_ingress_config_patch']:
     consoleRoutePatch,
-  [if openshiftConfigNsAnnotationPatch != null then '20_openshift_config_ns_annotation_patch']:
-    openshiftConfigNsAnnotationPatch,
 }
diff --git a/component/scripts/reconcile-console-secret.sh b/component/scripts/reconcile-console-secret.sh
new file mode 100755
index 0000000..df182d9
--- /dev/null
+++ b/component/scripts/reconcile-console-secret.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+set -euo pipefail
+
+test -n "${SECRET_NAME:-}" || (echo "SECRET_NAME is required" && exit 1)
+
+source_namespace="openshift-console"
+target_namespace="openshift-config"
+
+# # Wait for the secret to be created before trying to get it.
+# # TODO: --for=create is included with OCP 4.17
+# kubectl -n "${source_namespace}" wait secret "${SECRET_NAME}" --for=create --timeout=30m
+echo "Waiting for secret ${SECRET_NAME} to be created"
+while test -z "$(kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" --ignore-not-found -oname)" ; do
+   printf "."
+   sleep 1
+done
+printf "\n"
+
+# When using -w flag kubectl returns the secret once on startup and then again when it changes.
+kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" -ojson -w | jq -c --unbuffered | while read -r secret ; do
+   echo "Syncing secret: $(printf "%s" "$secret" | jq -r '.metadata.name')"
+
+   kubectl -n "$target_namespace" apply --server-side -f <(printf "%s" "$secret" | jq '{"apiVersion": .apiVersion, "kind": .kind, "metadata": {"name": .metadata.name}, "type": .type, "data": .data}')
+done
diff --git a/component/tls.libsonnet b/component/tls.libsonnet
index 97ef008..372295b 100644
--- a/component/tls.libsonnet
+++ b/component/tls.libsonnet
@@ -2,7 +2,6 @@ local cm = import 'lib/cert-manager.libsonnet';
 local com = import 'lib/commodore.libjsonnet';
 local kap = import 'lib/kapitan.libjsonnet';
 local kube = import 'lib/kube.libjsonnet';
-local kyverno = import 'lib/kyverno.libsonnet';
 
 local inv = kap.inventory();
 local params = inv.parameters.openshift4_console;
@@ -36,52 +35,141 @@ local secrets = std.filter(
   ]
 );
 
-local kyvernoAnnotation = {
-  'syn.tools/openshift4-console': 'secret-target-namespace',
-};
-
 local makeCert(c, cert) =
-  assert
-    std.member(inv.applications, 'kyverno') :
-    'You need to add component `kyverno` to the cluster to be able to deploy cert-manager Certificate resources for the the openshift web console.';
+  local sa = kube.ServiceAccount('openshift4-console-sync-' + c) {
+    metadata+: {
+      namespace: params.namespace,
+    },
+  };
+  local sourceNsRole = kube.Role('openshift4-console-sync-' + c) {
+    metadata+: {
+      namespace: params.namespace,
+    },
+    rules: [
+      {
+        apiGroups: [ '' ],
+        resources: [ 'secrets' ],
+        verbs: [ 'get', 'list', 'watch' ],
+      },
+    ],
+  };
+  local targetNsRole = kube.Role('openshift4-console-sync-' + c) {
+    metadata+: {
+      namespace: 'openshift-config',
+    },
+    rules: [
+      {
+        apiGroups: [ '' ],
+        resources: [ 'secrets' ],
+        verbs: [ 'get', 'update', 'patch' ],
+      },
+    ],
+  };
+
   [
     cm.cert(c) {
       metadata+: {
         // Certificate must be deployed in the same namespace as the web
         // console, otherwise OpenShift won't admit the HTTP01 solver route.
-        // We copy the resulting secret to namespace 'openshift-config' with
-        // Kyverno, see below.
+        // We copy the resulting secret to namespace 'openshift-config', see below.
         namespace: params.namespace,
       },
       spec+: {
         secretName: '%s' % c,
       },
     } + com.makeMergeable(cert),
-    kyverno.ClusterPolicy('openshift4-console-sync-' + c) {
-      spec: {
-        rules: [
-          {
-            name: 'Sync "%s" certificate secret to openshift-config' % c,
-            match: {
-              resources: {
-                kinds: [ 'Namespace' ],
-                // We copy the created TLS secret into all namespaces which
-                // have the annotation specified in `kyvernoAnnotation`.
-                annotations: kyvernoAnnotation,
-              },
+    kube.ConfigMap('openshift4-console-sync-' + c) {
+      metadata+: {
+        namespace: params.namespace,
+      },
+      data: {
+        'reconcile-console-secret.sh': (importstr 'scripts/reconcile-console-secret.sh'),
+      },
+    },
+    sa,
+    sourceNsRole,
+    targetNsRole,
+    kube.RoleBinding('openshift4-console-sync-' + c) {
+      metadata+: {
+        namespace: sourceNsRole.metadata.namespace,
+      },
+      subjects_: [ sa ],
+      roleRef_: sourceNsRole,
+    },
+    kube.RoleBinding('openshift4-console-sync-' + c) {
+      metadata+: {
+        namespace: targetNsRole.metadata.namespace,
+      },
+      subjects_: [ sa ],
+      roleRef_: targetNsRole,
+    },
+    kube.Deployment('openshift4-console-sync-' + c) {
+      metadata+: {
+        namespace: params.namespace,
+      },
+      spec+: {
+        strategy: {
+          type: 'Recreate',
+        },
+        replicas: 1,
+        selector: {
+          matchLabels: {
+            app: 'openshift4-console-sync-' + c,
+          },
+        },
+        template+: {
+          metadata: {
+            labels: {
+              app: 'openshift4-console-sync-' + c,
             },
-            generate: {
-              kind: 'Secret',
-              name: c,
-              namespace: '{{request.object.metadata.name}}',
-              synchronize: true,
-              clone: {
-                namespace: params.namespace,
-                name: c,
+          },
+          spec+: {
+            serviceAccountName: 'openshift4-console-sync-' + c,
+            containers: [
+              {
+                name: 'sync',
+                image: '%(registry)s/%(repository)s:%(tag)s' % params.images.oc,
+                workingDir: '/export',
+                env: [
+                  {
+                    name: 'SECRET_NAME',
+                    value: c,
+                  },
+                  {
+                    name: 'HOME',
+                    value: '/export',
+                  },
+                ],
+                command: [
+                  '/scripts/reconcile-console-secret.sh',
+                ],
+                volumeMounts: [
+                  {
+                    name: 'export',
+                    mountPath: '/export',
+                  },
+                  {
+                    name: 'scripts',
+                    mountPath: '/scripts',
+                  },
+                ],
               },
-            },
+            ],
+            volumes: [
+              {
+                name: 'scripts',
+                configMap: {
+                  name: 'openshift4-console-sync-' + c,
+                  defaultMode: 365,  // 365 = 0555
+                },
+              },
+              {
+                name: 'export',
+                emptyDir: {},
+              },
+            ],
           },
-        ],
+        },
       },
     },
   ];
@@ -104,5 +192,4 @@ local certs =
 {
   certs: certs,
   secrets: secrets,
-  kyvernoAnnotation: kyvernoAnnotation,
 }
diff --git a/docs/modules/ROOT/pages/references/parameters.adoc b/docs/modules/ROOT/pages/references/parameters.adoc
index b72f4aa..07afc51 100644
--- a/docs/modules/ROOT/pages/references/parameters.adoc
+++ b/docs/modules/ROOT/pages/references/parameters.adoc
@@ -169,8 +169,7 @@ The dictionary values are then directly directly merged into the mostly empty `C
 OpenShift won't admit the route for the HTTP01 solver pod unless the `Certificate` resources are deployed in the same namespace as the web console.
 This behavior is caused by a security feature in the OpenShift ingress controller operator to not allow malicious actors to abuse hostnames which are already in use in other namespaces.
 
-However, since OpenShift requires that custom TLS secrets for the OpenShift console are stored in namespace `openshift-config`, we deploy a Kyverno policy to clone the TLS secret created by cert-manager into namespace `openshift-config` for each `Certificate` resource.
-Because of that, the component requires that Kyverno is installed on the cluster via the https://hub.syn.tools/kyverno/[Commodore component `kyverno`], when `Certificate` resources are configured in the hierarchy.
+However, since OpenShift requires that custom TLS secrets for the OpenShift console are stored in namespace `openshift-config`, we deploy a script to clone the TLS secret created by cert-manager into namespace `openshift-config` for each `Certificate` resource.
 
 
 == Example: Custom hostname in cluster's app domain
diff --git a/tests/custom-route-managed-tls.yml b/tests/custom-route-managed-tls.yml
index bdc4e17..67e9a6a 100644
--- a/tests/custom-route-managed-tls.yml
+++ b/tests/custom-route-managed-tls.yml
@@ -1,5 +1,3 @@
-applications:
-  - kyverno
 parameters:
   kapitan:
     dependencies:
@@ -9,9 +7,6 @@ parameters:
       - type: https
         source: https://raw.githubusercontent.com/projectsyn/component-patch-operator/v1.2.0/lib/patch-operator.libsonnet
         output_path: vendor/lib/patch-operator.libsonnet
-      - type: https
-        source: https://raw.githubusercontent.com/projectsyn/component-kyverno/v1.4.0/lib/kyverno.libsonnet
-        output_path: vendor/lib/kyverno.libsonnet
 
   patch_operator:
     patch_serviceaccount:
diff --git a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml b/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml
index 8dbfcd1..4f03ea5 100644
--- a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml
+++ b/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/01_certs.yaml
@@ -14,27 +14,166 @@ spec:
     name: letsencrypt-staging
   secretName: console-cluster-example-org-tls
 ---
-apiVersion: kyverno.io/v1
-kind: ClusterPolicy
+apiVersion: v1
+data:
+  reconcile-console-secret.sh: |
+    #!/bin/bash
+    set -euo pipefail
+
+    test -n "${SECRET_NAME:-}" || (echo "SECRET_NAME is required" && exit 1)
+
+    source_namespace="openshift-console"
+    target_namespace="openshift-config"
+
+    # # Wait for the secret to be created before trying to get it.
+    # # TODO: --for=create is included with OCP 4.17
+    # kubectl -n "${source_namespace}" wait secret "${SECRET_NAME}" --for=create --timeout=30m
+    echo "Waiting for secret ${SECRET_NAME} to be created"
+    while test -z "$(kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" --ignore-not-found -oname)" ; do
+       printf "."
+       sleep 1
+    done
+    printf "\n"
+
+    # When using -w flag kubectl returns the secret once on startup and then again when it changes.
+    kubectl -n "${source_namespace}" get secret "${SECRET_NAME}" -ojson -w | jq -c --unbuffered | while read -r secret ; do
+       echo "Syncing secret: $(printf "%s" "$secret" | jq -r '.metadata.name')"
+
+       kubectl -n "$target_namespace" apply --server-side -f <(printf "%s" "$secret" | jq '{"apiVersion": .apiVersion, "kind": .kind, "metadata": {"name": .metadata.name}, "type": .type, "data": .data}')
+    done
+kind: ConfigMap
 metadata:
   annotations: {}
   labels:
     name: openshift4-console-sync-console-cluster-example-org-tls
   name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-console
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  annotations: {}
+  labels:
+    name: openshift4-console-sync-console-cluster-example-org-tls
+  name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-console
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  annotations: {}
+  labels:
+    name: openshift4-console-sync-console-cluster-example-org-tls
+  name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-console
+rules:
+  - apiGroups:
+      - ''
+    resources:
+      - secrets
+    verbs:
+      - get
+      - list
+      - watch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: Role
+metadata:
+  annotations: {}
+  labels:
+    name: openshift4-console-sync-console-cluster-example-org-tls
+  name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-config
+rules:
+  - apiGroups:
+      - ''
+    resources:
+      - secrets
+    verbs:
+      - get
+      - update
+      - patch
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  annotations: {}
+  labels:
+    name: openshift4-console-sync-console-cluster-example-org-tls
+  name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-console
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: openshift4-console-sync-console-cluster-example-org-tls
+subjects:
+  - kind: ServiceAccount
+    name: openshift4-console-sync-console-cluster-example-org-tls
+    namespace: openshift-console
+---
+apiVersion: rbac.authorization.k8s.io/v1
+kind: RoleBinding
+metadata:
+  annotations: {}
+  labels:
+    name: openshift4-console-sync-console-cluster-example-org-tls
+  name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-config
+roleRef:
+  apiGroup: rbac.authorization.k8s.io
+  kind: Role
+  name: openshift4-console-sync-console-cluster-example-org-tls
+subjects:
+  - kind: ServiceAccount
+    name: openshift4-console-sync-console-cluster-example-org-tls
+    namespace: openshift-console
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  annotations: {}
+  labels:
+    name: openshift4-console-sync-console-cluster-example-org-tls
+  name: openshift4-console-sync-console-cluster-example-org-tls
+  namespace: openshift-console
 spec:
-  rules:
-    - generate:
-        clone:
-          name: console-cluster-example-org-tls
-          namespace: openshift-console
-        kind: Secret
-        name: console-cluster-example-org-tls
-        namespace: '{{request.object.metadata.name}}'
-        synchronize: true
-      match:
-        resources:
-          annotations:
-            syn.tools/openshift4-console: secret-target-namespace
-          kinds:
-            - Namespace
-      name: Sync "console-cluster-example-org-tls" certificate secret to openshift-config
+  minReadySeconds: 30
+  replicas: 1
+  revisionHistoryLimit: 10
+  selector:
+    matchLabels:
+      app: openshift4-console-sync-console-cluster-example-org-tls
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        app: openshift4-console-sync-console-cluster-example-org-tls
+    spec:
+      containers:
+        - command:
+            - /scripts/reconcile-console-secret.sh
+          env:
+            - name: SECRET_NAME
+              value: console-cluster-example-org-tls
+            - name: HOME
+              value: /export
+          image: quay.io/appuio/oc:v4.15
+          name: sync
+          volumeMounts:
+            - mountPath: /export
+              name: export
+            - mountPath: /scripts
+              name: scripts
+          workingDir: /export
+      imagePullSecrets: []
+      initContainers: []
+      serviceAccountName: openshift4-console-sync-console-cluster-example-org-tls
+      terminationGracePeriodSeconds: 30
+      volumes:
+        - configMap:
+            defaultMode: 365
+            name: openshift4-console-sync-console-cluster-example-org-tls
+          name: scripts
+        - emptyDir: {}
+          name: export
diff --git a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/20_openshift_config_ns_annotation_patch.yaml b/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/20_openshift_config_ns_annotation_patch.yaml
deleted file mode 100644
index 46ef949..0000000
--- a/tests/golden/custom-route-managed-tls/openshift4-console/openshift4-console/20_openshift_config_ns_annotation_patch.yaml
+++ /dev/null
@@ -1,24 +0,0 @@
-apiVersion: redhatcop.redhat.io/v1alpha1
-kind: Patch
-metadata:
-  annotations:
-    argocd.argoproj.io/sync-options: SkipDryRunOnMissingResource=true
-    argocd.argoproj.io/sync-wave: '5'
-  labels:
-    name: namespace-openshift-config-2c8343f13594d63
-  name: namespace-openshift-config-2c8343f13594d63
-  namespace: syn-patch-operator
-spec:
-  patches:
-    namespace-openshift-config-2c8343f13594d63-patch:
-      patchTemplate: |-
-        "metadata":
-          "annotations":
-            "syn.tools/openshift4-console": "secret-target-namespace"
-      patchType: application/merge-patch+json
-      targetObjectRef:
-        apiVersion: v1
-        kind: Namespace
-        name: openshift-config
-  serviceAccountRef:
-    name: patch-sa