diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d500cc3176a3c5e79190d9a2de35e3ed28532b88..caaab8360eaed0926cd30bf2d01f0816cbefd3d7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,15 +18,13 @@ include:
   - |
     echo "Env vars:"
     echo
-    echo "HOSTNAME:                  $HOSTNAME"
-    echo "IP_ADDRESS:                $IP_ADDRESS"
+    env | grep -E '^(HOSTNAME|CLUSTER_NAME|FQDN|IP_ADDRESS|CLUSTER_DIR|ANSIBLE_HOST_KEY_CHECKING|KANIKO_BUILD_IMAGENAME|SSH_KEY_ID|SHELL|CI_PROJECT_DIR)='
+    echo
     echo "Uptime:                    $(uptime)"
-    echo "ANSIBLE_HOST_KEY_CHECKING: $ANSIBLE_HOST_KEY_CHECKING"
-    echo "KANIKO_BUILD_IMAGENAME:    $KANIKO_BUILD_IMAGENAME"
     echo "KANIKO build image ref:    ${CI_REGISTRY_IMAGE}/${KANIKO_BUILD_IMAGENAME}:${CI_CONTAINER_TAG}"
-    echo "SSH_KEY_ID:                $SSH_KEY_ID"
-    echo "SHELL                      $SHELL"
-    echo "CI_PROJECT_DIR:            $CI_PROJECT_DIR"
+    echo
+  - if [ -f .ci.env ]; then echo "Content of .ci.env:"; cat .ci.env; fi
+  - if [ -f .cluster.env ]; then echo "Content of .ci.env:"; cat .cluster.env; fi
 
 # The dotenv report requires us to report the artifacts in every job that is
 # required with a `needs:` from another job.
@@ -93,22 +91,6 @@ include:
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-monitoring/'
     - if: '$CI_COMMIT_BRANCH == "main"'
 
-.eventrouter_rules:
-  extends:
-    - .monitoring_rules
-
-.loki_rules:
-  extends:
-    - .monitoring_rules
-
-.promtail_rules:
-  extends:
-    - .monitoring_rules
-
-.kube_prometheus_stack_rules:
-  extends:
-    - .monitoring_rules
-
 .nextcloud_rules:
   rules:
     - changes:
@@ -122,14 +104,6 @@ include:
     - if: '$CI_COMMIT_MESSAGE =~ /TRIGGER_JOBS=.*enable-nextcloud/'
     - if: '$CI_COMMIT_BRANCH == "main"'
 
-.cert_manager_rules:
-  extends:
-    - .general_rules
-
-.local_path_provisioner_rules:
-  extends:
-    - .general_rules
-
 .single_sign_on_rules:
   rules:
     - changes:
@@ -187,14 +161,13 @@ include:
 
 stages:
   - build
-  - create-vps
-  - setup-cluster
-  - configure-cluster
-  - kustomization
-  - install-apps
-  - apps-kustomizations-ready
+  - install-cluster
+  - install-stackspin
+  - base-ready
+  - configure-stackspin
+  - optional-apps-ready
   - certs
-  - health-test
+  - cluster-health
   - integration-test
 
 variables:
@@ -221,7 +194,7 @@ ci-test-image-build:
     - *debug_information
   after_script:
     - |
-      echo "CI_CONTAINER_TAG=${CI_COMMIT_REF_SLUG}" > .ci.env
+      echo "CI_CONTAINER_TAG=${CI_COMMIT_REF_SLUG}" | tee .ci.env
   artifacts:
     paths:
       - .ci.env
@@ -256,13 +229,14 @@ report-ci-image-tag:
     - *debug_information
   script:
     - |
-      TAG_INFORMATION=$(curl https://open.greenhost.net/api/v4/projects/stackspin%2Fstackspin/registry/repositories/73/tags/${CI_COMMIT_REF_SLUG});
+      TAG_INFORMATION=$(curl -sS https://open.greenhost.net/api/v4/projects/stackspin%2Fstackspin/registry/repositories/73/tags/${CI_COMMIT_REF_SLUG});
       echo "Tag information: ${TAG_INFORMATION}"
       if [ "$TAG_INFORMATION" == '{"message":"404 Tag Not Found"}' ]; then
-        echo "CI_CONTAINER_TAG=main" > .ci.env
+        CI_CONTAINER_TAG="main"
       else
-        echo "CI_CONTAINER_TAG=${CI_COMMIT_REF_SLUG}" > .ci.env
+        CI_CONTAINER_TAG="${CI_COMMIT_REF_SLUG}"
       fi
+      echo "CI_CONTAINER_TAG=${CI_CONTAINER_TAG}" | tee .ci.env
   artifacts:
     paths:
       - .ci.env
@@ -286,13 +260,14 @@ report-ci-image-tag:
   interruptible: true
 
 
-# Stage: create-vps
-# =================
+# Stage: install-cluster
+# ======================
 #
-# Creates the vps for the pipeline
+# * Creates the vps for the pipeline
+# * Installs k8s with ansible
 
 create-vps:
-  stage: create-vps
+  stage: install-cluster
   variables:
     SUBDOMAIN: "${CI_COMMIT_REF_SLUG}.ci"
     DOMAIN: "stackspin.net"
@@ -304,7 +279,6 @@ create-vps:
     # Make sure .ci.env variables are not lost
     - cat .ci.env >> ${CLUSTER_DIR}/.cluster.env
   extends:
-    - .ssh_setup
     - .report_artifacts
     - .general_rules
   environment:
@@ -314,16 +288,11 @@ create-vps:
     auto_stop_in: 1 week
   interruptible: true
 
-
-# Stage: setup-cluster
-# ====================
-#
-# Installs Stackspin
-
 test-dns:
-  stage: setup-cluster
+  stage: install-cluster
   needs:
     - job: create-vps
+  # Needs a pytest ansible connection to get the configured system resolvers
   script:
     - *debug_information
     - cd ansible/
@@ -332,8 +301,10 @@ test-dns:
     - .general_rules
   interruptible: true
 
-setup-stackspin:
-  stage: setup-cluster
+install-k8s:
+  stage: install-cluster
+  needs:
+    - job: create-vps
   script:
     - *debug_information
     # Copy inventory files to ansible folder for use in install-apps step
@@ -341,60 +312,132 @@ setup-stackspin:
     - cp ${CLUSTER_DIR}/inventory.yml ansible/
     # Set up cluster
     - python3 -m stackspin $HOSTNAME install
+  extends:
+    - .ssh_setup
+    - .report_artifacts
+    - .general_rules
+  interruptible: true
+
+# Terminates a droplet and deletes the branch container image once the MR for it is merged
+terminate-droplet:
+  # Stage has to be the same as the step that created the VPS
+  # https://docs.gitlab.com/ee/ci/environments.html#automatically-stopping-an-environment
+  stage: install-cluster
+  # Gets triggered by on_stop of create-vps job
+  when: manual
+  variables:
+    GIT_STRATEGY: none
+  script:
+    - *debug_information
+    # Delete droplet
+    - python3 -c "import greenhost_cloud; greenhost_cloud.terminate_droplets_by_name(\"^${CI_COMMIT_REF_SLUG}\")"
+    # Delete container image if one was created
+    - >
+      "if [ \"$CI_CONTAINER_TAG\" != \"main\" ]; then
+         curl --request DELETE --header \"PRIVATE-TOKEN: ${CLEANER_TOKEN}\"
+         https://open.greenhost.net/api/v4/projects/stackspin%2Fstackspin/registry/repositories/73/tags/${CI_CONTAINER_TAG};
+       fi"
+  environment:
+    name: $CI_COMMIT_REF_SLUG
+    action: stop
+
+# Stage: install-stackspin
+# ========================
+#
+# Installs flux and stackspin with it
+
+install-stackspin:
+  stage: install-stackspin
+  needs:
+    - job: test-dns
+    - job: install-k8s
+  script:
+    - *debug_information
     # Customize env file, remove all comments and empty lines
-    - sed "s/1.2.3.4/$IP_ADDRESS/; s/example.org/$FQDN/; s/acme_staging=false/acme_staging=true/; s/acme-v02/acme-staging-v02/; /^\s*#.*$/d; /^\s*$/d" install/.flux.env.example >> ${CLUSTER_DIR}/.flux.env
+    - cp install/.flux.env.example ${CLUSTER_DIR}/.flux.env
+    - sed -i "s/1.2.3.4/$IP_ADDRESS/" ${CLUSTER_DIR}/.flux.env
+    - sed -i "s/example.org/$FQDN/"  ${CLUSTER_DIR}/.flux.env
+    - sed -i "/^\s*#.*$/d; /^\s*$/d" ${CLUSTER_DIR}/.flux.env
+    # Use LE Staging in CI
+    - sed -i "s/acme-v02.api.letsencrypt.org/acme-staging-v02.api.letsencrypt.org/" ${CLUSTER_DIR}/.flux.env
     # Deploy secret/stackspin-cluster-variables
     - cp install/kustomization.yaml ${CLUSTER_DIR}
     - kubectl create namespace flux-system
     - kubectl apply -k ${CLUSTER_DIR}
+    # NOTE: Temporarily disabled because ZeroSSL is unstable
     # Add an override so cert-manager uses the ZeroSSL ClusterIssuer
-    - kubectl create namespace cert-manager
-    - kubectl apply -n cert-manager -f ./install/overrides/stackspin-cert-manager-override.yaml
+    # - kubectl create namespace cert-manager
+    # - kubectl apply -n cert-manager -f ./install/overrides/stackspin-cert-manager-override.yaml
     # Install flux and general, non-app specific secrets
     - bash ./install/install-stackspin.sh
   extends:
-    - .ssh_setup
     - .report_artifacts
     - .general_rules
   interruptible: true
 
-
-# Stage: configure-cluster
-# ====================
-#
-# Configure cluster after basic installation
-# i.e. CI-related config like zerossl clusterIssuer
-
-configure-stackspin:
-  stage: configure-cluster
+.enable_app_template:
+  stage: install-stackspin
+  needs:
+    - job: install-stackspin
   script:
     - *debug_information
-    # Install custom ClusterIssuer for ZeroSSL production certificates
-    - bash ./.gitlab/ci_scripts/install_zerossl_issuer.sh
-  extends:
-    - .report_artifacts
-    - .general_rules
+    # Add optional override values we need for the CI pipeline only
+    - >
+      [ -f ./install/overrides/stackspin-${RESOURCE}-override.yaml ] &&
+        kubectl apply -n stackspin-apps -f ./install/overrides/stackspin-${RESOURCE}-override.yaml
+    - bash ./install/install-app.sh ${RESOURCE}
   interruptible: true
 
+enable-monitoring:
+  variables:
+    RESOURCE: "monitoring"
+  extends:
+    - .enable_app_template
+    - .monitoring_rules
 
+enable-nextcloud:
+  variables:
+    RESOURCE: "nextcloud"
+  extends:
+    - .enable_app_template
+    - .nextcloud_rules
 
+enable-wekan:
+  variables:
+    RESOURCE: "wekan"
+  extends:
+    - .enable_app_template
+    - .wekan_rules
 
-# Stage: kustomization
+enable-wordpress:
+  variables:
+    RESOURCE: "wordpress"
+  extends:
+    - .enable_app_template
+    - .wordpress_rules
+
+enable-zulip:
+  variables:
+    RESOURCE: "zulip"
+  extends:
+    - .enable_app_template
+    - .zulip_rules
+
+# Stage: base-ready
 # ====================
 #
-# Tests if all kustomizations are ready
+# Test if base kustomizations are ready, before configuration can get applied
+# that makes use of CRDs, i.e. clusterIssuer
 .kustomization-ready:
-  stage: kustomization
+  stage: base-ready
   needs:
-    - job: configure-stackspin
-    - job: test-dns
+    - job: install-stackspin
   script:
     - *debug_information
-    - cd ansible/
+    - cd test/
     - export KUBECONFIG="${PWD}/../clusters/${HOSTNAME}/kube_config_cluster.yml"
-    - pytest -v -s -m 'kustomizations' --resource="$RESOURCE" --connection=ansible --ansible-inventory=../${CLUSTER_DIR}/inventory.yml --hosts='ansible://*' --reruns 120 --reruns-delay 20
+    - pytest -v -s -m 'kustomizations' --resource="$RESOURCE" --reruns 120 --reruns-delay 20
   extends:
-    - .ssh_setup
     - .general_rules
   interruptible: true
 
@@ -458,70 +501,40 @@ stackspin-kustomization-ready:
   extends:
     - .kustomization-ready
 
-# Stage: install-apps
-# ==================
+# Stage: configure-stackspin
 #
-# Checks if application needs to get installed
-
-.enable_app_template:
-  stage: install-apps
+# Configure cluster after basic installation
+# i.e. CI-related config like zerossl clusterIssuer
+#
+configure-zerossl-issuer:
+  stage: configure-stackspin
+  needs:
+    - job: install-stackspin
+    - job: cert-manager-kustomization-ready
   script:
     - *debug_information
-    # Add optional override values we need for the CI pipeline only
-    - '[ -f ./install/overrides/stackspin-${RESOURCE}-override.yaml ] && kubectl apply -n stackspin-apps -f ./install/overrides/stackspin-${RESOURCE}-override.yaml'
-    - bash ./install/install-app.sh ${RESOURCE}
+    # Install custom ClusterIssuer for ZeroSSL production certificates
+    - bash ./.gitlab/ci_scripts/install_zerossl_issuer.sh
   extends:
-    - .ssh_setup
+    - .report_artifacts
+    - .general_rules
   interruptible: true
 
-enable-monitoring:
-  variables:
-    RESOURCE: "monitoring"
-  extends:
-    - .enable_app_template
-    - .monitoring_rules
-
-enable-nextcloud:
-  variables:
-    RESOURCE: "nextcloud"
-  extends:
-    - .enable_app_template
-    - .nextcloud_rules
 
-enable-wekan:
-  variables:
-    RESOURCE: "wekan"
-  extends:
-    - .enable_app_template
-    - .wekan_rules
 
-enable-wordpress:
-  variables:
-    RESOURCE: "wordpress"
-  extends:
-    - .enable_app_template
-    - .wordpress_rules
-
-enable-zulip:
-  variables:
-    RESOURCE: "zulip"
-  extends:
-    - .enable_app_template
-    - .zulip_rules
-
-# Stage: apps-kustomizations-ready
+# Stage: optional-apps-ready
 # ================
 #
 # Check that the kustomizations of all installed apps are ready.
 
 .app-kustomization-ready:
-  stage: apps-kustomizations-ready
+  stage: optional-apps-ready
   extends:
     - .kustomization-ready
 
 monitoring-kustomization-ready:
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: enable-monitoring
   variables:
     RESOURCE: "monitoring"
@@ -531,7 +544,7 @@ monitoring-kustomization-ready:
 
 nextcloud-kustomization-ready:
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: enable-nextcloud
   variables:
     RESOURCE: "nextcloud"
@@ -541,7 +554,7 @@ nextcloud-kustomization-ready:
 
 wekan-kustomization-ready:
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: enable-wekan
   variables:
     RESOURCE: "wekan"
@@ -551,7 +564,7 @@ wekan-kustomization-ready:
 
 wordpress-kustomization-ready:
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: enable-wordpress
   variables:
     RESOURCE: "wordpress"
@@ -561,7 +574,7 @@ wordpress-kustomization-ready:
 
 zulip-kustomization-ready:
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: enable-zulip
   variables:
     RESOURCE: "zulip"
@@ -578,18 +591,16 @@ zulip-kustomization-ready:
   stage: certs
   script:
     - *debug_information
-    - cd ansible/
-    - pytest -v -s -m 'certs' --resource="$RESOURCE" --connection=ansible --ansible-inventory=../${CLUSTER_DIR}/inventory.yml --hosts='ansible://*' --reruns 120 --reruns-delay 10
-  extends:
-    - .ssh_setup
+    - cd test/
+    - pytest -v -s -m 'certs' --resource="$RESOURCE" --reruns 120 --reruns-delay 10
   interruptible: true
 
 nextcloud-cert:
   variables:
     RESOURCE: "nextcloud"
   needs:
-    - job: enable-nextcloud
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
     - .nextcloud_rules
@@ -598,18 +609,18 @@ kube-prometheus-stack-cert:
   variables:
     RESOURCE: "kube-prometheus-stack"
   needs:
-    - job: enable-monitoring
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
-    - .kube_prometheus_stack_rules
+    - .monitoring_rules
 
 single-sign-on-cert:
   variables:
     RESOURCE: "single-sign-on"
   needs:
-    - job: single-sign-on-kustomization-ready
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
     - .general_rules
@@ -618,8 +629,8 @@ dashboard-cert:
   variables:
     RESOURCE: "dashboard"
   needs:
-    - job: dashboard-kustomization-ready
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
     - .general_rules
@@ -628,8 +639,8 @@ wekan-cert:
   variables:
     RESOURCE: "wekan"
   needs:
-    - job: enable-wekan
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
     - .wekan_rules
@@ -638,8 +649,8 @@ wordpress-cert:
   variables:
     RESOURCE: "wordpress"
   needs:
-    - job: enable-wordpress
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
     - .wordpress_rules
@@ -648,20 +659,22 @@ zulip-cert:
   variables:
     RESOURCE: "zulip"
   needs:
-    - job: enable-zulip
-    - job: configure-stackspin
+    - job: configure-zerossl-issuer
+    - job: install-stackspin
   extends:
     - .apps-cert
     - .zulip_rules
 
 
-# Stage: health-test
-# ==================
+# Stage: cluster-health
+# =====================
 #
 # General cluster health checks
 
 testinfra:
-  stage: health-test
+  stage: cluster-health
+  needs:
+    - job: install-stackspin
   script:
     - *debug_information
     - cd ansible/
@@ -671,22 +684,21 @@ testinfra:
     - .general_rules
   interruptible: true
 
-kube-prometheus-stack-alerts:
-  stage: health-test
+prometheus-alerts:
+  stage: cluster-health
+  needs:
+    - job: install-stackspin
+    - job: kube-prometheus-stack-cert
   variables:
-    # RESOURCE var is used in job specific rules (i.e. .kube_prometheus_stack_rules)
+    # RESOURCE var is used in job specific rules (i.e. ..monitoring_rules)
     RESOURCE: "kube-prometheus-stack"
-    # Enforce python requests using the system cert store, where LE staging
-    # cacert is added
-    REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt"
   script:
     - *debug_information
     - export BASIC_AUTH_PW=$(python3 -m stackspin $HOSTNAME secrets | grep stackspin-prometheus-basic-auth | cut -d'=' -f2)
     - cd test/
-    - bash ../.gitlab/ci_scripts/retry_cmd_until_success.sh 30 10 pytest -s -m 'prometheus' --connection=ansible --ansible-inventory=../${CLUSTER_DIR}/inventory.yml --hosts='ansible://*'
+    - bash ../.gitlab/ci_scripts/retry_cmd_until_success.sh 30 10 pytest -s -m prometheus
   extends:
-    - .ssh_setup
-    - .kube_prometheus_stack_rules
+    - .monitoring_rules
   interruptible: true
 
 
@@ -714,7 +726,7 @@ dashboard-taiko:
   variables:
     RESOURCE: "dashboard"
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: dashboard-cert
     - job: dashboard-kustomization-ready
   extends:
@@ -725,18 +737,18 @@ grafana-taiko:
   variables:
     RESOURCE: "grafana"
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: kube-prometheus-stack-cert
     - job: monitoring-kustomization-ready
   extends:
     - .taiko
-    - .kube_prometheus_stack_rules
+    - .monitoring_rules
 
 nextcloud-taiko:
   variables:
     RESOURCE: "nextcloud"
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: nextcloud-cert
     - job: nextcloud-kustomization-ready
   extends:
@@ -747,7 +759,7 @@ wekan-taiko:
   variables:
     RESOURCE: "wekan"
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: wekan-cert
     - job: wekan-kustomization-ready
   extends:
@@ -758,7 +770,7 @@ wordpress-taiko:
   variables:
     RESOURCE: "wordpress"
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: wordpress-cert
     - job: wordpress-kustomization-ready
   extends:
@@ -769,33 +781,9 @@ zulip-taiko:
   variables:
     RESOURCE: "zulip"
   needs:
-    - job: configure-stackspin
+    - job: install-stackspin
     - job: zulip-cert
     - job: zulip-kustomization-ready
   extends:
     - .taiko
     - .zulip_rules
-
-
-# Etc
-# ===
-
-
-# Terminates a droplet and deletes the branch container image once the MR for it is merged
-terminate-droplet:
-  # Stage has to be the same as the step that created the VPS
-  # https://docs.gitlab.com/ee/ci/environments.html#automatically-stopping-an-environment
-  stage: create-vps
-  # Gets triggered by on_stop of create-vps job
-  when: manual
-  variables:
-    GIT_STRATEGY: none
-  script:
-    - *debug_information
-    # Delete droplet
-    - python3 -c "import greenhost_cloud; greenhost_cloud.terminate_droplets_by_name(\"^${CI_COMMIT_REF_SLUG}\")"
-    # Delete container image if one was created
-    - "if [ \"$CI_CONTAINER_TAG\" != \"main\" ]; then curl --request DELETE --header \"PRIVATE-TOKEN: ${CLEANER_TOKEN}\" https://open.greenhost.net/api/v4/projects/stackspin%2Fstackspin/registry/repositories/73/tags/${CI_CONTAINER_TAG}; fi"
-  environment:
-    name: $CI_COMMIT_REF_SLUG
-    action: stop
diff --git a/.gitlab/ci_scripts/install_zerossl_issuer.sh b/.gitlab/ci_scripts/install_zerossl_issuer.sh
index d6f41df3a48e28de5f7ac9a39ce25e318ac53393..beed27a206c3dc695f297ebeb33c9503ea700d84 100755
--- a/.gitlab/ci_scripts/install_zerossl_issuer.sh
+++ b/.gitlab/ci_scripts/install_zerossl_issuer.sh
@@ -11,9 +11,6 @@ set -euo pipefail
 # Create secret with HMAC key
 b64tlskey=$(echo -n "${ZEROSSL_TLS_KEY}" | base64 -w0)
 
-# Wait until cert-manager is ready
-"$(dirname "$0")/retry_cmd_until_success.sh" 30 10 "flux get kustomization --status-selector ready=true --no-header | grep '^cert-manager'"
-
 # Add ZeroSSL ClusterIssuer
 kubectl apply -n cert-manager -f - <<EOF
 ---
diff --git a/.gitlab/ci_templates/helm_package.yml b/.gitlab/ci_templates/helm_package.yml
new file mode 100644
index 0000000000000000000000000000000000000000..08eaf68285d0e2af15fc095d8082298d3a2c1076
--- /dev/null
+++ b/.gitlab/ci_templates/helm_package.yml
@@ -0,0 +1,63 @@
+# Include this file if you want to package your helm chart to a helm registry.
+# You'll need to set two variables:
+#
+# 1. CHART_NAME: the name of the helm chart. Should correspond to the name in
+#    Chart.yaml
+# 2. CHART_DIR (optional): the directory where your helm chart is in you
+#    repository. HAS TO END WITH A SLASH if you choose to override it.
+
+variables:
+  CHART_DIR: ""
+
+.chart_release_rules:
+  rules:
+    - changes:
+      - ${CHART_DIR}Chart.yaml
+
+lint-helm:
+  stage: lint-helm-chart
+  image:
+    name: alpine/helm:3.7.1
+    entrypoint: ["/bin/sh", "-c"]
+  script:
+    - cd ${CHART_DIR:-"."}
+    - helm dep update
+    - helm lint .
+  artifacts:
+    paths:
+      - '${CHART_DIR}charts/**'
+    expire_in: 1 week
+    # Even if lint fails, upload the charts/ folder as artifact
+    when: always
+  rules:
+    - changes:
+      - ${CHART_DIR}*.yaml
+      - ${CHART_DIR}templates/*.yaml
+
+package-chart:
+  stage: package-helm-chart
+  image:
+    name: alpine/helm:3.7.1
+    entrypoint: ["/bin/sh", "-c"]
+  script:
+    - cd ${CHART_DIR:-"."}
+    - helm package .
+  artifacts:
+    paths:
+      - ${CHART_DIR}${CHART_NAME}-*
+    expire_in: 1 week
+  extends:
+    - .chart_release_rules
+
+# Push helm chart. Charts on the default branch are pushed to `stable`, others
+# are pushed to the `unstable` channel.
+release-helm:
+  image: "rancher/curlimages-curl:7.73.0"
+  stage: release-helm-chart
+  script:
+    - cd ${CHART_DIR:-"."}
+    - if [ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]; then export HELM_CHANNEL='stable'; else export HELM_CHANNEL='unstable'; fi
+    - export CHART_FILE=$(ls ${CHART_NAME}-*.tgz)
+    - curl --fail --request POST --user gitlab-ci-token:$CI_JOB_TOKEN --form "chart=@${CHART_FILE}" "${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/${HELM_CHANNEL}/charts"
+  extends:
+    - .chart_release_rules
diff --git a/.gitlab/commit_template.txt b/.gitlab/commit_template.txt
index de8d77085d583220987c6e2fba3afb5edfed0b1b..f87d232ce6dc7be34f585b3a324685abe0943e14 100644
--- a/.gitlab/commit_template.txt
+++ b/.gitlab/commit_template.txt
@@ -9,7 +9,7 @@
 # TRIGGER_JOBS=enable-nextcloud
 #
 # or trigger all jobs:
-# TRIGGER_JOBS=enable-monitoring,enable-nextcloud,enable-single-sign-on,enable-wekan,enable-wordpress,enable-zulip
+# TRIGGER_JOBS=enable-monitoring,enable-nextcloud,enable-wekan,enable-wordpress,enable-zulip
 #
 # Reference issue number with one of:
 #
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 21366c08015344a4d137f4e95ed17e97109d59f4..03dfb78c0492f6f92ce82508ed58b7eefccd189f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,6 +1,6 @@
 repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
-    rev: v3.4.0
+    rev: v4.0.1
     hooks:
       - id: check-added-large-files
       - id: check-ast
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4bbfba419c31ecb76cfc24339962c8de8ffa8fed..6cbcca3829a62dae86c10e1f8863ce0b4c7c6e5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,5 @@
 # Changelog
+
 All notable changes to this project will be documented in this file.
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
@@ -7,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 * Changed project name from OpenAppStack to Stackspin
 * Update:
-  * Kube-prometheus-stack to helm chart version 18.0.12
+  * Kube-prometheus-stack to helm chart version 22.0.0
 
 ## [0.7.0] - 2021-08-19
 
diff --git a/Dockerfile b/Dockerfile
index de840cd3f0e2f575a783b4760e8497e8a8f2cdca..7288c2fc7f2e8b685c3753b36c4a2b8d443d5c7c 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -10,6 +10,7 @@ ENV TAIKO_BROWSER_PATH=/usr/bin/chromium-browser
 ENV TAIKO_BROWSER_ARGS=--no-sandbox,--start-maximized,--disable-dev-shm-usage,--ignore-certificate-errors
 
 ADD https://github.com/fluxcd/flux2/releases/download/v0.22.0/flux_0.22.0_linux_amd64.tar.gz /tmp/
+COPY ./test/pytest/le-staging-bundle.pem /usr/local/share/ca-certificates/le-staging-bundle.pem
 COPY ./requirements.txt /requirements.txt
 RUN \
   # Install kubectl from alpine edge until alpine 3.16 is released
@@ -28,7 +29,7 @@ RUN \
     libffi-dev=~3.4.2-r1 \
     make=~4.3-r0 \
     # Needed for timestamp cmd "ts"
-    moreutils=~0.66-r0 \
+    moreutils=~0.66-r1 \
     npm=~8.1.3-r0 \
     openssh-client-default=~8.8_p1-r1 \
     py3-pip=~20.3.4-r1 \
@@ -37,6 +38,7 @@ RUN \
     yq=~4.14.1-r0 && \
   rm -rf /var/cache/* && \
   mkdir /var/cache/apk && \
+  update-ca-certificates && \
   pip install --no-cache-dir --ignore-installed six -r /requirements.txt && \
   ln -s /usr/bin/python3 /usr/bin/python && \
   tar -xzf /tmp/flux*.tar.gz && mv ./flux /usr/local/bin && \
diff --git a/docs/conf.py b/docs/conf.py
index 85ff4759ee5feef79897fd5c767ad47695656380..c41798da51e365d7acf639dd8098edf497d0c0ea 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -61,7 +61,6 @@ html_static_path = ['_static']
 # 'contents'
 master_doc = 'index'
 
-
 # https://www.sphinx-doc.org/en/master/usage/extensions/autosectionlabel.html
 #
 # Suppress autosectionlabel extension warnings about duplicate labels, i.e.
@@ -69,5 +68,7 @@ master_doc = 'index'
 #   docs/usage.rst:105: WARNING: duplicate label wordpress, other instance in docs/testing.rst
 #
 # https://www.sphinx-doc.org/en/master/usage/configuration.html#confval-suppress_warnings
-suppress_warnings = ['autosectionlabel.*']
 autosectionlabel_prefix_document = True
+autosectionlabel_maxdepth = 5
+
+suppress_warnings = ['autosectionlabel.*']
diff --git a/docs/index.rst b/docs/index.rst
index fe6d38116522326477fd536737b7293f85148c17..082dbdae91af60c29b0bbc0986ae42bcfd2eab43 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -41,6 +41,7 @@ For more information, go to `the Stackspin website`_.
    :maxdepth: 2
    :caption: Administration
 
+   logging
    maintenance
    upgrading
    customizing
diff --git a/docs/logging.rst b/docs/logging.rst
new file mode 100644
index 0000000000000000000000000000000000000000..6f2e4d779a981e8352fd390a17552f0442621b95
--- /dev/null
+++ b/docs/logging.rst
@@ -0,0 +1,179 @@
+Logging
+=======
+
+Logs from pods and containers can be read in different ways:
+
+-  In the cluster filesystem at ``/var/log/pods/`` or
+   ``/var/logs/containers/``.
+-  Using `kubectl logs`_
+-  Querying aggregated logs with Grafana, see below.
+
+Central log aggregation
+-----------------------
+
+We use `Promtail`_, `Loki`_ and `Grafana`_ for easy access of aggregated
+logs. The `Loki documentation`_ is a good starting point how this setup
+works.
+There are two ways of viewing aggregated logs:
+
+* Via the Grafana web interface
+* Using the ``logcli`` command line tool
+
+
+Viewing logs in Grafana
+~~~~~~~~~~~~~~~~~~~~~~~
+
+The `Using Loki in Grafana`_ gets you started with querying
+your cluster logs with Grafana.
+You will find the Loki Grafana integration on your cluster at
+https://grafana.stackspin.example.org/explore together with some generic query
+examples.
+
+Please follow :ref:`logging:LogQL query examples` for more LogQL query examples.
+
+Query logs with logcli
+~~~~~~~~~~~~~~~~~~~~~~~
+
+Please refer to `logcli`_ for installing ``logcli`` on your Laptop.
+The create a port-forwarding to your cluster using the ``kubectl`` tool:
+
+.. code:: bash
+
+   kubectl -n stackspin port-forward pod/loki-0 3100
+
+In another terminal you can now use ``logcli`` to query ``loki`` like this:
+
+.. code:: bash
+
+   logcli query '{app=~".+"}'
+
+Please follow :ref:`logging:LogQL query examples` for more LogQL query examples.
+
+Search older messages (in this case the last week and limit the output to 2000
+lines):
+
+.. code:: bash
+
+    logcli query --since=168h --limit=2000 --forward '{app="helm-controller"}'
+
+LogQL query examples
+~~~~~~~~~~~~~~~~~~~~
+
+Please also refer to the `LogQL documentation`_ and the
+`log queries documentation`_ .
+
+Query all aggregated logs (unfortunatly we can’t find a better way of
+doing this since LogQL always expects a stream label to get queried):
+
+.. code:: PromQL
+
+   {app=~".+"}
+
+Query all logs for a keyword:
+
+.. code::
+
+   {app=~".+"} |= "error"
+
+Query all k8s apps for errors using a regular expression:
+
+.. code::
+
+   {app=~".+"} |~ `(error|fail|exception|fatal)`
+
+Flux
+^^^^
+
+`Flux`_ is responsible for installing applications. It uses four
+controllers:
+
+-  ``source-controller`` that tracks Helm and Git repositories like
+   https://open.greenhost.net/stackspin/stackspin for updates.
+-  ``kustomize-controller`` to deploy ``kustomizations`` that often
+   install ``helmreleases``.
+-  ``helm-controller`` to deploy the ``helmreleases``.
+-  ``notification-controller`` that is responsible for inbound and
+   outbound flux messages
+
+Query all messages from the ``source-controller``:
+
+.. code:: PromQL
+
+   {app="source-controller"}
+
+Query all messages from ``flux`` and ``helm-controller``:
+
+.. code:: PromQL
+
+   {app=~"(source-controller|helm-controller)"}
+
+``helm-controller`` messages containing ``wordpress``:
+
+.. code:: PromQL
+
+   '{app = "helm-controller"} |= "wordpress"'
+
+``helm-controller`` messages containing ``wordpress`` without
+``unchanged`` events (to only show the installation messages):
+
+.. code:: PromQL
+
+   '{app = "helm-controller"} |= "wordpress" != "unchanged"'
+
+Filter out redundant ``helm-controller`` messages:
+
+.. code:: PromQL
+
+   '{app="helm-controller"} !~ `(unchanged|event=refreshed|method=Sync|component=checkpoint)`'
+
+
+Cert-manager
+^^^^^^^^^^^^
+
+Cert manager is responsible for requesting Let’s Encrypt TLS
+certificates.
+
+Query ``cert-manager`` messages containing ``chat``:
+
+.. code:: PromQL
+
+   '{app="cert-manager"} |= "chat"'
+
+Hydra
+^^^^^
+
+Hydra is the single sign-on system.
+
+Show only warnings and errors from ``hydra``:
+
+.. code:: PromQL
+
+   {container_name="hydra"} != "level=info"
+
+Debug oauth2 single sign-on with zulip:
+
+.. code:: PromQL
+
+   {container_name=~"(hydra|zulip)"}
+
+Etc
+^^^
+
+Query kubernetes events processed by the ``eventrouter`` app containing
+``warning``:
+
+.. code:: PromQL
+
+   '{app="eventrouter"} |~ "warning"'
+
+
+.. _kubectl logs: https://kubernetes.io/docs/concepts/cluster-administration/logging
+.. _Promtail: https://grafana.com/docs/loki/latest/clients/promtail/
+.. _Loki: https://grafana.com/oss/loki/
+.. _Grafana: https://grafana.com/
+.. _Loki documentation: https://grafana.com/docs/loki/latest/
+.. _Using Loki in Grafana: https://grafana.com/docs/grafana/latest/datasources/loki
+.. _logcli: https://grafana.com/docs/loki/latest/getting-started/logcli/
+.. _LogQL documentation: https://grafana.com/docs/loki/latest/logql
+.. _log queries documentation: https://grafana.com/docs/loki/latest/logql/log_queries/
+.. _Flux: https://fluxcd.io/
diff --git a/docs/maintenance.rst b/docs/maintenance.rst
index 81d002615a8ef4557e7ee267b716ae26ad2494f5..544fb0c92e13d358daa1329b2a6bd7861e3bc66d 100644
--- a/docs/maintenance.rst
+++ b/docs/maintenance.rst
@@ -1,133 +1,6 @@
 Maintenance
 ===========
 
-Logging
--------
-
-Logs from pods and containers can be read in different ways:
-
--  In the cluster filesystem at ``/var/log/pods/`` or
-   ``/var/logs/containers/``.
--  Using `kubectl logs`_
--  Querying aggregated logs with Grafana, see below.
-
-Central log aggregation
------------------------
-
-We use `Promtail`_, `Loki`_ and `Grafana`_ for easy access of aggregated
-logs. The `Loki documentation`_ is a good starting point how this setup
-works, and the `Using Loki in Grafana`_ gets you started with querying
-your cluster logs with Grafana.
-
-You will find the Loki Grafana integration on your cluster at
-https://grafana.stackspin.example.org/explore together with some generic query
-examples.
-
-LogQL query examples
-~~~~~~~~~~~~~~~~~~~~
-
-Please also refer to the `LogQL documentation`_.
-
-Query all aggregated logs (unfortunatly we can’t find a better way of
-doing this since LogQL always expects a stream label to get queried):
-
-.. code:: bash
-
-   logcli query '{foo!="bar"}'
-
-Query all logs for a keyword:
-
-.. code:: bash
-
-   logcli query '{foo!="bar"} |= "error"'
-
-Query all k8s apps for errors using a regular expression:
-
-.. code:: bash
-
-   logcli query '{job=~".*"} |~ "error|fail|exception|fatal"'
-
-Flux
-^^^^
-
-`Flux`_ is responsible for installing applications. It uses four
-controllers:
-
--  ``source-controller`` that tracks Helm and Git repositories like
-   https://open.greenhost.net/stackspin/stackspin for updates.
--  ``kustomize-controller`` to deploy ``kustomizations`` that often
-   install ``helmreleases``.
--  ``helm-controller`` to deploy the ``helmreleases``.
--  ``notification-controller`` that is responsible for inbound and
-   outbound flux messages
-
-Query all messages from the ``source-controller``:
-
-.. code:: bash
-
-   {app="source-controller"}
-
-Query all messages from ``flux`` and ``helm-controller``:
-
-.. code:: bash
-
-   {app=~"(source-controller|helm-controller)"}
-
-``helm-controller`` messages containing ``wordpress``:
-
-.. code:: bash
-
-   {app = "helm-controller"} |= "wordpress"
-
-``helm-controller`` messages containing ``wordpress`` without
-``unchanged`` events (to only show the installation messages):
-
-.. code:: bash
-
-   {app = "helm-controller"} |= "wordpress" != "unchanged"
-
-Filter out redundant ``helm-controller`` messages:
-
-.. code:: bash
-
-   { app = "helm-controller" } !~ "(unchanged | event=refreshed | method=Sync | component=checkpoint)"
-
-Debug oauth2 single sign-on with zulip:
-
-.. code:: bash
-
-   {container_name=~"(hydra|zulip)"}
-
-Query kubernetes events processed by the ``eventrouter`` app containing
-``warning``:
-
-.. code:: bash
-
-   logcli query '{app="eventrouter"} |~ "warning"'
-
-Cert-manager
-^^^^^^^^^^^^
-
-Cert manager is responsible for requesting Let’s Encrypt TLS
-certificates.
-
-Query ``cert-manager`` messages containing ``chat``:
-
-.. code:: bash
-
-   {app="cert-manager"} |= "chat"
-
-Hydra
-^^^^^
-
-Hydra is the single sign-on system.
-
-Show only warnings and errors from ``hydra``:
-
-.. code:: bash
-
-   {container_name="hydra"} != "level=info"
-
 Backup
 ------
 
@@ -204,14 +77,6 @@ following command that will apply the changes to all installed kustomizations:
     flux get -A kustomizations --no-header | awk -F' ' '{system("flux reconcile -n " $1 " kustomization " $2)}'
 
 
-.. _kubectl logs: https://kubernetes.io/docs/concepts/cluster-administration/logging
-.. _Promtail: https://grafana.com/docs/loki/latest/clients/promtail/
-.. _Loki: https://grafana.com/oss/loki/
-.. _Grafana: https://grafana.com/
-.. _Loki documentation: https://grafana.com/docs/loki/latest/
-.. _Using Loki in Grafana: https://grafana.com/docs/grafana/latest/datasources/loki
-.. _LogQL documentation: https://grafana.com/docs/loki/latest/logql
-.. _Flux: https://fluxcd.io/
 .. _Velero’s documentation: https://velero.io/docs/v1.4/
 .. _reach out to us: https://stackspin.net/contact.html
 .. _taints: https://kubernetes.io/docs/concepts/scheduling-eviction/taint-and-toleration/
diff --git a/docs/requirements.in b/docs/requirements.in
index 16f51618fa5d19bc02c2bb909ac79eebaca227f4..957a65b28f6b240a182ba3485567cf2826f5b153 100644
--- a/docs/requirements.in
+++ b/docs/requirements.in
@@ -5,7 +5,7 @@
 # Please add developer dependencies which are not needed to install
 # Stackspin to requirements-dev.txt!
 #
+recommonmark
 sphinx
 sphinx-design
 sphinx-rtd-theme
-recommonmark
diff --git a/docs/requirements.txt b/docs/requirements.txt
index f39411896b309d080bd07b6b01155ce8d45fc009..dfbd6ae61cc2f23d6c210d33e9d0e8fd46c2a70a 100644
--- a/docs/requirements.txt
+++ b/docs/requirements.txt
@@ -14,7 +14,7 @@ charset-normalizer==2.0.9
     # via requests
 commonmark==0.9.1
     # via recommonmark
-docutils==0.18.1
+docutils==0.17.1
     # via
     #   recommonmark
     #   sphinx
@@ -30,8 +30,10 @@ markupsafe==2.0.1
 packaging==21.3
     # via sphinx
 pygments==2.10.0
-    # via sphinx
-pyparsing==2.4.7
+    # via
+    #   -r requirements.in
+    #   sphinx
+pyparsing==3.0.6
     # via packaging
 pytz==2021.3
     # via babel
diff --git a/docs/troubleshooting.rst b/docs/troubleshooting.rst
index 10fee5a0593bf7a68b212bd64bd21ed2e5a9a6ba..5db3248aaacc9c95c42371490178508d287f1b74 100644
--- a/docs/troubleshooting.rst
+++ b/docs/troubleshooting.rst
@@ -95,7 +95,7 @@ Tests a specific application
 Known Issues
 ''''''''''''
 
-The Default ssh backend for testinfra tests is ``paramiko``, which doesn't work
+The default ssh backend for testinfra tests is ``paramiko``, which doesn't work
 out of the box. It fails to connect to the host because the ``ed25519`` hostkey
 was not verified. Therefore we need to force plain ssh:// with either
 ``connection=ssh`` or ``--hosts=ssh://…``
@@ -190,8 +190,8 @@ then:
 
     gitlab-runner exec docker --env CI_REGISTRY_IMAGE="$CI_REGISTRY_IMAGE" --env SSH_PRIVATE_KEY="$SSH_PRIVATE_KEY" --env COSMOS_API_TOKEN="$COSMOS_API_TOKEN" bootstrap
 
-Taiko tests
-'''''''''''
+Advanced Taiko tests
+''''''''''''''''''''
 
 If you want to use Taiko without invoking the stackspin CLI, go to the
 ``test/taiko`` directory and run:
diff --git a/docs/upgrading.rst b/docs/upgrading.rst
index 76824490a1ab9775af156ed9904997cfa2851965..9155d76909c906b3c35ab166d208aa2cb804ef20 100644
--- a/docs/upgrading.rst
+++ b/docs/upgrading.rst
@@ -96,10 +96,9 @@ renamed from ``oas`` to ``stackspin``. You can choose from these options:
 Rocket.Chat
 ~~~~~~~~~~~
 
-We replaced Rocket.Chat with `Zulip <https://zulip.com>`__ in this release.
-If you want to migrate your Rocket.Chat data to your new Zulip installation
-please refer to
-`Import from Rocket.Chat https://api.zulip.com/help/import-from-rocketchat`__.
+We replaced Rocket.Chat with `Zulip`_ in this release.
+If you want to migrate your Rocket.Chat data to your new `Zulip`_ installation
+please refer to `Import from Rocket.Chat`_.
 
 Monitoring
 ~~~~~~~~~~
@@ -131,13 +130,12 @@ from v0.6 to v0.7!**
 .. note::
   Before you start, please ensure that you have the right ``yq`` tool installed,
   because you will need it later.  There are two very different versions of
-  ``yq``. The one you need is the go based `yq from Mike Farah
-  <http://mikefarah.github.io/yq>`_, which installs the same binary name ``yq``
-  as the `python-yq <https://github.com/kislyuk/yq>`_, while both have different
-  command sets.
+  ``yq``. The one you need is the go based `yq from Mike Farah`_,
+  which installs the same binary name as the `python-yq`_ one, while both have
+  different command sets.
   The yq needed here can be installed by running ``sudo snap install yq``,
-  ``brew install yq`` or with other methods from the `yq installation
-  instructions <http://mikefarah.github.io/yq/#install>`_.
+  ``brew install yq`` or with other methods from the
+  `yq installation instructions`_.
 
   If you're unsure which ``yq`` you have installed, look at the output of
   ``yq --help`` and make sure ``eval`` shows up under ``Available Commands:``.
@@ -394,3 +392,8 @@ intervention.
 
 .. _reach out to us: https://openappstack.net/contact.html
 .. _Flux: https://fluxcd.io
+.. _yq from Mike Farah: https://mikefarah.github.io/yq
+.. _yq installation instructions: https://mikefarah.github.io/yq/#install
+.. _python-yq: https://github.com/kislyuk/yq
+.. _Zulip: https://zulip.com
+.. _Import from Rocket.Chat: https://api.zulip.com/help/import-from-rocketchat
diff --git a/docs/usage.rst b/docs/usage.rst
index b618c09fa914471f1a07cdfebd594ce35e9c9c19..d7ea1299e7f7a1b03872b917062dd4f63625b707 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -24,7 +24,7 @@ them access to applications, take a look at the `user panel documentation
 .. note::
 
    If you don't see applications, make sure you have installed at least one
-   optional application in :ref:`additional_apps` of the installation procedure.
+   optional application in :ref:`install_additional_apps` of the installation procedure.
 
 For creating users follow the `user creation documentation
 <https://docs.stackspin.net/projects/user-panel/en/latest/#creating-a-new-user>`_.
diff --git a/flux2/apps/monitoring/kube-prometheus-stack-release.yaml b/flux2/apps/monitoring/kube-prometheus-stack-release.yaml
index 1f4e6578f0b9885abaa81b52fd3d8bc7b312c112..a911f89766290df89d3ab2e8c6a2832ad1925ae7 100644
--- a/flux2/apps/monitoring/kube-prometheus-stack-release.yaml
+++ b/flux2/apps/monitoring/kube-prometheus-stack-release.yaml
@@ -11,7 +11,7 @@ spec:
       # https://artifacthub.io/packages/helm/prometheus-community/kube-prometheus-stack
       # renovate: registryUrl=https://prometheus-community.github.io/helm-charts
       chart: kube-prometheus-stack
-      version: 22.0.0
+      version: 23.2.0
       sourceRef:
         kind: HelmRepository
         name: prometheus-community
diff --git a/flux2/apps/monitoring/loki-values-configmap.yaml b/flux2/apps/monitoring/loki-values-configmap.yaml
index 3412fce66597c06ee4d0e7571825c8c69ed76332..86ec319cb6148f990373e68d1513f94166ef188b 100644
--- a/flux2/apps/monitoring/loki-values-configmap.yaml
+++ b/flux2/apps/monitoring/loki-values-configmap.yaml
@@ -6,6 +6,11 @@ metadata:
 data:
   values.yaml: |
     # https://github.com/grafana/helm-charts/blob/main/charts/loki/values.yaml
+    image:
+      repository: grafana/loki
+      # Downgrade loki because of mem leak
+      # (https://open.greenhost.net/stackspin/stackspin/-/issues/1077)
+      tag: 2.4.0
     resources:
       limits:
         cpu: 800m
diff --git a/flux2/cluster/base/single-sign-on.yaml b/flux2/cluster/base/single-sign-on.yaml
index 78133014d2b1560c39dce7ba9409144f8fad8e9d..e5efa49f8da028aa6d17f25a9203b03c7a3f96b4 100644
--- a/flux2/cluster/base/single-sign-on.yaml
+++ b/flux2/cluster/base/single-sign-on.yaml
@@ -30,11 +30,11 @@ spec:
       namespace: stackspin
     - apiVersion: apps/v1
       kind: Deployment
-      name: single-sign-on-userbackend
+      name: single-sign-on-hydra
       namespace: stackspin
     - apiVersion: apps/v1
       kind: Deployment
-      name: single-sign-on-hydra
+      name: single-sign-on-kratos
       namespace: stackspin
     - apiVersion: apps/v1
       kind: Deployment
diff --git a/flux2/core/base/metallb/release.yaml b/flux2/core/base/metallb/release.yaml
index faf53c8b5124755686ac934c05e2153ad9fcf565..083e6024648cc87d3474e4086219686ed4a38e0e 100644
--- a/flux2/core/base/metallb/release.yaml
+++ b/flux2/core/base/metallb/release.yaml
@@ -11,7 +11,7 @@ spec:
       # https://artifacthub.io/packages/helm/bitnami/metallb
       # renovate: registryUrl=https://charts.bitnami.com/bitnami
       chart: metallb
-      version: 2.5.13
+      version: 2.5.14
       sourceRef:
         kind: HelmRepository
         name: bitnami
diff --git a/flux2/core/base/single-sign-on/kustomization.yaml b/flux2/core/base/single-sign-on/kustomization.yaml
index 354f7bb3bf7f25e633ada0129e9c5f96284f468a..0103cabedd67afd76a28bf0a0d4621dd5425300b 100644
--- a/flux2/core/base/single-sign-on/kustomization.yaml
+++ b/flux2/core/base/single-sign-on/kustomization.yaml
@@ -3,7 +3,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 namespace: stackspin
 resources:
-  - pvc-userbackend.yaml
   - pvc-database.yaml
   - release.yaml
   - single-sign-on-values-configmap.yaml
diff --git a/flux2/core/base/single-sign-on/pvc-userbackend.yaml b/flux2/core/base/single-sign-on/pvc-userbackend.yaml
deleted file mode 100644
index e21e9d6f89efd66e56682d5d74dcaf3c193d3311..0000000000000000000000000000000000000000
--- a/flux2/core/base/single-sign-on/pvc-userbackend.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
----
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
-  name: single-sign-on-userbackend
-spec:
-  accessModes:
-    - ReadWriteOnce
-  volumeMode: Filesystem
-  resources:
-    requests:
-      storage: 1Gi
-  storageClassName: local-path
diff --git a/flux2/core/base/single-sign-on/release.yaml b/flux2/core/base/single-sign-on/release.yaml
index 02aefb7bf86fb14079fa560aecf22a64d30ab1db..7f240d648da115909bfba8978f287c03e2ac847c 100644
--- a/flux2/core/base/single-sign-on/release.yaml
+++ b/flux2/core/base/single-sign-on/release.yaml
@@ -17,9 +17,7 @@ spec:
         namespace: flux-system
   interval: 1h0m0s
   install:
-    remediation:
-      retries: 3
-    timeout: 10m
+    timeout: 20m
   upgrade:
     crds: CreateReplace
   valuesFrom:
diff --git a/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml b/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml
index 682841595af6cbf9a333390805333816f290bacc..35fadcef734a38e739fb95d416fa2097fb059063 100644
--- a/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml
+++ b/flux2/core/base/single-sign-on/single-sign-on-values-configmap.yaml
@@ -7,39 +7,11 @@ data:
   values.yaml: |
     singleSignOnHost: &SSO_HOST "sso.${domain}"
 
-    userpanel:
-      applicationName: &USER_PANEL user-panel
-      ingress:
-        host: "admin.${domain}"
-
-    userbackend:
-      applications:
-        - name: *USER_PANEL
-          description: Administration interface to manage user accounts
-        - name: &NEXTCLOUD nextcloud
-          description: "Nextcloud Files offers an on-premise Universal File Access and sync platform with powerful collaboration capabilities and desktop, mobile and web interfaces."
-        - name: &WORDPRESS wordpress
-          description: "WordPress website hosting."
-        - name: &GRAFANA grafana
-          description: "Grafana allows you to query, visualize, alert on and understand metrics generated by Stackspin. It can be used to create explore and share dashboards."
-        - name: &WEKAN wekan
-          description: "Wekan Kanban board."
-        - name: &ZULIP zulip
-          description: "Communicate and collaborate using team chat and switch to video or audio calls with screen sharing for more efficient teamwork."
-        - name: &DASHBOARD dashboard
-          description: "Stackspin dashboard."
-      username: "${userbackend_admin_username}"
-      password: "${userbackend_admin_password}"
-      email: "${admin_email}"
-      postgres:
-        password: "${userbackend_postgres_password}"
-      persistence:
-        enabled: true
-        size: 1Gi
-        existingClaim: single-sign-on-userbackend
-      podAnnotations:
-        # Let the backup system include nextcloud database data.
-        backup.velero.io/backup-volumes: "database"
+    login:
+      user: ${admin_email}
+      password: ${userbackend_admin_password}
+      db:
+        password: ${userbackend_postgres_password}
 
     postgresql:
       persistence:
@@ -59,8 +31,9 @@ data:
           urls:
             self:
               issuer: "https://sso.${domain}"
-            login: "https://sso.${domain}/login"
-            consent: "https://sso.${domain}/consent"
+            login: "https://sso.${domain}/login/auth"
+            consent: "https://sso.${domain}/login/consent"
+            logout: "https://sso.${domain}/login/logout"
           secrets:
             system:
               - "${hydra_system_secret}"
@@ -87,20 +60,35 @@ data:
       kratos:
         config:
           dsn: "postgres://kratos:${kratos_postgresql_password}@single-sign-on-postgresql:5432/kratos"
+          serve:
+            public:
+              base_url: https://sso.${domain}/api/
+          courier:
+            smtp:
+              connection_uri: smtp://${outgoing_mail_smtp_user}:${outgoing_mail_smtp_password}@${outgoing_mail_smtp_host}:${outgoing_mail_smtp_port}/
+              from_address: ${outgoing_mail_from_address}
+          secrets:
+            session:
+              - "${kratos_session_secret}"
+          selfservice:
+            # The URL to redirect to if there is a call to kratos on another URL
+            # than the flows listed below
+            default_browser_return_url: https://sso.${domain}/login/login
+            flows:
+              recovery:
+                ui_url: https://sso.${domain}/login/recovery
+              login:
+                ui_url: https://sso.${domain}/login/login
+              settings:
+                ui_url: https://sso.${domain}/login/settings
+              # Registration is not (yet) possible, but if it will be, it should
+              # be on this link:
+              registration:
+                ui_url: https://sso.${domain}/login/registration
+
 
     oAuthClients:
-    - clientName: *USER_PANEL
-      clientSecret: "${userpanel_oauth_client_secret}"
-      redirectUri: "https://admin.${domain}/callback"
-      scopes: "openid profile email stackspin_roles"
-      clientUri: "https://admin.${domain}"
-      clientLogoUri: "https://admin.${domain}/favicon.ico"
-      tokenEndpointAuthMethod: "client_secret_basic"
-      responseTypes:
-        - "token"
-      grantTypes:
-        - "implicit"
-    - clientName: *NEXTCLOUD
+    - clientName: nextcloud
       clientSecret: "${nextcloud_oauth_client_secret}"
       redirectUri: "https://files.${domain}/apps/sociallogin/custom_oidc/stackspin"
       scopes: "openid profile email stackspin_roles"
@@ -114,7 +102,7 @@ data:
         - "authorization_code"
         - "refresh_token"
         - "client_credentials"
-    - clientName: *WORDPRESS
+    - clientName: wordpress
       clientSecret: "${wordpress_oauth_client_secret}"
       redirectUri: "https://www.${domain}/wp-admin/admin-ajax.php?action=openid-connect-authorize"
       scopes: "openid profile email stackspin_roles offline_access"
@@ -129,7 +117,7 @@ data:
         - "refresh_token"
         - "client_credentials"
         - "implicit"
-    - clientName: *GRAFANA
+    - clientName: grafana
       clientSecret: "${grafana_oauth_client_secret}"
       redirectUri: "https://grafana.${domain}/login/generic_oauth"
       scopes: "openid profile email stackspin_roles"
@@ -144,7 +132,7 @@ data:
         - "refresh_token"
         - "client_credentials"
     # https://github.com/wekan/wekan/wiki/Keycloak
-    - clientName: *WEKAN
+    - clientName: wekan
       clientSecret: "${wekan_oauth_client_secret}"
       redirectUri: "https://wekan.${domain}/_oauth/oidc"
       scopes: "openid profile email"
@@ -160,13 +148,13 @@ data:
         - "client_credentials"
         - "implicit"
     # https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#openid-connect
-    - clientName: *ZULIP
+    - clientName: zulip
       clientSecret: "${zulip_oauth_client_secret}"
       redirectUri: "https://zulip.${domain}/complete/oidc/"
       scopes: "openid profile email"
       clientUri: "https://zulip.${domain}"
       clientLogoUri: "https://zulip.${domain}/static/images/zulip-logo.svg"
-    - clientName: *DASHBOARD
+    - clientName: dashboard
       clientSecret: "${dashboard_oauth_client_secret}"
       redirectUri: "https://dashboard.${domain}/_oauth/oidc"
       scopes: "openid profile email"
diff --git a/flux2/core/base/sources/single-sign-on.yaml b/flux2/core/base/sources/single-sign-on.yaml
index 4faf6956c6885e4795c1be458236e6512c4b1c21..87adf56b07fde830195830563f9b261c0cac9a6a 100644
--- a/flux2/core/base/sources/single-sign-on.yaml
+++ b/flux2/core/base/sources/single-sign-on.yaml
@@ -14,4 +14,4 @@ spec:
   # For all available options, see:
   # https://toolkit.fluxcd.io/components/source/api/#source.toolkit.fluxcd.io/v1beta1.GitRepositoryRef
   ref:
-    tag: 0.4.3
+    tag: 0.5.0
diff --git a/install/templates/stackspin-single-sign-on-variables.yaml.jinja b/install/templates/stackspin-single-sign-on-variables.yaml.jinja
index 70caab6571ab5622d5d681b67baa39696db28e83..56ccc93f9bc7c944bc362d67afa7c01d83cb28f5 100644
--- a/install/templates/stackspin-single-sign-on-variables.yaml.jinja
+++ b/install/templates/stackspin-single-sign-on-variables.yaml.jinja
@@ -4,10 +4,10 @@ kind: Secret
 metadata:
   name: stackspin-single-sign-on-variables
 data:
-  userbackend_admin_username: '{{ "admin" | b64encode }}'
-  userbackend_admin_password: "{{ 32 | generate_password | b64encode }}"
-  userbackend_postgres_password: "{{ 32 | generate_password | b64encode }}"
-  hydra_system_secret: "{{ 32 | generate_password | b64encode }}"
+  dashboard_postgresql_password: "{{ 32 | generate_password | b64encode }}"
   hydra_postgresql_password: "{{ 32 | generate_password | b64encode }}"
+  hydra_system_secret: "{{ 32 | generate_password | b64encode }}"
   kratos_postgresql_password: "{{ 32 | generate_password | b64encode }}"
-  dashboard_postgresql_password: "{{ 32 | generate_password | b64encode }}"
+  kratos_session_secret: "{{ 32 | generate_password | b64encode }}"
+  userbackend_admin_password: "{{ 32 | generate_password | b64encode }}"
+  userbackend_postgres_password: "{{ 32 | generate_password | b64encode }}"
diff --git a/requirements.txt b/requirements.txt
index 92e73521090300079b784fa30a98913b554436ea..a3f536f987dbd545af833aaa09393b464620a07a 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -25,7 +25,7 @@ cffi==1.15.0
     #   pynacl
 charset-normalizer==2.0.9
     # via requests
-cryptography==36.0.0
+cryptography==36.0.1
     # via
     #   ansible
     #   paramiko
@@ -86,7 +86,7 @@ pytest==6.2.5
     #   pytest-testinfra
 pytest-rerunfailures==10.2
     # via -r requirements.in
-pytest-testinfra==6.4.0
+pytest-testinfra==6.5.0
     # via -r requirements.in
 python-dateutil==2.8.2
     # via kubernetes
@@ -124,7 +124,7 @@ urllib3==1.26.7
     # via
     #   kubernetes
     #   requests
-websocket-client==1.2.2
+websocket-client==1.2.3
     # via kubernetes
 wheel==0.37.0
     # via -r requirements.in
diff --git a/test/pytest/test_certs.py b/test/pytest/test_certs.py
index 9cfb26f7e5be7437d21bc901d4461b5399e14cf5..5d1e2963d3919e0396fd91060652bf5d87508f80 100755
--- a/test/pytest/test_certs.py
+++ b/test/pytest/test_certs.py
@@ -2,9 +2,11 @@
 """Test if application ingress uses a valid certificate."""
 
 import os
+import shutil
 import socket
 import sys
 
+import certifi
 import pytest
 import requests
 from OpenSSL import SSL
@@ -17,6 +19,20 @@ def test_cert_validation(host, resource): # pylint: disable=too-many-statements
     Check is executed on the local provisioning machine.
     """
 
+    def add_custom_cert_authorities(ca_file: str,
+                                    custom_ca_files: list,
+                                    dest_file: str =
+                                    '/tmp/custom_ca_bundle.crt'):
+        """Concatenates existing cert bundle with custom CAs."""
+
+        destination = open(dest_file, 'wb')
+        with open(dest_file, 'wb') as destination, open(ca_file, 'rb') as cert_auth:
+            shutil.copyfileobj(cert_auth, destination)
+            for custom_ca_file in custom_ca_files:
+                with open(custom_ca_file, 'rb') as custom_ca:
+                    shutil.copyfileobj(custom_ca, destination)
+
+
     def fetch_certs(domain: str, port: int = 443):
         """Fetches cert fom given domain."""
 
@@ -52,8 +68,9 @@ def test_cert_validation(host, resource): # pylint: disable=too-many-statements
             print('CN: {0} (Issuer: {1})'.format(common_name, issuer))
 
 
-    def valid_cert(domain: str):
-        """Validate cert of given domain."""
+    def valid_cert(domain: str, ca_file: str = '/tmp/custom_ca_bundle.crt',
+                   app: str = "all"):
+        """Validate cert of given domain against a ca_file bundle."""
 
         valid = False
 
@@ -63,14 +80,17 @@ def test_cert_validation(host, resource): # pylint: disable=too-many-statements
         print_cert_info(certs)
 
         try:
-            requests.get(url)
+            requests.get(url, verify=ca_file)
         except requests.exceptions.SSLError as ex:
             print('SSL Verification Error {}'.format(ex))
-            #for cert in certs:
-            #    issuer = cert.get_issuer().CN
+            for cert in certs:
+                issuer = cert.get_issuer().CN
+                if issuer == 'cert-manager.local':
+                    print('Allowing exception for self-signed cert-mananger cert.')
+                    valid = True
             return valid
 
-        print('Successfully Verified SSL Cert. \n')
+        print('Successfully Verified SSL Cert.\n')
         return True
 
 
@@ -91,8 +111,7 @@ def test_cert_validation(host, resource): # pylint: disable=too-many-statements
     elif resource == 'kube-prometheus-stack':
         apps = ['grafana', 'prometheus']
     else:
-        assert resource in app_subdomains, \
-            "Error: Unknown app: {}".format(resource)
+        assert resource in app_subdomains, "Error: Unknown app: {}".format(app)
         apps = [resource]
 
     print('\n')
@@ -107,5 +126,8 @@ def test_cert_validation(host, resource): # pylint: disable=too-many-statements
             domain = ansible_vars["domain"]
             print("Using domain %s from ansible inventory." % domain)
 
+        add_custom_cert_authorities(certifi.where(),
+                                    ['pytest/le-staging-bundle.pem'])
+
         fqdn = app_subdomains[app_name] + '.' + domain
-        assert valid_cert(domain=fqdn)
+        assert valid_cert(domain=fqdn, app=resource)
diff --git a/test/pytest/test_resources.py b/test/pytest/test_resources.py
index 83d29ae2cd6e14947994196bfc353dba280f56c9..35373ed245ef5dacaa17445d3ba67cf21c8a6670 100644
--- a/test/pytest/test_resources.py
+++ b/test/pytest/test_resources.py
@@ -10,10 +10,10 @@ Documentation for the kubernetes client:
 * https://github.com/kubernetes-client/python/tree/master/examples
 """
 import os
-from kubernetes import client, config
-from kubernetes.client.rest import ApiException
 
 import pytest
+from kubernetes import client, config
+from kubernetes.client.rest import ApiException
 
 # Helper functions
 
@@ -134,7 +134,10 @@ def run_around_tests():
     Prepare kube config before running a test
     """
     cluster_dir = os.environ.get("CLUSTER_DIR")
-    kubeconfig = os.path.join('..', str(cluster_dir), 'kube_config_cluster.yml')
+    if cluster_dir:
+        kubeconfig = os.path.join('..', str(cluster_dir), 'kube_config_cluster.yml')
+    else:
+        kubeconfig = os.path.join(os.environ.get("KUBECONFIG"))
     config.load_kube_config(config_file=kubeconfig)
     yield