diff --git a/share/wildduck/haraka.tf b/share/wildduck/haraka.tf new file mode 100644 index 0000000..93232f0 --- /dev/null +++ b/share/wildduck/haraka.tf @@ -0,0 +1,187 @@ +locals { + haraka-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "haraka" + }) +} + +resource "kubectl_manifest" "haraka_deploy" { + yaml_body = <<-EOF + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "${var.instance}-haraka" + namespace: "${var.namespace}" + labels: ${jsonencode(local.haraka-labels)} + spec: + replicas: 1 + selector: + matchLabels: ${jsonencode(local.haraka-labels)} + template: + metadata: + labels: ${jsonencode(local.haraka-labels)} + spec: + securityContext: + fsGroup: 1000 + containers: + - name: wildduck + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + image: "${var.images.haraka.registry}/${var.images.haraka.repository}:${var.images.haraka.tag}" + imagePullPolicy: "${var.images.haraka.pullPolicy}" + ports: + - name: smtp + containerPort: 25 + protocol: TCP + livenessProbe: + tcpSocket: + port: smtp + initialDelaySeconds: 20 + periodSeconds: 30 + readinessProbe: + tcpSocket: + port: smtp + initialDelaySeconds: 20 + periodSeconds: 30 + resources: + {} + volumeMounts: + - name: config + mountPath: /app/config + volumes: + - name: config + configMap: + name: "${var.instance}-haraka" + EOF +} + +resource "kubectl_manifest" "haraka_config" { + yaml_body = <<-EOF + apiVersion: v1 + kind: ConfigMap + metadata: + name: "${var.instance}-haraka" + namespace: "${var.namespace}" + labels: ${jsonencode(local.haraka-labels)} + data: + me: |- + ${var.sub-domain}.${var.domain-name} + host_list: |- + # add hosts in here we want to accept mail for + ${var.sub-domain}.${var.domain-name} + ${var.domain-name} + ${join("\n ",var.additional-domains)} + rspamd.ini: |- + host = ${var.instance}-rspamd.${var.namespace}.svc.cluster.local + port = 11333 + add_headers = always + [dkim] + enabled = true + [header] + bar = X-Rspamd-Bar + report = X-Rspamd-Report + score = X-Rspamd-Score + spam = X-Rspamd-Spam + [check] + authenticated=true + private_ip=true + [reject] + spam = false + [soft_reject] + enabled = true + [rmilter_headers] + enabled = true + [spambar] + positive = + + negative = - + neutral = /' + wildduck.yaml: |- + ## Connect to a master instance or Redis + redis: + port: 6379 + host: "${var.instance}-${var.component}-redis.${var.namespace}.svc" + db: 3 + mongo: + # connection string for main messages database + url: 'mongodb://${var.component}:${local.mongo-password}@${var.instance}-${var.component}-mongo-svc.${var.namespace}.svc:27017/wildduck' + ## database name or connection string for the users db + #users: "users" + ## database name or connection string for the attachments db + #gridfs: "attachments" + ## database name or connection string for the outbound queue + sender: 'mongodb://${var.component}:${local.mongo-password}@${var.instance}-${var.component}-mongo-svc.${var.namespace}.svc:27017/zone-mta' + sender: + # Push messages to ZoneMTA queue for delivery + # if `false` then no messages are sent + enabled: true + # which ZoneMTA queue to use by default. This mostly affects forwarded messages + zone: 'default' + # Collection name for GridFS storage + gfs: 'mail' + # Collection name for the queue + # see [dbs].sender option for choosing correct database to use for ZoneMTA queues + # by default the main wildduck database is used + collection: 'zone-queue' + # Hashing secret for loop detection + # Must be shared with wildduck + # If not set then looping is not tracked + loopSecret: '${local.secrets.srs}' + srs: + # must be shared with ZoneMTA SRS config, otherwise messages sent from ZoneMTA are not recognized by Haraka + secret: 'secret value' + attachments: + type: 'gridstore' + bucket: 'attachments' + decodeBase64: true + log: + authlogExpireDays: 30 + limits: + windowSize: 3600 # 1 hour + rcptIp: 100 # allowed messages for the same recipient from same IP during window size + rcptWindowSize: 60 # 1 minute + rcpt: 60 # allowed messages for the same recipient during window size + gelf: + enabled: false + component: 'mx' + options: + graylogPort: 12201 + graylogHostname: '127.0.0.1' + connection: 'lan' + rspamd: + # do not process forwarding addresses for messages with the following spam score + forwardSkip: 10 + # if a message has one of the tags listed here with positive score, the message will be rejected + blacklist: + - DMARC_POLICY_REJECT + # if a message has one of the tags listed here with positive score, the message will be soft rejected + softlist: + - RBL_ZONE + # define special responses + responses: + DMARC_POLICY_REJECT: "Unauthenticated email from {host} is not accepted due to domain's DMARC policy" + RBL_ZONE: '[{host}] was found from Zone RBL' + EOF +} + +resource "kubectl_manifest" "haraka_service" { + yaml_body = <<-EOF + apiVersion: v1 + kind: Service + metadata: + name: "${var.instance}-haraka" + namespace: "${var.namespace}" + labels: ${jsonencode(local.haraka-labels)} + spec: + type: LoadBalancer + ports: + - port: 25 + targetPort: smtp + protocol: TCP + name: smtp + selector: ${jsonencode(local.haraka-labels)} + EOF +} diff --git a/share/wildduck/index.yaml b/share/wildduck/index.yaml new file mode 100644 index 0000000..11cb0e2 --- /dev/null +++ b/share/wildduck/index.yaml @@ -0,0 +1,252 @@ +--- +apiVersion: vinyl.solidite.fr/v1beta1 +kind: Component +category: share +metadata: + name: wildduck + description: null +options: + domain-name: + default: your_company.com + examples: + - your_company.com + type: string + domain: + default: your-company + examples: + - your-company + type: string + issuer: + default: letsencrypt-prod + examples: + - letsencrypt-prod + type: string + ingress-class: + default: traefik + examples: + - traefik + type: string + images: + default: + haraka: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-haraka + tag: 3.0.2 + rspamd: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-rspamd + tag: 3.18.3 + webmail: + pullPolicy: IfNotPresent + registry: docker.io + repository: nodemailer/wildduck-webmail + tag: 1.0.1 + wildduck: + pullPolicy: IfNotPresent + registry: docker.io + repository: nodemailer/wildduck + tag: 1.39.10 + zonemta: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-zonemta + tag: 3.4.0 + examples: + - haraka: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-haraka + tag: 3.0.2 + rspamd: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-rspamd + tag: 3.18.3 + webmail: + pullPolicy: IfNotPresent + registry: docker.io + repository: nodemailer/wildduck-webmail + tag: 1.0.1 + wildduck: + pullPolicy: IfNotPresent + registry: docker.io + repository: nodemailer/wildduck + tag: 1.39.10 + zonemta: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-zonemta + tag: 3.4.0 + properties: + haraka: + default: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-haraka + tag: 3.0.2 + properties: + pullPolicy: + default: IfNotPresent + type: string + registry: + default: docker.io + type: string + repository: + default: sebt3/wildduck-haraka + type: string + tag: + default: 3.0.2 + type: string + type: object + operator: + default: null + properties: + pullPolicy: + enum: + - Always + - Never + - IfNotPresent + rspamd: + default: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-rspamd + tag: 3.18.3 + properties: + pullPolicy: + default: IfNotPresent + type: string + registry: + default: docker.io + type: string + repository: + default: sebt3/wildduck-rspamd + type: string + tag: + default: 3.18.3 + type: string + type: object + webmail: + default: + pullPolicy: IfNotPresent + registry: docker.io + repository: nodemailer/wildduck-webmail + tag: 1.0.1 + properties: + pullPolicy: + default: IfNotPresent + type: string + registry: + default: docker.io + type: string + repository: + default: nodemailer/wildduck-webmail + type: string + tag: + default: 1.0.1 + type: string + type: object + wildduck: + default: + pullPolicy: IfNotPresent + registry: docker.io + repository: nodemailer/wildduck + tag: 1.39.10 + properties: + pullPolicy: + default: IfNotPresent + type: string + registry: + default: docker.io + type: string + repository: + default: nodemailer/wildduck + type: string + tag: + default: 1.39.10 + type: string + type: object + zonemta: + default: + pullPolicy: IfNotPresent + registry: docker.io + repository: sebt3/wildduck-zonemta + tag: 3.4.0 + properties: + pullPolicy: + default: IfNotPresent + type: string + registry: + default: docker.io + type: string + repository: + default: sebt3/wildduck-zonemta + type: string + tag: + default: 3.4.0 + type: string + type: object + type: object + redis: + default: + exporter: + enabled: true + image: quay.io/opstree/redis-exporter:v1.44.0 + image: quay.io/opstree/redis:v7.0.5 + storage: 2Gi + examples: + - exporter: + enabled: true + image: quay.io/opstree/redis-exporter:v1.44.0 + image: quay.io/opstree/redis:v7.0.5 + storage: 2Gi + properties: + exporter: + default: + enabled: true + image: quay.io/opstree/redis-exporter:v1.44.0 + properties: + enabled: + default: true + type: boolean + image: + default: quay.io/opstree/redis-exporter:v1.44.0 + type: string + type: object + image: + default: quay.io/opstree/redis:v7.0.5 + type: string + storage: + default: 2Gi + type: string + type: object + additional-domains: + default: [] + items: + type: string + type: array + sub-domain: + default: mail + examples: + - mail + type: string +dependencies: +- dist: null + category: dbo + component: mongo +- dist: null + category: dbo + component: redis +- dist: null + category: core + component: secret-generator +providers: + kubernetes: true + authentik: null + kubectl: true + postgresql: null + restapi: null + http: null +tfaddtype: null diff --git a/share/wildduck/ingress.tf b/share/wildduck/ingress.tf new file mode 100644 index 0000000..c34b146 --- /dev/null +++ b/share/wildduck/ingress.tf @@ -0,0 +1,76 @@ +locals { + dns-names = ["${var.sub-domain}.${var.domain-name}"] + cert-names = concat(local.dns-names, ["${var.domain-name}"]) + middlewares = ["${var.instance}-https"] + service = { + "name" = "${var.instance}-webmail" + "port" = { + "number" = 80 + } + } + rules = [ for v in local.dns-names : { + "host" = "${v}" + "http" = { + "paths" = [{ + "backend" = { + "service" = local.service + } + "path" = "/" + "pathType" = "Prefix" + }] + } + }] +} + +resource "kubectl_manifest" "prj_certificate" { + yaml_body = <<-EOF + apiVersion: "cert-manager.io/v1" + kind: "Certificate" + metadata: + name: "${var.instance}" + namespace: "${var.namespace}" + labels: ${jsonencode(local.webmail-labels)} + spec: + secretName: "${var.instance}-cert" + dnsNames: ${jsonencode(local.cert-names)} + issuerRef: + name: "${var.issuer}" + kind: "ClusterIssuer" + group: "cert-manager.io" + EOF +} + +resource "kubectl_manifest" "prj_https_redirect" { + yaml_body = <<-EOF + apiVersion: "traefik.containo.us/v1alpha1" + kind: "Middleware" + metadata: + name: "${var.instance}-https" + namespace: "${var.namespace}" + labels: ${jsonencode(local.webmail-labels)} + spec: + redirectScheme: + scheme: "https" + permanent: true + EOF +} + +resource "kubectl_manifest" "prj_ingress" { + force_conflicts = true + yaml_body = <<-EOF + apiVersion: "networking.k8s.io/v1" + kind: "Ingress" + metadata: + name: "${var.instance}" + namespace: "${var.namespace}" + labels: ${jsonencode(local.webmail-labels)} + annotations: + "traefik.ingress.kubernetes.io/router.middlewares": "${join(",", [for m in local.middlewares : format("%s-%s@kubernetescrd", var.namespace, m)])}" + spec: + ingressClassName: "${var.ingress-class}" + rules: ${jsonencode(local.rules)} + tls: + - hosts: ${jsonencode(local.dns-names)} + secretName: "${var.instance}-cert" + EOF +} diff --git a/share/wildduck/mongo.tf b/share/wildduck/mongo.tf new file mode 100644 index 0000000..47b279f --- /dev/null +++ b/share/wildduck/mongo.tf @@ -0,0 +1,101 @@ +locals { + mongo-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "mongo" + }) +} +resource "kubectl_manifest" "prj_mongo_secret" { + ignore_fields = ["metadata.annotations"] + yaml_body = <<-EOF + apiVersion: "secretgenerator.mittwald.de/v1alpha1" + kind: "StringSecret" + metadata: + name: "${var.instance}-${var.component}-mongo" + namespace: "${var.namespace}" + labels: ${jsonencode(local.mongo-labels)} + spec: + forceRegenerate: false + fields: + - fieldName: "password" + length: "16" + EOF +} +data "kubernetes_secret_v1" "prj_mongo_secret" { + depends_on = [ kubectl_manifest.prj_mongo_secret ] + metadata { + name = "${var.instance}-${var.component}-mongo" + namespace = var.namespace + } +} +locals { + mongo-password = data.kubernetes_secret_v1.prj_mongo_secret.data["password"] +} +resource "kubectl_manifest" "prj_mongo" { + yaml_body = <<-EOF + apiVersion: mongodbcommunity.mongodb.com/v1 + kind: MongoDBCommunity + metadata: + name: "${var.instance}-${var.component}-mongo" + namespace: "${var.namespace}" + labels: ${jsonencode(local.mongo-labels)} + spec: + members: 1 + type: ReplicaSet + version: "4.4.0" + security: + authentication: + modes: ["SCRAM"] + users: + - db: ${var.component} + name: ${var.component} + passwordSecretRef: + name: "${var.instance}-${var.component}-mongo" + roles: + - db: ${var.component} + name: readWrite + scramCredentialsSecretName: "${var.instance}-${var.component}-mongo-scram" + EOF +} +resource "kubectl_manifest" "prj_mongo_sa" { + yaml_body = <<-EOF + apiVersion: v1 + kind: ServiceAccount + metadata: + name: "mongodb-database" + namespace: "${var.namespace}" + labels: ${jsonencode(local.mongo-labels)} + EOF +} +resource "kubectl_manifest" "prj_mongo_role" { + yaml_body = <<-EOF + apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + name: "mongodb-database" + namespace: "${var.namespace}" + labels: ${jsonencode(local.mongo-labels)} + rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get"] + - apiGroups: [""] + resources: ["pods"] + verbs: ["patch", "delete", "get"] + EOF +} +resource "kubectl_manifest" "prj_mongo_rb" { + yaml_body = <<-EOF + apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + name: "mongodb-database" + namespace: "${var.namespace}" + labels: ${jsonencode(local.mongo-labels)} + subjects: + - kind: ServiceAccount + name: mongodb-database + roleRef: + kind: Role + name: mongodb-database + apiGroup: rbac.authorization.k8s.io + EOF +} diff --git a/share/wildduck/redis.tf b/share/wildduck/redis.tf new file mode 100644 index 0000000..0ac78d6 --- /dev/null +++ b/share/wildduck/redis.tf @@ -0,0 +1,32 @@ +locals { + redis-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "redis" + }) +} +resource "kubectl_manifest" "prj_redis" { + yaml_body = <<-EOF + apiVersion: "redis.redis.opstreelabs.in/v1beta1" + kind: "Redis" + metadata: + name: "${var.instance}-${var.component}-redis" + namespace: "${var.namespace}" + labels: ${jsonencode(local.redis-labels)} + spec: + kubernetesConfig: + image: "${var.redis.image}" + imagePullPolicy: "IfNotPresent" + storage: + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: "${var.redis.storage}" + redisExporter: + enabled: ${var.redis.exporter.enabled} + image: "${var.redis.exporter.image}" + securityContext: + runAsUser: 1000 + fsGroup: 1000 + EOF +} diff --git a/share/wildduck/rspamd.tf b/share/wildduck/rspamd.tf new file mode 100644 index 0000000..03486ff --- /dev/null +++ b/share/wildduck/rspamd.tf @@ -0,0 +1,106 @@ +locals { + rspamd-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "rspamd" + }) +} + +resource "kubectl_manifest" "rspamd_deploy" { + yaml_body = <<-EOF + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "${var.instance}-rspamd" + namespace: "${var.namespace}" + labels: ${jsonencode(local.rspamd-labels)} + spec: + replicas: 1 + selector: + matchLabels: ${jsonencode(local.rspamd-labels)} + template: + metadata: + labels: ${jsonencode(local.rspamd-labels)} + spec: + securityContext: + fsGroup: 101 + containers: + - name: wildduck + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 100 + image: "${var.images.rspamd.registry}/${var.images.rspamd.repository}:${var.images.rspamd.tag}" + imagePullPolicy: "${var.images.rspamd.pullPolicy}" + ports: + - name: rspamd + containerPort: 11333 + protocol: TCP + resources: + {} + volumeMounts: + - name: config + mountPath: /app/rspamd/worker-normal.conf + subPath: worker-normal.conf + - name: config + mountPath: /etc/rspamd/override.d/dmarc.conf + subPath: dmarc.conf + - name: config + mountPath: /etc/rspamd/override.d/redis.conf + subPath: redis.conf + volumes: + - name: config + configMap: + name: "${var.instance}-rspamd" + EOF +} + +resource "kubectl_manifest" "rspamd_config" { + yaml_body = <<-EOF + apiVersion: v1 + kind: ConfigMap + metadata: + name: "${var.instance}-rspamd" + namespace: "${var.namespace}" + labels: ${jsonencode(local.rspamd-labels)} + data: + worker-normal.conf: |- + # Included from top-level .conf file + + worker "normal" { + bind_socket = "*:11333"; + .include "$CONFDIR/worker-normal.inc" + .include(try=true; priority=1,duplicate=merge) "$LOCAL_CONFDIR/local.d/worker-normal.inc" + .include(try=true; priority=10) "$LOCAL_CONFDIR/override.d/worker-normal.inc" + } + dmarc.conf: |- + actions = { + quarantine = "add_header"; + reject = "reject"; + } + redis.conf: |- + servers = "${var.instance}-${var.component}-redis.${var.namespace}.svc:6379"; + db = "4"; + EOF +} + +resource "kubectl_manifest" "rspamd_service" { + yaml_body = <<-EOF + apiVersion: v1 + kind: Service + metadata: + name: "${var.instance}-rspamd" + namespace: "${var.namespace}" + labels: ${jsonencode(local.rspamd-labels)} + spec: + type: ClusterIP + ports: + - port: 11333 + targetPort: rspamd + protocol: TCP + name: rspamd + selector: ${jsonencode(local.rspamd-labels)} + EOF +} + diff --git a/share/wildduck/secret.tf b/share/wildduck/secret.tf new file mode 100644 index 0000000..a48fd19 --- /dev/null +++ b/share/wildduck/secret.tf @@ -0,0 +1,40 @@ +resource "kubectl_manifest" "wildduck_secret" { + ignore_fields = ["metadata.annotations"] + yaml_body = <<-EOF + apiVersion: "secretgenerator.mittwald.de/v1alpha1" + kind: "StringSecret" + metadata: + name: "${var.instance}" + namespace: "${var.namespace}" + labels: ${jsonencode(local.common-labels)} + spec: + forceRegenerate: false + fields: + - fieldName: "srs" + length: "32" + - fieldName: "zonemta" + length: "32" + - fieldName: "webmail" + length: "32" + - fieldName: "totp" + length: "32" + - fieldName: "dkim" + length: "32" + EOF +} +data "kubernetes_secret_v1" "wildduck" { + depends_on = [ kubectl_manifest.wildduck_secret ] + metadata { + name = var.instance + namespace = var.namespace + } +} +locals { + secrets = { + srs = data.kubernetes_secret_v1.wildduck.data["srs"] + zonemta = data.kubernetes_secret_v1.wildduck.data["zonemta"] + webmail = data.kubernetes_secret_v1.wildduck.data["webmail"] + totp = data.kubernetes_secret_v1.wildduck.data["totp"] + dkim = data.kubernetes_secret_v1.wildduck.data["dkim"] + } +} \ No newline at end of file diff --git a/share/wildduck/webmail.tf b/share/wildduck/webmail.tf new file mode 100644 index 0000000..5a6ca2c --- /dev/null +++ b/share/wildduck/webmail.tf @@ -0,0 +1,177 @@ +locals { + webmail-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "webmail" + }) +} + +resource "kubectl_manifest" "webmail_deploy" { + yaml_body = <<-EOF + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "${var.instance}-webmail" + namespace: "${var.namespace}" + labels: ${jsonencode(local.webmail-labels)} + spec: + replicas: 1 + selector: + matchLabels: ${jsonencode(local.webmail-labels)} + template: + metadata: + labels: ${jsonencode(local.webmail-labels)} + spec: + securityContext: + fsGroup: 1000 + containers: + - name: webmail + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + image: "${var.images.webmail.registry}/${var.images.webmail.repository}:${var.images.webmail.tag}" + imagePullPolicy: "${var.images.webmail.pullPolicy}" + args: + - "--config=./config/webmail.toml" + ports: + - name: http + containerPort: 80 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + scheme: HTTP + readinessProbe: + httpGet: + path: / + port: http + scheme: HTTP + resources: + {} + volumeMounts: + - name: config + mountPath: /app/config/webmail.toml + subPath: webmail.toml + volumes: + - name: config + configMap: + name: "${var.instance}-webmail" + EOF +} + +resource "kubectl_manifest" "webmail_config" { + yaml_body = <<-EOF + apiVersion: v1 + kind: ConfigMap + metadata: + name: "${var.instance}-webmail" + namespace: "${var.namespace}" + labels: ${jsonencode(local.webmail-labels)} + data: + webmail.toml: |- + name="Wild Duck Mail" + + title="wildduck-www" + + [service] + # email domain for new users + domain="${var.domain-name}" + # default quotas for new users + quota=1024 + recipients=2000 + forwards=2000 + identities=10 + allowIdentityEdit=true + allowJoin=true + enableSpecial=false # if true the allow creating addresses with special usernames + # allowed domains for new addresses + domains=["${var.domain-name}"] + + generalNotification="" # static notification to show on top of the page + + [service.sso.http] + enabled = false + header = "X-UserName" # value from this header is treated as logged in username + authRedirect = "http:/127.0.0.1:3000/login" # URL to redirect non-authenticated users + logoutRedirect = "http:/127.0.0.1:3000/logout" # URL to redirect when user clicks on "log out" + + [api] + url="http://wildduck.vynil-mail.svc.cluster.local:80" + accessToken="wildduck1234" + + [dbs] + # mongodb connection string for the main database + mongo="mongodb://${var.component}:${local.mongo-password}@${var.instance}-${var.component}-mongo-svc.${var.namespace}.svc:27017/wildduck-webmail" + + # redis connection string for Express sessions + redis="redis://${var.instance}-${var.component}-redis.${var.namespace}.svc:6379/5" + + [www] + host=false + port=80 + proxy=false + postsize="5MB" + log="dev" + secret="${local.secrets.webmail}" + secure=false + listSize=20 + + [recaptcha] + enabled=false + siteKey="" + secretKey="" + + [totp] + # Issuer name for TOTP, defaults to config.name + issuer=false + # once setup do not change as it would invalidate all existing 2fa sessions + secret="${local.secrets.totp}" + + [u2f] + # set to false if not using HTTPS + enabled=true + # must be https url or use default + appId="https://${var.domain-name}" + + [log] + level="silly" + mail=true + + [setup] + # these values are shown in the configuration help page + [setup.imap] + hostname="${var.sub-domain}.${var.domain-name}" + secure=true + port=143 + [setup.pop3] + hostname="${var.sub-domain}.${var.domain-name}" + secure=true + port=110 + [setup.smtp] + hostname="${var.sub-domain}.${var.domain-name}" + secure=true + port=25 + EOF +} + +resource "kubectl_manifest" "webmail_service" { + yaml_body = <<-EOF + apiVersion: v1 + kind: Service + metadata: + name: "${var.instance}-webmail" + namespace: "${var.namespace}" + labels: ${jsonencode(local.webmail-labels)} + spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: ${jsonencode(local.webmail-labels)} + EOF +} diff --git a/share/wildduck/wildduck.tf b/share/wildduck/wildduck.tf new file mode 100644 index 0000000..33cfcbe --- /dev/null +++ b/share/wildduck/wildduck.tf @@ -0,0 +1,402 @@ +locals { + wildduck-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "wildduck" + }) +} + +resource "kubectl_manifest" "wildduck_deploy" { + yaml_body = <<-EOF + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "${var.instance}-wildduck" + namespace: "${var.namespace}" + labels: ${jsonencode(local.wildduck-labels)} + spec: + replicas: 1 + selector: + matchLabels: ${jsonencode(local.wildduck-labels)} + template: + metadata: + labels: ${jsonencode(local.wildduck-labels)} + spec: + securityContext: + fsGroup: 1000 + containers: + - name: wildduck + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + image: "${var.images.wildduck.registry}/${var.images.wildduck.repository}:${var.images.wildduck.tag}" + imagePullPolicy: "${var.images.wildduck.pullPolicy}" + ports: + - name: http + containerPort: 8000 + protocol: TCP + - name: imap + containerPort: 1430 + protocol: TCP + - name: pop3 + containerPort: 1100 + protocol: TCP + livenessProbe: + httpGet: + path: /users + port: http + scheme: HTTP + httpHeaders: + - name: X-Access-Token + value: wildduck1234 + readinessProbe: + httpGet: + path: /users + port: http + scheme: HTTP + httpHeaders: + - name: X-Access-Token + value: wildduck1234 + resources: + {} + volumeMounts: + - name: wildduck-config-volume + mountPath: /wildduck/config + volumes: + - name: config + configMap: + name: "${var.instance}-wildduck" + EOF +} + +resource "kubectl_manifest" "wildduck_config" { + yaml_body = <<-EOF + apiVersion: v1 + kind: ConfigMap + metadata: + name: "${var.instance}-wildduck" + namespace: "${var.namespace}" + labels: ${jsonencode(local.wildduck-labels)} + data: + default.toml: |- + # Uncomment if you start the app as root and want to downgrade + # once all privileged actions are completed + # If you do not use privileged ports then you can start the app already under required user account + #user="wildduck" + #group="wildduck" + # process title + ident="wildduck" + # how many processes to start + processes=1 + # default quota storage in MB (can be overriden per user) + maxStorage=1024 + # default smtp recipients for 24h (can be overriden per user) + maxRecipients=2000 + # default forwarded messages for 24h (can be overriden per user) + maxForwards=2000 + # If usernames are not email addresses then use this domain as hostname part + #emailDomain="mydomain.info" + [dbs] + # @include "dbs.toml" + [totp] + # If enabled then encrypt TOTP seed tokens with the secret password. By default TOTP seeds + # are not encrypted and stored as cleartext. Once set up do not change these values, + # otherwise decrypting totp seeds is going to fail + cipher="aes192" + secret="${local.secrets.totp}" + [u2f] + # Fully qualified URL of your website (must use HTTPS!) + appId="https://localhost:3000" + [attachments] + # @include "attachments.toml" + [log] + level="debug" + skipFetchLog=false # if true, then does not output individual * FETCH responses to log + # delete authentication log entries after 30 days + # changing this value only affects new entries + # set to false to not log authentication events + # set to 0 to keep the logs infinitely + authlogExpireDays=30 + [log.gelf] + enabled=false + hostname=false # defaults to os.hostname() + component="wildduck" + [log.gelf.options] + graylogPort=12201 + graylogHostname="127.0.0.1" + connection="lan" + [imap] + # @include "imap.toml" + [tls] + # @include "tls.toml" + [lmtp] + # @include "lmtp.toml" + [pop3] + # @include "pop3.toml" + [api] + # @include "api.toml" + [sender] + # @include "sender.toml" + [dkim] + # @include "dkim.toml" + [acme] + # @include "acme.toml" + [plugins] + # @include "plugins/*.toml" + [tasks] + # if enabled then process jobs like deleting expired messages etc + enabled=true + [smtp.setup] + # Public configuration for SMTP MDA, needed for mobileconfig files + hostname="${var.sub-domain}.${var.domain-name}" + secure=true + port=465 + [webhooks] + # At least one server must have webhook processing enabled, + # otherwise events would pile up in the Redis queue. + enabled = true + api.toml: |- + enabled=true + port=8000 + # by default bind to localhost only + host="0.0.0.0" + # Use `true` (HTTPS) for port 443 and `false` (HTTP) for 80 + secure=false + # If set requires all API calls to have accessToken query argument with that value + # http://localhost:8080/users?accessToken=somesecretvalue + accessToken="wildduck1234" + [accessControl] + # If true then require a valid access token to perform API calls + # If a client provides a token then it is validated even if using a token is not required + enabled=false + # Secret for HMAC + # Changing this value invalidates all tokens + secret="a secret cat" + # Generated access token TTL in seconds. Token TTL gets extended by this value every time the token is used. Defaults to 14 days + #tokenTTL=1209600 + # Generated access token max lifetime in seconds. Defaults to 180 days + #tokenLifetime=15552000 + [roles] + # @include "roles.json" + [tls] + # If certificate path is not defined, use global or built-in self-signed certs + #key="/path/to/server/key.pem" + #cert="/path/to/server/cert.pem" + [mobileconfig] + # plist configuration for OSX/iOS profile files that are generated with Application Specific Passwords + # Use {email} in the description strings to replace it with account email address + # A reverse-DNS style identifier (com.example.myprofile, for example) that identifies the profile. + # This string is used to determine whether a new profile should replace an existing one or should be added. Username is prepended to this value. + identifier="com.email.wildduck" + # A human-readable name for the profile. This value is displayed on the Detail screen. It does not have to be unique. + displayName="WildDuck Mail" + # A human-readable string containing the name of the organization that provided the profile. + organization="WildDuck Mail Services" + # A description of the profile, shown on the Detail screen for the profile. This should be descriptive enough to help the user decide whether to install the profile. + displayDescription="Install this profile to setup {email}" + # A user-visible description of the email account, shown in the Mail and Settings applications. + accountDescription="WildDuck ({email})" + [mobileconfig.tls] + # If certificate path is not defined, use global or built-in self-signed certs + #key="/path/to/server/key.pem" + #cert="/path/to/server/cert.pem" + [cors] + origins = ["*"] + dbs.toml: |- + # mongodb connection string for the main database + mongo="mongodb://${var.component}:${local.mongo-password}@${var.instance}-${var.component}-mongo-svc.${var.namespace}.svc:27017/wildduck" + # redis connection string to connect to a single master (see below for Sentinel example) + redis="redis://${var.instance}-${var.component}-redis.${var.namespace}.svc:6379/3" + # WildDuck allows using different kind of data in different databases + # If you do not provide a database config value, then main database connection + # is used for everything + # You can either use a database name (uses shared connection) or a configutaion + # url (creates a separate connection) for each databases + + # Optional database name or connection url for GridFS if you do not want to + # use the main db for storing attachments. Useful if you want + # to use a different mount folder or storage engine + #gridfs="wildduck" + + # Optional database name or connection url for users collection if you do not want to + # use the main db for storing user/address data. Useful if you want + # to use a different mount folder or storage engine + #users="wildduck" + + # Optional database name or connection url for ZoneMTA queue database. This is + # used to push outbound emails to the sending queue + sender="zone-mta" + + #queued="mail" + dkim.toml: |- + # If enabled then encrypt DKIM keys with the secret password. By default DKIM keys + # are not encrypted and stored as cleartext. Once set up do not change these values, + # otherwise decrypting DKIM keys is going to fail + cipher="aes192" + secret="${local.secrets.dkim}" + # If true then spwans openssl command line executable for generating DKIM keys + # Otherwise forge library is used which is cross-environment but slower + useOpenSSL=true + # Define path to openssl if not in default path + #pathOpenSSL="/usr/local/bin/openssl" + # If true then also adds a signature for the outbound domain + # Affects WildDuck ZoneMTA plugin only + signTransportDomain=false + # do not change this + hashAlgo="sha256" + imap.toml: |- + # If enabled then WildDuck exposes an IMAP interface for listing and fetching emails + enabled=true + port=1430 + host="0.0.0.0" + # Use `true` for port 993 and `false` for 143. If connection is not secured + # on connection then WildDuck enables STARTTLS extension + secure=false + # Max size for messages uploaded via APPEND + maxMB=25 + # delete messages from \Trash and \Junk after retention days + retention=30 + # Default max donwload bandwith per day in megabytes + maxDownloadMB=10000 + # Default max upload bandwith per day in megabytes + maxUploadMB=10000 + # Default max concurrent connections per service per client + maxConnections=15 + # if `true` then do not autodelete expired messages + disableRetention=false + # If true, then disables STARTTLS support + disableSTARTTLS=true + # If true, then advertises COMPRESS=DEFLATE capability + enableCompression=false + # If true, then expect HAProxy PROXY header as the first line of data + useProxy=false + # useProxy=true # expect PROXY from all conections + # useProxy=['*'] # expect PROXY from all conections + # useProxy=['1.2.3.4', '1.2.3.5'] # expect PROXY only from connections from listed IP addresses + # an array of IP addresses to ignore (not logged) + ignoredHosts=[] + #name="WildDuck IMAP" + #version="1.0.0" + #vendor="WildDuck" + # Add extra IMAP interfaces + #[[interface]] + #enabled=true + #port=9143 + #host="0.0.0.0" + #secure=false + #ignoreSTARTTLS=true + # If true then EXPUNGE is called after a message gets a \Deleted flag set + autoExpunge=true + [setup] + # Public configuration for IMAP + hostname="${var.sub-domain}.${var.domain-name}" + secure=true + # port defaults to imap.port + port=9930 + [tls] + ## If certificate path is not defined, use global or built-in self-signed certs + #key="/path/to/server/key.pem" + #cert="/path/to/server/cert.pem" + ## You can also define extra options for specific TLS settings: + #ciphers="ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS" + #secureProtocol="SSLv23_server_method" + ## constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_TLSv1 => 100663296 + #secureOptions=100663296 + #[[interface]] + #enabled=true + #port=9143 + #host="0.0.0.0" + #secure=false + #ignoreSTARTTLS=false + pop3.toml: |- + # If enabled then WildDuck exposes a limited POP3 interface for listing and fetching emails + enabled=true + port=1100 + # by default bind to localhost only + host="0.0.0.0" + # If true, then disables STARTTLS support + disableSTARTTLS=true + # Use `true` for port 995 and `false` for 110 + secure=false + # If true, then do not show server info in CAPA response + disableVersionString=false + # How many latest messages to list for LIST and UIDL + # POP3 server never lists all messages but only a limited length list + maxMessages=250 + # Max donwload bandwith per day in megabytes + maxDownloadMB=10000 + # If true, then expect HAProxy PROXY header as the first line of data + useProxy=false + # an array of IP addresses to ignore (not logged) + ignoredHosts=[] + #name="WildDuck POP3" + #version="1.0.0" + [tls] + # If certificate path is not defined, use global or built-in self-signed certs + #key="/path/to/server/key.pem" + #cert="/path/to/server/cert.pem" + [setup] + # Public configuration for POP3 + hostname="${var.sub-domain}.${var.domain-name}" + secure=true + # port defaults to pop3.port + port=995 + sender.toml: |- + # which ZoneMTA queue to use by default + zone="default" + # Collection name for GridFS storage + gfs="mail" + # Collection name for the queue + # see [dbs].sender option for choosing correct database to use for ZoneMTA queues + # by default the main wildduck database is used + collection="zone-queue" + # Hashing secret for loop detection + # Must be shared with haraka-plugin-wildduck + # If not set then looping is not tracked + loopSecret="${local.secrets.srs}" + EOF +} + +resource "kubectl_manifest" "wildduck_service_api" { + yaml_body = <<-EOF + apiVersion: v1 + kind: Service + metadata: + name: "${var.instance}-wildduck-api" + namespace: "${var.namespace}" + labels: ${jsonencode(local.wildduck-labels)} + spec: + type: ClusterIP + ports: + - port: 80 + targetPort: http + protocol: TCP + name: http + selector: ${jsonencode(local.wildduck-labels)} + EOF +} + +resource "kubectl_manifest" "wildduck_service" { + yaml_body = <<-EOF + apiVersion: v1 + kind: Service + metadata: + name: "${var.instance}-wildduck-mail" + namespace: "${var.namespace}" + labels: ${jsonencode(local.wildduck-labels)} + spec: + type: LoadBalancer + ports: + - port: 143 + targetPort: imap + protocol: TCP + name: imap + - port: 110 + targetPort: pop3 + protocol: TCP + name: pop3 + selector: ${jsonencode(local.wildduck-labels)} + EOF +} diff --git a/share/wildduck/zonemta.tf b/share/wildduck/zonemta.tf new file mode 100644 index 0000000..b199017 --- /dev/null +++ b/share/wildduck/zonemta.tf @@ -0,0 +1,197 @@ +locals { + zonemta-labels = merge(local.common-labels, { + "app.kubernetes.io/component" = "zonemta" + }) +} + +resource "kubectl_manifest" "zonemta_deploy" { + yaml_body = <<-EOF + apiVersion: apps/v1 + kind: Deployment + metadata: + name: "${var.instance}-zonemta" + namespace: "${var.namespace}" + labels: ${jsonencode(local.zonemta-labels)} + spec: + replicas: 1 + selector: + matchLabels: ${jsonencode(local.zonemta-labels)} + template: + metadata: + labels: ${jsonencode(local.zonemta-labels)} + spec: + securityContext: + fsGroup: 1000 + containers: + - name: wildduck + securityContext: + capabilities: + drop: + - ALL + readOnlyRootFilesystem: true + runAsNonRoot: true + runAsUser: 1000 + image: "${var.images.zonemta.registry}/${var.images.zonemta.repository}:${var.images.zonemta.tag}" + imagePullPolicy: "${var.images.zonemta.pullPolicy}" + ports: + - name: smtp + containerPort: 5870 + protocol: TCP + livenessProbe: + tcpSocket: + port: smtp + initialDelaySeconds: 20 + periodSeconds: 30 + readinessProbe: + tcpSocket: + port: smtp + initialDelaySeconds: 20 + periodSeconds: 30 + resources: + {} + volumeMounts: + - name: tls + mountPath: "/var/opt/certs" + readOnly: true + - name: config + mountPath: /app/config + - name: wildduck-zonemta-interfaces-config-volume + mountPath: /app/config/interfaces + - name: wildduck-zonemta-plugins-config-volume + mountPath: /app/config/plugins + - name: wildduck-zonemta-zones-config-volume + mountPath: /app/config/zones + volumes: + - name: config + configMap: + name: "${var.instance}-zonemta" + - name: tls + secret: + secretName: "${var.instance}-cert" + EOF +} + +resource "kubectl_manifest" "zonemta_config" { + yaml_body = <<-EOF + apiVersion: v1 + kind: ConfigMap + metadata: + name: "${var.instance}-zonemta" + namespace: "${var.namespace}" + labels: ${jsonencode(local.zonemta-labels)} + data: + feeder.toml: |- + # Default SMTP interface for accepting mail for delivery + [feeder] + enabled=true + # How many worker processes to spawn + processes=1 + # Maximum allowed message size 30MB + maxSize=31457280 + # Local IP and port to bind to + host="0.0.0.0" + port=5870 + # Set to true to require authentication + # If authentication is enabled then you need to use a plugin with an authentication hook + authentication=true + # How many recipients to allow per message + maxRecipients=1000 + # Set to true to enable STARTTLS. Do not forget to change default TLS keys + starttls=true + # set to true to start in TLS mode if using port 465 + # this probably does not work as TLS support with 465 in ZoneMTA is a bit buggy + secure=false + # define keys for STARTTLS/TLS. These paths are relative to CWD + # NB! Keys must be accessible by process user or SMTP authentication will fail. + key="/var/opt/certs/tls.key" + cert="/var/opt/certs/tls.crt" + dbs-production.toml: |- + # Database configuration + # this file is loaded when NODE_ENV=production + # MongoDB connection string + mongo="mongodb://${var.component}:${local.mongo-password}@${var.instance}-${var.component}-mongo-svc.${var.namespace}.svc:27017/wildduck" + # Redis connection string + redis="redis://${var.instance}-${var.component}-redis.${var.namespace}.svc:6379/2" + # Database name for ZoneMTA data in MongoDB. In most cases it should be the same as in the connection string + sender="wildduck" + # Database name for Wild Duck users + # users="wildduck" + # Database name for Wild Duck attachments + # gridfs="wildduck" + pools.toml: |- + # List local IP addresses that can be used for outbound tcp connections + # Server process must be able to locally bind to these addresses + [[default]] + address="0.0.0.0" + name="${var.sub-domain}.${var.domain-name}" + # + #[[default]] + #address="1.2.3.5" + #name="ip-2.hostname" + loop-breaker.toml: |- + ["modules/zonemta-loop-breaker"] + enabled="sender" + secret="${local.secrets.zonemta}" + algo="md5" + wildduck.toml: |- + ["modules/zonemta-wildduck"] + enabled=["receiver", "sender"] + # to which SMTP interfaces this plugin applies to. Use "*" for all interfaces + interfaces=["feeder"] + # optional hostname to be used in headers + # defaults to os.hostname() + hostname="${var.sub-domain}.${var.domain-name}" + # How long to keep auth records in log + authlogExpireDays=30 + # default smtp recipients for 24h (can be overriden per user) + maxRecipients=2000 + disableUploads=false # if true then messages are not uploaded to Sent Mail folder + uploadAll=false # if false then messages from Outlook are not uploaded to Sent Mail folder + # SRS settings for forwarded emails + # --------------------------------- + ["modules/zonemta-wildduck".srs] + # Handle rewriting of forwarded emails. If false then SRS is not used + # Only affect messages that have interface set to "forwarder" + enabled=true + # SRS secret value. Must be the same as in the MX side + secret="${local.secrets.srs}" + # SRS domain, must resolve back to MX + rewriteDomain="${var.domain-name}" + # DKIM Settings + # ------------- + ["modules/zonemta-wildduck".dkim] + # If true then also adds a signature for the outbound domain + signTransportDomain=false + # If set then decrypt encrypted DKIM keys using this password + #secret="a secret cat" + # Cipher to use to decrypt encrypted DKIM keys + #cipher="aes192" + ["modules/zonemta-wildduck".gelf] + enabled=false + component="mta" + ["modules/zonemta-wildduck".gelf.options] + graylogPort=12201 + graylogHostname='127.0.0.1' + connection='lan' + + EOF +} + +resource "kubectl_manifest" "zonemta_service" { + yaml_body = <<-EOF + apiVersion: v1 + kind: Service + metadata: + name: "${var.instance}-zonemta" + namespace: "${var.namespace}" + labels: ${jsonencode(local.zonemta-labels)} + spec: + type: LoadBalancer + ports: + - port: 587 + targetPort: smtp + protocol: TCP + name: smtp + selector: ${jsonencode(local.zonemta-labels)} + EOF +}