Contao ships with a sensible default structure, but a fresh install on shared hosting often leaves several openings that attackers scan for automatically. The document root usually points at public/ (or web/ on older 4.x layouts), yet the project root above it contains config/, vendor/, var/, and environment files that must never be served over HTTP. When a control-panel document root is misconfigured, or when a subfolder install exposes the whole project tree, those directories become reachable and leak database credentials, cached secrets, and framework version data. On top of that, the Contao Manager (contao-manager.phar.php) and the install/upgrade routines are powerful endpoints that only need to be reachable during maintenance windows.
The good news is that every meaningful hardening step on a managed shared plan can be done without root. LiteSpeed on our AlmaLinux and CloudLinux stack reads .htaccess the same way Apache does, honors per-directory .user.ini for PHP settings, and cPanel Jupiter and DirectAdmin Evolution both expose File Manager, PHP Selector, and phpMyAdmin. That toolset is enough to correct permissions, wall off sensitive paths, force encrypted sessions, and add the response headers that stop a large share of opportunistic attacks.
Correcting file and directory permissions
The single most common weakness on shared Contao sites is over-permissive files. Installers or FTP clients sometimes push everything to 0777 "to make it work," which lets any process on the server write to your code and lets a compromised script drop a web shell. On CloudLinux the correct model is 0644 for files and 0755 for directories, with tighter permissions on anything that stores secrets.
Open the cPanel File Manager (Files → File Manager) or the DirectAdmin File Manager, enable Show Hidden Files (dotfiles) from the settings gear, and navigate to your Contao project root. Select a file, click Permissions, and confirm the numeric mode. Contao only needs write access to a small set of paths: var/ (cache, logs, sessions), public/files/ (or files/), public/share/, public/assets/, and config/ only during setup. Everything under vendor/ and src/ should be read-only for the web process.
The environment file deserves special attention. Contao 4.9+ and Contao 5 read database and secret values from .env and .env.local in the project root. Set those to 0600 so only your account owner can read them:
.env 0600
.env.local 0600
config/ 0755
var/ 0755 (directories) / 0644 (files)
public/files 0755 (directories) / 0644 (files)If you have many files stuck at the wrong mode, File Manager lets you select a folder and apply permissions recursively through the Permissions dialog. Avoid the temptation to leave APP_SECRET or the database DSN world-readable inside a parameters.yml under config/; on legacy installs that file must also be 0600.
Blocking exposed admin, install, and manager paths
The correct starting point is verifying that your domain's document root points at the public/ directory. In cPanel this is set under Domains, and in DirectAdmin under Domain Setup → the public path field. When the root sits one level too high, requests like /vendor/autoload.php or /.env resolve directly. Even with the root set correctly, a defense-in-depth .htaccess at the project root and inside public/ closes the remaining gaps.
Place this in the .htaccess file inside your public/ directory to deny access to dotfiles, environment files, and known-sensitive extensions that should never be requested directly:
# Block dotfiles and env files
<FilesMatch "^\.">
Require all denied
</FilesMatch>
<FilesMatch "\.(env|ya?ml|ini|log|sql|md|lock|dist|json)$">
Require all denied
</FilesMatch>
# Never serve the composer manifest or PHAR installer casually
<FilesMatch "^(composer\.(json|lock)|symfony\.lock)$">
Require all denied
</FilesMatch>The Contao Manager is a self-contained PHAR at public/contao-manager.phar.php. It is genuinely useful for updates, but leaving it openly reachable invites brute-force and version fingerprinting. Restrict it to your own IP while you are not actively maintaining the site by adding a scoped block to public/.htaccess:
<Files "contao-manager.phar.php">
Require ip 203.0.113.45
</Files>Replace 203.0.113.45 with your current address (check it via any "what is my IP" lookup) and add extra Require ip lines for additional locations. When your address changes you can temporarily comment the block, run maintenance, then restore it. The Contao back end itself lives at /contao. It should stay reachable so editors can log in, but you can add a second authentication layer with cPanel's Directory Privacy or a manual .htpasswd so a valid HTTP login is required before the Contao login form even appears. Create the password file outside the document root, then reference it:
<LocationMatch "^/contao">
AuthType Basic
AuthName "Restricted"
AuthUserFile "/home/USERNAME/.htpasswds/contao"
Require valid-user
</LocationMatch>Because Contao routes everything through index.php, prefer cPanel's Directory Privacy tool pointed at the public folder if LocationMatch behaves inconsistently on a routed front controller, and confirm the login prompt appears before the framework loads.
HTTP security headers, session cookies, and PHP settings
Missing response headers let browsers fall back to unsafe defaults. Add the following to your public/.htaccess to enforce HTTPS, prevent clickjacking, stop MIME sniffing, and control referrer leakage:
<IfModule mod_headers.c>
Header always set X-Content-Type-Options "nosniff"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set Permissions-Policy "geolocation=(), microphone=(), camera=()"
</IfModule>
# Force HTTPS
RewriteEngine On
RewriteCond %{HTTPS} !=on
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]Enable an AutoSSL or Let's Encrypt certificate in cPanel (Security → SSL/TLS Status) or DirectAdmin (SSL Certificates) before turning on HSTS, otherwise you can lock visitors out of an unencrypted site. A Content-Security-Policy is the strongest header available, but Contao's back end and many front-end templates rely on inline scripts, so start in report mode by sending Content-Security-Policy-Report-Only and tighten it gradually.
Session security is set through PHP rather than Contao's own options. Use a .user.ini at the document root to harden cookies without touching global config:
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
expose_php = Off
Select a currently supported PHP branch in cPanel's MultiPHP Manager or the PHP Selector (Contao 5 requires PHP 8.1+), because unmaintained runtimes accumulate unpatched issues. Inside the Contao back end at System → Settings, enforce a strong password policy and enable two-factor authentication under each user's profile at Contao → Profile → Two-factor authentication; every account with back-end access should have it active. Also review System → Settings → Security and confirm the front-end preview and debug options are disabled on production, and that APP_ENV in .env.local is set to prod rather than dev, since dev mode exposes the Symfony profiler and detailed stack traces.
Finally, watch your evidence. When a rule blocks something legitimate, the local error_log in the affected directory and var/logs/prod.log inside the Contao project record the exact request. Reviewing them after each change confirms the hardening holds without breaking editor workflows or scheduled cron tasks.