Hardening an Installation

Hardening is a short list: a custom admin path, 2FA, pub as the document root, read-only files, and fewer modules.

Syntaxbin/magento setup:config:set --backend-frontname=<segment>

A default install is reachable in ways you did not choose: a guessable admin path, a document root that serves app/etc/env.php, modules nobody configured, and files the web server can rewrite.

The admin

The admin URL segment lives in app/etc/env.php under backend/frontName. Change it with bin/magento setup:config:set --backend-frontname=admin_8f3k; only letters, digits and underscores are accepted. Two-factor authentication ships with 2.4 as the module Magento_TwoFactorAuth and is on by default. Its providers are configured at Stores > Configuration > Security > 2FA, which writes twofactorauth/general/force_providers. Leave Add Secret Key to URLs (admin/security/use_form_key) at Yes, and shorten admin/security/session_lifetime from its default.

The filesystem

The web server's document root must be pub, not the install directory. With pub as the root, app, var, vendor and generated are not served at all, and no .htaccess rule has to hold the line. For a read-only production deployment Adobe removes group write from code and static directories and removes all access for others from the configuration file; var stays writable because logs and session files are written there at runtime.

Modules

Disable what the site does not use, for example bin/magento module:disable Magento_Swagger Magento_Version, then run bin/magento setup:upgrade and bin/magento setup:di:compile. A disabled module serves no routes.

Example

Example · bash
# Run from the Magento root as the file system owner, not as the web user.

# 1. Move the admin off /admin. Letters, digits and underscores only.
php bin/magento setup:config:set --backend-frontname=admin_8f3k
# You saved the new configuration.

# 2. Admin session and secret keys in URLs. Magento already ships
#    admin/security/session_lifetime at 900 seconds, so set a lower
#    value if you want to shorten it (the minimum accepted is 60).
php bin/magento config:set admin/security/session_lifetime 600
php bin/magento config:set admin/security/use_form_key 1

# 3. Turn off endpoints the site does not use.
php bin/magento module:disable Magento_Swagger Magento_Version
# The following modules have been disabled:
# - Magento_Swagger
# - Magento_Version
#
# Cache cleared successfully.
# Generated classes cleared successfully. Please run the 'setup:di:compile' command...

php bin/magento setup:upgrade
php bin/magento setup:di:compile

# 4. Remove write access from code and static directories, and hide env.php.
find app/code lib pub/static app/etc generated/code generated/metadata \
    var/view_preprocessed \( -type d -or -type f \) -exec chmod g-w {} + \
    && chmod o-rwx app/etc/env.php

php bin/magento cache:flush

When to use it

  • Finish a server build where the document root still points at the install directory.
  • Move a site off /admin after a credential-stuffing attempt shows up in the logs.
  • Produce the read-only file permissions a production deployment is supposed to have.
  • Shrink the attack surface by turning off Swagger and the version endpoint.

More examples

What the admin path looks like in env.php

setup:config:set --backend-frontname writes exactly this. The file also holds the database password and the encryption key, which is why chmod o-rwx app/etc/env.php matters and why pub must be the document root. document_root_is_pub tells Magento the web server is serving pub, so generated URLs and static file paths are built correctly; the installer sets it when you install with pub as the root.

Example · php
<?php
// app/etc/env.php (fragment - this file returns a plain array)
return [
    'backend' => [
        'frontName' => 'admin_8f3k'
    ],
    'db' => [
        // ... connection details ...
    ],
    'crypt' => [
        'key' => '...'
    ],
    'directories' => [
        'document_root_is_pub' => true
    ]
];

Confirming the directories are not served

Run this after every deploy to a new server, because a document root set to the install directory looks completely normal from the storefront and only shows up as a readable env.php. Any 200 here is the finding. Do the checks against the real host name, not against 127.0.0.1, so the request carries the right Host header.

Example · bash
# With pub as the document root these must not return the file.
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/app/etc/env.php
# 404

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/var/log/system.log
# 404

curl -s -o /dev/null -w '%{http_code}\n' https://example.com/vendor/autoload.php
# 404

# A disabled module serves nothing.
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/swagger
# 404

# The old admin path is gone; the new one answers.
curl -s -o /dev/null -w '%{http_code}\n' https://example.com/admin
# 404

Checking which modules are actually enabled

module:disable writes to app/etc/config.php, which belongs in version control, so the change travels with the deploy rather than being made by hand on each server. Disabling a module another module depends on is refused with the dependency named, so the command is safe to try. Never disable Magento_TwoFactorAuth in the configuration your production deploy is built from, even if you disable it locally.

Example · bash
# Everything currently on
php bin/magento module:status --enabled

# Everything currently off
php bin/magento module:status --disabled

# Candidates on a site with no third-party API consumers and no
# Braintree, Vertex or Amazon Pay integration in use. Check each one
# against the store's actual configuration before disabling it.
php bin/magento module:disable Magento_Swagger Magento_Version

php bin/magento setup:upgrade
php bin/magento setup:di:compile
php bin/magento cache:flush

Discussion

  • Be the first to comment on this lesson.