Security

How to Secure a Self-Hosted Laravel CMS

By Lara Dashboard 2 views
How to Secure a Self-Hosted Laravel CMS
You run a self-hosted Laravel CMS on a VPS. You own the box, the deploy keys, and the blast radius when something goes wrong.
Shared hosts and managed WordPress hide a lot of that. On Laravel you see every layer: TLS, SSH, .env, roles, uploads, backups, and logs. That control is useful. It also means security is your checklist, not a plugin popup.
This guide is a practical how-to for securing a self-hosted Laravel CMS in production. It covers threat model, server basics, secrets, app hardening, auth, updates, file permissions, backups, and monitoring. Use it as a living checklist, not a one-time audit.
If you are still comparing stacks, start with Laravel vs WordPress. If you are mid-migration, keep the WordPress to Laravel migration checklist and rebuild without losing rankings guides nearby. Security work starts before DNS flips and continues after.

1. Threat model for a self-hosted Laravel CMS

Start with what you are protecting and who can reach it. Skip invented CVE lists. Focus on your attack surface.
Admin panel. The login URL, password reset flow, and any "remember me" cookie are high-value targets. Brute force, credential stuffing, and session theft hit here first.
Plugins and modules. Extra Composer packages and CMS modules widen the surface. Unmaintained code with file upload or eval paths is a common failure mode. Treat every new dependency as a trust decision.
Media uploads. User or editor uploads that land in public/ or a public disk can become remote code if you allow PHP, SVG with scripts, or double extensions. Validate type and store outside the web root when you can.
SSH and server access. Password SSH, shared root logins, and leftover deploy keys outlive the project. Compromised SSH often beats a perfect app firewall.
Secrets in git and backups. A leaked .env in a public repo or an unencrypted backup dump gives database and mail credentials without touching your login form.
Write three sentences for your own site: what data you store, who needs admin, and what "bad" looks like (defacement, data theft, ransomware on the VPS). Your checklist should map to those risks.

2. Server basics: HTTPS, firewall, SSH, deploy user

Lock the host before you polish Laravel config. App hardening on an open SSH port is incomplete.

HTTPS and TLS

Terminate TLS at nginx, Caddy, or your load balancer. Redirect HTTP to HTTPS. Prefer current TLS versions and disable weak ciphers. Use a trusted certificate (Let's Encrypt is fine for most CMS sites).
After TLS works, set APP_URL to the https URL. Force HTTPS in the app (next section) so generated links and cookies stay consistent.

Firewall

Allow only what you need: 80/443 for the site, and SSH from known IPs when possible. Block public MySQL/Postgres ports. If you use a panel or Redis, do not expose them to the internet.
On Ubuntu, ufw is enough for many single-VPS setups. On cloud providers, also set security groups. Two layers beat one misconfigured rule.

SSH keys and non-root deploy

Disable password SSH. Use key-based auth. Prefer ed25519 keys. Do not share one root key across staging and production.
Create a non-root deploy user in the www or deploy group. That user owns the app files. sudo only for package updates and service restarts. Day-to-day deploys should not need root.
Rotate keys when people leave. Remove unused authorized_keys lines. If you use a bastion or VPN, document it so the next engineer does not open SSH to 0.0.0.0 "temporarily."

3. Laravel env secrets: .env, APP_KEY, never commit secrets

Your .env file holds database passwords, mail keys, and APP_KEY. Treat it like production credentials, not config candy.
Set filesystem permissions so only the deploy user (and maybe the web/PHP user) can read .env. Typical pattern: owner deploy, mode 640 or tighter, and no world read. Confirm the web server cannot list the project root.
APP_KEY encrypts cookies and other encrypted values. Generate it once with php artisan key:generate on first setup. Do not rotate casually without a plan for existing encrypted data. Never commit APP_KEY to git.
Add .env to .gitignore. Commit .env.example with empty placeholders only. Scan git history if the repo was ever public with secrets. Rotate anything that leaked.
For teams, prefer a secrets manager or encrypted CI variables over Slack pastes. Staging should use different credentials than production. Shared "dev" DB passwords in production are a recurring incident pattern.

4. App hardening: debug off, HTTPS forced, cookies, CSRF, rate limits

Laravel gives you solid defaults. Production still needs explicit settings.

APP_DEBUG and environment

Set APP_ENV=production and APP_DEBUG=false. Debug mode leaks stack traces, paths, and sometimes env values to anyone who triggers an error. That is free recon for attackers.
Confirm on a staging copy that real 500 pages do not dump exceptions to the browser. Log detail belongs in storage/logs, not the public HTML.

Force HTTPS and trusted proxies

Use URL::forceScheme('https') or middleware that redirects to HTTPS when behind TLS termination. Configure TrustProxies so Laravel sees the real client IP and HTTPS scheme from your load balancer.
Wrong proxy trust breaks rate limiting and IP bans. It also creates mixed-content warnings that train users to ignore browser safety cues.
In config/session.php (or env overrides), use secure cookies on HTTPS, http_only true, and a sensible same_site (Lax or Strict depending on your flows). Set a short idle timeout for admin sessions if editors leave browsers open on shared machines.
Store sessions in a driver you control (database, Redis). File sessions on shared hosts are harder to reason about under concurrent deploys.

CSRF and login rate limiting

Keep Laravel CSRF tokens on all state-changing forms. Do not disable VerifyCsrfToken for the admin panel "to fix AJAX." Fix the token header instead.
Rate-limit login and password reset routes. Laravel's rate limiter and throttle middleware are enough for many CMS installs. Start with something like five to ten attempts per minute per IP, then tune from logs.
If you expose an API, use token auth (Sanctum or similar) with separate rate limits. Do not reuse the session cookie model for public machine clients without review.

5. Auth: passwords, 2FA, least privilege roles

Most CMS breaches start with a weak or reused admin password, not a novel exploit.

Strong passwords and account hygiene

Require long passwords for admin users. Prefer a password manager. Ban shared "editor" accounts. Every human gets their own user so you can revoke access without rotating one shared secret.
Disable or delete unused admin accounts after contractors leave. Review users monthly on small teams. On larger teams, tie offboarding to your HR checklist.

Two-factor authentication

If your stack or package supports 2FA (TOTP), enable it for all Superadmin and admin roles. Backup codes belong in a password manager, not a sticky note.
LaraDashboard and many Spatie-based setups can grow 2FA via packages. Treat "we will add 2FA later" as debt with a date. Until then, tighten rate limits and IP allowlists for admin paths when your threat model needs it.

Least privilege with Spatie / LaraDashboard roles

Disclosure: LaraDashboard is our open-source Laravel admin and CMS. It uses role-based access (commonly with Spatie Permission patterns) so you can separate Superadmin, editor, and viewer duties.
Give editors content permissions only. Do not hand out module install, user management, or settings access by default. A compromised editor account should not equal full server-equivalent power in the admin UI.
For a deeper roles walkthrough, see our role-based access control guide with LaraDashboard. Map each role to a job, not to "power user who asked once."

6. Updates and patching: Composer, Laravel, OS

Unpatched dependencies are a steady source of real incidents. Schedule updates. Do not wait for a scare thread on social media.
Run composer audit (or your CI equivalent) on a schedule. Apply Laravel framework and package security releases after reading the notes. Pin versions thoughtfully, but do not freeze forever.
Patch the OS too: kernel, OpenSSH, nginx/Caddy, PHP-FPM. Unattended upgrades for security patches help on single-VPS CMS hosts if you still reboot on schedule and watch services.
Staging first. Deploy updates to staging. Smoke-test login, media upload, publish, and one critical editor flow. Then promote to production. Hot-patching production without a rollback path is how "quick security fixes" become outages.
Keep a short changelog of what you upgraded and when. When a new advisory drops, you need to know if you are already current.

7. File and media permissions, upload validation

Wrong permissions and naive uploads are classic self-hosted CMS failures.

Permissions

App code should be owned by the deploy user. The web/PHP user needs write only where Laravel must write: storage/ and bootstrap/cache/. Avoid 777. Prefer group write with careful membership.
Do not make the entire project writable by the web user. That turns a low-severity upload bug into full code rewrite.

Upload validation

Validate MIME type and extension on the server. Allowlist images and documents you actually need. Reject executables and HTML when your editors do not need them.
Store uploads on a non-public disk when possible, then serve through a controller or signed URL. If files must live under public/storage, deny script execution in that directory at the web server layer.
Strip or sanitize SVG if you allow it. SVG can carry script. Many teams ban SVG uploads for non-design roles.
Cap file size. Virus-scan if your compliance needs it. Quarantine is better than "hope the CDN caches a clean copy."

8. Backups: database, storage, and restore drills

A secure CMS that cannot restore is still fragile. Backups are part of security because ransomware and bad deploys happen.
Back up the database on a schedule (nightly is a common start). Back up storage/app and any media disk. Encrypt backups at rest if they leave your VPC. Restrict who can download them.
Store copies off the production VPS. Same-disk backups die with the disk. Object storage in another region or account is a simple improvement.
Restore drill. Once a quarter, restore DB and media to a scratch environment. Time it. Document the steps. A backup you never restored is a hope file, not a plan.
Test that secrets in backup archives are not world-readable in your bucket. Old "public" backup links are a quiet breach path.

9. Monitoring and logs: failed logins and 5xx

You cannot fix what you do not see. Wire basic signals before you buy a fancy SIEM.
Watch failed logins. Spikes mean credential stuffing or a targeted attempt. Correlate with IP and user agent. Block or challenge repeat offenders.
Watch 5xx rates and latency. Error storms after a deploy often mean a bad config or exhausted workers. They also hide attack noise if you only look at marketing analytics.
Ship laravel.log (and nginx/Caddy access/error logs) to a place that survives disk fill. Disk-full on a VPS silences the app and the logs together.
Set alerts for: certificate expiry, disk usage, high 5xx, and unusual admin login geography if that fits your team. Keep the alert list short so people still respond.
Review logs after every production deploy for a few minutes. Many "security" issues are misconfigs that show up as auth errors or permission denials first.

10. Production security checklist

Use this table during launch and every quarter. Assign an owner per row.
Print it. Tick it. Revisit after major module installs or hosting changes.

Where LaraDashboard fits

Disclosure: LaraDashboard is our open-source Laravel admin and CMS. We recommend it when you want a Laravel-native back office with roles, content, and media without building every admin screen from scratch.
It does not replace server firewalls, TLS, or backup discipline. You still own the VPS and the checklist above. What it gives you is a structured place for least-privilege roles, content workflows, and modular features so you are not bolting random admin packages together under time pressure.
For product context, read what Lara Dashboard is. For stack tradeoffs, keep Laravel vs WordPress open. Security is easier when the admin layer is intentional.

FAQ

Is a self-hosted Laravel CMS less safe than managed WordPress?

Not inherently. Managed WordPress shifts some host patching to the vendor. Self-hosted Laravel shifts ownership to you. Either can be safe or sloppy. Your process for TLS, secrets, updates, and roles decides more than the brand name on the CMS.

What should I lock down first on a new VPS?

SSH keys and firewall, then TLS, then .env permissions and APP_DEBUG=false, then admin auth and rate limits. App polish after the host is not an open door.

Do I need a WAF on day one?

A WAF can help with noisy bots later. It is not a substitute for patching, strong auth, and correct upload rules. Get the checklist basics green before you add another layer you must tune.

How often should I run Composer and OS updates?

Review security releases weekly. Apply routine patches on a monthly cadence with staging first, faster for known critical framework or PHP issues. OS security updates can be more frequent if you use unattended upgrades carefully.

Where should media files live?

Prefer a private disk with controlled download URLs when files are sensitive. For public images, use a dedicated public disk and block script execution in that path at the web server. Never leave arbitrary PHP uploadable into a web-reachable folder.

What is the minimum backup set?

Database dump plus media/storage, stored off the production server, with a restore test you have actually run. Config and infrastructure as code help too, but data is what you cannot recreate from git alone.

Ending note

Securing a self-hosted Laravel CMS is a checklist you revisit, not a badge you earn once. Know your threat model. Close the host. Protect secrets. Turn debug off. Harden cookies and login. Use least privilege. Patch on purpose. Validate uploads. Back up and restore. Watch failed logins and 5xx.
Do the boring rows first. Most incidents on small CMS installs still start with open SSH, leaked env files, weak admin passwords, or unpatched packages.
If you want a Laravel-native admin and CMS starting point while you run this checklist, try the LaraDashboard demo or read the docs on the site. Tell us in the comments which checklist row you still postpone on your own servers.

Try Lara Dashboard for Free

Explore every feature live — no sign-up required.

Launch Live Demo