What this is for#
A self-hosted AcelleMail install handles:
- Personally-identifiable subscriber data — email addresses, names, custom fields
- Sometimes regulated data — GDPR subjects' personal data, possibly HIPAA-relevant in healthcare contexts
- Business-critical campaign content — templates, sending lists, automation logic
- Credentials — sending-provider API keys, your own API tokens, customer billing info
Encryption is the baseline defense if any layer is compromised — a stolen backup, a leaked disk image, a database dump found in a forgotten S3 bucket. This guide walks the 4 layers you should encrypt + how.
This isn't compliance theater — each layer protects against specific real attack scenarios.
Layer 1 — TLS in transit (the easy one)#
Protects against: network interception (man-in-the-middle, ISP snooping, Wi-Fi sniffing)
What: every HTTP request/response is encrypted with TLS. Visitor to admin, admin to API, AcelleMail to sending provider, all over TLS.
How:
# Install certbot + nginx plugin
sudo apt install -y certbot python3-certbot-nginx
# Provision cert via Let's Encrypt
sudo certbot --nginx -d mail.example.com \
--non-interactive --agree-tos --email you@example.com --redirect
Verify your nginx config has the right protocols (TLS 1.2 + 1.3 only):
server {
listen 443 ssl http2;
server_name mail.example.com;
ssl_certificate /etc/letsencrypt/live/mail.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/mail.example.com/privkey.pem;
# Modern, secure cipher suite
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:...;
# HSTS — instructs browsers to refuse plain HTTP for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
# 80 → 443 redirect (certbot --redirect adds this automatically)
server {
listen 80;
server_name mail.example.com;
return 301 https://$server_name$request_uri;
}
Verify with https://www.ssllabs.com/ssltest/analyze.html?d=mail.example.com — target A or A+ grade.
Auto-renewal: certbot's systemd timer renews ~30 days before expiry. Verify it's running:
sudo systemctl status certbot-renew.timer # Rocky
sudo systemctl status certbot.timer # Ubuntu/Debian (or check /etc/cron.d/certbot)
See Install AcelleMail on Ubuntu 24.04 LTS Step 7 for the full install context.
Layer 2 — MySQL encryption at rest#
Protects against: stolen disk image / lost backup files / casual snooping by a sysadmin who shouldn't be looking
What: InnoDB tablespace encryption — the database files on disk are encrypted with a key managed by MySQL.
How — first enable the keyring plugin (in /etc/mysql/mysql.conf.d/mysqld.cnf):
[mysqld]
early-plugin-load = keyring_file.so
keyring_file_data = /var/lib/mysql-keyring/keyring
Then create the keyring directory + restart:
sudo mkdir -p /var/lib/mysql-keyring
sudo chown mysql:mysql /var/lib/mysql-keyring
sudo chmod 750 /var/lib/mysql-keyring
sudo systemctl restart mysql
Now encrypt tables that hold PII:
ALTER TABLE subscribers ENCRYPTION='Y';
ALTER TABLE contacts ENCRYPTION='Y';
ALTER TABLE customers ENCRYPTION='Y';
ALTER TABLE email_log ENCRYPTION='Y';
-- Test encryption works:
SHOW CREATE TABLE subscribers\G -- should show ENCRYPTION='Y'
Requires MySQL 5.7.11+ or MariaDB 10.1+. For all-new tables, set default_table_encryption = ON in mysqld.cnf so they're encrypted at creation.
Key management — the keyring_file plugin stores the master key in a file on the same server. For more-secure setups (regulated industries), use AWS KMS / HashiCorp Vault via their keyring plugins.
Performance impact: ~3-5% CPU overhead for InnoDB tablespace encryption. Negligible for most AcelleMail workloads.
Layer 3 — Filesystem encryption for backups#
Protects against: lost / stolen backup files, including offsite copies in S3 / B2 / Wasabi
What: every backup file is encrypted at rest. Encryption happens during the backup process, before the file leaves your server.
Option A — gpg encryption per file#
# Daily backup script — encrypt MySQL dump with gpg
mysqldump --single-transaction --routines acellemail | gzip | \
gpg --symmetric --cipher-algo AES256 --batch --passphrase-file /root/.backup-pass \
-o /var/backups/acellemail-$(date +%F).sql.gz.gpg
/root/.backup-pass contains the passphrase (chmod 0400). Store the passphrase ALSO in your password manager — without it the backup is unrecoverable.
Decryption (during restore):
gpg --decrypt --batch --passphrase-file /root/.backup-pass /var/backups/acellemail-2026-05-16.sql.gz.gpg | gunzip | mysql acellemail
Option B — restic (content-addressed + encrypted)#
restic handles encryption + incremental + offsite in one tool:
# One-time init
sudo restic -r s3:s3.amazonaws.com/your-bucket/acellemail init
# (prompts for repo password — STORE IT)
# Daily backup
sudo restic -r s3:s3.amazonaws.com/your-bucket/acellemail backup /var/backups/acellemail
# Restore
sudo restic -r s3:s3.amazonaws.com/your-bucket/acellemail restore latest --target /tmp/restore
Restic encrypts with the repo password client-side; even the cloud provider can't see the contents.
Option C — full-disk LUKS#
For encrypting the entire filesystem of the AcelleMail server (not just backups), use LUKS:
# At install time only — LUKS encryption of the data partition
sudo cryptsetup luksFormat /dev/sda2
sudo cryptsetup luksOpen /dev/sda2 acelle-data
sudo mkfs.ext4 /dev/mapper/acelle-data
sudo mount /dev/mapper/acelle-data /var/www/acellemail
Pros: comprehensive. Cons: server can't auto-boot without the passphrase (or stored key) — operationally complex.
For most AcelleMail installs, Option B (restic) is the right answer — handles backup + encryption + offsite in one tool. See Automated Database Backups for the full backup strategy.
Layer 4 — Application-level encryption (Laravel encrypt/decrypt)#
Protects against: the database being compromised (or dumped, or accessed by a rogue DBA) while application secrets are still safe
What: specific sensitive fields are encrypted by AcelleMail's code before being stored in MySQL. Even with raw DB access, the field values are ciphertext.
Use cases:
- GDPR consent records (legal-grade evidence; encrypt for tamper resistance)
- API tokens you store on behalf of customers
- Sensitive custom subscriber fields (PHI in healthcare, financial info)
How — Laravel ships encrypt() and decrypt() helpers. In a custom extension (see extending source code):
// Storing
$subscriber->consent_text = encrypt($consentText);
$subscriber->save();
// Retrieving
$plainText = decrypt($subscriber->consent_text);
For models, use the encrypted cast (cleaner — automatic encrypt/decrypt):
// In your model
protected $casts = [
'consent_text' => 'encrypted',
'sensitive_field' => 'encrypted',
];
Now $subscriber->consent_text = $plain; is auto-encrypted on save; $subscriber->consent_text is auto-decrypted on read.
The APP_KEY discipline#
Laravel's encryption uses APP_KEY from .env as the master key. Protect it like a password manager protects a vault key:
- Never commit
.env to git
- File permission
0640 owner=www-data (per hardening checklist Control 15)
- Back up
.env SEPARATELY (to your password manager / secrets vault, NOT in your regular file backups — restoring an old backup with the wrong APP_KEY can't decrypt fields encrypted with the new key)
Key rotation#
For high-security installs, rotate APP_KEY annually:
- Generate new key:
php artisan key:generate --show (don't apply yet)
- For each encrypted field, decrypt with old key + re-encrypt with new key (custom artisan command; iterate models)
- Once re-encryption is done, swap APP_KEY in
.env
php artisan config:clear + restart workers
- Old key archived in secrets vault (for restoring old backups)
This is operationally heavy — most operators don't rotate. Set it up only if your compliance regime requires it.
What NOT to encrypt#
You don't need to encrypt:
- Public marketing pages
- Static assets (CSS, JS, images)
- Non-PII configuration (timezone settings, default templates)
- Log files (rotate them quickly instead; over-encrypting logs makes debugging painful)
- Plain-text email content WHILE IN TRANSIT through your sending pipeline (it's already encrypted on the wire via SMTP TLS; encrypting again wastes CPU)
Over-encryption is a real cost — slower performance, harder debugging, more failure modes. Encrypt what's actually risky.
Common mistakes#
| Mistake |
Why it bites |
| TLS 1.0/1.1 still enabled |
Deprecated; PCI-DSS non-compliant; some browsers will hard-fail |
| Self-signed cert in production |
Browsers show warnings; SES rejects connections; bad signal |
InnoDB encryption enabled but keyring plugin not loaded |
MySQL won't start after ALTER TABLE ... ENCRYPTION='Y' |
| Backup passphrase stored in same backup directory |
"Encrypted backup" is encryption theater if the key is alongside it |
| APP_KEY in version control |
Anyone with git access can decrypt your sensitive fields |
| Restoring backup after APP_KEY rotation |
Old-key-encrypted fields can't be decrypted — you need to keep old keys |
| LUKS without a recovery plan |
Lose the passphrase = lose the data, period |
| Encrypting everything |
CPU overhead + debugging pain; encrypt only what's actually sensitive |
FAQ#
Is filesystem encryption enough? Do I still need MySQL encryption? Filesystem (LUKS) protects against physical theft of the disk. It does NOT protect against a logged-in attacker who can read DB files. MySQL InnoDB encryption adds a second layer.
TLS for DB connections from AcelleMail to MySQL? If MySQL is on the same host (loopback), no benefit. If MySQL is on a separate host (managed RDS, separate droplet), yes — set DB_SSL=true in .env + configure MySQL to require TLS.
End-to-end encryption for the emails themselves (S/MIME, PGP)? AcelleMail doesn't ship native support. Recipients would need to verify your signing key — operationally complex for marketing email. Only worth it for specific regulated transactional flows.
What about email content stored in the DB before send? AcelleMail stores campaign HTML in campaigns.html_content. With MySQL InnoDB encryption (Layer 2), this is encrypted at rest. Field-level encryption (Layer 4) would make the WYSIWYG editor + send pipeline significantly more complex; usually not worth it for marketing content.
Hardware Security Modules (HSM)? For regulated industries — AWS CloudHSM, Azure Key Vault HSM, etc. — store APP_KEY there instead of on the server filesystem. Reduces blast radius if the server is compromised. Niche; expensive.
Is bcrypt for passwords the same as encrypt()? No. bcrypt is a one-way HASH (can't be reversed). Used for storing user passwords (compare on login by hashing the attempt + comparing hashes). encrypt() is two-way (decrypt to recover the original). Use the right one for each use case.
What about subscriber unsubscribe tokens? These should be cryptographically signed (HMAC), not encrypted — they need to be readable by the unsubscribe URL endpoint but can't be forged. Laravel's signed URLs handle this natively.
Related articles#