Cypht
One inbox over many IMAP accounts.
Cypht (source) is a modular PHP webmail client whose point is aggregation: several IMAP accounts and SMTP profiles behind one login, a combined view across them, and a module system that lets the deployment switch off everything it does not need. It keeps no mail of its own — messages stay on the upstream servers, only accounts, settings, sessions and cached attachments live locally.
Alternatives considered
Cloud Hosted
| Tool | Open Source | Free Tier | Notes |
|---|---|---|---|
| Gmail | No | Yes | Aggregates other accounts, but hands them to Google |
| Fastmail | No | No | Paid per user |
Self Hosted
| Tool | Open Source | Full Features | Notes |
|---|---|---|---|
| Roundcube | Yes | Yes | The classic; one account per login by default |
| SnappyMail | Yes | Yes | Fast, multi-account, no external database needed |
| Bichon | Yes | Yes | Already deployed; Rust-based, a different feature set |
Installation
Architecture
- Deployment: Single
cyphtdeployment in thecyphtnamespace,replicas: 1andRecreatestrategy — a ReadWriteOnce volume plus SQLite must never see two pods at once - Image:
cypht/cypht(digest-pinned) - Storage: Longhorn PVC (
cypht-data, 1Gi,longhorn-encrypted) on/var/lib/hm3holding the SQLite file, per-user settings and attachments;/tmpand/var/lib/nginxare emptyDirs so the image's nginx can write its temp dirs. No database component - Networking: ClusterIP service on port 80, HTTPRoute via the public gateway on
cypht.web.kueber.eu - Modules:
CYPHT_MODULEStrims the module list to what a multi-IMAP reader and replier needs (core, contacts, imap, smtp, profiles, folders, search, keyboard shortcuts, themes)
Security
- The upstream image entrypoint starts as root (it chowns the data dirs and rewrites the
/var/wwwsymlink) before supervisord drops nginx and php-fpm towww-data. So, unlike almost every other app here, the pod is notrunAsNonRoot: it drops all capabilities and adds back onlyCHOWN,SETUID,SETGIDandNET_BIND_SERVICE, withallowPrivilegeEscalation: false, seccompRuntimeDefaultandfsGroup: 33for the data volume - The namespace enforces the
baselinePod Security Standard and auditsrestricted; Kyverno's restricted policies fire as warnings only, so no exception is needed - Ingress only from the public gateway's Envoy pods
- Egress is a least-privilege set on top of the baseline: IMAPS (993), Submission (587), SMTPS (465) and 443/80 to the internet, RFC1918 excluded. The shared gateway-egress rule is deleted here because Cypht does no OIDC back-channel
- Remote image loading is off (
ALLOW_EXTERNAL_IMAGE_SOURCES), long sessions are off, debug is off tier-3priority class, 50m / 128Mi requested, 384Mi limit
Updates
Managed by Renovate. The image is digest-pinned.
Data Management
- PVC:
cypht-data(1Gi, Longhorn-encrypted), annotatedk8up.io/backup: "true" - Backups: k8up
Schedules to Hetzner and Backblaze (workload-talos-cypht-*) via restic, a file-level copy of the SQLite database and the attachments
User Management
Accounts are local: AUTH_TYPE, SESSION_TYPE and USER_CONFIG_TYPE all point at the SQLite database, and there is no OIDC, neither in-app nor at the gateway. The first login user is seeded from the SOPS-encrypted cypht-auth secret (AUTH_USERNAME / AUTH_PASSWORD); rotating it is a sops edit followed by a Flux reconcile.
Configuration Management
Everything is env vars on the base Deployment. One gotcha worth keeping: for the SQLite driver Cypht builds its DSN as sqlite: plus DB_NAME, so DB_NAME must be the absolute file path on the volume. DB_CONNECTION_TYPE and DB_SOCKET only apply to the host and socket drivers and are ignored here — setting them left DB_NAME at its relative default, a file off the volume, which is why login used to fail.
Usage
Log in with the seeded user, add IMAP accounts and SMTP profiles under the settings pages, and read or reply across them from the combined views. Remote content in messages is blocked by default.
Cluster-specific deviations from the above live in the per-cluster README — see k8s/apps/cypht/talos/README.md.
Cluster Deployment
Cypht — Talos cluster
Deviations from defaults
- Starts as root, then drops to www-data. The upstream image entrypoint chowns the data dirs and rewrites the
/var/wwwsymlink beforesupervisorddrops nginx/php-fpm towww-data. So — unlike every other app — the pod is notrunAsNonRoot; it keepsCHOWN/SETUID/SETGID/NET_BIND_SERVICE(all else dropped,allowPrivilegeEscalation: false, seccompRuntimeDefault). The namespace enforces thebaselinePSS and auditsrestricted. Kyverno's restricted policies fire as warnings only (audit mode), so noPolicyExceptionis needed today. - SQLite backend, no database. Auth/sessions/user-config all live in a SQLite file on the
cypht-dataPVC (longhorn-encrypted, RWO) — hencereplicas: 1+strategy: Recreate. No CNPG component. - Mail-protocol egress (
network-policies/cypht-np.yaml): on top of the component's default-deny baseline (DNS, same namespace, gateway),egress-cyphtopens IMAPS/SMTPS/Submission (993/465/587) plus 443/80 to the internet and deletes the sharedegress-envoy-gatewayrule the app does not need. Trim 587/443 if unused.
Initial login secret
secret-auth.yaml (SOPS-encrypted) seeds the first login user via AUTH_USERNAME / AUTH_PASSWORD. To rotate, edit with sops and let Flux reconcile:
sops k8s/apps/cypht/talos/secret-auth.yaml
- Image:
cypht/cypht:2.13.0@sha256:e0f6c85508c37b004924b6bc8456a327d3b7f2acdeaff51fe1b7cdc01c507259
Rendered manifests (kustomize build)
apiVersion: apps/v1
kind: Deployment
metadata:
annotations:
kustomize.toolkit.fluxcd.io/force: enabled
labels:
app.kubernetes.io/name: cypht
name: cypht
namespace: cypht
spec:
replicas: 1
revisionHistoryLimit: 2
selector:
matchLabels:
app.kubernetes.io/name: cypht
strategy:
type: Recreate
template:
metadata:
labels:
app.kubernetes.io/name: cypht
spec:
automountServiceAccountToken: false
containers:
- env:
- name: DB_DRIVER
value: sqlite
- name: DB_NAME
value: /var/lib/hm3/cypht.sqlite3
- name: AUTH_TYPE
value: DB
- name: SESSION_TYPE
value: DB
- name: USER_CONFIG_TYPE
value: DB
- name: USER_SETTINGS_DIR
value: /var/lib/hm3/users
- name: ATTACHMENT_DIR
value: /var/lib/hm3/attachments
- name: ENABLE_DEBUG
value: 'false'
- name: LOG_LEVEL
value: WARNING
- name: ALLOW_EXTERNAL_IMAGE_SOURCES
value: 'false'
- name: ALLOW_LONG_SESSION
value: 'false'
- name: CYPHT_MODULES
value: core,contacts,local_contacts,imap,smtp,account,idle_timer,themes,nux,profiles,imap_folders,advanced_search,saved_searches,keyboard_shortcuts
envFrom:
- secretRef:
name: cypht-auth
image: cypht/cypht:2.13.0@sha256:e0f6c85508c37b004924b6bc8456a327d3b7f2acdeaff51fe1b7cdc01c507259
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /
port: http
periodSeconds: 30
name: cypht
ports:
- containerPort: 80
name: http
readinessProbe:
httpGet:
path: /
port: http
periodSeconds: 10
resources:
limits:
memory: 384Mi
requests:
cpu: 50m
memory: 128Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
add:
- CHOWN
- SETUID
- SETGID
- NET_BIND_SERVICE
drop:
- ALL
startupProbe:
failureThreshold: 30
httpGet:
path: /
port: http
periodSeconds: 5
volumeMounts:
- mountPath: /var/lib/hm3
name: data
- mountPath: /tmp
name: tmp
- mountPath: /var/lib/nginx
name: nginx-lib
priorityClassName: tier-3
securityContext:
fsGroup: 33
seccompProfile:
type: RuntimeDefault
volumes:
- name: data
persistentVolumeClaim:
claimName: cypht-data
- emptyDir:
sizeLimit: 256Mi
name: tmp
- emptyDir:
sizeLimit: 256Mi
name: nginx-lib