diff --git a/ansible/group_vars/all/oas.yml b/ansible/group_vars/all/oas.yml
index de93837a61b514015851c7c0b2451f6522f95757..584c869650bc06b58f3c510b9d1d66fc32d94426 100644
--- a/ansible/group_vars/all/oas.yml
+++ b/ansible/group_vars/all/oas.yml
@@ -51,7 +51,7 @@ helm:
   # (https://open.greenhost.net/openappstack/openappstack/issues/338), so we
   # use a pinned version for now.
   # We use the official helm install script for now which has no checksum.
-  version: '2.14.3'
+  version: '3.1.1'
 
 krew:
   # https://github.com/kubernetes-sigs/krew/releases
@@ -68,15 +68,6 @@ rke:
   # checksum: 'sha256:https://github.com/rancher/rke/releases/download/v0.2.4/sha256sum.txt'
   checksum: 'sha256:96b366fe1faaa668b3e47f5b6d4bfd6334224e33c21e55dc79ec96f85e0e48e8'
 
-cert_manager:
-  # cert-manager requires custom resource definitions applied before installing
-  # the helm chart. See https://hub.helm.sh/charts/jetstack/cert-manager for
-  # details
-  crd_version: '0.11.0'
-
-prometheus:
-  crd_version: 'v0.34.0'
-
 # If true, let the auto-update mechanism (flux) follow a cluster-local git
 # repo, not one hosted on open.greenhost.net.
 local_flux: false
diff --git a/ansible/roles/apps/tasks/cert-manager.yml b/ansible/roles/apps/tasks/cert-manager.yml
index 055cb85c47692ab91b4d43293d87f5fc7b68b3c4..e99a2bedc4e2937fc207a7edbc3c8e17ef660111 100644
--- a/ansible/roles/apps/tasks/cert-manager.yml
+++ b/ansible/roles/apps/tasks/cert-manager.yml
@@ -1,49 +1,4 @@
 ---
-- name: Install CRDs for cert-manager
-  tags:
-    - cert-manager
-  # TODO: Remove `--validate=false` once we use Kubernetes 1.16 or above
-  command: '/snap/bin/kubectl apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/v{{ cert_manager.crd_version }}/deploy/manifests/00-crds.yaml'
-
-- name: Prevent validation deadlock for cert-manager
-  tags:
-    - cert-manager
-  command: /snap/bin/kubectl label --overwrite namespace oas certmanager.k8s.io/disable-validation="true"
-  register: cert_manager_label_namespace
-  failed_when:
-    # If the namespace doesn't yet exist, that's OK, we don't need to do anything in that case.
-    - "'NotFound' not in cert_manager_label_namespace.stderr"
-    - "cert_manager_label_namespace.rc != 0"
-
-- name: Install LetsEncrypt ClusterIssuers
-  tags:
-    - cert-manager
-  k8s:
-    state: present
-    definition:
-      apiVersion: cert-manager.io/v1alpha2
-      kind: ClusterIssuer
-      metadata:
-        name: letsencrypt-{{ item.name }}
-      spec:
-        acme:
-          email: "{{ admin_email }}"
-          server: "{{ item.server }}"
-          privateKeySecretRef:
-            # Secret resource used to store the account's private key.
-            name: letsencrypt-{{ item.name }}-account-key
-          # Enable the HTTP01 challenge mechanism for this Issuer
-          solvers:
-          - selector: {}
-            http01:
-              ingress:
-                class: nginx
-    merge_type: merge
-  with_items:
-    - name: staging
-      server: "https://acme-staging-v02.api.letsencrypt.org/directory"
-    - name: production
-      server: "https://acme-v02.api.letsencrypt.org/directory"
 
 - name: Create Kubernetes secret with cert-manager settings
   tags:
diff --git a/ansible/roles/apps/tasks/flux.yml b/ansible/roles/apps/tasks/flux.yml
index 4138fc5aec1743429496d7733c8ad729e2d2f9b7..533252980bc9833dc64f788f5d6cb8ecc79feeb4 100644
--- a/ansible/roles/apps/tasks/flux.yml
+++ b/ansible/roles/apps/tasks/flux.yml
@@ -17,7 +17,7 @@
   #   --install
   #   --repo "https://charts.fluxcd.io"
   #   --namespace oas
-  #   --version 0.16.0
+  #   --version 1.2.0
   #   # The git repo that flux listens to for changes.
   #   --set git.url="{{ git_url }}"
   #   # The branch of the git repo that flux listens to for changes.
@@ -38,7 +38,7 @@
   #   flux
   #   # Chart name
   #   flux
-  shell: helm upgrade --install --repo "https://charts.fluxcd.io" --namespace oas --version 0.16.0 --set git.url="{{ git_url }}" --set git.branch="{{ git_branch }}" --set git.path="{{ git_path }}" --set git.readonly=true --set registry.excludeImage='*' --set sync.state="secret" --set syncGarbageCollection.enabled=true --set git.pollInterval=1h flux flux
+  shell: helm upgrade --install --repo "https://charts.fluxcd.io" --namespace oas --version 1.2.0 --set git.url="{{ git_url }}" --set git.branch="{{ git_branch }}" --set git.path="{{ git_path }}" --set git.readonly=true --set registry.excludeImage='*' --set sync.state="secret" --set syncGarbageCollection.enabled=true --set git.pollInterval=1h flux flux
 
 - name: Install helm-operator
   tags:
@@ -49,8 +49,12 @@
   #   --install
   #   --repo "https://charts.fluxcd.io"
   #   --namespace oas
-  #   --version 0.3.0
-  #   --set createCRD=true
+  #   --version 0.7.0
+  #   --set helm.versions=v3
+  #   # Helm 3 doesn't have the stable repository enabled by default.
+  #   --set configureRepositories.enable=true
+  #   --set configureRepositories.repositories[0].name=stable
+  #   --set configureRepositories.repositories[0].url=https://kubernetes-charts.storage.googleapis.com
   #   # Reconcile actual helm releases with HelmRelease objects with this
   #   # interval.
   #   --set chartsSyncInterval=20m
@@ -60,7 +64,7 @@
   #   helm-operator
   #   # Chart name
   #   helm-operator
-  shell: helm upgrade --install --repo "https://charts.fluxcd.io" --namespace oas --version 0.3.0 --set createCRD=true --set chartsSyncInterval=20m --set statusUpdateInterval=30s helm-operator helm-operator
+  shell: helm upgrade --install --repo "https://charts.fluxcd.io" --namespace oas --version 0.7.0 --set helm.versions=v3 --set configureRepositories.enable=true --set configureRepositories.repositories[0].name=stable --set configureRepositories.repositories[0].url=https://kubernetes-charts.storage.googleapis.com --set chartsSyncInterval=20m --set statusUpdateInterval=30s helm-operator helm-operator
 
 - name: Install fluxctl via snap
   tags:
diff --git a/ansible/roles/apps/tasks/letsencrypt.yml b/ansible/roles/apps/tasks/letsencrypt.yml
new file mode 100644
index 0000000000000000000000000000000000000000..87b2ab8e36d1d1e70fc32c32b16a94b3ccc8f399
--- /dev/null
+++ b/ansible/roles/apps/tasks/letsencrypt.yml
@@ -0,0 +1,21 @@
+---
+
+- name: Create Kubernetes secret with settings for letsencrypt issuers
+  tags:
+    - config
+    - flux
+    - cert-manager
+  vars:
+    flux:
+      name: "letsencrypt-{{ item }}-settings"
+      namespace: "oas"
+  include_tasks:
+    file: flux_secret.yml
+    apply:
+      tags:
+        - config
+        - flux
+        - cert-manager
+  with_items:
+    - "production"
+    - "staging"
diff --git a/ansible/roles/apps/tasks/main.yml b/ansible/roles/apps/tasks/main.yml
index 8e43e01e196ca08e9aff0ce766acd42ac6a2dbde..6f8644fc7da1451a6226099dd4c2e4add2f948d2 100644
--- a/ansible/roles/apps/tasks/main.yml
+++ b/ansible/roles/apps/tasks/main.yml
@@ -9,6 +9,9 @@
 - name: Tasks pertaining to cert-manager
   import_tasks: cert-manager.yml
 
+- name: Tasks specific to letsencrypt
+  import_tasks: letsencrypt.yml
+
 - name: Tasks pertaining to nginx
   import_tasks: nginx.yml
 
diff --git a/ansible/roles/apps/tasks/prometheus.yml b/ansible/roles/apps/tasks/prometheus.yml
index af180c9755811d24db8de79a341fc330e8174f84..c403505e3ec23fecee153b6cd56924d6e84928a1 100644
--- a/ansible/roles/apps/tasks/prometheus.yml
+++ b/ansible/roles/apps/tasks/prometheus.yml
@@ -1,35 +1,5 @@
 ---
 
-- name: Make Prometheus custom resource definitions
-  tags:
-    - prometheus
-  command: '/snap/bin/kubectl apply -f https://raw.githubusercontent.com/coreos/prometheus-operator/{{ prometheus.crd_version }}/example/prometheus-operator-crd/{{ item }}'
-  loop:
-    - alertmanager.crd.yaml
-    - prometheus.crd.yaml
-    - prometheusrule.crd.yaml
-    - servicemonitor.crd.yaml
-    - podmonitor.crd.yaml
-
-- name: Get prometheus PV name
-  tags:
-    - prometheus
-  shell: "kubectl -n oas get pvc prometheus-prometheus-oas-{{ release_name }}-prometheus-promet-prometheus-0 -o=jsonpath='{.spec.volumeName}'"
-  register: prometheus_pv_name
-  failed_when: false
-  changed_when: false
-
-# Needed because previously we ran prometheus as root
-- name: Ensure prometheus volume is accessible by the prometheus pod
-  tags:
-    - prometheus
-  file:
-    dest: "{{ data_directory }}/local-storage/{{ prometheus_pv_name.stdout }}"
-    owner: '1000'
-    group: '2000'
-    recurse: true
-  when: prometheus_pv_name.stdout
-
 - name: Create Kubernetes secret with monitoring settings
   tags:
     - config
diff --git a/ansible/roles/apps/templates/letsencrypt-production-settings.yaml b/ansible/roles/apps/templates/letsencrypt-production-settings.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bc749009c570592ab4111d2f45ae1ad7f14915a3
--- /dev/null
+++ b/ansible/roles/apps/templates/letsencrypt-production-settings.yaml
@@ -0,0 +1 @@
+email: "{{ admin_email }}"
diff --git a/ansible/roles/apps/templates/letsencrypt-staging-settings.yaml b/ansible/roles/apps/templates/letsencrypt-staging-settings.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..bc749009c570592ab4111d2f45ae1ad7f14915a3
--- /dev/null
+++ b/ansible/roles/apps/templates/letsencrypt-staging-settings.yaml
@@ -0,0 +1 @@
+email: "{{ admin_email }}"
diff --git a/ansible/roles/configure/tasks/main.yml b/ansible/roles/configure/tasks/main.yml
index fdffbb49562fffa05b849e34d14bc0f0c731f1b6..71d9a5e49c957adc463c4805c3700180a5b92489 100644
--- a/ansible/roles/configure/tasks/main.yml
+++ b/ansible/roles/configure/tasks/main.yml
@@ -45,6 +45,18 @@
   set_fact:
     configuration_directory: "{{ configuration_directory }}"
 
+- name: Install kubectl snap
+  # kubectl needs to get installed as "classic" snap
+  command: snap install --classic kubectl
+  args:
+    creates: /snap/bin/kubectl
+
+- name: Create kubectl symlink to /usr/local/bin
+  file:
+    state: link
+    src: /snap/bin/kubectl
+    dest: /usr/local/bin/kubectl
+
 - name: Remove old helm snap installation
   tags:
     - helm
@@ -83,14 +95,16 @@
   command: /usr/local/bin/get-helm --version v{{ helm.version }}
   when: helm_version.stdout != helm.version
 
-- name: Install kubectl snap
-  # kubectl needs to get installed as "classic" snap
-  command: snap install --classic kubectl
-  args:
-    creates: /snap/bin/kubectl
+- name: Get list of installed helm repos
+  tags:
+    - helm
+  command: /usr/local/bin/helm repo list
+  # `helm repo list` exits with an error code if the list of repos is empty.
+  failed_when: false
+  register: helm_repo_list
 
-- name: Create kubectl symlink to /usr/local/bin
-  file:
-    state: link
-    src: /snap/bin/kubectl
-    dest: /usr/local/bin/kubectl
+- name: Add helm stable repo
+  tags:
+    - helm
+  command: /usr/local/bin/helm repo add stable https://kubernetes-charts.storage.googleapis.com/
+  when: not (helm_repo_list.stdout | regex_search('^stable\\s', multiline=True))
diff --git a/ansible/roles/setup/tasks/main.yml b/ansible/roles/setup/tasks/main.yml
index e6ef312353f85abe9a7a8c7dd98c75ec41079b83..118995ceac8e37b04cd9ff243132b04c650fd257 100644
--- a/ansible/roles/setup/tasks/main.yml
+++ b/ansible/roles/setup/tasks/main.yml
@@ -1,6 +1,5 @@
 ---
 - import_tasks: ssh.yml
 - import_tasks: rke.yml
-- import_tasks: tiller.yml
 - import_tasks: krew.yml
 - import_tasks: namespaces.yml
diff --git a/ansible/roles/setup/tasks/tiller.yml b/ansible/roles/setup/tasks/tiller.yml
deleted file mode 100644
index e0570fdef0b993d604b78a801da342bb65237205..0000000000000000000000000000000000000000
--- a/ansible/roles/setup/tasks/tiller.yml
+++ /dev/null
@@ -1,51 +0,0 @@
----
-- name: Configure Tiller serviceaccount
-  tags:
-    - tiller
-  k8s:
-    state: present
-    api_version: v1
-    kind: ServiceAccount
-    name: tiller
-    namespace: kube-system
-
-- name: Configure Tiller ClusterRoleBinding
-  tags:
-    - tiller
-  k8s:
-    state: present
-    definition:
-      kind: ClusterRoleBinding
-      apiVersion: rbac.authorization.k8s.io/v1beta1
-      metadata:
-        name: tiller-clusterrolebinding
-      subjects:
-      - kind: ServiceAccount
-        name: tiller
-        namespace: kube-system
-      roleRef:
-        kind: ClusterRole
-        name: cluster-admin
-        apiGroup: ""
-
-- name: Check if tiller is already installed
-  tags:
-    - helm
-    - tiller
-  command: /usr/local/bin/helm ls
-  failed_when: false
-  register: helm_ls
-  changed_when: false
-
-- name: Initialize helm
-  tags:
-    - tiller
-    - helm
-  command: /usr/local/bin/helm init --upgrade --service-account=tiller
-  when: helm_ls.stderr.find('Error') != -1
-
-- name: Wait for tiller to become available
-  tags:
-    - tiller
-  command: /snap/bin/kubectl rollout status -n kube-system deployment/tiller-deploy
-  changed_when: false
diff --git a/flux/letsencrypt-production.yaml b/flux/letsencrypt-production.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a25ad1f276aafed91cb612c7ef9aac0275a76381
--- /dev/null
+++ b/flux/letsencrypt-production.yaml
@@ -0,0 +1,18 @@
+---
+apiVersion: helm.fluxcd.io/v1
+kind: HelmRelease
+metadata:
+  name: letsencrypt-production
+  namespace: oas
+  annotations:
+    flux.weave.works/automated: "false"
+spec:
+  releaseName: letsencrypt-production
+  chart:
+    git: https://open.greenhost.net/openappstack/letsencrypt-issuer
+    ref: 346ce53321fe6880dd76e7cff7e2a71e57f667d8
+    path: .
+  valuesFrom:
+    - secretKeyRef:
+        name: letsencrypt-production-settings
+        key: values.yaml
diff --git a/flux/letsencrypt-staging.yaml b/flux/letsencrypt-staging.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..96522de904e22a6717d511be0bdea5f8c4aabb55
--- /dev/null
+++ b/flux/letsencrypt-staging.yaml
@@ -0,0 +1,22 @@
+---
+apiVersion: helm.fluxcd.io/v1
+kind: HelmRelease
+metadata:
+  name: letsencrypt-staging
+  namespace: oas
+  annotations:
+    flux.weave.works/automated: "false"
+spec:
+  releaseName: letsencrypt-staging
+  chart:
+    git: https://open.greenhost.net/openappstack/letsencrypt-issuer
+    ref: 346ce53321fe6880dd76e7cff7e2a71e57f667d8
+    path: .
+  valuesFrom:
+    - secretKeyRef:
+        name: letsencrypt-staging-settings
+        key: values.yaml
+  values:
+    issuer:
+      name: letsencrypt-staging
+      server: "https://acme-staging-v02.api.letsencrypt.org/directory"