Security

Harden a Laravel CMS for Production: HTTPS, Env, and File Permissions

By Lara Dashboard 5 views
Harden a Laravel CMS for Production: HTTPS, Env, and File Permissions
You shipped your Laravel CMS to a VPS. The app boots. Editors can log in. DNS points at the box.
Production hardening is the next gate. HTTPS and TLS, .env secrets, file permissions, debug off, cookie flags, and a web root that does not leak your project tree.
This guide is a deep checklist to harden a Laravel CMS for production. It focuses on HTTPS, env, and file permissions. Use it as a deploy gate you can reuse on every release.
It sits next to our broader guide, How to Secure a Self-Hosted Laravel CMS. That pillar covers threat model, auth, updates, backups, and monitoring. This post zooms in on the host and app config you must get right before you call the site "live."
If you are still choosing a stack, keep Laravel vs WordPress open. If you are mid-move, the WordPress to Laravel migration checklist pairs well with this deploy gate.

What "hardened for production" means

Hardened means the public site cannot reach secrets, source, or debug output. It means TLS is real, not optional. It means the PHP user can write only where Laravel must write.
It is not a full security program by itself. You still need strong auth, patching, backups, and monitoring. Those live in the pillar article. Here you close the production config gaps that show up on almost every first VPS deploy.
Treat this list as a gate. No public launch until every row is green. Re-run it after hosting moves, PHP upgrades, or major module installs.

HTTPS and TLS for a Laravel CMS

Browsers and search engines expect HTTPS. So do secure cookies. Start at the edge, then align Laravel.

Terminate TLS at the edge

Terminate TLS on nginx, Caddy, or your load balancer. Use a trusted certificate. Let's Encrypt works for most CMS installs.
Prefer modern TLS versions. Disable weak ciphers. Renew certificates on a schedule you can prove. Expiry is a quiet outage and a trust hit.

Redirect HTTP to HTTPS

Every HTTP request should redirect to HTTPS with a permanent redirect. Do this at the reverse proxy so Laravel never serves mixed schemes by accident.
Test with curl and a browser. Confirm www and apex both land on your canonical https URL.

APP_URL, forceScheme, TrustProxies

Set APP_URL to the full https URL. Generated links, password resets, and asset helpers follow this value.
Force the https scheme in the app when you sit behind TLS termination. A common pattern is URL::forceScheme('https') in a service provider, or middleware that redirects to HTTPS.
Configure TrustProxies for your load balancer or reverse proxy. Laravel must see the real client IP and the https scheme from X-Forwarded-* headers. Wrong trust breaks rate limits, logs, and secure cookie detection.

HSTS basics

Once HTTPS is stable, send Strict-Transport-Security from the edge. Start with a modest max-age. Raise it when you trust your cert renewal path.
Do not enable HSTS preload until every subdomain you use is ready. HSTS mistakes are hard to undo quickly.

Env and secrets: protect .env like credentials

Your .env file holds database passwords, mail keys, and APP_KEY. Treat it as production credentials, not a convenience file.

Permissions on .env

Keep .env owned by the deploy user. Mode 640 or tighter is a common pattern. No world read. Confirm the web server cannot list the project root.
Only the deploy user and the PHP-FPM user (via group if needed) should read it. Broader access is how backups and shared shells leak secrets.

APP_KEY

Generate APP_KEY once with php artisan key:generate on first setup. It encrypts cookies and other encrypted values.
Do not rotate it casually. Rotation can invalidate existing encrypted data and sessions. Never commit APP_KEY to git.

Gitignore and examples

Add .env to .gitignore. Commit .env.example with empty placeholders only. No real passwords in sample files "for convenience."
If the repo was ever public with secrets, scan history and rotate everything that leaked. Assume a public paste was copied.

Staging vs production credentials

Staging and production must use different database, mail, and API credentials. Shared "dev" passwords in production are a recurring incident pattern.
Prefer a secrets manager or encrypted CI variables over chat pastes. Document who can read production secrets. Revoke access when people leave.

APP_ENV, APP_DEBUG, and config cache

Set APP_ENV=production and APP_DEBUG=false before public traffic. Debug mode leaks stack traces, paths, and sometimes env values to anyone who triggers an error.
Confirm on staging that real 500 pages do not dump exceptions to the browser. Log detail belongs in storage/logs, not public HTML.
After env is correct, run php artisan config:cache (and route/view cache as your deploy uses). Cached config freezes values from the env at cache time. Change env, then rebuild the cache.
Never leave APP_DEBUG=true "just for a minute" on a public host. Minutes become days. Attackers do not wait for your reminder.

File permissions and the deploy user

Wrong ownership turns a small upload bug into full code rewrite. Get ownership and write paths right on day one.

Deploy user owns the app

Create a non-root deploy user. That user owns the application tree. Day-to-day deploys should not need root.
PHP-FPM runs as a separate user or as a member of a shared group. sudo is for package updates and service restarts only.

Writable paths only where Laravel needs them

The web/PHP user needs write access to storage/ and bootstrap/cache/. Those directories hold logs, sessions (if file-based), compiled views, and cached config.
Prefer group write with careful membership over world write. Avoid 777. It is a shortcut that invites trouble on shared or multi-tenant hosts.

public/ should not be writable

Keep public/ free of write access for the PHP user except for controlled symlinks you manage. Editors should not be able to drop PHP into the document root through a media bug.
If you use php artisan storage:link, the link targets storage outside the writable public tree. Still deny script execution in media directories at the web server.

Deny script execution in media

At nginx or Caddy, block PHP (and other executables) under upload and media paths. Validate MIME and extension in the app too. Defense in depth matters here.
Ban or sanitize SVG if editors do not need it. SVG can carry script. Cap upload size. Allowlist the types you actually publish.

Web root and document root layout

Point nginx or Caddy at public/, not the project root. The document root must be the Laravel public folder.
That layout keeps .env, vendor/, and application source outside the web root. A mis-pointed root is one of the fastest ways to expose secrets on a fresh VPS.
Confirm directory listing is off. Confirm requests for /.env and similar paths return 404 or 403 from the edge. Do not rely on "nobody will guess the path."
On shared hosts without a custom root, you may need a different layout. Prefer a VPS or host that lets you set the document root to public/ cleanly.
With HTTPS live, lock down session cookies. In config/session.php or env overrides, set secure cookies, http_only true, and a sane same_site value (Lax or Strict for your flows).
Secure cookies only travel over HTTPS. HttpOnly keeps them out of JavaScript. SameSite reduces cross-site request abuse on cookie-based auth.
Set a sensible idle timeout for admin sessions. Editors leave browsers open on shared machines. Short admin idle time cuts that window.
Prefer a session driver you control, such as database or Redis. File sessions on shared disks are harder to reason about across concurrent deploys.

Deploy checklist: HTTPS, env, perms, debug, cookies, document root

Use this table as a go/no-go gate. Assign an owner. Tick every row before DNS goes public.
GateCheckDone
HTTPS / TLSCert live; HTTP→HTTPS; modern TLS
APP_URLhttps canonical URL set
forceScheme / proxiesHTTPS forced; TrustProxies correct
HSTSHeader set after HTTPS is stable
.env permsDeploy-owned; no world read
SecretsNot in git; staging ≠ prod creds
APP_KEYUnique; never committed
DebugAPP_ENV=production; APP_DEBUG=false
Config cacheconfig:cache after env settle
OwnershipDeploy user owns app tree
Writable dirsOnly storage/ + bootstrap/cache
No 777Group write, not world write
Document rootPoints at public/; .env outside web
Media execNo PHP/scripts in upload paths
CookiesSecure + HttpOnly + SameSite
Print it. Keep it in the deploy runbook. Revisit after every hosting change.

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 TLS, .env discipline, or file permission hygiene. You still own the VPS and this checklist. What it gives you is a structured admin layer so production hardening sits next to clear roles and content workflows.
For product context, read what Lara Dashboard is. For role design after the host is locked down, see our role-based access control guide.

FAQ

Is HTTPS enough to call the CMS production-ready?

No. HTTPS is required, not complete. You still need debug off, tight .env perms, a correct document root, and writable paths limited to storage and cache.

Can I point nginx at the project root if I deny .env?

Do not. Deny rules fail. Point the document root at public/. Keep source and secrets outside the web tree by design.

What permission mode should .env use?

Owner read/write for the deploy user, group read only if PHP must share the group, never world read. Many teams land on 640 or 600. Verify your PHP user can still boot the app.

Why does APP_DEBUG=false still show errors?

You may be looking at an old config cache, a mis-set env on the wrong host, or a custom exception page. Clear caches, confirm the env file on that server, and check the edge is hitting the right release path.

Do I need HSTS on day one?

Get HTTPS redirects and cert renewal solid first. Add HSTS once you trust that path. Start with a short max-age, then increase.

How does this relate to the broader security guide?

This post is the production config gate. The self-hosted Laravel CMS security guide covers threat model, auth, updates, uploads policy, backups, and monitoring. Run both.

Ending note

To harden a Laravel CMS for production, close the boring gates first: HTTPS and TLS, protected env secrets, debug off, tight file permissions, a public-only document root, and secure cookies.
Do those rows before you polish theme details. Most early VPS incidents still start with open debug, a readable .env, or a document root aimed at the wrong folder.
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. Pair this gate with the self-hosted security pillar and keep shipping with a clear deploy bar.

Try Lara Dashboard for Free

Explore every feature live — no sign-up required.

Launch Live Demo