Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
d34a4bf
feat: upgrade thunder version to 0.32.0 and update related configurat…
Aravinda-HWK Apr 23, 2026
6ea5fb5
feat: add console runtime configuration and update database path in d…
Aravinda-HWK Apr 23, 2026
84cfe5d
feat: update docker-compose and gen-thunder script for postfix volume…
Aravinda-HWK Apr 23, 2026
c8e3789
feat: enhance certbot and OpenDKIM scripts for improved domain handli…
Aravinda-HWK Apr 23, 2026
a83075f
feat: remove deprecated Thunder user scripts and update configuration…
Aravinda-HWK Apr 24, 2026
a1eebbf
Add script to create a single-page application resource in Thunder
Aravinda-HWK Apr 24, 2026
4150a75
feat: upgrade authentication to use XOAUTH2 and remove password requi…
Aravinda-HWK Apr 24, 2026
bdcbee2
feat: update NewMailer function to use sender as username for XOAUTH2…
Aravinda-HWK Apr 24, 2026
0a06c66
refactor: simplify XOAUTH2 user identifier handling in NewMailer
Aravinda-HWK Apr 24, 2026
21cda1e
feat: add OAuth client email authorization configuration and update R…
Aravinda-HWK Apr 24, 2026
a1148b7
Merge branch 'main' into 26-feature-upgrade-the-thunder-version-to-th…
Aravinda-HWK Apr 24, 2026
6d4be28
refactor: remove unused sample resource creation logic from script
Aravinda-HWK Apr 24, 2026
72266ee
feat: upgrade server to enforce HTTPS and update related configurations
Aravinda-HWK Apr 25, 2026
ef0998b
feat: update email recipient handling to support multiple recipients
Aravinda-HWK Apr 25, 2026
f1fe55a
feat: add initial Helm chart for OpenDKIM with configuration and temp…
Aravinda-HWK May 23, 2026
0027ab8
Merge remote-tracking branch 'origin/main' into helm-chart-opendkim
Aravinda-HWK May 25, 2026
65adc12
feat: add Dockerfile and entrypoint script for OpenDKIM setup; update…
Aravinda-HWK Jun 3, 2026
8bebfa3
feat: add RequireSafeKeys configuration for OpenDKIM to support OpenS…
Aravinda-HWK Jun 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions mail-infra/helm/opendkim-server/.helmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Patterns to ignore when building packages.
.DS_Store
.git/
.gitignore
.idea/
.vscode/
*.tmproj
*.swp
*.bak
*.tgz
21 changes: 21 additions & 0 deletions mail-infra/helm/opendkim-server/Chart.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
apiVersion: v2
name: silver-opendkim
description: |
Helm chart for the OpenDKIM milter (ghcr.io/lsflk/silver-dkim) used by the
Pingmailer / Silver Mail stack. Generates per-domain DKIM signing keys on
first start, persists them in a PVC, and exposes the Milter socket on
:8891 for Postfix to consume.
type: application
version: 0.1.0
appVersion: "main"
keywords:
- opendkim
- dkim
- milter
- email
- pingmailer
maintainers:
- name: LSFLK
url: https://github.com/lsflk
sources:
- https://github.com/LSFLK/silver
123 changes: 123 additions & 0 deletions mail-infra/helm/opendkim-server/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# silver-opendkim Helm chart

Helm chart for the OpenDKIM milter (`ghcr.io/lsflk/silver-dkim`) — the
Kubernetes counterpart of the `opendkim-server` service in this repo's
[docker-compose.yml](../../../docker-compose.yml).

OpenDKIM signs (and verifies) outbound SMTP traffic from Postfix. This chart:

1. Generates `opendkim.conf`, `KeyTable`, `SigningTable`, `TrustedHosts`, and
`silver.yaml` deterministically from your `domains` list and packs them
into a `ConfigMap`.
2. Runs the `silver-dkim` image, which inspects `silver.yaml` on startup and
calls `opendkim-genkey` for any domain whose private key is not yet on
disk at `/etc/dkimkeys/<domain>/<selector>.private`.
3. Persists those generated keys on a `PersistentVolumeClaim` so subsequent
pod restarts reuse the same keys (and therefore the same published DNS
TXT records).
4. Exposes the Milter protocol on `ClusterIP :8891` so Postfix
(`smtpd_milters=inet:opendkim-server:8891`) can reach it.

## DKIM key management — the trade-off you're picking

The two mainstream patterns in Kubernetes are:

| Pattern | How | Pros | Cons |
|---|---|---|---|
| **Secret-mount** | Pre-generate the private keys, `kubectl create secret`, mount it at `/etc/dkimkeys` | etcd encryption-at-rest, RBAC-gated, easy to mirror across clusters | Manual bootstrap + manual rotation |
| **PVC-mount (this chart)** | Container self-bootstraps via `opendkim-genkey` on a PVC | Zero-touch — install and forget | Encryption depends on your `StorageClass`; you own backup |

The `silver-dkim` image is **designed for the PVC pattern** — its entrypoint
runs `opendkim-genkey` for any missing key and writes into `/etc/dkimkeys/`.
If you want the Secret pattern instead, point `persistence.existingClaim` at
a CSI Secret-backed PVC (or kustomize-patch the Deployment to mount a
Secret at `/etc/dkimkeys` and skip the PVC altogether).

## Install

Sensitive value (the domain list) is intentionally absent from `values.yaml`.
Supply it on the command line or via a private values file:

```bash
# my-domains.yaml (do not commit)
domains:
- domain: example.com
selector: mail
keySize: 2048
- domain: another.example.com
selector: mail-2026
```

```bash
helm upgrade --install opendkim-server ./mail-infra/helm/opendkim-server \
--namespace pingmailer --create-namespace \
-f my-domains.yaml
```

Or inline:

```bash
helm upgrade --install opendkim-server ./mail-infra/helm/opendkim-server \
--namespace pingmailer --create-namespace \
--set 'domains[0].domain=example.com'
```

The chart `fail`s loudly when `domains` is empty.

## After install: get the DNS TXT records

`opendkim-genkey` writes the public-key TXT record alongside the private key.
The pod prints them on startup; you can also grab them on demand:

```bash
kubectl -n pingmailer exec deploy/opendkim-server -- \
sh -c 'for d in /etc/dkimkeys/*; do echo "--- $(basename "$d") ---"; cat "$d"/*.txt; done'
```

Publish each at `<selector>._domainkey.<domain>` in your authoritative DNS.

## Wire Postfix to OpenDKIM

In the smtp-server (Postfix) chart / config:

```
smtpd_milters = inet:opendkim-server:8891
non_smtpd_milters = inet:opendkim-server:8891
milter_default_action = accept
```

The default `fullnameOverride: opendkim-server` in [values.yaml](values.yaml)
keeps the Service DNS short so the docker-compose Postfix `master.cf`
(`-o smtpd_milters=inet:opendkim-server:8891`) works without edits when both
charts share a namespace.

## Values

| Key | Default | Notes |
|------------------------------|--------------------------|-----------------------------------------------------------------------|
| `domains` | `[]` (**required**) | List of `{domain, selector?, keySize?}`. `selector` defaults to `mail`, `keySize` to `2048` |
| `image.repository` | `ghcr.io/lsflk/silver-dkim` | |
| `image.tag` | `main` | |
| `opendkim.mode` | `sv` | `s` = sign only, `v` = verify only, `sv` = both |
| `opendkim.trustedHosts` | RFC1918 + loopback | Chart appends each configured domain + `*.<domain>` automatically |
| `service.port` | `8891` | |
| `persistence.enabled` | `true` | Set `existingClaim` to bring your own PVC |
| `persistence.size` | `256Mi` | RSA-2048 keys are tiny; this is a generous default |
| `podSecurityContext.fsGroup` | `999` | `opendkim` GID inside the image — needed for PVC writes |
| `replicaCount` | `1` (**do not change**) | DKIM is intrinsically singleton + PVC is RWO |

## Rotating a DKIM key

1. Pick a new `selector` (the old DNS record stays live so in-flight mail is still verifiable).
2. Update `domains[i].selector` in your values file.
3. `helm upgrade …` — the new selector triggers `opendkim-genkey` for a fresh key.
4. Publish the new TXT record alongside the old.
5. After your DKIM TTL has lapsed across the internet, delete the old TXT record.

## Uninstall

```bash
helm uninstall opendkim-server -n pingmailer
```

PVC stays behind — see NOTES.txt to delete it explicitly.
43 changes: 43 additions & 0 deletions mail-infra/helm/opendkim-server/templates/NOTES.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
silver-opendkim has been installed.

Release: {{ .Release.Name }}
Namespace: {{ .Release.Namespace }}
Service: {{ include "silver-opendkim.fullname" . }}:{{ .Values.service.port }} (Milter)

Domains configured:
{{- range .Values.domains }}
• {{ .domain }} (selector={{ default "mail" .selector }}, keySize={{ default 2048 .keySize }})
{{- end }}

----- 1. Watch first-time key generation -----

kubectl -n {{ .Release.Namespace }} logs -f deploy/{{ include "silver-opendkim.fullname" . }}

The pod prints DKIM DNS TXT records on first start. Capture them with:

kubectl -n {{ .Release.Namespace }} exec deploy/{{ include "silver-opendkim.fullname" . }} \
-- sh -c 'for d in /etc/dkimkeys/*; do echo "--- $(basename "$d") ---"; cat "$d"/*.txt; done'

Publish each TXT record at `<selector>._domainkey.<domain>` in your DNS.

----- 2. Wire Postfix to opendkim -----

In your Postfix smtp-server chart / configmap, add:

smtpd_milters = inet:{{ include "silver-opendkim.fullname" . }}:{{ .Values.service.port }}
non_smtpd_milters = inet:{{ include "silver-opendkim.fullname" . }}:{{ .Values.service.port }}
milter_default_action = accept

(In docker-compose terms: replace `opendkim-server:8891` with the K8s Service
DNS — by default the fullnameOverride is `opendkim-server`, so the existing
hostname will Just Work as long as both charts share a namespace.)

----- 3. Persistent storage -----

DKIM private keys live on the PVC `{{ include "silver-opendkim.claimName" . }}`
({{ .Values.persistence.size }}). The PVC survives `helm uninstall` — delete it
explicitly to force key regeneration:

kubectl -n {{ .Release.Namespace }} delete pvc {{ include "silver-opendkim.claimName" . }}

Back this PVC up: losing it means publishing new TXT records.
85 changes: 85 additions & 0 deletions mail-infra/helm/opendkim-server/templates/_helpers.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "silver-opendkim.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Fully qualified app name.
*/}}
{{- define "silver-opendkim.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 }}

{{/*
Chart label.
*/}}
{{- define "silver-opendkim.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Common labels.
*/}}
{{- define "silver-opendkim.labels" -}}
helm.sh/chart: {{ include "silver-opendkim.chart" . }}
{{ include "silver-opendkim.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
app.kubernetes.io/component: opendkim
{{- end }}

{{/*
Selector labels.
*/}}
{{- define "silver-opendkim.selectorLabels" -}}
app.kubernetes.io/name: {{ include "silver-opendkim.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Service account name.
*/}}
{{- define "silver-opendkim.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "silver-opendkim.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}

{{/*
PVC claim name (existing or chart-managed).
*/}}
{{- define "silver-opendkim.claimName" -}}
{{- if .Values.persistence.existingClaim }}
{{- .Values.persistence.existingClaim }}
{{- else }}
{{- printf "%s-keys" (include "silver-opendkim.fullname" .) }}
{{- end }}
{{- end }}

{{/*
Normalize a single domain entry to a dict with domain/selector/keySize.
Pass the entry as the context.
*/}}
{{- define "silver-opendkim.domain" -}}
{{- $entry := . -}}
{{- $selector := default "mail" $entry.selector -}}
{{- $keySize := default 2048 $entry.keySize -}}
domain: {{ $entry.domain }}
selector: {{ $selector }}
keySize: {{ $keySize }}
{{- end }}
69 changes: 69 additions & 0 deletions mail-infra/helm/opendkim-server/templates/configmap.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{{- if not .Values.domains }}
{{- fail "domains is required — pass via -f my-values.yaml or --set 'domains[0].domain=example.com'. Each entry needs at least a `domain` field; `selector` defaults to 'mail' and `keySize` to 2048." }}
{{- end }}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: {{ include "silver-opendkim.fullname" . }}-conf
labels:
{{- include "silver-opendkim.labels" . | nindent 4 }}
data:
opendkim.conf: |
AutoRestart {{ .Values.opendkim.autoRestart }}
AutoRestartRate {{ .Values.opendkim.autoRestartRate }}
UMask {{ .Values.opendkim.umask }}
Syslog {{ .Values.opendkim.syslog }}
SyslogSuccess {{ .Values.opendkim.syslogSuccess }}
LogWhy {{ .Values.opendkim.logWhy }}

Canonicalization {{ .Values.opendkim.canonicalization }}

ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable

Mode {{ .Values.opendkim.mode }}
PidFile /var/run/opendkim/opendkim.pid
SignatureAlgorithm {{ .Values.opendkim.signatureAlgorithm }}
{{- with .Values.opendkim.requireSafeKeys }}

RequireSafeKeys {{ . }}
{{- end }}
{{- with .Values.opendkim.userID }}

UserID {{ . }}
{{- end }}

Socket {{ .Values.opendkim.socket }}

TrustedHosts: |
{{- range .Values.opendkim.trustedHosts }}
{{ . }}
{{- end }}
{{- range .Values.domains }}
{{ .domain }}
*.{{ .domain }}
{{- end }}

SigningTable: |
{{- range .Values.domains }}
{{- $selector := default "mail" .selector }}
*@{{ .domain }} {{ $selector }}._domainkey.{{ .domain }}
*@*.{{ .domain }} {{ $selector }}._domainkey.{{ .domain }}
{{- end }}

KeyTable: |
{{- range .Values.domains }}
{{- $selector := default "mail" .selector }}
{{ $selector }}._domainkey.{{ .domain }} {{ .domain }}:{{ $selector }}:/etc/dkimkeys/{{ .domain }}/{{ $selector }}.private
{{- end }}

silver.yaml: |
domains:
{{- range .Values.domains }}
- domain: {{ .domain }}
dkim-selector: {{ default "mail" .selector }}
dkim-key-size: {{ default 2048 .keySize }}
{{- end }}
Loading
Loading