1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
|
---
name: protonbridge-aerc
description: "Manages the local aerc connection to Proton Mail Bridge running in the f3s k3s cluster through a persistent kubectl port-forward, pinned Bridge certificate, and systemd user service. Use when setting up, starting, validating, or troubleshooting aerc, Proton Bridge IMAP/SMTP, certificate errors, credentials, or the protonbridge-k3s-tunnel service. Triggers on: protonbridge aerc, aerc mail, Proton Bridge tunnel, aerc IMAP, aerc SMTP."
---
# Proton Bridge for aerc
Maintain the secure path from aerc on `earth` to Proton Mail Bridge in the f3s
k3s cluster. Do not expose the Bridge-generated password in output, prompts,
logs, Git, or task annotations.
## Architecture
```text
aerc
├─ IMAP STARTTLS 127.0.0.1:1143
└─ SMTP STARTTLS 127.0.0.1:1025
│
▼
protonbridge-k3s-tunnel.service
└─ kubectl port-forward service/protonbridge -n services
│
▼
Proton Bridge pod in f3s k3s
```
The port-forward is preferable to a fixed NodePort address because Kubernetes
selects the healthy pod and the setup survives r0/r1/r2 placement changes.
## Canonical files
| Purpose | Path |
|---|---|
| aerc account | `~/.config/aerc/accounts.conf` |
| Bridge password | `~/.config/aerc/protonbridge-password` (mode `0600`) |
| Pinned Bridge certificate | `~/.config/aerc/protonbridge-ca-bundle.pem` |
| aerc wrapper setting `SSL_CERT_FILE` | `~/bin/aerc` |
| Persistent tunnel | `~/.config/systemd/user/protonbridge-k3s-tunnel.service` |
| Disabled old local Bridge autostart | `~/.config/autostart/Proton Mail Bridge.desktop` |
| k3s Deployment | `/home/paul/git/conf/f3s/protonbridge/helm-chart/templates/deployment.yaml` |
| k3s Service | `/home/paul/git/conf/f3s/protonbridge/helm-chart/templates/service.yaml` |
| Argo CD Application | `/home/paul/git/conf/f3s/argocd-apps/services/protonbridge.yaml` |
Load the [`f3s-k3s`](../f3s-k3s/SKILL.md) skill for cluster access, Argo CD,
node, or control-plane problems. This skill owns only the Proton Bridge/aerc
path.
## f3s Argo CD deployment
The cluster-side Proton Bridge is a GitOps workload in the `conf` repository,
not an independently managed pod:
- Argo CD Application: `protonbridge` in namespace `cicd`
- Application manifest:
`/home/paul/git/conf/f3s/argocd-apps/services/protonbridge.yaml`
- Argo source repository:
`http://git-server.cicd.svc.cluster.local/conf.git`
- Source revision and path: `master`, `f3s/protonbridge/helm-chart`
- Destination: the in-cluster API, namespace `services`
- Desired policy: automated sync with `prune: true` and `selfHeal: true`
The Helm chart owns:
- `templates/deployment.yaml` — headless Bridge CLI, persistent account/update
state, probes, and loopback-to-pod forwarding
- `templates/service.yaml` — IMAP/SMTP Service ports plus LAN NodePorts
- `templates/persistent-volumes.yaml` — retained Bridge state on the f3s volume
Treat these files as the source of truth. Make durable cluster-side fixes in
the chart, validate them, commit them to the `conf` repository, and push through
the established repository workflow so Argo CD deploys them. A direct
`kubectl apply` is acceptable for a short diagnostic only; reproduce any valid
fix in Git and remove live drift afterward.
Validate a chart change before pushing:
```bash
cd /home/paul/git/conf
helm lint f3s/protonbridge/helm-chart
helm template protonbridge f3s/protonbridge/helm-chart \
| kubectl apply --dry-run=server -f -
```
Inspect reconciliation and the deployed revision:
```bash
kubectl get application -n cicd protonbridge \
-o jsonpath='{.status.sync.status}{" "}{.status.health.status}{" "}{.status.sync.revision}{"\n"}'
kubectl get deployment,pod -n services -l app=protonbridge
kubectl get service,endpoints -n services protonbridge
```
If automated sync was temporarily disabled during diagnosis, restore the
declared Application and verify the policy rather than leaving it disabled:
```bash
kubectl apply -f /home/paul/git/conf/f3s/argocd-apps/services/protonbridge.yaml
kubectl get application -n cicd protonbridge \
-o jsonpath='{.spec.syncPolicy.automated}{"\n"}{.status.sync.status}{" "}{.status.health.status}{"\n"}'
```
Expected output includes `{"prune":true,"selfHeal":true}` and
`Synced Healthy`. Never delete the retained PV/PVC or Bridge vault as a generic
troubleshooting step; they contain the persisted account session, generated
credentials, certificate, and self-update state.
## Bootstrap order
When recreating the setup on `earth`:
1. Confirm `kubectl` can reach the f3s cluster and Proton Bridge is healthy.
2. Create and enable the systemd port-forward described below.
3. Attach to Bridge, run `info`, and store its generated password without
printing or committing it.
4. Capture and pin the current Bridge certificate through the localhost tunnel.
5. Create the aerc account with credential commands instead of inline secrets.
6. Install the `~/bin/aerc` wrapper and confirm it is first on `PATH`.
7. Disable the obsolete laptop-local Bridge autostart to prevent port conflicts.
8. Run TLS, IMAP login/folder, SMTP authentication, and aerc startup checks.
Do not continue to a later layer when an earlier one is unhealthy.
## Expected aerc account
The account uses STARTTLS through localhost and obtains the password through a
credential command:
```ini
[paul]
source = imap://mail%40paul.buetow.org@127.0.0.1:1143
source-cred-cmd = cat ~/.config/aerc/protonbridge-password
outgoing = smtp://mail%40paul.buetow.org@127.0.0.1:1025
outgoing-cred-cmd = cat ~/.config/aerc/protonbridge-password
default = INBOX
from = Paul Buetow <mail@paul.buetow.org>
copy-to = Sent
cache-headers = true
```
Never put the password back into either URI. It is the Bridge-generated
password shown by the Bridge `info` command, not the Proton account password.
The `~/bin/aerc` wrapper must precede `/usr/bin` on `PATH` and contain:
```sh
#!/bin/sh
export SSL_CERT_FILE="$HOME/.config/aerc/protonbridge-ca-bundle.pem"
exec /usr/bin/aerc "$@"
```
## Start and inspect
Start or restart the persistent tunnel:
```bash
systemctl --user enable --now protonbridge-k3s-tunnel.service
systemctl --user restart protonbridge-k3s-tunnel.service
```
Check it without printing secrets:
```bash
systemctl --user status protonbridge-k3s-tunnel.service --no-pager
journalctl --user -u protonbridge-k3s-tunnel.service --since '-15 min' --no-pager
ss -ltn | rg '127\.0\.0\.1:(1025|1143)\b'
type -a aerc
```
Expected state:
- service is `enabled` and `active`
- `kubectl port-forward` listens on localhost ports 1143 and 1025
- `~/bin/aerc` resolves before `/usr/bin/aerc`
- no laptop-local `/usr/lib/protonmail/bridge/` process is running
Launch normally with `aerc`.
## End-to-end health check
Check the cluster first:
```bash
kubectl get pod -n services -l app=protonbridge
kubectl get service,endpoints -n services protonbridge
kubectl get application -n cicd protonbridge \
-o jsonpath='{.status.sync.status}{" "}{.status.health.status}{"\n"}'
```
The pod should be `1/1 Running`, endpoints should include IMAP and SMTP, and
Argo CD should report `Synced Healthy`.
Validate TLS without authenticating:
```bash
/usr/bin/openssl s_client -starttls imap \
-connect 127.0.0.1:1143 \
-verify_return_error \
-CAfile ~/.config/aerc/protonbridge-ca-bundle.pem \
-brief </dev/null
/usr/bin/openssl s_client -starttls smtp \
-connect 127.0.0.1:1025 \
-verify_return_error \
-CAfile ~/.config/aerc/protonbridge-ca-bundle.pem \
-brief </dev/null
```
Both must report `Verification: OK`. Do not work around failures with
`imap+insecure` or `smtp+insecure`: in aerc 0.21 those schemes disable STARTTLS
and expose credentials as plaintext.
For an authenticated check, read the password from the credential file inside
the test process; never echo it. Use Python `imaplib.IMAP4` followed by
`starttls()`, and `smtplib.SMTP` followed by `starttls()`, with an SSL context
created from `protonbridge-ca-bundle.pem`. Confirm IMAP login/folder listing and
SMTP login, then log out without sending mail.
## Recreate the tunnel
The systemd user unit should contain:
```ini
[Unit]
Description=Forward local aerc ports to the Proton Bridge k3s service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/kubectl port-forward --address 127.0.0.1 --namespace services service/protonbridge 1143:1143 1025:1025
Restart=always
RestartSec=5
[Install]
WantedBy=default.target
```
After creating or changing it:
```bash
systemctl --user daemon-reload
systemctl --user enable --now protonbridge-k3s-tunnel.service
```
## Refresh credentials
Refresh only after an authenticated check proves the saved password is stale.
The Bridge pod runs an attachable CLI:
```bash
pod=$(kubectl get pod -n services -l app=protonbridge \
-o jsonpath='{.items[0].metadata.name}')
kubectl attach -it -n services "$pod"
```
Press Enter if needed, run `info`, and use its Bridge-generated password. Write
it directly to `~/.config/aerc/protonbridge-password`, apply mode `0600`, and do
not show it in command output. Typing `quit` exits Bridge; Kubernetes restarts
the pod and reconnects with its persisted session.
If the account itself is absent, run `login` in the attached CLI. The user must
enter the Proton credentials and 2FA interactively. Never request or retain the
Proton account password.
## Refresh the pinned certificate
Bridge uses a self-signed certificate whose SAN is only `127.0.0.1`. This is
why aerc connects through the localhost port-forward instead of directly to a
NodePort. If Bridge rotates the certificate, replace the pinned leaf after
verifying that the cluster pod and account are expected:
```bash
umask 077
tmp=$(mktemp)
timeout 10 /usr/bin/openssl s_client \
-starttls imap -connect 127.0.0.1:1143 -showcerts \
</dev/null 2>/dev/null \
| /usr/bin/openssl x509 -outform PEM >"$tmp"
test -s "$tmp"
install -m 0644 "$tmp" ~/.config/aerc/protonbridge-ca-bundle.pem
rm -f "$tmp"
```
Then rerun both TLS checks. Keep this file limited to the current Bridge
certificate; combining it with Fedora's CA bundle can select an older
self-signed Bridge certificate with the same `127.0.0.1` subject.
## Troubleshooting order
1. **aerc binary** — `type -a aerc`; ensure `~/bin/aerc` is first.
2. **Local listeners** — check ports 1143/1025 with `ss`.
3. **Tunnel service** — inspect status and journal; restart it once after
reading the error.
4. **kubectl access** — run `kubectl get service -n services protonbridge`.
Fix kube context/network access before changing aerc.
5. **Cluster health** — inspect pod readiness, restarts, logs, endpoints, and
Argo CD status.
6. **TLS** — run the pinned-certificate checks. Refresh the pin only if the
live certificate changed intentionally.
7. **Authentication** — run a non-printing IMAP/SMTP login check. Refresh the
Bridge-generated password only if login fails after transport and TLS pass.
Common failures:
- `connection refused`: tunnel inactive, port collision, or pod unavailable.
- `error upgrading connection` / port-forward exits: stale kube context,
unreachable API server, or no ready Proton Bridge pod.
- `certificate signed by unknown authority`: wrapper bypassed or pin missing.
- certificate name mismatch: aerc is connecting directly to an r-node address
instead of `127.0.0.1`.
- `no such user` / SMTP authentication failure: stale Bridge-generated
password or wrong full email address.
- local port already in use: stop old laptop-local Bridge processes and keep
`~/.config/autostart/Proton Mail Bridge.desktop` disabled (`Hidden=true`).
- pod `0/1` with `No active accounts`: attach and perform interactive `login`.
- pod restart after `kubectl attach`: EOF stopped the CLI; wait for Kubernetes
to restart it and confirm readiness before retrying.
Do not patch around cluster failures by running a second local Bridge. Repair
the owning layer, validate the full path, and preserve the single persisted
Bridge session in k3s.
|