A fresh Senddera install is functionally complete the moment the web installer turns green — but it isn't yet production-grade. The default configuration leaves several easy-to-fix exposures: SSH password auth on, no fail2ban, MySQL bound to 0.0.0.0 (in some installs), no automated backups, no monitoring. This checklist closes each one in order. Run through it within the first hour of going live.
Each item lists the what, the why (so you can decide if it applies to your threat model), and the command. The whole list is ~30 minutes of work. None of it is theoretical — every entry has been the cause of a real incident in someone's Senddera deployment.
SSH — 4 controls
1. Disable root login
Why: root SSH is the #1 brute-force target. Any intruder with root credentials owns the box; with a non-root sudo user, they need both the password and to know which user is privileged.
sudo sed -i 's/^#*PermitRootLogin .*/PermitRootLogin no/' /etc/ssh/sshd_config
sudo systemctl reload ssh
2. Disable password authentication
Why: even a 16-character password is brute-forceable given enough time. SSH keys are not. Once you've copied your key (ssh-copy-id), turn passwords off.
sudo sed -i 's/^#*PasswordAuthentication .*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*ChallengeResponseAuthentication .*/ChallengeResponseAuthentication no/' /etc/ssh/sshd_config
sudo sed -i 's/^#*UsePAM .*/UsePAM no/' /etc/ssh/sshd_config
sudo systemctl reload ssh
Verify from a different terminal — never close your active SSH session before confirming the new config works.
3. Move SSH off port 22
Why: port 22 attracts the bulk of bot scanning. Moving to a non-standard port (e.g. 2200) cuts log noise and makes fail2ban's job easier. This is security through obscurity, but it materially reduces noise — and combined with the other controls, it's worthwhile.
sudo sed -i 's/^#*Port .*/Port 2200/' /etc/ssh/sshd_config
sudo ufw allow 2200/tcp
sudo systemctl reload ssh
Don't forget to update your hosts file or SSH config: Host mail-server\n HostName mail.example.com\n Port 2200.
4. Install fail2ban
Why: even with key auth + non-standard port, the SSH daemon will see thousands of probe attempts/day. fail2ban auto-bans IPs that fail multiple times.
sudo apt install -y fail2ban
sudo tee /etc/fail2ban/jail.local <<'CONF'
[DEFAULT]
bantime = 1h
findtime = 10m
maxretry = 5
[sshd]
enabled = true
port = 2200
[nginx-limit-req]
enabled = true
filter = nginx-limit-req
action = iptables-multiport[name=ReqLimit, port="http,https", protocol=tcp]
logpath = /var/log/nginx/*error.log
maxretry = 10
findtime = 10m
bantime = 1h
CONF
sudo systemctl enable --now fail2ban
The nginx jail catches admin-panel brute-forcers (hitting /admin/login with a password list).
Firewall — 1 control
5. Confirm UFW is on with the right rules
Why: Senddera only needs SSH + HTTP + HTTPS inbound. Anything else (MySQL on 3306, Redis on 6379, php-fpm on 9000 if TCP) should never be reachable from the internet.
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 2200/tcp # SSH (or 22 if you skipped #3)
sudo ufw allow 'Nginx Full' # 80 + 443
sudo ufw enable
sudo ufw status verbose
For Rocky Linux: firewall-cmd --permanent --add-service=ssh --add-service=http --add-service=https && firewall-cmd --reload.
MySQL — 3 controls
6. Bind MySQL to localhost
Why: MySQL bound to 0.0.0.0 is reachable from anywhere unless the firewall blocks it. If you forgot the firewall, your database is internet-exposed. Defense in depth.
# /etc/mysql/mysql.conf.d/mysqld.cnf (Ubuntu/Debian)
# Confirm: bind-address = 127.0.0.1
sudo grep -E '^bind-address' /etc/mysql/mysql.conf.d/mysqld.cnf
# If it's 0.0.0.0, change it to 127.0.0.1 and:
sudo systemctl restart mysql
If your DB is on a separate host (RDS, DO Managed DB), bind-address is irrelevant — instead, the DB's security group should restrict to the application instance's IP.
7. Create a non-root DB user with minimum privileges
Why: the Senddera DB user only needs DML on its own database. If the install instructions had you create one with GRANT ALL (which most do), narrow it to actual privileges.
-- Already created in install: 'Senddera'@'localhost' with ALL on Senddera.*
-- That's already locally-scoped, so this is mostly belt-and-suspenders.
-- For paranoid hardening, replace ALL with the minimum set:
REVOKE ALL PRIVILEGES ON Senddera.* FROM 'Senddera'@'localhost';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER,
LOCK TABLES, CREATE TEMPORARY TABLES, REFERENCES
ON Senddera.* TO 'Senddera'@'localhost';
FLUSH PRIVILEGES;
8. Take a baseline backup
Why: the first backup is the cheapest. After 6 months of campaigns, the cost (and the temptation to "just skip it this time") is much higher.
sudo apt install -y mysql-client
mysqldump --single-transaction --routines Senddera > ~/baseline-$(date +%F).sql
PHP — 2 controls
9. Disable dangerous PHP functions
Why: Senddera itself doesn't use exec/shell_exec/passthru/system — they're high-impact targets if a vulnerability lets an attacker write a webshell.
echo 'disable_functions = exec,shell_exec,system,passthru,popen,proc_open,parse_ini_file,show_source' | \
sudo tee /etc/php/8.3/fpm/conf.d/99-disable-functions.ini
sudo systemctl restart php8.3-fpm
Verify Senddera still works after this. If anything in the admin breaks, narrow the list — proc_open is the one most likely to be needed by a Laravel app's diagnostic tooling.
10. Hide PHP version
Why: expose_php = Off removes the X-Powered-By: PHP/8.3.x header. Doesn't stop a determined attacker (you can fingerprint version from behavior), but reduces casual reconnaissance.
sudo sed -i 's/^expose_php .*/expose_php = Off/' /etc/php/8.3/fpm/php.ini
sudo systemctl restart php8.3-fpm
Nginx — 3 controls
11. Hide nginx version
sudo sed -i '/^http {/a \ server_tokens off;' /etc/nginx/nginx.conf
sudo nginx -t && sudo systemctl reload nginx
12. Add security headers
Why: modern browsers enforce these — they're free defense against XSS, clickjacking, content-type sniffing, mixed-content downgrade.
Add to your Senddera server block:
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
CSP is more involved — Senddera's admin uses inline scripts in places, so a strict CSP needs testing. Start with Content-Security-Policy-Report-Only and a report-uri to learn what's actually emitted.
13. Rate-limit admin login
Why: /admin/login is the highest-value brute-force target on the box. Rate limiting cuts attacker throughput dramatically.
# In http context (nginx.conf):
limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m;
# In the Senddera server block, on the login location:
location = /admin/login {
limit_req zone=login burst=10 nodelay;
try_files $uri /index.php?$query_string;
}
5 requests/minute per source IP — enough for legitimate users, devastating for password sprayers.
File permissions — 2 controls
14. Storage + cache writable; everything else read-only
Why: if php-fpm is compromised, an attacker who can write /var/www/Senddera/public/index.php owns the site. Locking writes to storage/ and bootstrap/cache/ only prevents this.
sudo find /var/www/Senddera -type d -exec chmod 0755 {} \;
sudo find /var/www/Senddera -type f -exec chmod 0644 {} \;
sudo chmod -R 0775 /var/www/Senddera/storage /var/www/Senddera/bootstrap/cache
sudo chown -R www-data:www-data /var/www/Senddera
15. .env file readable only by web user
sudo chmod 0640 /var/www/Senddera/.env
sudo chown www-data:www-data /var/www/Senddera/.env
Application — 4 controls
16. Set APP_ENV=production and APP_DEBUG=false
Why: debug mode leaks framework state, including environment variables, on any uncaught exception. Production sites must never run with debug on.
sudo grep -E '^APP_(ENV|DEBUG)' /var/www/Senddera/.env
# Should show: APP_ENV=production / APP_DEBUG=false
Adjust the .env, then php artisan config:clear.
17. Force HTTPS in .env
echo 'FORCE_HTTPS=true' | sudo tee -a /var/www/Senddera/.env
This makes route() and url() emit https:// even when behind a proxy that terminates TLS.
18. Generate a fresh APP_KEY
cd /var/www/Senddera && sudo -u www-data php artisan key:generate --force
Only run once, immediately after install. Re-running invalidates encrypted session cookies + cached queue payloads.
19. Configure 2FA for admin accounts
Why: even with all of the above, an admin password leak is plausible (phishing, credential reuse). 2FA closes the gap.
In Senddera Admin → Account Settings → Two-Factor Authentication → enable. Use Google Authenticator, 1Password, or any TOTP app.
Backups + monitoring — 3 controls
20. Daily automated DB backup
sudo tee /etc/cron.daily/Senddera-db-backup <<'CRON'
#!/bin/bash
DEST=/var/backups/Senddera
mkdir -p $DEST
mysqldump --single-transaction --routines Senddera | gzip > $DEST/db-$(date +\%F).sql.gz
find $DEST -name 'db-*.sql.gz' -mtime +30 -delete
CRON
sudo chmod +x /etc/cron.daily/Senddera-db-backup
For off-site, add a restic or borg job that ships /var/backups/Senddera/ to S3/B2/Wasabi. See the backup strategy cookbook.
21. Off-host log shipping (optional but recommended)
If you have it: ship /var/log/nginx/*.log, /var/log/auth.log, /var/www/Senddera/storage/logs/*.log to a SIEM (Datadog, Better Stack, Loki). The auth log especially — fail2ban is silent until you query it.
22. Uptime monitoring + bill alarms
External uptime check on https://mail.example.com/admin/login (UptimeRobot free tier is fine for non-critical, Better Uptime for serious). Plus a billing alarm at your hosting provider so a runaway egress bill can't surprise you.
Verification
After the 22 controls, run this audit:
# SSH — should be key-only, non-standard port, root denied
sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|port'
# Firewall on, with the expected rules
sudo ufw status verbose
# MySQL bound to localhost
sudo grep '^bind-address' /etc/mysql/mysql.conf.d/mysqld.cnf
# fail2ban watching SSH + nginx
sudo fail2ban-client status
# Senddera in production mode
grep -E '^APP_(ENV|DEBUG)' /var/www/Senddera/.env
# Daily DB backup running
ls /var/backups/Senddera/ | tail -5
If any of these is wrong, fix it before you publish the Senddera URL anywhere.
Related reading
- Install Senddera on Ubuntu 24.04 LTS
- Backup strategy cookbook
- Setting up queue workers and cron jobs
- Disaster recovery runbook
- GDPR compliance deep dive — for EU-resident sender programs
FAQ
Do I really need all 22?
Yes. Each control closes a different category of attack — dropping any one widens the blast radius for that category. The whole list is 30 minutes; skipping a step to save 90 seconds is a poor trade.
What about WAF / Cloudflare in front?
A WAF (Cloudflare, AWS WAF, etc.) is a reasonable additional layer. It doesn't substitute for the 22 controls — defense in depth means both. Cloudflare also gives you free DDoS protection + a CDN for static assets. The trade-off: an extra dependency, plus the small operational cost of managing the WAF rules.
What if I'm running Senddera on Docker?
Same checklist, applied at the container/host boundary instead of the OS. SSH controls apply to the host. nginx + PHP controls apply to the nginx/php-fpm containers. MySQL controls apply to the mysql container. The Docker deployment guide covers Docker-specific patterns.
How often should I re-audit?
Annually at minimum. After any significant configuration change (new component added, sending volume crossed an order-of-magnitude threshold). And immediately after any disclosed CVE in nginx, PHP, MySQL, or Senddera itself.