From 62eff7cfc3683c03fc636e8042692405945c8edb Mon Sep 17 00:00:00 2001
From: Sheogorath <sheogorath@shivering-isles.com>
Date: Sat, 25 Jun 2022 04:30:36 +0200
Subject: [PATCH] feat(mok): Add helm chart for Mail on Kubernetes

Adding a new helm chart to deploy functional mail server on Kubernetes.
It's composed of postfix and dovecot so far and handles user management
using Kubernetes secrets.
---
 charts/mok/.helmignore                        |  25 +++
 charts/mok/Chart.yaml                         |  12 ++
 charts/mok/Makefile                           |  10 +
 charts/mok/README.md                          |  88 +++++++++
 charts/mok/templates/_helpers.tpl             |  62 ++++++
 charts/mok/templates/dovecot.yaml             | 140 +++++++++++++
 .../mok/templates/persistentvolumeclaim.yaml  |   9 +
 charts/mok/templates/postfix.yaml             | 153 +++++++++++++++
 charts/mok/templates/secret.yaml              |  75 +++++++
 charts/mok/templates/serviceaccount.yaml      |  12 ++
 .../tests/__snapshot__/domains_test.yaml.snap |  45 +++++
 .../tests/__snapshot__/dovecot_test.yaml.snap | 167 ++++++++++++++++
 .../tests/__snapshot__/postfix_test.yaml.snap | 154 +++++++++++++++
 .../tests/__snapshot__/relay_test.yaml.snap   |  38 ++++
 charts/mok/tests/domains_test.yaml            |  57 ++++++
 charts/mok/tests/dovecot_test.yaml            |  85 ++++++++
 charts/mok/tests/helmlabels_test.yaml         |  30 +++
 charts/mok/tests/postfix_test.yaml            |  45 +++++
 charts/mok/tests/relay_test.yaml              |  48 +++++
 charts/mok/values.yaml                        | 184 ++++++++++++++++++
 docs/src/SUMMARY.md                           |   3 +
 docs/src/charts/mok.md                        |   1 +
 22 files changed, 1443 insertions(+)
 create mode 100644 charts/mok/.helmignore
 create mode 100644 charts/mok/Chart.yaml
 create mode 100644 charts/mok/Makefile
 create mode 100644 charts/mok/README.md
 create mode 100644 charts/mok/templates/_helpers.tpl
 create mode 100644 charts/mok/templates/dovecot.yaml
 create mode 100644 charts/mok/templates/persistentvolumeclaim.yaml
 create mode 100644 charts/mok/templates/postfix.yaml
 create mode 100644 charts/mok/templates/secret.yaml
 create mode 100644 charts/mok/templates/serviceaccount.yaml
 create mode 100644 charts/mok/tests/__snapshot__/domains_test.yaml.snap
 create mode 100644 charts/mok/tests/__snapshot__/dovecot_test.yaml.snap
 create mode 100644 charts/mok/tests/__snapshot__/postfix_test.yaml.snap
 create mode 100644 charts/mok/tests/__snapshot__/relay_test.yaml.snap
 create mode 100644 charts/mok/tests/domains_test.yaml
 create mode 100644 charts/mok/tests/dovecot_test.yaml
 create mode 100644 charts/mok/tests/helmlabels_test.yaml
 create mode 100644 charts/mok/tests/postfix_test.yaml
 create mode 100644 charts/mok/tests/relay_test.yaml
 create mode 100644 charts/mok/values.yaml
 create mode 120000 docs/src/charts/mok.md

diff --git a/charts/mok/.helmignore b/charts/mok/.helmignore
new file mode 100644
index 000000000..4430ae9ae
--- /dev/null
+++ b/charts/mok/.helmignore
@@ -0,0 +1,25 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
+# helm-unittest
+tests/
diff --git a/charts/mok/Chart.yaml b/charts/mok/Chart.yaml
new file mode 100644
index 000000000..83e6f456f
--- /dev/null
+++ b/charts/mok/Chart.yaml
@@ -0,0 +1,12 @@
+apiVersion: v2
+name: mok
+description: |
+  Mail on Kubernetes (MoK) is a project to deploy a functional mailserver that runs without a database server on Kubernetes, taking advantage of configmaps and secret.
+type: application
+version: 0.1.0
+sources:
+  - https://de.postfix.org/ftpmirror/index.html
+  - https://github.com/dovecot/core
+maintainers:
+  - name: Sheogorath
+    url: https://shivering-isles.com/contribute
diff --git a/charts/mok/Makefile b/charts/mok/Makefile
new file mode 100644
index 000000000..d996e967d
--- /dev/null
+++ b/charts/mok/Makefile
@@ -0,0 +1,10 @@
+lint:
+	koolbox helm lint
+
+test: lint helm-unittest
+
+helm-unittest:
+	podman run -ti --rm -v $$(pwd):/apps docker.io/quintush/helm-unittest:3.7.1-0.2.8 -3 .
+
+docs:
+	koolbox helm-docs
diff --git a/charts/mok/README.md b/charts/mok/README.md
new file mode 100644
index 000000000..1c51b608e
--- /dev/null
+++ b/charts/mok/README.md
@@ -0,0 +1,88 @@
+# mok
+
+![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![Type: application](https://img.shields.io/badge/Type-application-informational?style=flat-square)
+
+Mail on Kubernetes (MoK) is a project to deploy a functional mailserver that runs without a database server on Kubernetes, taking advantage of configmaps and secret.
+
+## Maintainers
+
+| Name | Email | Url |
+| ---- | ------ | --- |
+| Sheogorath |  | <https://shivering-isles.com/contribute> |
+
+## Source Code
+
+* <https://de.postfix.org/ftpmirror/index.html>
+* <https://github.com/dovecot/core>
+
+## Values
+
+| Key | Type | Default | Description |
+|-----|------|---------|-------------|
+| domains | object | `{}` | list of configured domains and users. See values.yaml for details. |
+| dovecot.affinity | object | `{}` |  |
+| dovecot.image.pullPolicy | string | `"IfNotPresent"` |  |
+| dovecot.image.repository | string | `"quay.io/shivering-isles/dovecot"` | dovecot container image |
+| dovecot.image.tag | string | `"0.1.0"` | Overrides the image tag whose default is "latest" |
+| dovecot.imagePullSecrets | list | `[]` | pull secret to access the afore defined image |
+| dovecot.nodeSelector | object | `{}` |  |
+| dovecot.podAnnotations | object | `{}` |  |
+| dovecot.podSecurityContext | object | `{}` |  |
+| dovecot.replicaCount | int | `1` | Number of Dovecot pods. **Important**: With the current configuration, it's not recommended to scale beyond 1 |
+| dovecot.resources.limits.cpu | string | `"500m"` |  |
+| dovecot.resources.limits.memory | string | `"512Mi"` |  |
+| dovecot.resources.requests.cpu | string | `"100m"` |  |
+| dovecot.resources.requests.memory | string | `"128Mi"` |  |
+| dovecot.securityContext.allowPrivilegeEscalation | bool | `false` |  |
+| dovecot.securityContext.capabilities.add[0] | string | `"SYS_CHROOT"` | required to setup chroot for dovecot https://wiki.dovecot.org/HowTo/Rootless |
+| dovecot.securityContext.capabilities.add[1] | string | `"CHOWN"` | required to set up file structure |
+| dovecot.securityContext.capabilities.add[2] | string | `"CAP_NET_BIND_SERVICE"` | required to bind privileged ports in the container, such as 993, 143, 24, etc. |
+| dovecot.securityContext.capabilities.add[3] | string | `"SETUID"` | required to drop privileges with dovecot process |
+| dovecot.securityContext.capabilities.add[4] | string | `"SETGID"` | required to drop privileges with dovecot process |
+| dovecot.securityContext.capabilities.drop[0] | string | `"ALL"` | required to drop privileges by default |
+| dovecot.securityContext.runAsNonRoot | bool | `false` |  |
+| dovecot.service.internal.type | string | `"ClusterIP"` | type of the public endpoint for lmtp, metrics, authentication |
+| dovecot.service.public.type | string | `"LoadBalancer"` | type of the public endpoint for pop3, imap, and sieve **Note**: It's configured to share the IP with postfix in case of metallb |
+| dovecot.tls.secretName | string | `"nil"` | secret holding the TLS keys for dovecot. **Required** |
+| dovecot.tolerations | list | `[]` |  |
+| dovecot.volumes.vmail.accessModes | list | `["ReadWriteMany"]` | Volume access mode, using ReadWriteMany in order to prepare setup with dovcecot director |
+| dovecot.volumes.vmail.resources.requests.storage | string | `"5Gi"` |  |
+| dovecot.volumes.vmail.volumeMode | string | `"Filesystem"` |  |
+| fullnameOverride | string | `""` |  |
+| nameOverride | string | `""` |  |
+| postfix.affinity | object | `{}` |  |
+| postfix.image.pullPolicy | string | `"IfNotPresent"` |  |
+| postfix.image.repository | string | `"quay.io/shivering-isles/postfix"` | postfix container image |
+| postfix.image.tag | string | `"0.1.0"` | Overrides the image tag whose default is "latest" |
+| postfix.imagePullSecrets | list | `[]` |  |
+| postfix.nodeSelector | object | `{}` |  |
+| postfix.podAnnotations | object | `{}` |  |
+| postfix.podSecurityContext | object | `{}` |  |
+| postfix.replicaCount | int | `1` | Number of postfix pods. |
+| postfix.resources.limits.cpu | string | `"500m"` |  |
+| postfix.resources.limits.memory | string | `"512Mi"` |  |
+| postfix.resources.requests.cpu | string | `"100m"` |  |
+| postfix.resources.requests.memory | string | `"128Mi"` |  |
+| postfix.securityContext.allowPrivilegeEscalation | bool | `false` | prevent any process in the container to regain capabilities once dropped |
+| postfix.securityContext.capabilities.add[0] | string | `"SYS_CHROOT"` | required to setup chroot with postfix |
+| postfix.securityContext.capabilities.add[1] | string | `"CHOWN"` | required to adjust ownership of files using supervisord |
+| postfix.securityContext.capabilities.add[2] | string | `"CAP_NET_BIND_SERVICE"` | required to bind privileged ports like 25, 465, 587 |
+| postfix.securityContext.capabilities.add[3] | string | `"SETUID"` | required to change user id as supervisord as well as postfix |
+| postfix.securityContext.capabilities.add[4] | string | `"SETGID"` | required to change group id as supervisord as well as postfix |
+| postfix.securityContext.capabilities.add[5] | string | `"FOWNER"` | required to set up the chroot directory on startup |
+| postfix.securityContext.capabilities.drop[0] | string | `"ALL"` | getting rid of all capabilities since we already have too many |
+| postfix.securityContext.runAsNonRoot | bool | `false` |  |
+| postfix.service.public.type | string | `"LoadBalancer"` | type of the public endpoint for smtp, submission, and submissions. **Note**: It's configured to share the IP with dovecot in case of metallb |
+| postfix.tls.secretName | string | `"nil"` | secret holding the TLS keys for postfix. **Required** |
+| postfix.tolerations | list | `[]` |  |
+| postfix.volumes.spool.accessModes[0] | string | `"ReadWriteOnce"` |  |
+| postfix.volumes.spool.resources.requests.storage | string | `"1Gi"` |  |
+| relay.relayHosts | object | `{}` | relay hosts used as part of the deployment |
+| relay.saslPasswords | object | `{}` | passwords for the relay hosts |
+| relay.tlsPolicies | string | `""` | tls policy in postfix https://www.postfix.org/TLS_README.html#client_tls_policy |
+| serviceAccount.annotations | object | `{}` |  |
+| serviceAccount.create | bool | `true` |  |
+| serviceAccount.name | string | `""` |  |
+
+----------------------------------------------
+Autogenerated from chart metadata using [helm-docs v1.10.0](https://github.com/norwoodj/helm-docs/releases/v1.10.0)
diff --git a/charts/mok/templates/_helpers.tpl b/charts/mok/templates/_helpers.tpl
new file mode 100644
index 000000000..11c0252b0
--- /dev/null
+++ b/charts/mok/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "mok.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "mok.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "mok.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "mok.labels" -}}
+helm.sh/chart: {{ include "mok.chart" . }}
+{{ include "mok.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "mok.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "mok.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "mok.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "mok.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/charts/mok/templates/dovecot.yaml b/charts/mok/templates/dovecot.yaml
new file mode 100644
index 000000000..30d4c0ce3
--- /dev/null
+++ b/charts/mok/templates/dovecot.yaml
@@ -0,0 +1,140 @@
+apiVersion: v1
+kind: Service
+metadata:
+  annotations:
+    metallb.universe.tf/allow-shared-ip: "{{ include "mok.fullname" . }}-mail"
+  name: {{ include "mok.fullname" . }}-dovecot
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+spec:
+  ports:
+  - port: 110
+    name: pop3
+    protocol: TCP
+  - port: 143
+    name: imap4
+    protocol: TCP
+  - port: 993
+    name: imaps
+    protocol: TCP
+  - port: 995
+    name: pop3s
+    protocol: TCP
+  - port: 4190
+    name: sieve
+    protocol: TCP
+  selector:
+    {{- include "mok.selectorLabels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+  type: {{ .Values.dovecot.service.public.type }}
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mok.fullname" . }}-dovecot-internal
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+spec:
+  ports:
+  - port: 24
+    name: lmtp
+  - port: 9090
+    name: metrics
+  - port: 12345
+    name: auth
+  selector:
+    {{- include "mok.selectorLabels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+  type: {{ .Values.dovecot.service.internal.type }}
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+  name: {{ include "mok.fullname" . }}-dovecot
+spec:
+  selector:
+    matchLabels:
+      {{- include "mok.selectorLabels" . | nindent 6 }}
+      app.kubernetes.io/component: dovecot
+  replicas: {{ .Values.postfix.replicaCount }}
+  strategy:
+    type: Recreate
+  template:
+    metadata:
+      labels:
+        {{- include "mok.selectorLabels" . | nindent 8 }}
+        app.kubernetes.io/component: dovecot
+      {{- with .Values.dovecot.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+    spec:
+      {{- with .Values.dovecot.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      serviceAccountName: {{ include "mok.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.dovecot.podSecurityContext | nindent 8 }}
+      terminationGracePeriodSeconds: 300
+      containers:
+        - name: dovecot
+          image: "{{ .Values.dovecot.image.repository }}:{{.Values.dovecot.image.tag | default "latest" }}"
+          imagePullPolicy: {{ .Values.dovecot.image.pullPolicy }}
+          ports:
+            - containerPort: 24
+              name: lmtp
+            - containerPort: 110
+              name: pop3
+            - containerPort: 143
+              name: imap4
+            - containerPort: 993
+              name: imaps
+            - containerPort: 995
+              name: pop3s
+            - containerPort: 4190
+              name: sieve
+            - containerPort: 9090
+              name: metrics
+            - containerPort: 12345
+              name: auth
+          resources:
+            {{- toYaml .Values.dovecot.resources | nindent 12 }}
+          securityContext:
+            {{- toYaml .Values.dovecot.securityContext | nindent 12 }}
+          volumeMounts:
+            - name: vmail
+              mountPath: /srv/mail/mailboxes/
+            - name: users
+              mountPath: "/srv/passdb/"
+              readOnly: true
+            - name: tls
+              mountPath: "/srv/tls/"
+              readOnly: true
+      {{- with .Values.dovecot.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.dovecot.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.dovecot.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      volumes:
+        - name: vmail
+          persistentVolumeClaim:
+            claimName: {{ include "mok.fullname" . }}-dovecot-vmail
+        - name: users
+          secret:
+            secretName: {{ include "mok.fullname" . }}-dovecot-users
+        - name: tls
+          secret:
+            secretName: {{ required "TLS secret for dovecot is required" .Values.postfix.tls.secretName }}
diff --git a/charts/mok/templates/persistentvolumeclaim.yaml b/charts/mok/templates/persistentvolumeclaim.yaml
new file mode 100644
index 000000000..8d8f5f67d
--- /dev/null
+++ b/charts/mok/templates/persistentvolumeclaim.yaml
@@ -0,0 +1,9 @@
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+  name: {{ include "mok.fullname" . }}-dovecot-vmail
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+spec:
+  {{- toYaml .Values.dovecot.volumes.vmail | nindent 2 }}
diff --git a/charts/mok/templates/postfix.yaml b/charts/mok/templates/postfix.yaml
new file mode 100644
index 000000000..3fb47c26b
--- /dev/null
+++ b/charts/mok/templates/postfix.yaml
@@ -0,0 +1,153 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "mok.fullname" . }}-postfix
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: postfix
+  annotations:
+    metallb.universe.tf/allow-shared-ip: "{{ include "mok.fullname" . }}-mail"
+spec:
+  ports:
+  - port: 25
+    name: smtp
+    protocol: TCP
+  - port: 465
+    name: submissions
+    protocol: TCP
+  - port: 587
+    name: submission
+    protocol: TCP
+  selector:
+    {{- include "mok.selectorLabels" . | nindent 4 }}
+    app.kubernetes.io/component: postfix
+  type: {{ .Values.postfix.service.public.type }}
+---
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: {{ include "mok.fullname" . }}-postfix
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: postfix
+spec:
+  replicas: {{ .Values.postfix.replicaCount }}
+  serviceName: {{ include "mok.fullname" . }}-postfix-statefulset
+  selector:
+    matchLabels:
+      {{- include "mok.selectorLabels" . | nindent 6 }}
+      app.kubernetes.io/component: postfix
+  volumeClaimTemplates:
+    - metadata:
+        name: spool
+      spec:
+        {{- toYaml .Values.postfix.volumes.spool | nindent 8 }}
+  {{- if semverCompare ">=1.23.0" .Capabilities.KubeVersion.Version }}
+  persistentVolumeClaimRetentionPolicy:
+    whenDeleted: Retain
+    whenScaled: Delete
+  {{- end }}
+  template:
+    metadata:
+      {{- with .Values.postfix.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      labels:
+        {{- include "mok.selectorLabels" . | nindent 8 }}
+        app.kubernetes.io/component: postfix
+    spec:
+      {{- with .Values.postfix.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      serviceAccountName: {{ include "mok.serviceAccountName" . }}
+      securityContext:
+        {{- toYaml .Values.postfix.podSecurityContext | nindent 8 }}
+      containers:
+        - name: postfix
+          image: {{ .Values.postfix.image.repository }}:{{.Values.postfix.image.tag | default "latest" }}
+          imagePullPolicy: {{ .Values.postfix.image.pullPolicy }}
+          lifecycle:
+            preStop:
+              exec:
+                # flush all emails before shutting down
+                command:
+                  - postqueue
+                  - -f
+          readinessProbe:
+            initialDelaySeconds: 5
+            periodSeconds: 60
+            timeoutSeconds: 5
+            exec:
+              command:
+                - sh
+                - -c
+                - >-
+                  printf "EHLO healthcheck\n" | nc 127.0.0.1 587 |
+                  grep -qE "^220.*ESMTP Postfix"
+          livenessProbe:
+            initialDelaySeconds: 5
+            periodSeconds: 5
+            failureThreshold: 1
+            exec:
+              command:
+                - sh
+                - -c
+                - >-
+                  ps axf | fgrep -v grep | fgrep -q "supervisord" &&
+                  ps axf | fgrep -v grep | fgrep -q "/usr/libexec/postfix/master"
+          startupProbe:
+            initialDelaySeconds: 2
+            periodSeconds: 5
+            failureThreshold: 12
+            exec:
+              command:
+                - sh
+                - -c
+                - >-
+                  ps axf | fgrep -v grep | fgrep -q "supervisord" &&
+                  ps axf | fgrep -v grep | fgrep -q "/usr/libexec/postfix/master"
+          ports:
+            - containerPort: 25
+              name: smtp
+            - containerPort: 465
+              name: submissions
+            - containerPort: 587
+              name: submission
+          resources:
+            {{- toYaml .Values.postfix.resources | nindent 12 }}
+          securityContext:
+            {{- toYaml .Values.postfix.securityContext | nindent 12 }}
+          volumeMounts:
+            - name: spool
+              mountPath: /var/spool/postfix/
+            - name: cache
+              mountPath: "/srv/tmp"
+            - name: maps
+              mountPath: "/srv/virtual"
+              readOnly: true
+            - name: tls
+              mountPath: "/srv/tls"
+              readOnly: true
+      {{- with .Values.postfix.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.postfix.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.postfix.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      volumes:
+        - name: maps
+          secret:
+            secretName: {{ include "mok.fullname" . }}-postfix-maps
+        - name: tls
+          secret:
+            secretName: {{ required "TLS secret for postfix is required" .Values.postfix.tls.secretName }}
+        - name: cache
+          emptyDir: {}
diff --git a/charts/mok/templates/secret.yaml b/charts/mok/templates/secret.yaml
new file mode 100644
index 000000000..019525340
--- /dev/null
+++ b/charts/mok/templates/secret.yaml
@@ -0,0 +1,75 @@
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ include "mok.fullname" . }}-dovecot-users
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: dovecot
+type: Opaque
+stringData:
+  passwd: |
+    {{- range $domain,$config := .Values.domains }}
+    {{- range $config.users }}
+    {{ .name }}@{{ $domain }}:{{.passwordHash}}
+    {{- end }}
+    {{- end }}
+---
+apiVersion: v1
+kind: Secret
+metadata:
+  name: {{ include "mok.fullname" . }}-postfix-maps
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+    app.kubernetes.io/component: postfix
+type: Opaque
+stringData:
+  aliases: |
+    {{- range $domain,$config := .Values.domains }}
+    {{- range $config.users }}
+    {{- $username := .name }}
+    {{- range .aliases }}
+    {{ . }} {{ $username }}@{{ $domain }}
+    {{- end }}
+    {{- end }}
+    {{- end }}
+  domains: |
+    {{- $domainList := list }}
+    {{- range $domain,$config := .Values.domains }}
+    {{- $domainList = (append $domainList $domain | uniq) }}
+    {{- range $config.users }}
+    {{- $username := .name }}
+    {{- range .aliases }}
+    {{- $domainList = (append $domainList  (regexFind "@.*" .) | uniq) }}
+    {{- end }}
+    {{- end }}
+    {{- end }}
+
+    {{- range $domainList }}
+    {{ trimPrefix "@" . }} OK
+    {{- end }}
+  mailboxes: |
+    {{- range $domain,$config := .Values.domains }}
+    {{- range $config.users }}
+    {{ .name }}@{{ $domain }} OK
+    {{- end }}
+    {{- end }}
+  relayhosts: |
+    {{- range $domain,$relay := .Values.relay.relayHosts }}
+    {{ $domain }} {{ $relay }}
+    {{- end }}
+  sender-login-maps: |
+    {{- range $domain,$config := .Values.domains }}
+    {{- range $config.users }}
+    {{- $username := .name }}
+    {{ $username }}@{{ $domain }} {{ $username }}@{{ $domain }}
+    {{- range .aliases }}
+    {{ . }} {{ $username }}@{{ $domain }}
+    {{- end }}
+    {{- end }}
+    {{- end }}
+  sasl_passwd: |
+    {{- range $relay,$password := .Values.relay.saslPasswords }}
+    {{ $relay }} {{ $password }}
+    {{- end }}
+  tls-policies: |
+    {{- .Values.relay.tlsPolicies | nindent 4 }}
diff --git a/charts/mok/templates/serviceaccount.yaml b/charts/mok/templates/serviceaccount.yaml
new file mode 100644
index 000000000..e06dd08c0
--- /dev/null
+++ b/charts/mok/templates/serviceaccount.yaml
@@ -0,0 +1,12 @@
+{{- if .Values.serviceAccount.create -}}
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: {{ include "mok.serviceAccountName" . }}
+  labels:
+    {{- include "mok.labels" . | nindent 4 }}
+  {{- with .Values.serviceAccount.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+{{- end }}
diff --git a/charts/mok/tests/__snapshot__/domains_test.yaml.snap b/charts/mok/tests/__snapshot__/domains_test.yaml.snap
new file mode 100644
index 000000000..809373a23
--- /dev/null
+++ b/charts/mok/tests/__snapshot__/domains_test.yaml.snap
@@ -0,0 +1,45 @@
+keeps stays the same:
+  1: |
+    apiVersion: v1
+    kind: Secret
+    metadata:
+      labels:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-dovecot-users
+    stringData:
+      passwd: |
+        john@example.com:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg
+    type: Opaque
+  2: |
+    apiVersion: v1
+    kind: Secret
+    metadata:
+      labels:
+        app.kubernetes.io/component: postfix
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-postfix-maps
+    stringData:
+      aliases: |
+        steve@example.net john@example.com
+        @example.info john@example.com
+      domains: |
+        example.com OK
+        example.net OK
+        example.info OK
+      mailboxes: |
+        john@example.com OK
+      relayhosts: ""
+      sasl_passwd: ""
+      sender-login-maps: |
+        john@example.com john@example.com
+        steve@example.net john@example.com
+        @example.info john@example.com
+      tls-policies: ""
+    type: Opaque
diff --git a/charts/mok/tests/__snapshot__/dovecot_test.yaml.snap b/charts/mok/tests/__snapshot__/dovecot_test.yaml.snap
new file mode 100644
index 000000000..a0353e1bf
--- /dev/null
+++ b/charts/mok/tests/__snapshot__/dovecot_test.yaml.snap
@@ -0,0 +1,167 @@
+should match snapshot:
+  1: |
+    apiVersion: v1
+    kind: Service
+    metadata:
+      annotations:
+        metallb.universe.tf/allow-shared-ip: RELEASE-NAME-mok-mail
+      labels:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-dovecot
+    spec:
+      ports:
+      - name: pop3
+        port: 110
+        protocol: TCP
+      - name: imap4
+        port: 143
+        protocol: TCP
+      - name: imaps
+        port: 993
+        protocol: TCP
+      - name: pop3s
+        port: 995
+        protocol: TCP
+      - name: sieve
+        port: 4190
+        protocol: TCP
+      selector:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/name: mok
+      type: LoadBalancer
+  2: |
+    apiVersion: v1
+    kind: Service
+    metadata:
+      labels:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-dovecot-internal
+    spec:
+      ports:
+      - name: lmtp
+        port: 24
+      - name: metrics
+        port: 9090
+      - name: auth
+        port: 12345
+      selector:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/name: mok
+      type: ClusterIP
+  3: |
+    apiVersion: apps/v1
+    kind: Deployment
+    metadata:
+      labels:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-dovecot
+    spec:
+      replicas: 1
+      selector:
+        matchLabels:
+          app.kubernetes.io/component: dovecot
+          app.kubernetes.io/instance: RELEASE-NAME
+          app.kubernetes.io/name: mok
+      strategy:
+        type: Recreate
+      template:
+        metadata:
+          labels:
+            app.kubernetes.io/component: dovecot
+            app.kubernetes.io/instance: RELEASE-NAME
+            app.kubernetes.io/name: mok
+        spec:
+          containers:
+          - image: quay.io/shivering-isles/dovecot:0.1.0
+            imagePullPolicy: IfNotPresent
+            name: dovecot
+            ports:
+            - containerPort: 24
+              name: lmtp
+            - containerPort: 110
+              name: pop3
+            - containerPort: 143
+              name: imap4
+            - containerPort: 993
+              name: imaps
+            - containerPort: 995
+              name: pop3s
+            - containerPort: 4190
+              name: sieve
+            - containerPort: 9090
+              name: metrics
+            - containerPort: 12345
+              name: auth
+            resources:
+              limits:
+                cpu: 500m
+                memory: 512Mi
+              requests:
+                cpu: 100m
+                memory: 128Mi
+            securityContext:
+              allowPrivilegeEscalation: false
+              capabilities:
+                add:
+                - SYS_CHROOT
+                - CHOWN
+                - CAP_NET_BIND_SERVICE
+                - SETUID
+                - SETGID
+                drop:
+                - ALL
+              runAsNonRoot: false
+            volumeMounts:
+            - mountPath: /srv/mail/mailboxes/
+              name: vmail
+            - mountPath: /srv/passdb/
+              name: users
+              readOnly: true
+            - mountPath: /srv/tls/
+              name: tls
+              readOnly: true
+          securityContext: {}
+          serviceAccountName: RELEASE-NAME-mok
+          terminationGracePeriodSeconds: 300
+          volumes:
+          - name: vmail
+            persistentVolumeClaim:
+              claimName: RELEASE-NAME-mok-dovecot-vmail
+          - name: users
+            secret:
+              secretName: RELEASE-NAME-mok-dovecot-users
+          - name: tls
+            secret:
+              secretName: nil
+  4: |
+    apiVersion: v1
+    kind: PersistentVolumeClaim
+    metadata:
+      labels:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-dovecot-vmail
+    spec:
+      accessModes:
+      - ReadWriteMany
+      resources:
+        requests:
+          storage: 5Gi
+      volumeMode: Filesystem
diff --git a/charts/mok/tests/__snapshot__/postfix_test.yaml.snap b/charts/mok/tests/__snapshot__/postfix_test.yaml.snap
new file mode 100644
index 000000000..92f369083
--- /dev/null
+++ b/charts/mok/tests/__snapshot__/postfix_test.yaml.snap
@@ -0,0 +1,154 @@
+should match snapshot:
+  1: |
+    apiVersion: v1
+    kind: Service
+    metadata:
+      annotations:
+        metallb.universe.tf/allow-shared-ip: RELEASE-NAME-mok-mail
+      labels:
+        app.kubernetes.io/component: postfix
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-postfix
+    spec:
+      ports:
+      - name: smtp
+        port: 25
+        protocol: TCP
+      - name: submissions
+        port: 465
+        protocol: TCP
+      - name: submission
+        port: 587
+        protocol: TCP
+      selector:
+        app.kubernetes.io/component: postfix
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/name: mok
+      type: LoadBalancer
+  2: |
+    apiVersion: apps/v1
+    kind: StatefulSet
+    metadata:
+      labels:
+        app.kubernetes.io/component: postfix
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-postfix
+    spec:
+      replicas: 1
+      selector:
+        matchLabels:
+          app.kubernetes.io/component: postfix
+          app.kubernetes.io/instance: RELEASE-NAME
+          app.kubernetes.io/name: mok
+      serviceName: RELEASE-NAME-mok-postfix-statefulset
+      template:
+        metadata:
+          labels:
+            app.kubernetes.io/component: postfix
+            app.kubernetes.io/instance: RELEASE-NAME
+            app.kubernetes.io/name: mok
+        spec:
+          containers:
+          - image: quay.io/shivering-isles/postfix:0.1.0
+            imagePullPolicy: IfNotPresent
+            lifecycle:
+              preStop:
+                exec:
+                  command:
+                  - postqueue
+                  - -f
+            livenessProbe:
+              exec:
+                command:
+                - sh
+                - -c
+                - ps axf | fgrep -v grep | fgrep -q "supervisord" && ps axf | fgrep -v
+                  grep | fgrep -q "/usr/libexec/postfix/master"
+              failureThreshold: 1
+              initialDelaySeconds: 5
+              periodSeconds: 5
+            name: postfix
+            ports:
+            - containerPort: 25
+              name: smtp
+            - containerPort: 465
+              name: submissions
+            - containerPort: 587
+              name: submission
+            readinessProbe:
+              exec:
+                command:
+                - sh
+                - -c
+                - printf "EHLO healthcheck\n" | nc 127.0.0.1 587 | grep -qE "^220.*ESMTP
+                  Postfix"
+              initialDelaySeconds: 5
+              periodSeconds: 60
+              timeoutSeconds: 5
+            resources:
+              limits:
+                cpu: 500m
+                memory: 512Mi
+              requests:
+                cpu: 100m
+                memory: 128Mi
+            securityContext:
+              allowPrivilegeEscalation: false
+              capabilities:
+                add:
+                - SYS_CHROOT
+                - CHOWN
+                - CAP_NET_BIND_SERVICE
+                - SETUID
+                - SETGID
+                - FOWNER
+                drop:
+                - ALL
+              runAsNonRoot: false
+            startupProbe:
+              exec:
+                command:
+                - sh
+                - -c
+                - ps axf | fgrep -v grep | fgrep -q "supervisord" && ps axf | fgrep -v
+                  grep | fgrep -q "/usr/libexec/postfix/master"
+              failureThreshold: 12
+              initialDelaySeconds: 2
+              periodSeconds: 5
+            volumeMounts:
+            - mountPath: /var/spool/postfix/
+              name: spool
+            - mountPath: /srv/tmp
+              name: cache
+            - mountPath: /srv/virtual
+              name: maps
+              readOnly: true
+            - mountPath: /srv/tls
+              name: tls
+              readOnly: true
+          securityContext: {}
+          serviceAccountName: RELEASE-NAME-mok
+          volumes:
+          - name: maps
+            secret:
+              secretName: RELEASE-NAME-mok-postfix-maps
+          - name: tls
+            secret:
+              secretName: example-tls
+          - emptyDir: {}
+            name: cache
+      volumeClaimTemplates:
+      - metadata:
+          name: spool
+        spec:
+          accessModes:
+          - ReadWriteOnce
+          resources:
+            requests:
+              storage: 1Gi
diff --git a/charts/mok/tests/__snapshot__/relay_test.yaml.snap b/charts/mok/tests/__snapshot__/relay_test.yaml.snap
new file mode 100644
index 000000000..66c75fa96
--- /dev/null
+++ b/charts/mok/tests/__snapshot__/relay_test.yaml.snap
@@ -0,0 +1,38 @@
+keeps stays the same:
+  1: |
+    apiVersion: v1
+    kind: Secret
+    metadata:
+      labels:
+        app.kubernetes.io/component: dovecot
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-dovecot-users
+    stringData:
+      passwd: ""
+    type: Opaque
+  2: |
+    apiVersion: v1
+    kind: Secret
+    metadata:
+      labels:
+        app.kubernetes.io/component: postfix
+        app.kubernetes.io/instance: RELEASE-NAME
+        app.kubernetes.io/managed-by: Helm
+        app.kubernetes.io/name: mok
+        helm.sh/chart: mok-0.1.0
+      name: RELEASE-NAME-mok-postfix-maps
+    stringData:
+      aliases: ""
+      domains: ""
+      mailboxes: ""
+      relayhosts: |
+        @example.com [mail.example.org]:587
+      sasl_passwd: |
+        [mail.example.org]:587 somesecretpassword
+      sender-login-maps: ""
+      tls-policies: |
+        example.edu             none
+    type: Opaque
diff --git a/charts/mok/tests/domains_test.yaml b/charts/mok/tests/domains_test.yaml
new file mode 100644
index 000000000..43cff2e70
--- /dev/null
+++ b/charts/mok/tests/domains_test.yaml
@@ -0,0 +1,57 @@
+suite: Domain configuration
+templates:
+  - secret.yaml
+tests:
+  - it: can configure domains and users
+    set:
+      domains:
+        "example.com":
+          users:
+            - name: john
+              passwordHash: "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg"
+              aliases:
+                - "steve@example.net"
+                - "@example.info"
+    asserts:
+      - equal:
+          path: stringData.aliases
+          value: |
+            steve@example.net john@example.com
+            @example.info john@example.com
+        documentIndex: 1
+      - equal:
+          path: stringData.domains
+          value: |
+            example.com OK
+            example.net OK
+            example.info OK
+        documentIndex: 1
+      - equal:
+          path: stringData.mailboxes
+          value: |
+            john@example.com OK
+        documentIndex: 1
+      - equal:
+          path: stringData.sender-login-maps
+          value: |
+            john@example.com john@example.com
+            steve@example.net john@example.com
+            @example.info john@example.com
+        documentIndex: 1
+      - equal:
+          path: stringData.passwd
+          value: |
+            john@example.com:{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg
+        documentIndex: 0
+  - it: keeps stays the same
+    set:
+      domains:
+        "example.com":
+          users:
+            - name: john
+              passwordHash: "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg"
+              aliases:
+                - "steve@example.net"
+                - "@example.info"
+    asserts:
+      - matchSnapshot: {}
diff --git a/charts/mok/tests/dovecot_test.yaml b/charts/mok/tests/dovecot_test.yaml
new file mode 100644
index 000000000..bcd2d8452
--- /dev/null
+++ b/charts/mok/tests/dovecot_test.yaml
@@ -0,0 +1,85 @@
+suite: Dovecot
+templates:
+  - dovecot.yaml
+  - persistentvolumeclaim.yaml
+tests:
+  - it: should match snapshot
+    set:
+      dovecot:
+        tls:
+          secretName: example-tls
+    asserts:
+      - matchSnapshot: {}
+  - it: has a public service
+    set:
+      dovecot:
+        tls:
+          secretName: example-tls
+    release:
+      name: "test-suite"
+    asserts:
+      - isKind:
+          of: Service
+        documentIndex: 0
+        template: dovecot.yaml
+      - equal:
+          path: metadata.name
+          value: test-suite-mok-dovecot
+        documentIndex: 0
+        template: dovecot.yaml
+  - it: has lmtp port
+    set:
+      dovecot:
+        tls:
+          secretName: example-tls
+    asserts:
+      - contains:
+          path: spec.ports
+          content:
+            port: 24
+            name: lmtp
+        documentIndex: 1
+        template: dovecot.yaml
+      - contains:
+          path: spec.template.spec.containers[0].ports
+          content:
+            containerPort: 24
+            name: lmtp
+        documentIndex: 2
+        template: dovecot.yaml
+  - it: has pop3 & pop3s port
+    set:
+      dovecot:
+        tls:
+          secretName: example-tls
+    asserts:
+      - contains:
+          path: spec.ports
+          content:
+            port: 110
+            name: pop3
+            protocol: TCP
+        documentIndex: 0
+        template: dovecot.yaml
+      - contains:
+          path: spec.ports
+          content:
+            port: 995
+            name: pop3s
+            protocol: TCP
+        documentIndex: 0
+        template: dovecot.yaml
+      - contains:
+          path: spec.template.spec.containers[0].ports
+          content:
+            containerPort: 110
+            name: pop3
+        documentIndex: 2
+        template: dovecot.yaml
+      - contains:
+          path: spec.template.spec.containers[0].ports
+          content:
+            containerPort: 995
+            name: pop3s
+        documentIndex: 2
+        template: dovecot.yaml
diff --git a/charts/mok/tests/helmlabels_test.yaml b/charts/mok/tests/helmlabels_test.yaml
new file mode 100644
index 000000000..d2322d978
--- /dev/null
+++ b/charts/mok/tests/helmlabels_test.yaml
@@ -0,0 +1,30 @@
+suite: Kubernetes recommendations
+templates:
+  - dovecot.yaml
+  - persistentvolumeclaim.yaml
+  - postfix.yaml
+  - secret.yaml
+  - serviceaccount.yaml
+tests:
+  - it: should have the kubernetes recommended labels
+    set:
+      dovecot:
+        tls:
+          secretName: example-tls
+      postfix:
+        tls:
+          secretName: example-tls
+    release:
+      name: "test-suite"
+    chart:
+      version: 1.2.3
+    asserts:
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/instance]
+          value: "test-suite"
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/managed-by]
+          value: "Helm"
+      - equal:
+          path: metadata.labels.[app.kubernetes.io/name]
+          value: "mok"
diff --git a/charts/mok/tests/postfix_test.yaml b/charts/mok/tests/postfix_test.yaml
new file mode 100644
index 000000000..3e38ee45c
--- /dev/null
+++ b/charts/mok/tests/postfix_test.yaml
@@ -0,0 +1,45 @@
+suite: Postfix
+templates:
+  - postfix.yaml
+tests:
+  - it: should match snapshot
+    set:
+      postfix:
+        tls:
+          secretName: example-tls
+    asserts:
+      - matchSnapshot: {}
+  - it: has a public service
+    set:
+      postfix:
+        tls:
+          secretName: example-tls
+    release:
+      name: "test-suite"
+    asserts:
+      - isKind:
+          of: Service
+        documentIndex: 0
+      - equal:
+          path: metadata.name
+          value: test-suite-mok-postfix
+        documentIndex: 0
+  - it: has smtp port
+    set:
+      postfix:
+        tls:
+          secretName: example-tls
+    asserts:
+      - contains:
+          path: spec.ports
+          content:
+            port: 25
+            name: smtp
+            protocol: TCP
+        documentIndex: 0
+      - contains:
+          path: spec.template.spec.containers[0].ports
+          content:
+            containerPort: 25
+            name: smtp
+        documentIndex: 1
diff --git a/charts/mok/tests/relay_test.yaml b/charts/mok/tests/relay_test.yaml
new file mode 100644
index 000000000..c46334066
--- /dev/null
+++ b/charts/mok/tests/relay_test.yaml
@@ -0,0 +1,48 @@
+suite: Relay configuration
+templates:
+  - secret.yaml
+tests:
+  - it: can configure relay hosts
+    set:
+      relay:
+        relayHosts:
+          "@example.com": "[mail.example.org]:587"
+    asserts:
+      - equal:
+          path: stringData.relayhosts
+          value: |
+            @example.com [mail.example.org]:587
+        documentIndex: 1
+  - it: can configure sasl
+    set:
+      relay:
+        saslPasswords:
+          "[mail.example.org]:587": somesecretpassword
+    asserts:
+      - equal:
+          path: stringData.sasl_passwd
+          value: |
+            [mail.example.org]:587 somesecretpassword
+        documentIndex: 1
+  - it: can configure tls policies
+    set:
+      relay:
+        tlsPolicies: |
+          example.edu             none
+    asserts:
+      - equal:
+          path: stringData.tls-policies
+          value: |
+            example.edu             none
+        documentIndex: 1
+  - it: keeps stays the same
+    set:
+      relay:
+        relayHosts:
+          "@example.com": "[mail.example.org]:587"
+        saslPasswords:
+          "[mail.example.org]:587": somesecretpassword
+        tlsPolicies: |
+          example.edu             none
+    asserts:
+      - matchSnapshot: {}
diff --git a/charts/mok/values.yaml b/charts/mok/values.yaml
new file mode 100644
index 000000000..5f66282d3
--- /dev/null
+++ b/charts/mok/values.yaml
@@ -0,0 +1,184 @@
+# Default values for mok.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+nameOverride: ""
+fullnameOverride: ""
+
+# -- list of configured domains and users. See values.yaml for details.
+domains: {}
+  # "example.com":
+  #   users:
+  #     - name: john
+  #       # -- pregenerated password hash. Generate it using `podman run --rm -it --entrypoint=doveadm registry.shivering-isles.com/sheogorath/dovecot:latest pw -s ARGON2ID`
+  #       passwordHash: "{ARGON2ID}$argon2id$v=19$m=65536,t=3,p=1$FSJttpvCBJDcjH/mXjRNhA$ND42CDWAA6dOF2RgtzhYuYsS3Sjpab5p3pCWQHrB8Cg"
+  #       aliases:
+  #         - "steve@example.net"
+  #         - "@example.info"
+relay:
+  # -- tls policy in postfix https://www.postfix.org/TLS_README.html#client_tls_policy
+  tlsPolicies: |
+  # example.edu             none
+  # example.mil             may
+  # example.gov             encrypt ciphers=high
+  # example.com             verify match=hostname:dot-nexthop ciphers=high
+  # example.net             secure
+  # .example.net            secure match=.example.net:example.net
+  # [mail.example.org]:587  secure match=nexthop
+  # example.info            may protocols=>=TLSv1 ciphers=medium exclude=3DES
+
+  # -- relay hosts used as part of the deployment
+  relayHosts: {}
+    # "@example.com": "[mail.example.org]:587"
+  # -- passwords for the relay hosts
+  saslPasswords: {}
+    # "[mail.example.org]:587": somesecretpassword
+
+
+postfix:
+  # -- Number of postfix pods.
+  replicaCount: 1
+
+  image:
+    # -- postfix container image
+    repository: quay.io/shivering-isles/postfix
+    pullPolicy: IfNotPresent
+    # -- Overrides the image tag whose default is "latest"
+    tag: "0.1.0"
+
+  imagePullSecrets: []
+
+  podAnnotations: {}
+
+  podSecurityContext: {}
+
+  securityContext:
+    # -- prevent any process in the container to regain capabilities once dropped
+    allowPrivilegeEscalation: false
+    capabilities:
+      add:
+        # -- required to setup chroot with postfix
+        - SYS_CHROOT
+        # -- required to adjust ownership of files using supervisord
+        - CHOWN
+        # -- required to bind privileged ports like 25, 465, 587
+        - CAP_NET_BIND_SERVICE
+        # -- required to change user id as supervisord as well as postfix
+        - SETUID
+        # -- required to change group id as supervisord as well as postfix
+        - SETGID
+        # -- required to set up the chroot directory on startup
+        - FOWNER
+      drop:
+        # -- getting rid of all capabilities since we already have too many
+        - ALL
+    runAsNonRoot: false
+
+  service:
+    public:
+      # -- type of the public endpoint for smtp, submission, and submissions. **Note**: It's configured to share the IP with dovecot in case of metallb
+      type: LoadBalancer
+
+  tls:
+    # -- secret holding the TLS keys for postfix. **Required**
+    secretName: nil
+
+  resources:
+    limits:
+      cpu: 500m
+      memory: 512Mi
+    requests:
+      cpu: 100m
+      memory: 128Mi
+
+  volumes:
+    spool:
+      accessModes:
+        - "ReadWriteOnce"
+      resources:
+        requests:
+          storage: 1Gi
+
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+
+dovecot:
+  # -- Number of Dovecot pods. **Important**: With the current configuration, it's not recommended to scale beyond 1
+  replicaCount: 1
+
+  image:
+    # -- dovecot container image
+    repository: quay.io/shivering-isles/dovecot
+    pullPolicy: IfNotPresent
+    # -- Overrides the image tag whose default is "latest"
+    tag: "0.1.0"
+
+  # -- pull secret to access the afore defined image
+  imagePullSecrets: []
+
+  podAnnotations: {}
+
+  podSecurityContext: {}
+    # fsGroup: 2000
+
+  securityContext:
+    allowPrivilegeEscalation: false
+    capabilities:
+      add:
+        # -- required to setup chroot for dovecot https://wiki.dovecot.org/HowTo/Rootless
+        - SYS_CHROOT
+        # -- required to set up file structure
+        - CHOWN
+        # -- required to bind privileged ports in the container, such as 993, 143, 24, etc.
+        - CAP_NET_BIND_SERVICE
+        # -- required to drop privileges with dovecot process
+        - SETUID
+        # -- required to drop privileges with dovecot process
+        - SETGID
+      drop:
+        # -- required to drop privileges by default
+        - ALL
+    runAsNonRoot: false
+
+  service:
+    public:
+      # -- type of the public endpoint for pop3, imap, and sieve **Note**: It's configured to share the IP with postfix in case of metallb
+      type: LoadBalancer
+    internal:
+      # -- type of the public endpoint for lmtp, metrics, authentication
+      type: ClusterIP
+
+  resources:
+    limits:
+      cpu: 500m
+      memory: 512Mi
+    requests:
+      cpu: 100m
+      memory: 128Mi
+  tls:
+    # -- secret holding the TLS keys for dovecot. **Required**
+    secretName: nil
+
+  volumes:
+    vmail:
+      # -- Volume access mode, using ReadWriteMany in order to prepare setup with dovcecot director
+      accessModes:
+        - ReadWriteMany
+      volumeMode: Filesystem
+      resources:
+        requests:
+          storage: 5Gi
+
+  nodeSelector: {}
+  tolerations: []
+  affinity: {}
+
+serviceAccount:
+  # Specifies whether a service account should be created
+  create: true
+  # Annotations to add to the service account
+  annotations: {}
+  # The name of the service account to use.
+  # If not set and create is true, a name is generated using the fullname template
+  name: ""
diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md
index 633281235..883a8c534 100644
--- a/docs/src/SUMMARY.md
+++ b/docs/src/SUMMARY.md
@@ -28,6 +28,9 @@
   - [Nextcloud]()
   - [Registry]()
 
+# Helm Charts
+- [MoK](charts/mok.md)
+
 ---
 
 # Links
diff --git a/docs/src/charts/mok.md b/docs/src/charts/mok.md
new file mode 120000
index 000000000..6b605c8be
--- /dev/null
+++ b/docs/src/charts/mok.md
@@ -0,0 +1 @@
+../../../charts/mok/README.md
\ No newline at end of file
-- 
GitLab