Security checklist
This is the production hardening checklist for a Drupal 11 site: the settings and habits that close the holes attackers actually use. Each item is grounded in a specific core setting, file, or process you can check today. Work through it before a site takes public traffic, and revisit it on every deploy.
Reach a state where the running site rejects spoofed requests, refuses to execute uploaded code, runs over HTTPS, grants no permission it does not need, and stays patched against published advisories. This page is the operational checklist. The mechanics of who can do what inside the application are on the access concept page; here the concern is the perimeter and the operating environment.
Prerequisites
Section titled “Prerequisites”- Shell and file-system access to the production environment.
- The ability to edit
settings.phpon that environment. - An administrator account on the site.
- Composer-managed code, per Anatomy of a Drupal project, so updates are a Composer operation.
Each step is a checklist item: the risk, the core mechanism, and how to confirm it.
Set trusted host patterns
Section titled “Set trusted host patterns”Configure which Host header values the site will answer to, in settings.php.
$settings['trusted_host_patterns'] = [ '^www\.example\.com$', '^example\.com$',];Without this, an attacker can send a request with a forged Host header, and Drupal will use that host when building absolute URLs: password-reset links, cached page URLs, and redirects then point at the attacker’s domain. The patterns are anchored regular expressions, without delimiters, matched against the incoming host. Core treats a missing trusted_host_patterns as a security error: \Drupal\Core\Site\Settings exposes the setting, and the status report flags its absence as a red error line. List every hostname the site legitimately serves, including bare and www variants and every domain on a multisite. Verify at Administer → Reports → Status report/admin/reports/status: the Trusted Host Settings line reports the configured patterns instead of a warning.
Block PHP execution in the files directory
Section titled “Block PHP execution in the files directory”The public files directory holds user uploads. If the web server will execute a .php file uploaded there, a single image-upload bug becomes remote code execution. Core writes a protective .htaccess into the files directory that turns off script handling and disables the PHP engine.
# sites/default/files/.htaccess (written by core)Options -Indexes -ExecCGI -Includes -MultiViewsSetHandler Drupal_Security_Do_Not_Remove_See_SA_2006_006<IfModule mod_php.c> php_flag engine off</IfModule>\Drupal\Core\File\HtaccessWriter writes and maintains this file on Apache. On nginx, .htaccess does nothing: you must replicate the rule in the server configuration, denying PHP execution under the files path so uploaded scripts are served as plain text or not at all. Confirm by requesting an uploaded test file with a .php extension and checking it is not executed.
Get file and directory permissions right
Section titled “Get file and directory permissions right”The web server must not be able to write to code, and settings.php must not be writable once the site is installed.
settings.phpand the site directory (sites/default) are read-only to the web server after installation. Core hardens these at the end of install and warns on the status report if they stay writable, unless$settings['skip_permissions_hardening']is set (which belongs only in local development).- Code (
core/,modules/,themes/,vendor/) is owned by the deploy user and not writable by the web server, so a compromise cannot rewrite executables. - The files directories (
sites/default/files, the private files directory) are the only writable trees, and they carry the script-blocking rules above.
Confirm settings.php is not writable:
test -w web/sites/default/settings.php && echo WRITABLE || echo "read-only (good)"Store the private files directory outside the web root
Section titled “Store the private files directory outside the web root”If a directory is reachable by URL, its access rules are the only thing standing between an attacker and its contents. Put the private files directory outside the document root entirely, then point Drupal at it.
$settings['file_private_path'] = '/var/www/private';A path outside the web root cannot be requested directly at all; access runs through Drupal’s file-download access checks instead of relying solely on a .htaccess deny rule. Keep the configuration sync directory outside the web root for the same reason. The default scaffold does not: with the line commented out, DDEV and the installer fall back to a directory under sites/default/files. Set it explicitly in settings.php:
$settings['config_sync_directory'] = '../config/sync';Confirm with vendor/bin/drush status --field=config, which prints ../config/sync.
Serve everything over HTTPS
Section titled “Serve everything over HTTPS”Terminate TLS at the proxy or web server and redirect all HTTP to HTTPS, so credentials and session cookies never cross the network in clear text. Drupal then secures the session cookie on its own: \Drupal\Core\Session\SessionConfiguration sets the cookie_secure flag from $request->isSecure() and issues a distinct SSESS cookie over HTTPS, so a secure request never reuses an insecure cookie. The condition for all of that is that Drupal sees the request as secure.
When the site sits behind a TLS-terminating proxy, set the reverse-proxy settings from Performance and caching so Drupal detects HTTPS from the forwarded headers; otherwise it sees plain HTTP behind an https:// front door, builds http:// URLs, and marks the cookie insecure. Add an HSTS (HTTP Strict Transport Security) response header at the proxy to stop protocol-downgrade attempts.
Practice permission and role hygiene
Section titled “Practice permission and role hygiene”Permissions are how a logged-in account turns into damage. Keep the grants minimal.
- Grant
administer site configuration,administer permissions,administer modules, and similar only to roles that truly administer the site. Any one of them is close to total control. - Treat permissions the status report marks as security-sensitive as such:
administer filtersand the ability to use a text format that allows arbitrary HTML or PHP-like markup let a user inject script. Restrict full-HTML formats to trusted roles. - Keep the anonymous and authenticated roles thin. A permission on the authenticated role is a permission for every account that can register.
- Audit grants on the production site:
vendor/bin/drush role:listshows each role’s permissions, and the access concept page explains how core resolves them.
Keep core and contributed code patched
Section titled “Keep core and contributed code patched”Unpatched published vulnerabilities are the most common way Drupal sites are compromised, because the advisory tells the attacker exactly what to exploit. Staying current is the single highest-value habit on this list.
The Drupal Security Team coordinates this process. Researchers report issues privately; the team works with maintainers on a fix, assigns a Security Advisory (SA) with a risk score, and publishes core advisories on a predictable schedule (a Security Release Window on Wednesdays) so site teams can plan. Advisories cover Drupal core and contributed projects that opt into security-advisory coverage; a project’s coverage status is shown on its drupal.org page, and the Update Status module surfaces available security updates on your own site.
Apply updates with Composer in your local environment, the same way you manage all code, commit the updated composer.lock, then deploy it through the deployment workflow:
ddev composer update "drupal/core-*" --with-all-dependenciesddev composer update drupal/pathautoCheck what needs updating from Administer → Reports → Available updates/admin/reports/updates, where the Update Status module lists installed projects with outstanding security releases. From the command line, ddev composer audit reports installed packages with known published advisories.
It is recommended to plan the deploy around the advisory’s risk score: critical and highly critical fixes the same day they are published, lower-risk updates with your next regular deploy.
Verify
Section titled “Verify”- Administer → Reports → Status report
/admin/reports/statusshows no security errors: Trusted Host Settings configured, configuration andsettings.phpnot writable, no pending security updates. ddev composer auditreports no packages with known advisories, and Available updates lists no security releases.- A
.phpfile placed in the files directory is not executed when requested. - The site redirects
http://tohttps://, andsettings.phpis not writable by the web server. vendor/bin/drush role:listshows no unexpected administrative permission on the anonymous or authenticated role.
Variations
Section titled “Variations”Multisite host patterns
Section titled “Multisite host patterns”On a multisite install, trusted_host_patterns must list every domain across every site, since the setting is evaluated before the site is selected. Anchor each pattern so a subdomain wildcard (^.+\.example\.com$) does not accidentally trust a domain you do not control.
Private file serving on nginx
Section titled “Private file serving on nginx”Apache reads the .htaccess rules core writes; nginx ignores them. On nginx, deny direct access to the private path and to .php files under the public files path in the server block, and route private downloads through Drupal’s /system/files path. Test both: a direct request to a private file must fail, and an authorized download through Drupal must succeed.
Related
Section titled “Related”- Access: users, roles, and permissions for how permissions and roles resolve into what an account may do.
- Deployment workflow for applying the updates this checklist requires.
- Performance and caching for the reverse-proxy settings that keep HTTPS detection correct.
- Anatomy of a Drupal project for the directory layout these permissions apply to.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.