[{"data":1,"prerenderedAt":640},["ShallowReactive",2],{"\u002Fdocs\u002Fself-hosting\u002Fhardening":3},{"id":4,"title":5,"body":6,"description":632,"extension":633,"meta":634,"navigation":635,"path":636,"seo":637,"stem":638,"__hash__":639},"docs\u002Fdocs\u002Fself-hosting\u002Fhardening.md","Production Hardening",{"type":7,"value":8,"toc":619},"minimark",[9,13,28,33,119,123,131,146,150,172,238,254,258,291,295,323,327,334,345,349,362,365,434,449,453,460,551,561,565,592,596,604,608,615],[10,11,5],"h1",{"id":12},"production-hardening",[14,15,16,17,21,22,27],"p",{},"Delivr ships with secure defaults — encrypted credentials, hashed tokens, no mail at rest, and sanitized rendering. This checklist covers the parts that depend on ",[18,19,20],"strong",{},"your"," environment. For how Delivr itself protects data, see the ",[23,24,26],"a",{"href":25},"\u002Fsecurity","Security overview",".",[29,30,32],"h2",{"id":31},"checklist","Checklist",[34,35,38,53,59,70,80,86,92,101,107,113],"ul",{"className":36},[37],"contains-task-list",[39,40,43,48,49,52],"li",{"className":41},[42],"task-list-item",[44,45],"input",{"disabled":46,"type":47},true,"checkbox"," All traffic is served over ",[18,50,51],{},"HTTPS"," with HSTS",[39,54,56,58],{"className":55},[42],[44,57],{"disabled":46,"type":47}," Only the reverse proxy is reachable from the internet",[39,60,62,64,65,69],{"className":61},[42],[44,63],{"disabled":46,"type":47}," ",[66,67,68],"code",{},"DLA_ENCRYPTION_KEY"," is long, random, backed up, and not in version control",[39,71,73,75,76,79],{"className":72},[42],[44,74],{"disabled":46,"type":47}," Secret files have ",[66,77,78],{},"600"," permissions",[39,81,83,85],{"className":82},[42],[44,84],{"disabled":46,"type":47}," Self-service sign-up is disabled",[39,87,89,91],{"className":88},[42],[44,90],{"disabled":46,"type":47}," The API reference is disabled if you don't need it",[39,93,95,64,97,100],{"className":94},[42],[44,96],{"disabled":46,"type":47},[66,98,99],{},"DLA_TRUST_PROXY=true"," is set, and the login endpoint is also rate-limited at the proxy",[39,102,104,106],{"className":103},[42],[44,105],{"disabled":46,"type":47}," Security headers are set on the web client",[39,108,110,112],{"className":109},[42],[44,111],{"disabled":46,"type":47}," Backups are encrypted and stored off-site",[39,114,116,118],{"className":115},[42],[44,117],{"disabled":46,"type":47}," You're watching for new releases",[29,120,122],{"id":121},"transport-security","Transport security",[14,124,125,126,130],{},"Serve both domains over HTTPS only and redirect plain HTTP. Add an HSTS header so browsers never fall back to HTTP — see the ",[23,127,129],{"href":128},"#security-headers","header examples"," below.",[14,132,133,134,137,138,141,142,145],{},"The API connects to mail servers over TLS when the mail account is configured with SSL\u002FTLS enabled. Prefer port ",[66,135,136],{},"993"," (IMAPS) and ",[66,139,140],{},"465"," (SMTPS) or ",[66,143,144],{},"587"," (STARTTLS) and avoid unencrypted ports.",[29,147,149],{"id":148},"network-exposure","Network exposure",[34,151,152,169],{},[39,153,154,155,158,159,163,164,168],{},"Bind Delivr to ",[66,156,157],{},"127.0.0.1"," (the ",[23,160,162],{"href":161},"\u002Fdocs\u002Fself-hosting\u002Fdocker","Docker Compose file"," and the ",[23,165,167],{"href":166},"\u002Fdocs\u002Fself-hosting\u002Fmanual","manual guide"," do this) or keep it on an internal Docker network.",[39,170,171],{},"Allow only SSH and your proxy's ports through the firewall:",[173,174,179],"pre",{"className":175,"code":176,"language":177,"meta":178,"style":178},"language-bash shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","sudo ufw default deny incoming\nsudo ufw allow OpenSSH\nsudo ufw allow 80,443\u002Ftcp\nsudo ufw enable\n","bash","",[66,180,181,203,216,228],{"__ignoreMap":178},[182,183,186,190,194,197,200],"span",{"class":184,"line":185},"line",1,[182,187,189],{"class":188},"sBMFI","sudo",[182,191,193],{"class":192},"sfazB"," ufw",[182,195,196],{"class":192}," default",[182,198,199],{"class":192}," deny",[182,201,202],{"class":192}," incoming\n",[182,204,206,208,210,213],{"class":184,"line":205},2,[182,207,189],{"class":188},[182,209,193],{"class":192},[182,211,212],{"class":192}," allow",[182,214,215],{"class":192}," OpenSSH\n",[182,217,219,221,223,225],{"class":184,"line":218},3,[182,220,189],{"class":188},[182,222,193],{"class":192},[182,224,212],{"class":192},[182,226,227],{"class":192}," 80,443\u002Ftcp\n",[182,229,231,233,235],{"class":184,"line":230},4,[182,232,189],{"class":188},[182,234,193],{"class":192},[182,236,237],{"class":192}," enable\n",[239,240,241],"note",{},[14,242,243,244,247,248,250,251,27],{},"Docker publishes ports by editing iptables directly, which bypasses ",[66,245,246],{},"ufw",". That's why the Compose file binds ports to ",[66,249,157],{}," — don't change it to ",[66,252,253],{},"0.0.0.0",[29,255,257],{"id":256},"secrets","Secrets",[34,259,260,267,278,285],{},[39,261,262,263,266],{},"Generate the encryption key with ",[66,264,265],{},"openssl rand -hex 32"," — never reuse a password or an example value.",[39,268,269,270,273,274,277],{},"Keep ",[66,271,272],{},".env"," and Compose files containing secrets at ",[66,275,276],{},"chmod 600",", owned by root or the service user.",[39,279,280,281,284],{},"Store a copy of the key in a password manager or vault, ",[18,282,283],{},"not"," next to your database backups.",[39,286,287,288,27],{},"After setting up the admin account, delete ",[66,289,290],{},"config\u002Finitial_admin_password_reset_token.txt",[29,292,294],{"id":293},"accounts-and-access","Accounts and access",[34,296,297,306,313,316],{},[39,298,269,299,302,303,27],{},[66,300,301],{},"NUXT_PUBLIC_IS_SIGNUP_ENABLED=false"," and create users from ",[18,304,305],{},"Admin → Users",[39,307,308,309,312],{},"Give the ",[18,310,311],{},"Admin"," role to as few people as possible.",[39,314,315],{},"Remove accounts of people who leave.",[39,317,318,319,322],{},"Encourage users to give ",[18,320,321],{},"API keys"," a description and an expiry date, and to revoke keys they no longer use.",[29,324,326],{"id":325},"api-reference","API reference",[14,328,329,330,333],{},"The interactive API reference at ",[66,331,332],{},"\u002Fdocs\u002Fv1"," is handy during setup and development but lists every route. If nobody on your instance uses it, turn it off:",[173,335,339],{"className":336,"code":337,"language":338,"meta":178,"style":178},"language-dotenv shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","DLA_DISABLE_DOCS=true\n","dotenv",[66,340,341],{"__ignoreMap":178},[182,342,343],{"class":184,"line":185},[182,344,337],{},[29,346,348],{"id":347},"rate-limiting-at-the-proxy","Rate limiting at the proxy",[14,350,351,352,354,355,358,359,361],{},"Delivr limits failed sign-ins in memory — five per client and username, fifteen per username, in any five-minute window. Behind a reverse proxy, every request comes from the proxy's address, so set ",[66,353,99],{}," to have Delivr use the client address from ",[66,356,357],{},"X-Forwarded-For"," instead. Only do this when the API port is reachable solely through the proxy (as in the ",[23,360,162],{"href":161},"); otherwise clients could forge the header.",[14,363,364],{},"For defense in depth, add a limit at the proxy as well — for example with Nginx:",[173,366,370],{"className":367,"code":368,"language":369,"meta":178,"style":178},"language-nginx shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","# In the http {} block\nlimit_req_zone $binary_remote_addr zone=delivr_login:10m rate=10r\u002Fm;\n\n# In the API server {} block\nlocation = \u002Fv1\u002Fauth\u002Flogin {\n    limit_req zone=delivr_login burst=5 nodelay;\n    proxy_pass http:\u002F\u002F127.0.0.1:14123;\n    proxy_set_header Host $host;\n    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n    proxy_set_header X-Forwarded-Proto $scheme;\n}\n","nginx",[66,371,372,377,382,387,392,398,404,410,416,422,428],{"__ignoreMap":178},[182,373,374],{"class":184,"line":185},[182,375,376],{},"# In the http {} block\n",[182,378,379],{"class":184,"line":205},[182,380,381],{},"limit_req_zone $binary_remote_addr zone=delivr_login:10m rate=10r\u002Fm;\n",[182,383,384],{"class":184,"line":218},[182,385,386],{"emptyLinePlaceholder":46},"\n",[182,388,389],{"class":184,"line":230},[182,390,391],{},"# In the API server {} block\n",[182,393,395],{"class":184,"line":394},5,[182,396,397],{},"location = \u002Fv1\u002Fauth\u002Flogin {\n",[182,399,401],{"class":184,"line":400},6,[182,402,403],{},"    limit_req zone=delivr_login burst=5 nodelay;\n",[182,405,407],{"class":184,"line":406},7,[182,408,409],{},"    proxy_pass http:\u002F\u002F127.0.0.1:14123;\n",[182,411,413],{"class":184,"line":412},8,[182,414,415],{},"    proxy_set_header Host $host;\n",[182,417,419],{"class":184,"line":418},9,[182,420,421],{},"    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n",[182,423,425],{"class":184,"line":424},10,[182,426,427],{},"    proxy_set_header X-Forwarded-Proto $scheme;\n",[182,429,431],{"class":184,"line":430},11,[182,432,433],{},"}\n",[14,435,436,437,440,441,444,445,448],{},"Apply the same idea to ",[66,438,439],{},"\u002Fv1\u002Fauth\u002Freset-password\u002Frequest",". Tools like fail2ban can additionally block repeat offenders based on your proxy's access log (",[66,442,443],{},"429"," and ",[66,446,447],{},"401"," responses on these paths).",[29,450,452],{"id":451},"security-headers","Security headers",[14,454,455,456,459],{},"Set these on the ",[18,457,458],{},"web client"," domain. They prevent clickjacking, MIME sniffing, and referrer leaks:",[461,462,463,520],"code-group",{},[173,464,469],{"className":465,"code":466,"filename":467,"language":468,"meta":178,"style":178},"language-caddy shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","mail.example.com {\n    header {\n        Strict-Transport-Security \"max-age=31536000; includeSubDomains\"\n        X-Content-Type-Options \"nosniff\"\n        X-Frame-Options \"DENY\"\n        Referrer-Policy \"no-referrer\"\n        Permissions-Policy \"camera=(), microphone=(), geolocation=()\"\n    }\n    reverse_proxy 127.0.0.1:14128\n}\n","Caddy","caddy",[66,470,471,476,481,486,491,496,501,506,511,516],{"__ignoreMap":178},[182,472,473],{"class":184,"line":185},[182,474,475],{},"mail.example.com {\n",[182,477,478],{"class":184,"line":205},[182,479,480],{},"    header {\n",[182,482,483],{"class":184,"line":218},[182,484,485],{},"        Strict-Transport-Security \"max-age=31536000; includeSubDomains\"\n",[182,487,488],{"class":184,"line":230},[182,489,490],{},"        X-Content-Type-Options \"nosniff\"\n",[182,492,493],{"class":184,"line":394},[182,494,495],{},"        X-Frame-Options \"DENY\"\n",[182,497,498],{"class":184,"line":400},[182,499,500],{},"        Referrer-Policy \"no-referrer\"\n",[182,502,503],{"class":184,"line":406},[182,504,505],{},"        Permissions-Policy \"camera=(), microphone=(), geolocation=()\"\n",[182,507,508],{"class":184,"line":412},[182,509,510],{},"    }\n",[182,512,513],{"class":184,"line":418},[182,514,515],{},"    reverse_proxy 127.0.0.1:14128\n",[182,517,518],{"class":184,"line":424},[182,519,433],{},[173,521,524],{"className":367,"code":522,"filename":523,"language":369,"meta":178,"style":178},"add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\nadd_header X-Content-Type-Options \"nosniff\" always;\nadd_header X-Frame-Options \"DENY\" always;\nadd_header Referrer-Policy \"no-referrer\" always;\nadd_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n","Nginx",[66,525,526,531,536,541,546],{"__ignoreMap":178},[182,527,528],{"class":184,"line":185},[182,529,530],{},"add_header Strict-Transport-Security \"max-age=31536000; includeSubDomains\" always;\n",[182,532,533],{"class":184,"line":205},[182,534,535],{},"add_header X-Content-Type-Options \"nosniff\" always;\n",[182,537,538],{"class":184,"line":218},[182,539,540],{},"add_header X-Frame-Options \"DENY\" always;\n",[182,542,543],{"class":184,"line":230},[182,544,545],{},"add_header Referrer-Policy \"no-referrer\" always;\n",[182,547,548],{"class":184,"line":394},[182,549,550],{},"add_header Permissions-Policy \"camera=(), microphone=(), geolocation=()\" always;\n",[552,553,554],"caution",{},[14,555,556,557,560],{},"If you also add a ",[18,558,559],{},"Content-Security-Policy",", test it thoroughly: the web client registers its service worker with a small inline script, and email content and BIMI logos load images from other origins. A too-strict policy breaks the app or hides images users chose to load.",[29,562,564],{"id":563},"updates-and-monitoring","Updates and monitoring",[34,566,567,574,581],{},[39,568,569,570,573],{},"Watch both repositories for releases (",[18,571,572],{},"Watch → Custom → Releases",") and apply security updates promptly.",[39,575,576,577,580],{},"Monitor the health endpoint ",[66,578,579],{},"https:\u002F\u002Fapi.mail.example.com\u002Fhealth"," with your uptime tool.",[39,582,583,584,587,588,591],{},"Keep the API's log level at ",[66,585,586],{},"info"," in production; use ",[66,589,590],{},"debug"," only temporarily, as it's verbose.",[29,593,595],{"id":594},"backups","Backups",[14,597,598,599,603],{},"Follow the ",[23,600,602],{"href":601},"\u002Fdocs\u002Fself-hosting\u002Fupgrading#backups","backup guide",". Encrypt backups, keep them off-site, and test a restore at least once.",[29,605,607],{"id":606},"reporting-vulnerabilities","Reporting vulnerabilities",[14,609,610,611,27],{},"Found a security issue in Delivr itself? Please report it privately — see ",[23,612,614],{"href":613},"\u002Fsecurity#disclosure","Responsible disclosure",[616,617,618],"style",{},"html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":178,"searchDepth":205,"depth":205,"links":620},[621,622,623,624,625,626,627,628,629,630,631],{"id":31,"depth":205,"text":32},{"id":121,"depth":205,"text":122},{"id":148,"depth":205,"text":149},{"id":256,"depth":205,"text":257},{"id":293,"depth":205,"text":294},{"id":325,"depth":205,"text":326},{"id":347,"depth":205,"text":348},{"id":451,"depth":205,"text":452},{"id":563,"depth":205,"text":564},{"id":594,"depth":205,"text":595},{"id":606,"depth":205,"text":607},"A security checklist for Delivr instances exposed to the internet: TLS, secrets, firewalls, security headers, rate limiting, and updates.","md",{},{"title":5},"\u002Fdocs\u002Fself-hosting\u002Fhardening",{"title":5,"description":632},"docs\u002Fself-hosting\u002Fhardening","f9NO8xAxOJAW_xM38vVqJ5jHUK6oQMaE9RQllrsTH-E",1791069427428]