This document collects detailed security guidance, operational recommendations, and recovery procedures for OpenInboundEmail. It explains the security model implemented in the codebase, why certain design decisions were taken, what threats remain, and how to operate the service safely in production.
The project ships as a self-hosted monorepo. The guidance below assumes control of the deployment host. For high-security or regulated environments adopt a centralized secrets manager and host-level hardening beyond the recommendations here.
- Admin store encryption now uses AES-256-GCM (AEAD) when
ADMIN_STORE_KEY
is configured. This prevents undetected ciphertext tampering. - Admin store reads/writes are atomic. The
data/
directory is created with strict permissions (0o700). Admin files are written as 0o600 and rotated via a temporary file + rename. - Backups prefer
age
(modern, minimal dependency) whenAGE_RECIPIENT
is provided. Ifage
is not available the script will try OpenSSL AES-256-GCM, and only fall back to AES-256-CBC if absolutely necessary. - Message raw payloads are stored as Base64 in the DB to preserve attachments and avoid encoding corruption.
- Mail spool directories and files are created with restrictive permissions (0o700 directories, 0o600 files) and written atomically.
- The web inbox no longer injects unsanitized HTML into the DOM. HTML can be viewed only in a sandboxed iframe behind an explicit action.
- If admin store decryption fails, the server surfaces a distinct error (
ADMIN_STORE_DECRYPTION_FAILED
) and refuses to silently reinitialize credentials.
Primary threats addressed:
- Remote attackers attempting to gain administrative access via token theft or XSS.
- Tampering with the persisted admin token store.
- Exfiltration of sensitive backups.
- Malicious email payloads (HTML/JS) trying to execute in the admin UI.
Assumptions:
- The operator controls the host and can protect files in
/etc
and service accounts. - For high-assurance requirements, integrate a secrets manager and external key storage (not local env files).
-
Prefer an external secrets manager for production. Examples: HashiCorp Vault, AWS Secrets Manager, Azure Key Vault.
-
If you use local secrets, put them in a file managed by the OS with strict ownership and permissions:
/etc/openinbound.env
chown root:root /etc/openinbound.env
chmod 600 /etc/openinbound.env
Systemd unit should reference this file via
EnvironmentFile=
. -
Admin store (
apps/server/data/admin.enc
):- Prefer storing the admin token only inside a secrets manager.
- If using
ADMIN_STORE_KEY
, keep the key out of environment logs and avoid placing it in process command-lines. Store the key in the env file above and restrict access.
-
Backups & recipient keys:
- Configure
AGE_RECIPIENT
with an operator public key and useage
to encrypt archives. - If you cannot use
age
, use OpenSSL AES-256-GCM. Verify your OpenSSL supports GCM. - Do not store unencrypted backups on disk in production.
- Configure
-
Rotation & automation:
- Automate rotation via a secrets manager. The included
tools/rotate-admin-token.ts
prints to stdout — do not rely on STDOUT for storage in production; instead pipe output to your secrets API.
- Automate rotation via a secrets manager. The included
If you see ADMIN_STORE_DECRYPTION_FAILED
when the service tries to read the admin store, follow these steps:
- Verify you are using the exact key that was used to create the encrypted store. Keys are sensitive to whitespace and newlines.
- If you used an env file for
ADMIN_STORE_KEY
, ensure the file has correct permissions and the systemd unit reads it before starting the service. - If the key is irretrievably lost, you cannot recover the previous token. Recovery options:
- Restore a previously taken operator backup of
admin.enc
from a secure backup that is known-good. - If no backup exists, you must create a new admin account. Be aware: re-creating credentials requires updating any systems that relied on the old token.
- Restore a previously taken operator backup of
- After recovery or rotation, rotate any dependent secrets (cloud DNS tokens, third-party integrations) and update the secret manager.
Operator safety: the server intentionally fails loudly on decryption errors to prevent accidental reinitialization and reduces the chance of an attacker creating a new admin when an operator's key is wrong.
- The UI does not inject raw HTML into the admin page DOM. Instead, administrators may opt-in to view HTML in a sandboxed iframe with
sandbox
enabled, reducing the risk of script execution. - If you require inline sanitized HTML inside the app (instead of the iframe), sanitize server-side with a vetted library (e.g. DOMPurify) and limit allowed tags/attributes. Do not rely only on client-side sanitization.
- Because email can contain remote resources, displaying HTML in an iframe mitigates automatic loading of third-party trackers but is not bulletproof; prefer manual review for high-risk messages.
-
Backup encryption:
-
Preferred: use
age
withAGE_RECIPIENT
and store the recipient in your ops vault. Example:age -r -o openinbound-data-YYYYMMDD.age openinbound-data-YYYYMMDD.tar.gz
-
Fallback: OpenSSL AES-256-GCM (AEAD). Verify OpenSSL version supports
-aes-256-gcm
.
-
-
Backup retention & verification:
- Keep a rotation policy (7/30/90 day tiers depending on your compliance needs).
- Periodically verify restores: decrypt a random older archive and validate file integrity.
-
Where to store backups:
- Prefer cold storage or object storage with server-side encryption + IAM rules (S3 with SSE and restricted bucket policy), rather than unstructured host files.
-
Restore steps:
- Use
age -d
or OpenSSL decrypt with the correct key. Do not attempt to guess keys; incorrect decryption may overwrite operational files if scripts are not careful.
- Use
The example deploy/openinbound.service
already includes baseline hardening. Add the following before production rollout:
-
Protect environment file and service user:
chown root:root /etc/openinbound.env
chmod 600 /etc/openinbound.env
- Create a dedicated system user
inbound
with no interactive shell.
-
Tighten the systemd unit:
- Add or confirm these options where applicable:
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictAddressFamilies=AF_INET AF_INET6
(restrict to required families)CapabilityBoundingSet=CAP_NET_BIND_SERVICE
- Add or confirm these options where applicable:
-
Use an ExecStart wrapper that drops privileges explicitly and avoids shell interpolation — prefer absolute paths.
-
Ensure network-level protections (firewall rules, fail2ban for abusive connections) and monitor SMTP connection metrics.
- Database: move raw message payloads to object storage if you expect high volume. Storing large binaries in SQLite can cause performance and backup issues.
- Implement server-side retention policies (e.g. purge messages older than N days) and consider archiving older messages to encrypted object storage.
- Integrate a secrets manager so tokens never live in files on disk in production.
- Replace fallback encryption with
age
as default and requireAGE_RECIPIENT
for production backups. - Add a secure admin login flow that issues short-lived httpOnly cookies and disables localStorage recommendations in docs.
- Add automated test for backup/restore and admin store decryption validation.
If you discover a security issue, open an issue titled SECURITY:
and include steps to reproduce. For sensitive reports, email the maintainer privately and mark the issue as private if your hosting provider supports it.
Protect env file example:
sudo chown root:root /etc/openinbound.env
sudo chmod 600 /etc/openinbound.env
Backup encrypt with age:
AGE_RECIPIENT="age1..." age -r "$AGE_RECIPIENT" -o /backups/openinbound-20250928.age /tmp/openinbound-data-20250928.tar.gz
Decrypt with age:
age -d -i /path/to/private-key -o out.tar.gz in.age
OpenSSL decrypt (if used):
openssl enc -d -aes-256-gcm -pbkdf2 -in openinbound.tar.gz.enc -out openinbound.tar.gz -pass env:ADMIN_STORE_KEY
This document should be reviewed and adapted to your organization's security policies before production deployment.