1"use strict";(globalThis.webpackChunkemailengine_temp=globalThis.webpackChunkemailengine_temp||[]).push([[1061],{18952(e,n,i){i.r(n),i.d(n,{assets:()=>a,contentTitle:()=>l,default:()=>h,frontMatter:()=>c,metadata:()=>s,toc:()=>o});const s=JSON.parse('{"id":"advanced/encryption","title":"Secret Encryption","description":"Enable field-level encryption for sensitive data like passwords and OAuth tokens","source":"@site/docs/advanced/encryption.md","sourceDirName":"advanced","slug":"/advanced/encryption","permalink":"/docs/advanced/encryption","draft":false,"unlisted":false,"tags":[],"version":"current","sidebarPosition":3,"frontMatter":{"title":"Secret Encryption","sidebar_position":3,"description":"Enable field-level encryption for sensitive data like passwords and OAuth tokens"},"sidebar":"docsSidebar","previous":{"title":"Message IDs Explained","permalink":"/docs/advanced/ids-explained"},"next":{"title":"Logging","permalink":"/docs/advanced/logging"}}');var r=i(74848),t=i(28453);const c={title:"Secret Encryption",sidebar_position:3,description:"Enable field-level encryption for sensitive data like passwords and OAuth tokens"},l="Secret Encryption",a={},o=[{value:"Overview",id:"overview",level:2},{value:"Why Enable Encryption?",id:"why-enable-encryption",level:2},{value:"Security Benefits",id:"security-benefits",level:3},{value:"What Gets Encrypted",id:"what-gets-encrypted",level:3},{value:"Important Considerations",id:"important-considerations",level:2},{value:"How Encryption Works with Existing Data",id:"how-encryption-works-with-existing-data",level:3},{value:"Enabling Encryption on New Instance",id:"enabling-encryption-on-new-instance",level:2},{value:"1. Set Encryption Secret",id:"1-set-encryption-secret",level:3},{value:"2. Start EmailEngine",id:"2-start-emailengine",level:3},{value:"Environment Variable Best Practices",id:"environment-variable-best-practices",level:3},{value:"Enabling Encryption on Existing Instance",id:"enabling-encryption-on-existing-instance",level:2},{value:"Process Overview",id:"process-overview",level:3},{value:"Step-by-Step Instructions",id:"step-by-step-instructions",level:3},{value:"1. Stop EmailEngine",id:"1-stop-emailengine",level:4},{value:"2. Run Encryption Migration",id:"2-run-encryption-migration",level:4},{value:"3. Start EmailEngine",id:"3-start-emailengine",level:4},{value:"Changing Encryption Secret",id:"changing-encryption-secret",level:2},{value:"When to Change",id:"when-to-change",level:3},{value:"Process",id:"process",level:3},{value:"1. Stop EmailEngine",id:"1-stop-emailengine-1",level:4},{value:"2. Run Migration with Old and New Secret",id:"2-run-migration-with-old-and-new-secret",level:4},{value:"3. Start EmailEngine with New Secret",id:"3-start-emailengine-with-new-secret",level:4},{value:"Multiple Old Secrets",id:"multiple-old-secrets",level:3},{value:"Disabling Encryption",id:"disabling-encryption",level:2},{value:"When to Disable",id:"when-to-disable",level:3},{value:"Process",id:"process-1",level:3},{value:"1. Stop EmailEngine",id:"1-stop-emailengine-2",level:4},{value:"2. Run Decryption Migration",id:"2-run-decryption-migration",level:4},{value:"3. Start EmailEngine Without Secret",id:"3-start-emailengine-without-secret",level:4},{value:"Secret Management Best Practices",id:"secret-management-best-practices",level:2},{value:"1. Use Strong Secrets",id:"1-use-strong-secrets",level:3},{value:"2. Secret Rotation",id:"2-secret-rotation",level:3},{value:"3. Backup Considerations",id:"3-backup-considerations",level:3},{value:"Using Secret Management Systems",id:"using-secret-management-systems",level:2},{value:"HashiCorp Vault",id:"hashicorp-vault",level:3},{value:"AWS Secrets Manager",id:"aws-secrets-manager",level:3},{value:"Kubernetes Secrets",id:"kubernetes-secrets",level:3},{value:"Docker Secrets",id:"docker-secrets",level:3},{value:"Migration Planning",id:"migration-planning",level:2},{value:"Migration Steps",id:"migration-steps",level:3},{value:"Rollback Plan",id:"rollback-plan",level:3},{value:"Key Points",id:"key-points",level:2},{value:"See Also",id:"see-also",level:2}];function d(e){const n={a:"a",admonition:"admonition",code:"code",h1:"h1",h2:"h2",h3:"h3",h4:"h4",header:"header",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,t.R)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.header,{children:(0,r.jsx)(n.h1,{id:"secret-encryption",children:"Secret Encryption"})}),"\n",(0,r.jsx)(n.p,{children:"Learn how to enable field-level encryption for sensitive data stored by EmailEngine, including passwords, OAuth tokens, and API secrets."}),"\n",(0,r.jsx)(n.h2,{id:"overview",children:"Overview"}),"\n",(0,r.jsx)(n.p,{children:"By default, EmailEngine stores all data in cleartext in Redis. This is fine for testing but not recommended for production environments."}),"\n",(0,r.jsxs)(n.p,{children:["EmailEngine offers ",(0,r.jsx)(n.strong,{children:"field-level encryption"})," that encrypts all sensitive fields using the ",(0,r.jsx)(n.strong,{children:"AES-256-GCM"})," cipher:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Account passwords"}),"\n",(0,r.jsx)(n.li,{children:"OAuth access and refresh tokens"}),"\n",(0,r.jsx)(n.li,{children:"OAuth2 application client secrets and service account keys"}),"\n",(0,r.jsx)(n.li,{children:"The settings and TLS private keys listed below"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"why-enable-encryption",children:"Why Enable Encryption?"}),"\n",(0,r.jsx)(n.h3,{id:"security-benefits",children:"Security Benefits"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Data at rest protection"}
1),": A copy of the Redis database, or of its backups, does not expose the stored credentials without the secret"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Compliance"}),": Encryption of stored credentials is a requirement of many security standards"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Defense in depth"}),": An additional layer beyond network access control on Redis"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"what-gets-encrypted",children:"What Gets Encrypted"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Account credentials"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"IMAP passwords"}),"\n",(0,r.jsx)(n.li,{children:"SMTP passwords"}),"\n",(0,r.jsx)(n.li,{children:"OAuth access tokens"}),"\n",(0,r.jsx)(n.li,{children:"OAuth refresh tokens"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"OAuth2 applications"})," (",(0,r.jsx)(n.code,{children:"clientSecret"}),", ",(0,r.jsx)(n.code,{children:"serviceKey"}),", ",(0,r.jsx)(n.code,{children:"externalAccount"})," and ",(0,r.jsx)(n.code,{children:"accessToken"})," in the app record):"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Client secrets"}),"\n",(0,r.jsx)(n.li,{children:"Service account keys and external-account (workload identity federation) configurations"}),"\n",(0,r.jsx)(n.li,{children:"The app-level access token of an application-access (client credentials) app"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"SMTP gateways"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Gateway passwords"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Settings"})," (",(0,r.jsx)(n.code,{children:"smtpServerPassword"}),", ",(0,r.jsx)(n.code,{children:"imapProxyServerPassword"}),", ",(0,r.jsx)(n.code,{children:"serviceSecret"}),", ",(0,r.jsx)(n.code,{children:"cookiePassword"}),", ",(0,r.jsx)(n.code,{children:"totpSeed"}),", ",(0,r.jsx)(n.code,{children:"openAiAPIKey"}),", ",(0,r.jsx)(n.code,{children:"documentStorePassword"}),", and the legacy ",(0,r.jsx)(n.code,{children:"gmailClientSecret"}),", ",(0,r.jsx)(n.code,{children:"outlookClientSecret"}),", ",(0,r.jsx)(n.code,{children:"mailRuClientSecret"}),", ",(0,r.jsx)(n.code,{children:"gmailServiceKey"})," and ",(0,r.jsx)(n.code,{children:"gmailServiceExternalAccount"})," values):"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"The SMTP server and IMAP proxy global passwords"}),"\n",(0,r.jsxs)(n.li,{children:["The ",(0,r.jsx)(n.code,{children:"serviceSecret"})," used for signing, the admin session cookie password and the admin TOTP seed"]}),"\n",(0,r.jsx)(n.li,{children:"The OpenAI API key and the Document Store password"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"TLS private keys"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"The ACME account key and the private key of every certificate EmailEngine provisions for its own listeners"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Not encrypted"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Email content (not stored by default)"}),"\n",(0,r.jsx)(n.li,{children:"Metadata (subject lines, senders, etc.)"}),"\n",(0,r.jsx)(n.li,{children:"Account IDs, and the rest of the account record: the IMAP and SMTP host names, the account's webhook URL and its custom headers, including an authorization header set there"}),"\n",(0,r.jsx)(n.li,{children:"Every other setting"}),"\n",(0,r.jsx)(n.li,{children:"Access tokens, which are not stored at all: only their SHA-256 hashes are, so the stored value cannot be used as a token"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"important-considerations",children:"Important Considerations"}),"\n",(0,r.jsx)(n.admonition,{type:"warning",children:(0,r.jsxs)(n.p,{children:["To encrypt credentials that are already stored, run the encryption migration tool. Setting ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," on its own only affects values written after that point, so existing credentials stay in cleartext."]})}),"\n",(0,r.jsx)(n.h3,{id:"how-encryption-works-with-existing-data",children:"How Encryption Works with Existing Data"}),"\n",(0,r.jsxs)(n.p,{children:["When you enable ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," on an instance with existing accounts:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Existing accounts continue working"})," - EmailEngine can read both encrypted and unencrypted credentials"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Existing credentials remain unencrypted"})," - They are not automatically migrated"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"New accounts get encrypted credentials"})," - Any account added after enabling encryption stores credentials encrypted"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"OAuth2 tokens encrypt on renew
1al"})," - When EmailEngine refreshes an OAuth2 access token, the new tokens are stored encrypted"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"IMAP/SMTP passwords stay unencrypted"})," - They are encrypted the next time the account's credentials are saved, or when you run the migration tool; until then they remain in cleartext"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:["This means you can enable encryption without downtime, but for full protection you should run the ",(0,r.jsx)(n.code,{children:"emailengine encrypt"})," migration tool to encrypt all existing credentials."]}),"\n",(0,r.jsx)(n.h2,{id:"enabling-encryption-on-new-instance",children:"Enabling Encryption on New Instance"}),"\n",(0,r.jsx)(n.p,{children:"If you don't have any email accounts set up yet, this is the easiest approach."}),"\n",(0,r.jsx)(n.h3,{id:"1-set-encryption-secret",children:"1. Set Encryption Secret"}),"\n",(0,r.jsxs)(n.p,{children:["Create a ",(0,r.jsx)(n.code,{children:".env"})," file in your working directory:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'echo "EENGINE_SECRET=your-secret-password-here" > .env\n'})}),"\n",(0,r.jsx)(n.p,{children:"Or generate a random secret:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'echo "EENGINE_SECRET=$(openssl rand -hex 32)" > .env\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Note:"})," EmailEngine loads environment variables from a ",(0,r.jsx)(n.code,{children:".env"})," file in the current working directory (through ",(0,r.jsx)(n.code,{children:"dotenv"}),"), so this file is read on the next start."]}),"\n",(0,r.jsx)(n.h3,{id:"2-start-emailengine",children:"2. Start EmailEngine"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"emailengine\n"})}),"\n",(0,r.jsx)(n.p,{children:"Every credential stored from now on is encrypted."}),"\n",(0,r.jsx)(n.h3,{id:"environment-variable-best-practices",children:"Environment Variable Best Practices"}),"\n",(0,r.jsxs)(n.admonition,{type:"tip",children:[(0,r.jsxs)(n.p,{children:["Don't provide environment variables using the ",(0,r.jsx)(n.code,{children:"export"})," command in production. Instead:"]}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"SystemD Service"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-ini",children:'[Service]\nEnvironment="EENGINE_SECRET=secret-password"\n'})}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Docker Compose"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"services:\n emailengine:\n environment:\n - EENGINE_SECRET=secret-password\n"})}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Docker Run"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker run -e EENGINE_SECRET=secret-password postalsys/emailengine\n"})}),(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:".env File"}),":"]}),(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# .env file in working directory\nEENGINE_SECRET=secret-password\n"})})]}),"\n",(0,r.jsx)(n.h2,{id:"enabling-encryption-on-existing-instance",children:"Enabling Encryption on Existing Instance"}),"\n",(0,r.jsx)(n.p,{children:"If you already have email accounts configured, you need to encrypt existing data before enabling encryption."}),"\n",(0,r.jsx)(n.h3,{id:"process-overview",children:"Process Overview"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Stop EmailEngine"}),"\n",(0,r.jsx)(n.li,{children:"Run encryption migration tool"}),"\n",(0,r.jsx)(n.li,{children:"Start EmailEngine with encryption enabled"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"step-by-step-instructions",children:"Step-by-Step Instructions"}),"\n",(0,r.jsx)(n.h4,{id:"1-stop-emailengine",children:"1. Stop EmailEngine"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# SystemD\nsudo systemctl stop emailengine\n\n# Docker\ndocker stop emailengine\n\n# PM2\npm2 stop emailengine\n\n# Direct process\npkill emailengine\n"})}),"\n",(0,r.jsx)(n.h4,{id:"2-run-encryption-migration",children:"2. Run Encryption Migration"}),"\n",(0,r.jsxs)(n.p,{children:["The encryption migration tool is the same ",(0,r.jsx)(n.code,{children:"emailengine"})," command with the ",(0,r.jsx)(n.code,{children:"encrypt"})," argument. You can run this command from any machine that has network access to the Redis database."]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'emailengine encrypt \\\n --dbs.redis="redis://localhost:6379/8" \\\n --service.secret="your-secret-password-here"\n'})}),"\n",(0,r.jsx)(n.p,{children:"Or using environment variables:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'export EENGINE_SECRET="your-secret-password-here"\nexport EENGINE_REDIS="redis://localhost:6379/8"\nemailengine encrypt\n'})}),"\n",(0,r.jsx)(n.admonition,{title:"Run From Anywhere",type:"tip",children:(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"encrypt"})," command only needs Redis connectivity. You can run it from your local machine, a CI/CD pipeline, or any server with access to the Redis database."]})}),"\n",(0,r.jsx)(n.p,{children:"The tool will:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Connect to Redis"}),"\n",(0,r.jsxs)(n.li,{children:["Find all unencrypted secrets in every store listed under ",(0,r.jsx)(n.a,{href:"#what-gets-encrypted",children:"What Gets Encrypted"})]}),"\n",(0,r.jsx)(n.li,{children:"Encrypt them with the provided secret"}),"\n",(0,r.jsx)(n.li,{children:"Store encrypted values back to Redis"}),"\n",(0,r.jsx)(n.li,{children:"Exit"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"EENGINE_SECRET_FILE"})," and ",(0,r.jsx)(n.code,{children:"EENGINE_REDIS_FILE"}
1)," work here the same way as for the server, so the secret can be read from a mounted file rather than passed on the command line."]}),"\n",(0,r.jsx)(n.h4,{id:"3-start-emailengine",children:"3. Start EmailEngine"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'export EENGINE_SECRET="your-secret-password-here"\nemailengine\n'})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"SystemD"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl start emailengine\n"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Docker"}),":"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"docker start emailengine\n"})}),"\n",(0,r.jsx)(n.h2,{id:"changing-encryption-secret",children:"Changing Encryption Secret"}),"\n",(0,r.jsx)(n.h3,{id:"when-to-change",children:"When to Change"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Suspected secret compromise"}),"\n",(0,r.jsx)(n.li,{children:"Regular security rotation policy"}),"\n",(0,r.jsx)(n.li,{children:"Security audit requirements"}),"\n",(0,r.jsx)(n.li,{children:"Compliance regulations"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"process",children:"Process"}),"\n",(0,r.jsx)(n.h4,{id:"1-stop-emailengine-1",children:"1. Stop EmailEngine"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl stop emailengine\n"})}),"\n",(0,r.jsx)(n.h4,{id:"2-run-migration-with-old-and-new-secret",children:"2. Run Migration with Old and New Secret"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'emailengine encrypt \\\n --dbs.redis="redis://localhost:6379/8" \\\n --service.secret="new-secret-password" \\\n --decrypt="old-secret-password"\n'})}),"\n",(0,r.jsx)(n.p,{children:"This will:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Decrypt using old secret"}),"\n",(0,r.jsx)(n.li,{children:"Re-encrypt using new secret"}),"\n",(0,r.jsx)(n.li,{children:"Store updated values"}),"\n"]}),"\n",(0,r.jsx)(n.p,{children:"The command reports what it rotated, and it covers every store that holds an encrypted value. Settings holding secrets come first, one line per setting that changed, then one line per account, gateway, app and certificate entry that was rewritten, each store closing with a count:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{children:"smtpServerPassword: Updated setting value\nuser123: updated\nuser456: updated\nUpdated 2/2 accounts\nGateway sendgrid: updated\nUpdated 1/1 SMTP gateways\nOAuth2 App AAABhaBPHscAAAAI: updated\nUpdated 1/1 OAuth2 apps\nCertificate entry domain:emailengine.example.com:privateKey: updated\nUpdated 1 TLS private keys\n"})}),"\n",(0,r.jsx)(n.p,{children:"The first number in each count is how many records were rewritten, the second how many exist. A record that held nothing to change, because it stores no secret or was already encrypted with the new secret, is not counted, so a lower first number is not an error on its own."}),"\n",(0,r.jsxs)(n.admonition,{title:'Check for "Could not process" lines before starting EmailEngine again',type:"warning",children:[(0,r.jsxs)(n.p,{children:["A value that none of the supplied secrets could decrypt is reported on stderr as ",(0,r.jsx)(n.code,{children:'Could not process "imap.auth.pass" for user123. Check decryption secrets.'})," (the field and record vary) and is left untouched, so it remains readable only with the ",(0,r.jsx)(n.strong,{children:"old"})," secret. Whatever owns it breaks on next use, with no self-healing path. Keep the old secret until a run completes without such lines."]}),(0,r.jsxs)(n.p,{children:["Rotating everything requires EmailEngine v2.77.0 or newer. Earlier versions reported ",(0,r.jsx)(n.code,{children:"Updated 0/0 SMTP gateways"})," because they read the wrong index and never visited a gateway, left the ",(0,r.jsx)(n.code,{children:"externalAccount"})," field of OAuth2 apps using workload identity federation under the old secret, and never touched the TLS private keys, while exiting successfully."]})]}),"\n",(0,r.jsx)(n.h4,{id:"3-start-emailengine-with-new-secret",children:"3. Start EmailEngine with New Secret"}),"\n",(0,r.jsx)(n.p,{children:"Update your EmailEngine configuration to use the new secret, then start:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl start emailengine\n"})}),"\n",(0,r.jsx)(n.h3,{id:"multiple-old-secrets",children:"Multiple Old Secrets"}),"\n",(0,r.jsx)(n.p,{children:"If you have accounts encrypted with different secrets (after a botched migration), you can provide multiple old secrets:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'emailengine encrypt \\\n --dbs.redis="redis://localhost:6379/8" \\\n --service.secret="new-secret" \\\n --decrypt="old-secret-1" \\\n --decrypt="old-secret-2" \\\n --decrypt="old-secret-3"\n'})}),"\n",(0,r.jsx)(n.p,{children:"The tool will try each old secret until one works for each account."}),"\n",(0,r.jsx)(n.h2,{id:"disabling-encryption",children:"Disabling Encryption"}),"\n",(0,r.jsx)(n.h3,{id:"when-to-disable",children:"When to Disable"}),"\n",(0,r.jsx)(n.p,{children:"Generally not recommended for production, but valid for:"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Moving to development environment"}),"\n",(0,r.jsx)(n.li,{children:"Testing unencrypted performance"}),"\n",(0,r.jsx)(n.li,{children:"Troubleshooting encryption issues"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"process-1",children:"Process"}),"\n",(0,r.jsx)(n.h4,{id:"1-stop-emailengine-2",children:"1. Stop EmailEngine"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl stop emailengine\n"})}),"\n",(0,r.jsx)(n.h4,{id:"2-run-decryption-migration",children:"2. Run Decryption Migration"}),"\n",(0,r.jsxs)(n.p,{children:["Provide old secret with ",(0,r.jsx)(n.code,{children:"--decrypt"})," but no new secret:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'emailengine encrypt \\\n --dbs.redis="redis://localhost:6379/8" \\\n --decrypt="old-secret-password"\n'})}),"\n",(0,r.jsx)(n.p,{children:"This decrypts all secrets and stores them in cleartext."}),"\n",(0,r.jsxs)(n.p,{children:["The tool takes the encryption secret from ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," first, including the value a ",(0,r.jsx)(n.code,{children:".env"})," file in the working directory sets, and falls back to ",(0,r.jsx)(n.code,{children:"--service.secret"}),". Clear ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," from the environment and from ",(0,r.jsx)(n.code,{children:".env"})," before this run, otherwise the values are re-encrypted with it instead of being written back in cleartext."]}),"\n",(0,r.jsx)(n.h4,{id:"3-start-emailengine-without-secret",children:"3. Start EmailEngine Without Secret"}),"\n",(0,r.jsxs)(n.p,{children:["Remove ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," from your EmailEngine configuration, then start:"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl start emailengine\n"})}),"\n",(0,r.jsx)(n.h2,{id:"secret-management-best-practices",children:"Secret Management Best Practices"}),"\n",(0,r.jsx)(n.h3,{id:"1-use-strong-secrets",children:"1. Use Strong Secrets"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Generate strong random secret\nopenssl rand -base64 32\n\n# Or use password generator\npwgen -s 64 1\n"})}),"\n",(0,r.jsx)(n.p,{children:"EmailEngine derives the AES-256 key from the secret with scrypt (Node.js defaults: N=16384, r=8, p=1) and a random 16-byte salt per stored value, and does not enforce a minimum length. Treat a 32-byte random value as the floor, and do not reuse the secret anywhere else."}),"\n",(0,r.jsx)(n.h3,{id:"2-secret-rotation",children:"2. Secret Rotation"}),"\n",(0,r.jsx)(n.p,{children:"Implement regular rotation schedule:"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Recommended schedule"}),":"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"High security"}),": Every 30-90 days"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"Normal security"}),": Every 6-12 months"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.strong,{children:"After incidents"}),": Immediately"]}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Process"}),":"]}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsx)(n.li,{children:"Generate new secret"}),"\n",(0,r.jsx)(n.li,{children:"Schedule maintenance window"}),"\n",(0,r.jsx)(n.li,{children:'Run migration (see "Changing Encryption Secret")'}),"\n",(0,r.jsx)(n.li,{children:"Update secret storage systems"}),"\n",(0,r.jsx)(n.li,{children:"Verify all services working"}),"\n",(0,r.jsx)(n.li,{children:"Document change"}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"3-backup-considerations",children:"3. Backup Considerations"}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Encrypted backups"}),": Redis backups contain encrypted data, but you MUST securely store:"]}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"The encryption secret itself"}),"\n",(0,r.jsx)(n.li,{children:"Recovery procedures"}),"\n",(0,r.jsx)(n.li,{children:"Documentation of encryption status"}),"\n"]}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Without the secret"}),": Encrypted data is unrecoverable."]}),"\n",(0,r.jsx)(n.h2,{id:"using-secret-management-systems",children:"Using Secret Management Systems"}),"\n",(0,r.jsx)(n.h3,{id:"hashicorp-vault",children:"HashiCorp Vault"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"#!/bin/bash\n# Fetch secret from Vault\nexport EENGINE_SECRET=$(vault kv get -field=encryption_key secret/emailengine)\nemailengine\n"})}),"\n",(0,r.jsx)(n.h3,{id:"aws-secrets-manager",children:"AWS Secrets Manager"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"#!/bin/bash\n# Fetch from AWS Secrets Manager\nexport EENGINE_SECRET=$(aws secretsmanager get-secret-value \\\n --secret-id emailengine/encryption-key \\\n --query SecretString \\\n --output text)\nemailengine\n"})}),"\n",(0,r.jsx)(n.h3,{id:"kubernetes-secrets",children:"Kubernetes Secrets"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-yaml",children:"apiVersion: v1\nkind: Secret\nmetadata:\n name: emailengine-secrets\ntype: Opaque\
1nstringData:\n encryption-key: your-secret-here\n---\napiVersion: v1\nkind: Pod\nmetadata:\n name: emailengine\nspec:\n containers:\n - name: emailengine\n image: postalsys/emailengine\n env:\n - name: EENGINE_SECRET\n valueFrom:\n secretKeyRef:\n name: emailengine-secrets\n key: encryption-key\n"})}),"\n",(0,r.jsx)(n.h3,{id:"docker-secrets",children:"Docker Secrets"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'# Create secret\necho "your-secret-password" | docker secret create ee_encryption_key -\n\n# Use in service\ndocker service create \\\n --name emailengine \\\n --secret ee_encryption_key \\\n --env EENGINE_SECRET_FILE=/run/secrets/ee_encryption_key \\\n postalsys/emailengine\n'})}),"\n",(0,r.jsxs)(n.p,{children:["The ",(0,r.jsx)(n.code,{children:"_FILE"})," suffix tells EmailEngine to read the secret from the specified file path. Most other environment variables accept the same suffix - see ",(0,r.jsx)(n.a,{href:"/docs/configuration/environment-variables#loading-values-from-files",children:"Loading Values From Files"}),"."]}),"\n",(0,r.jsx)(n.h2,{id:"migration-planning",children:"Migration Planning"}),"\n",(0,r.jsx)(n.h3,{id:"migration-steps",children:"Migration Steps"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Backup"})," Redis database"]}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"redis-cli --rdb /backup/redis-backup-$(date +%Y%m%d).rdb\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Test in staging"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"# Restore backup to staging\n# Run migration\n# Verify functionality\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Schedule maintenance"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Choose low-traffic period"}),"\n",(0,r.jsx)(n.li,{children:"The tool rewrites one Redis hash per account, gateway and app, so the run is short even for large instances, but EmailEngine is stopped for its duration"}),"\n",(0,r.jsx)(n.li,{children:"Have team on standby"}),"\n"]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Execute migration"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'sudo systemctl stop emailengine\n\n# If enabling encryption for the first time (no existing encryption):\nemailengine encrypt \\\n --dbs.redis="redis://localhost:6379/8" \\\n --service.secret="your-new-secret"\n\n# If changing an existing encryption secret:\nemailengine encrypt \\\n --dbs.redis="redis://localhost:6379/8" \\\n --service.secret="your-new-secret" \\\n --decrypt="your-old-secret"\n\nsudo systemctl start emailengine\n'})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Verify"})}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsx)(n.li,{children:"Check logs for errors"}),"\n",(0,r.jsx)(n.li,{children:"Test account connections"}),"\n",(0,r.jsx)(n.li,{children:"Verify emails sending/receiving"}),"\n",(0,r.jsx)(n.li,{children:"Monitor for issues"}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h3,{id:"rollback-plan",children:"Rollback Plan"}),"\n",(0,r.jsx)(n.p,{children:"If migration fails:"}),"\n",(0,r.jsxs)(n.ol,{children:["\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Stop EmailEngine"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl stop emailengine\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Restore Redis backup"})}),"\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.code,{children:"redis-cli --rdb"})," only takes a snapshot;
1 it cannot load one. Restoring means stopping Redis, replacing its ",(0,r.jsx)(n.code,{children:"dump.rdb"})," with the backup, and starting Redis again. See ",(0,r.jsx)(n.a,{href:"/docs/configuration/redis",children:"Redis Configuration"})," for where that file lives on your install."]}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Start without encryption"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"unset EENGINE_SECRET\nsudo systemctl start emailengine\n"})}),"\n"]}),"\n",(0,r.jsxs)(n.li,{children:["\n",(0,r.jsxs)(n.p,{children:[(0,r.jsx)(n.strong,{children:"Investigate"})," issue before retrying"]}),"\n"]}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"key-points",children:"Key Points"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:["Set ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," before an instance stores any credentials, so nothing is ever written in the clear"]}),"\n",(0,r.jsx)(n.li,{children:"EmailEngine must be stopped while enabling, rotating, or removing the secret"}),"\n",(0,r.jsxs)(n.li,{children:["Keep the old secret until a rotation completes without a ",(0,r.jsx)(n.code,{children:"Could not process"})," line"]}),"\n",(0,r.jsx)(n.li,{children:"Back up Redis before any migration, and rehearse it against a copy first"}),"\n",(0,r.jsx)(n.li,{children:"Store the secret where it survives the loss of the server: without it, every stored credential is unrecoverable"}),"\n"]}),"\n",(0,r.jsx)(n.h2,{id:"see-also",children:"See Also"}),"\n",(0,r.jsxs)(n.ul,{children:["\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/configuration/environment-variables",children:"Environment Variables"})," - ",(0,r.jsx)(n.code,{children:"EENGINE_SECRET"})," and the ",(0,r.jsx)(n.code,{children:"_FILE"})," form for mounted secrets"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/configuration/cli",children:"CLI Reference"})," - Full options for the ",(0,r.jsx)(n.code,{children:"encrypt"})," command"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/deployment/security",children:"Security Hardening"})," - The other half of protecting an instance"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/deployment/compliance",children:"Compliance"})," - What EmailEngine stores, encrypted and not"]}),"\n",(0,r.jsxs)(n.li,{children:[(0,r.jsx)(n.a,{href:"/docs/configuration/redis",children:"Redis Configuration"})," - Persistence and access control for the store holding this data"]}),"\n"]})]})}function h(e={}){const{wrapper:n}={...(0,t.R)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},28453(e,n,i){i.d(n,{R:()=>c,x:()=>l});var s=i(96540);const r={},t=s.createContext(r);function c(e){const n=s.useContext(t);return s.useMemo(function(){return"function"==typeof e?e(n):{...n,...e}},[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),s.createElement(t.Provider,{value:n},e.children)}}}]);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.