Secure Your LAMP Stack: Step-by-Step Hardening Guide

· Published · 27 min read

Administrator monitoring a protected request path through internet edge, Apache server, isolated PHP workers, and a private database

A secure LAMP server is not one hardened configuration file. Linux, Apache, PHP, MariaDB, the application, secrets, network path, logs, backups, and administrator access form one trust system. This guide builds that system step by step, retains useful hardening commands, marks unsafe or obsolete snippets clearly, and provides verification and rollback at every high-risk boundary.

This guide was technically reviewed on 11 August 2026 against current Apache HTTP Server 2.4, PHP, MariaDB, OWASP, and operating-system guidance. Use supported distribution packages and the documentation for the versions actually installed. Paths differ between Debian/Ubuntu and RHEL-family systems, and application requirements must decide the final policy.

Understand the secure LAMP request path

A typical request reaches a firewall or load balancer, negotiates TLS at Apache or an approved proxy, passes through an Apache virtual host, invokes a PHP-FPM pool, and reaches MariaDB through a local Unix socket or private network. The response travels back through the same controlled layers. Each layer should have the minimum identity, network reach, filesystem access, modules, and secrets required for its job.

LayerPrimary boundaryMinimum evidenceCommon failure
Linux hostSupported packages, administrator access, firewall, MAC policy, time, storageOS release, package origin, enabled services, listeners, firewall, SELinux/AppArmor, updatesUnsupported OS, exposed administration, world-writable files, disabled confinement
ApacheVirtual hosts, modules, request limits, TLS, headers, logs, PHP handoffLoaded modules, parsed virtual hosts, syntax test, listener map, representative responsesDefault site exposed, broad overrides, directory listing, unsafe proxy or handler mapping
PHP-FPMPool identity, socket, extensions, runtime limits, sessions, secretsLoaded INI files, pool configuration test, process identity, socket permissions, application testsOne shared overprivileged pool, secrets in web root, executable upload directory
MariaDBBind address, account hosts and grants, TLS, data files, backup and recoveryListeners, users, grants, TLS variables, backup result, restore testRemote root, wildcard-host account, application using administrator credentials
ApplicationAuthentication, authorization, input/output handling, dependencies, uploadsThreat model, tests, dependency inventory, deploy record, security eventsAssuming server headers or a WAF fixes insecure application logic

Step 1: define scope, access, and rollback

Record the hostname, environment, business owner, domain names, expected clients, maintenance window, recovery objectives, sensitive data, dependencies, certificate owner, database location, and approved administrator path. Confirm console or out-of-band access before changing SSH, firewall, routes, SELinux, Apache listeners, PHP-FPM sockets, or database authentication.

Unsafe when copied blindly: package removal, recursive ownership or mode changes, firewall replacement, SELinux disablement, database account deletion, certificate replacement, service restart, and bulk PHP function disabling can cause an outage or weaken controls. Use a verified target, backup, syntax test, staged change, health check, and rollback.

Capture configuration and data separately. A configuration archive is not a database backup, and a database dump is not a complete application recovery point.

date -u
hostnamectl
cat /etc/os-release

# Ubuntu/Debian example: use only paths that exist on the verified host.
sudo tar --xattrs --acls -czf /root/lamp-config-before.tgz \
  /etc/apache2 /etc/php /etc/mysql

# RHEL-family alternative: do not run both examples blindly.
sudo tar --xattrs --acls -czf /root/lamp-config-before.tgz \
  /etc/httpd /etc/php.ini /etc/php.d /etc/my.cnf /etc/my.cnf.d

sudo mariadb-dump --single-transaction --routines --events \
  --all-databases --result-file=/root/mariadb-before.sql
sudo sha256sum /root/lamp-config-before.tgz /root/mariadb-before.sql

The example writes sensitive material under /root. Restrict it, transfer it to an approved protected repository, test restoration, and remove the temporary copy according to policy. --single-transaction provides a consistent snapshot for transactional tables but does not make every storage engine, DDL change, or large production workload safe. Use the database backup method designed for the actual topology.

Step 2: inventory before changing anything

uname -a
ip -brief address
ip route show
ss -lntup
systemctl --failed
systemctl list-unit-files --state=enabled
apachectl -V 2>/dev/null || httpd -V
apachectl -M 2>/dev/null || httpd -M
apachectl -S 2>/dev/null || httpd -S
php --version
php --ini
php-fpm --test 2>/dev/null || php-fpm -t
mariadb --version
sudo mariadb -e "SELECT VERSION(); SHOW VARIABLES LIKE 'bind_address';"

Do not assume the command name identifies the active binary. Distribution alternatives, containers, custom prefixes, and multiple PHP versions can place another executable earlier in PATH. Record package ownership and service unit names:

command -v apachectl httpd php php-fpm mariadb
systemctl status apache2 httpd php-fpm mariadb --no-pager
dpkg-query -W 'apache2*' 'php*' 'mariadb*' 2>/dev/null
rpm -qa 'httpd*' 'php*' 'mariadb*' 2>/dev/null | sort

A failed command may simply target the other distribution family. Use the relevant branch; do not install duplicate stacks to make every command succeed.

Step 3: install supported packages

Ubuntu or Debian family

sudo apt update
apt list --upgradable
sudo apt install apache2 php-fpm php-cli php-mysql \
  php-opcache mariadb-server
sudo a2enmod proxy proxy_fcgi headers ssl http2
sudo systemctl enable --now apache2 mariadb
systemctl list-unit-files 'php*-fpm.service'

# Example only after confirming this exact installed unit:
sudo systemctl enable --now php8.3-fpm

The installed versioned PHP-FPM unit may differ from php8.3-fpm. Discover it first and enable only the approved service. If the site uses the distribution-provided PHP-FPM Apache configuration instead of the explicit socket mapping later in this guide, inspect and enable the matching versioned configuration with a2enconf. Do not enable two PHP handlers for the same files.

RHEL, Rocky Linux, AlmaLinux, or Fedora family

sudo dnf check-update
sudo dnf install httpd php-fpm php-cli php-mysqlnd php-opcache mariadb-server
sudo systemctl enable --now httpd php-fpm mariadb

dnf check-update commonly returns exit status 100 when updates are available. Treat that documented status correctly in scripts. Use repositories supported by the operating-system and application lifecycle; do not add an unknown repository merely to obtain a larger version number.

Step 4: expose only required network services

A public single-host LAMP server normally needs inbound TCP 80 for redirect or certificate validation and TCP 443 for HTTPS. Administration should come through the approved VPN, bastion, or management source. MariaDB should normally remain on a Unix socket or loopback address when the application is on the same host.

ss -lntup
sudo nft list ruleset
sudo firewall-cmd --list-all 2>/dev/null
sudo ufw status verbose 2>/dev/null

These are read-only checks. Do not combine ufw, firewalld, hand-written nftables, a cloud security group, and a hosting firewall without defining which layer owns each rule. When applying a remote firewall change, preserve an alternate management path and an automatic rollback where supported. The firewall operations guide explains the staged control pattern.

Step 5: establish an Apache baseline

Use a small included file owned by root rather than editing a vendor file extensively. On Debian/Ubuntu, a site-specific security file may live under /etc/apache2/conf-available/ and be enabled with a2enconf. On RHEL-family systems, use a controlled file under /etc/httpd/conf.d/.

ServerSignature Off
ServerTokens Prod
TraceEnable Off

<Directory "/">
    AllowOverride None
    Require all denied
</Directory>

<Directory "/var/www/example/public">
    Options -Indexes -Includes -ExecCGI -MultiViews
    AllowOverride None
    Require all granted
</Directory>

LimitRequestBody 10485760
RequestReadTimeout header=20-40,MinRate=500 body=20,MinRate=500

Replace the document root and request-size limit with tested application values. A global Require all denied creates a safe filesystem default; explicit directory blocks then expose only required content. TraceEnable Off directly handles TRACE, so a separate rewrite rule for TRACE and the non-standard TRACK method is normally unnecessary.

ServerTokens Prod and ServerSignature Off reduce casual version disclosure. They do not hide implementation fingerprints or fix a vulnerable server. Patch management remains the control.

Remove unused modules and default content

apachectl -M 2>/dev/null | sort
apachectl -S 2>/dev/null
find /var/www -maxdepth 3 -type f -print
find /etc/apache2/sites-enabled /etc/httpd/conf.d -maxdepth 1 -type f -print 2>/dev/null

Disable unused status, proxy, autoindex, CGI, WebDAV, user-directory, and example configurations only after proving the site does not depend on them. On Debian, use a2dismod, a2disconf, and a2dissite with exact names. On RHEL, remove or disable the applicable included file or module package through change control. Do not delete every file in conf.d.

Step 6: configure virtual hosts and PHP-FPM safely

Run PHP through a dedicated FPM pool rather than giving every site one shared, broadly writable Apache identity. A minimal Apache mapping depends on the distribution's FPM socket and enabled proxy modules:

<VirtualHost *:80>
    ServerName app.example.net
    Redirect permanent / https://app.example.net/
</VirtualHost>

<VirtualHost *:443>
    ServerName app.example.net
    DocumentRoot /var/www/example/public

    <Directory "/var/www/example/public">
        Options -Indexes -Includes -ExecCGI -MultiViews
        AllowOverride None
        Require all granted
    </Directory>

    <FilesMatch "\.php$">
        SetHandler "proxy:unix:/run/php/example.sock|fcgi://localhost/"
    </FilesMatch>

    <DirectoryMatch "^/.*/(?:uploads|cache)/">
        <FilesMatch "\.ph(?:p|ar|tml)$">
            Require all denied
        </FilesMatch>
    </DirectoryMatch>
</VirtualHost>

Do not copy the socket path until the matching FPM pool creates it. The upload rule is defense in depth; keep uploaded data outside the document root when the application supports it, generate server-side names, validate type and size, scan where required, and serve downloads through a controlled handler or non-executable storage.

Step 7: build a least-privilege PHP-FPM pool

[example]
user = exampleapp
group = exampleapp

listen = /run/php/example.sock
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

pm = dynamic
pm.max_children = 20
pm.start_servers = 2
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500

clear_env = yes
security.limit_extensions = .php
catch_workers_output = yes
decorate_workers_output = no

php_admin_value[error_log] = /var/log/php-fpm/example-error.log
php_admin_flag[log_errors] = on
php_admin_flag[display_errors] = off
php_admin_value[upload_tmp_dir] = /var/lib/example/tmp
php_admin_value[session.save_path] = /var/lib/example/sessions

On RHEL-family systems the Apache group is commonly apache, not www-data. Create the application identity, runtime directories, and log destination with explicit ownership. Size the process manager from measured memory and concurrency. Too many workers can exhaust RAM and kill the database; too few can create a queue and apparent outage.

Step 8: harden PHP without breaking the application

Find every loaded INI file with php --ini for CLI and a controlled FPM diagnostic. CLI and FPM often load different configuration. Put reviewed overrides in a dedicated file such as 99-security.ini in the active conf.d directory.

expose_php = Off
display_errors = Off
log_errors = On
html_errors = Off

allow_url_include = Off
file_uploads = On
upload_max_filesize = 10M
post_max_size = 12M
max_execution_time = 30
max_input_time = 60
memory_limit = 256M

session.use_cookies = On
session.use_only_cookies = On
session.use_strict_mode = On
session.cookie_secure = On
session.cookie_httponly = On
session.cookie_samesite = Lax
session.use_trans_sid = Off

Set upload and runtime limits to the application contract. session.cookie_secure=On requires HTTPS. SameSite=Lax is a reasonable starting point for many applications but may not fit cross-site identity or embedded workflows. SameSite is defense in depth, not a replacement for application CSRF tokens.

Do not treat disable_functions as a sandbox

The following broad pattern is unsafe as a universal recommendation because it can break HTTP clients, queues, image processing, libraries, maintenance tools, and legitimate application features:

; Unsafe as a copied global list: test per application instead.
disable_functions = allow_url_fopen,apache_child_terminate,apache_setenv,chgrp,chmod,chown,curl_exec,curl_multi_exec,dbase_open,dbmopen,define_syslog_variables,exec,fsockopen,gzinflate,parse_ini_file,passthru,pclose,pcntl_exec,pfsockopen,pg_lo_import,phpinfo,popen,posix_getpwuid,posix_kill,posix_mkfifo,posix_setpgid,posix_setsid,posix_setuid,posix_uname,proc_close,proc_nice,proc_open,proc_terminate,shell_exec,show_source,symlink,system

PHP documents disable_functions as a way to remove named functions; it is not an isolation boundary. As of PHP 8.0, a disabled function is undefined and user code can define that name. Prefer separate Unix identities and pools, minimal filesystem permissions, constrained services, SELinux/AppArmor, container or VM boundaries where appropriate, safe application APIs, and removal of unused extensions. If a specific application does not need process execution functions, test a short explicit list in that pool and verify web, CLI, cron, queue, backup, and deployment paths.

allow_url_fopen was sometimes placed inside a function list, but it is an INI directive, not a function. Decide it separately:

; Disable only if the application does not require URL-aware file wrappers.
allow_url_fopen = Off
allow_url_include = Off

Disabling URL wrappers does not prevent server-side request forgery through cURL or other clients. Applications must validate destinations, restrict schemes, resolve and re-check addresses, block metadata and private networks where required, control redirects, and apply network egress policy.

Step 9: protect files, secrets, and writable directories

The web server should read public code but should not modify it. The application pool should write only to named runtime directories. Deployment should use a separate identity. Keep secrets outside the document root and source repository.

sudo find /var/www/example -xdev -type d -printf '%m %u:%g %p\n' | sort
sudo find /var/www/example -xdev -type f -printf '%m %u:%g %p\n' | sort
sudo find /var/www/example -xdev -perm /002 -ls
sudo find /var/www/example -xdev -type f \
  \( -name '.env' -o -name '*.key' -o -name '*.pem' -o -name '*~' \) -ls

Review before changing. A typical immutable release might be owned by a deployment identity and readable by the application group, while var/, cache, session, and upload locations have narrower application ownership. Do not run chmod -R 777 or blindly chown -R the entire site to the Apache user. Those shortcuts let a compromised web process replace executable code and can destroy intended ACLs, setgid behavior, and deployment ownership.

Step 10: deploy TLS with supported defaults

Use an approved public certificate for an internet service, automate renewal, protect the private key, and verify the full chain and every hostname. Prefer the operating system's supported cryptographic policy and current Apache mod_ssl defaults over a cipher string copied from an undated example.

SSLEngine On
SSLCertificateFile /etc/letsencrypt/live/app.example.net/fullchain.pem
SSLCertificateKeyFile /etc/letsencrypt/live/app.example.net/privkey.pem
SSLProtocol -all +TLSv1.2 +TLSv1.3
SSLSessionTickets Off

Confirm that the installed Apache and TLS library support these directives and that policy does not require a different protocol set. TLS 1.3 cipher suites are not controlled by the classic SSLCipherSuite directive in the same way as TLS 1.2. Do not add SSLProxyProtocol or SSLProxyCipherSuite unless Apache actually makes outbound TLS proxy connections.

sudo apachectl configtest 2>/dev/null || sudo httpd -t
openssl s_client -connect app.example.net:443 -servername app.example.net \
  -verify_return_error </dev/null
curl --fail --show-error --silent --head https://app.example.net/
systemctl list-timers | grep -i cert
journalctl -u certbot --since '30 days ago' 2>/dev/null

Deprecated TLS configuration to remove

Snippet or ideaStatusCurrent treatment
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1Intent is still useful, but the allow-list form is clearerUse SSLProtocol -all +TLSv1.2 +TLSv1.3 when supported and required.
Long hand-maintained SSLCipherSuiteAges and can conflict with system policyStart with supported distribution crypto policy; add a reviewed TLS 1.2 suite only for a documented need.
SSLHonorCipherOrder on/offRelevant mainly to pre-TLS 1.3 negotiationFollow current platform policy and test negotiated TLS 1.2 and 1.3 behavior.
SSLCompression offOften already disabled or unsupported in current librariesVerify effective behavior; keeping an accepted directive is harmless but not the primary TLS control.
SSLRandomSeed and SSLCryptoDevice builtinObsolete platform-era tuningUse the supported OS random source and cryptographic library.
BrowserMatch "MSIE [2-5]" ...Obsolete browser workaroundRemove after configuration validation; do not design current TLS for those clients.
Default localhost certificate in productionWrong identity for the public hostnameInstall an approved certificate containing every served name and automate renewal.
SSLUseStapling On without monitoringMay be useful, but deployment depends on chain and server behaviorTest stapling, resolver reachability, cache, renewal, and failure behavior before relying on it.

Step 11: add response headers per application

Header always set X-Content-Type-Options "nosniff"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
Header always set Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'; form-action 'self'; upgrade-insecure-requests"

; Enable only after HTTPS works for every applicable host and subdomain.
Header always set Strict-Transport-Security "max-age=300"

A Content Security Policy must match the application. The permissive pattern script-src *; style-src *; img-src * does not provide a meaningful same-origin script boundary and may still break legitimate content. Start in Content-Security-Policy-Report-Only, collect reports at a protected endpoint, remove inline code or use nonces/hashes, test authentication, payments, analytics, images, fonts, and error pages, then enforce.

HSTS is cached by browsers. Begin with a short max-age, prove HTTPS and renewal, then increase it. Add includeSubDomains only when every present and future subdomain is HTTPS-ready. Submit to the preload list only after meeting its current requirements and accepting the difficult rollback.

X-Frame-Options: SAMEORIGIN remains a compatibility control, but CSP frame-ancestors is the flexible current policy. X-XSS-Protection is obsolete and should not be used as the XSS defense. Prevent XSS through contextual output encoding, safe DOM APIs, input handling, templating, and CSP defense in depth.

Step 12: restrict MariaDB exposure and accounts

Run the distribution-provided mariadb-secure-installation interactively on a new standalone server, understand each prompt, and re-check the final account state. Current MariaDB commonly uses Unix socket authentication for local root, so setting a database root password is not automatically required.

sudo mariadb-secure-installation
sudo mariadb -e "SELECT User,Host,plugin FROM mysql.user ORDER BY User,Host;"
sudo mariadb -e "SHOW VARIABLES LIKE 'bind_address'; SHOW VARIABLES LIKE 'local_infile';"
ss -lntp | grep ':3306'

For a same-host application, create a late-sorting custom file such as /etc/mysql/mariadb.conf.d/z-custom-my.cnf or /etc/my.cnf.d/z-custom-my.cnf:

[mariadb]
bind-address = 127.0.0.1
local_infile = 0

If the application uses a Unix socket exclusively, evaluate skip-networking. If the database is remote, bind to the correct private address, permit only named application sources through the firewall, require TLS, verify the server identity, and monitor connections. Changing the default port alone is obscurity and not an access control.

Create a least-privilege application account

CREATE DATABASE example_app CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'example_app'@'localhost' IDENTIFIED BY 'REPLACE_WITH_SECRET_FROM_VAULT';
GRANT SELECT, INSERT, UPDATE, DELETE ON example_app.* TO 'example_app'@'localhost';
SHOW GRANTS FOR 'example_app'@'localhost';

Do not paste a real password into shell history or a migration committed to source control. Provision it through the approved secrets workflow. Many applications also need schema migration privileges. Give those to a separate deployment identity for the migration window rather than permanently granting ALTER, DROP, or global privileges to the runtime account.

Require TLS for a remote database

[mariadb]
ssl_cert = /etc/mysql/tls/server-cert.pem
ssl_key = /etc/mysql/tls/server-key.pem
ssl_ca = /etc/mysql/tls/ca.pem
require_secure_transport = ON
SHOW GLOBAL VARIABLES LIKE 'have_ssl';
SHOW GLOBAL VARIABLES LIKE 'require_secure_transport';
SHOW SESSION STATUS LIKE 'Ssl_version';

Use a CA and hostname verification model understood by both server and client. A TLS connection without verified server identity can remain vulnerable to interception. MariaDB version and packaging affect zero-configuration TLS and available options, so verify the current server and connector documentation.

Step 13: secure application code and dependencies

  • Keep the framework, plugins, themes, Composer packages, JavaScript packages, and OS libraries supported and inventoried.
  • Use parameterized queries; never build SQL by concatenating untrusted input.
  • Enforce authorization server-side on every object and action, not only in menus.
  • Encode output for its HTML, attribute, URL, JavaScript, CSS, or other context.
  • Use framework-supported CSRF tokens on state-changing requests.
  • Hash passwords through current framework APIs; do not implement a custom password scheme.
  • Keep production debug mode off and return generic client errors while recording controlled server-side detail.
  • Store secrets in a protected platform or root-readable configuration path outside the document root; define rotation and recovery.
  • Validate uploads by allowed business type, content, size, destination, name, and download policy.
  • Review outbound HTTP destinations and enforce egress boundaries to reduce SSRF impact.

Use OWASP ASVS as a testable application-security requirement set. A WAF such as ModSecurity can reduce selected exploit traffic and provide useful telemetry, but it cannot prove authorization, business logic, safe deployment, or data handling.

Step 14: keep SELinux or AppArmor enforcing

getenforce 2>/dev/null
sestatus 2>/dev/null
aa-status 2>/dev/null
journalctl -t setroubleshoot --since '1 hour ago' 2>/dev/null
journalctl -k --since '1 hour ago' | grep -Ei 'apparmor|avc:.*denied'

Do not respond to an access denial with setenforce 0, SELINUX=disabled, or a broad custom allow rule. Confirm the application path, label, service identity, expected operation, and packaged policy. Use supported labeling tools or a narrowly reviewed policy, then retest. Temporary permissive diagnostics require authorization, a short window, evidence, and immediate restoration.

Step 15: log, monitor, and detect

Centralize Apache access and error logs, PHP-FPM errors and slow requests, MariaDB errors and selected audit events, authentication and privilege events, firewall changes, package changes, certificate renewal, backup results, endpoint alerts, and application security events. Synchronize time and monitor collection gaps.

journalctl -u apache2 -u httpd -u php-fpm -u mariadb --since '2 hours ago'
tail -n 100 /var/log/apache2/error.log 2>/dev/null
tail -n 100 /var/log/httpd/error_log 2>/dev/null
systemctl --failed
df -hT
df -ih

Never log passwords, session IDs, authorization headers, database connection strings, private keys, full payment data, or unnecessary personal data. Protect logs from application write access, define retention, and test alerts for repeated authentication failure, new administrator access, unexpected listeners, web-shell indicators, security control failure, backup deletion, and certificate expiry.

Step 16: validate before reload, then test externally

sudo apachectl configtest 2>/dev/null || sudo httpd -t
sudo php-fpm --test 2>/dev/null || sudo php-fpm -t
sudo mariadbd --validate-config 2>/dev/null

sudo systemctl reload apache2 2>/dev/null || sudo systemctl reload httpd
sudo systemctl reload php-fpm

curl --fail --show-error --silent --head http://app.example.net/
curl --fail --show-error --silent --head https://app.example.net/
curl --fail --show-error --silent https://app.example.net/healthz

Some MariaDB builds do not provide --validate-config; check the server's help and use a supported validation method. A reload may not activate every PHP or Apache change, and package-specific unit names differ. Confirm new worker process start times and effective behavior.

curl -sSI https://app.example.net/ | sed -n '1,30p'
openssl s_client -connect app.example.net:443 -servername app.example.net \
  -status -verify_return_error </dev/null
ss -lntup
sudo mariadb -e "SHOW PROCESSLIST; SHOW GLOBAL STATUS LIKE 'Threads_connected';"

Test from outside the server and from each intended client path. Confirm HTTP redirects to the correct HTTPS hostname, unknown virtual hosts do not expose a default site, TLS presents the correct chain, required pages and APIs work, forbidden files return an intentional denial, uploads cannot execute, sessions survive or expire as designed, database traffic uses the intended socket or private TLS path, and logs contain attributable results without secrets.

Step 17: back up and prove recovery

Back up database data, application content, release artifact references, Apache and PHP configuration, MariaDB configuration, certificates or their reproducible issuance state, secrets through their approved platform, system policy customizations, scheduled jobs, and infrastructure definitions. Separate backup administration from the production identity plane and use immutable or offline protection where appropriate.

  1. Select a recovery point that meets the service's recovery point objective and incident timeline.
  2. Restore into an isolated environment using documented emergency access.
  3. Validate database consistency, application files, owners, modes, ACLs, labels, secrets, certificates, and dependencies.
  4. Run application, authentication, authorization, upload, queue, cron, email, and health checks.
  5. Measure elapsed time against the recovery time objective.
  6. Record evidence, defects, owner, remediation date, and the next exercise.

A successful backup job is not proof of recovery. The proof is a controlled restore that produces a secure, functioning service within its objectives.

Step 18: troubleshoot by layer

SymptomRead-only checksLikely boundaryUnsafe shortcut to avoid
Connection refusedss -lntup, service status, local curl, firewall and load balancer healthListener, address, port, service start, routeOpening every port or disabling the firewall
Wrong site or certificateapachectl -S, SNI with openssl s_client, DNS, virtual-host logsDNS, SNI, default vhost, certificate mappingCopying one certificate to every site
403 ForbiddenApache error log, directory sections, Unix traversal modes, ACLs, SELinux/AppArmorAuthorization, filesystem, MAC policychmod -R 777 or disabling SELinux
502/503 from PHPFPM service, socket existence and mode, pool logs, worker capacity, handler mappingApache-to-FPM handoff or exhausted poolMaking the socket world-writable
PHP file downloads as textFilesMatch/handler, enabled modules, FPM socket, virtual-host selectionMissing PHP handlerLeaving source code public while experimenting
Database access deniedExact User@Host, grants, socket/TCP selection, secret source, server logAccount match, privilege, credentialGranting global privileges to the runtime user
Database connection timeoutListener, bind address, route, firewall, TLS, connector timeoutNetwork or TLS pathBinding MariaDB to every interface without a source restriction
Changes not activeLoaded config paths, syntax test, include order, worker start time, effective responseWrong file, service, pool, or reload behaviorRepeated blind restarts
Disk suddenly fulldf -hT, df -ih, logs, database growth, temp and deleted-open filesLogs, uploads, database, inode exhaustionDeleting unknown database or log files directly

Complete configuration coverage: Apache directories and methods

The following blocks preserve every Apache directory and method configuration commonly encountered in a LAMP hardening checklist. They are shown for recognition and migration review, not as one file to paste. The status below each block determines what should be active.

Autoindex icons directory block

# Common packaged path: /etc/httpd/conf.d/autoindex.conf
<Directory "/usr/share/httpd/icons">
    Options -Indexes -MultiViews -FollowSymLinks
    AllowOverride None
    Require all granted
</Directory>

Current action: keep -Indexes, -MultiViews, and AllowOverride None when they match the application. Do not grant public access to /usr/share/httpd/icons unless the active configuration uses it. The block also lacks a closing </Directory> in some copied versions; Apache configuration must always close the section and pass apachectl configtest. -FollowSymLinks may affect performance and application behavior, so decide it for the actual directory rather than globally.

TRACE and TRACK rewrite block

# Sometimes placed in /etc/httpd/conf.d/tracetrack.conf
RewriteEngine On
RewriteCond %{REQUEST_METHOD} ^(TRACE|TRACK)
RewriteRule .* - [F]

Current action: TraceEnable Off is the direct Apache control for TRACE and also affects proxy behavior. TRACK is not a standard Apache method. Keep the rewrite block only if a tested application or policy requires the additional explicit denial and mod_rewrite is already approved. Do not load an otherwise unused module only for a redundant rule.

UserDir disabled block

# Common packaged path: /etc/httpd/conf.d/userdir.conf
UserDir disabled

# Do not enable this unless per-user public sites are an approved requirement.
# UserDir public_html

<Directory "/home/*/public_html">
    AllowOverride FileInfo AuthConfig Limit Indexes
    Options -MultiViews -Indexes -SymLinksIfOwnerMatch -IncludesNoExec
    Require method GET POST OPTIONS
</Directory>

Current action: keep UserDir disabled for a normal application server. The enablement example is retained because its AllowOverride, Options, symlink, and method restrictions are part of the configuration set, but it should remain inactive unless the server intentionally hosts per-user sites. If enabled, isolate users and applications, review home-directory traversal permissions, PHP execution, authentication files, uploads, logs, and resource limits.

Complete configuration coverage: response-header block

This full block preserves every header directive for review. It contains useful controls, obsolete behavior, and an overly permissive CSP. Do not activate it unchanged.

# Review-only block: do not paste unchanged.
Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Header always set X-XSS-Protection "1; mode=block"
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set Content-Security-Policy "default-src 'self'; font-src *; img-src * data:; script-src *; style-src *;"
Header always set Permissions-Policy "geolocation=(), midi=(), sync-xhr=(), microphone=(), camera=(), magnetometer=(), gyroscope=(), fullscreen=(self), payment=()"
Header always set Referrer-Policy "strict-origin"
DirectiveCurrent decisionReplacement or validation
HSTS one year, subdomains, preloadUnsafe as a first deploymentStart with max-age=300, prove HTTPS and renewal, then deliberately increase duration and scope.
X-XSS-ProtectionObsoleteRemove it. Use contextual output encoding, safe DOM APIs, framework controls, and a tested CSP.
X-Frame-Options SAMEORIGINUseful compatibility controlAlso express the intended framing policy with CSP frame-ancestors.
X-Content-Type-Options nosniffKeepSend accurate Content-Type values and test downloads and static assets.
Wildcard font, image, script, and style sourcesReplaceBuild an application-specific allowlist; avoid script-src * and style-src *.
Permissions Policy feature listRetain only relevant featuresUnknown or retired features may be ignored. Test the current application and browser behavior.
Referrer-Policy strict-originValid policyKeep if it matches the privacy and application contract; strict-origin-when-cross-origin is another common current choice.

Complete configuration coverage: TLS virtual host

The full TLS configuration below is retained so no directive disappears from the migration. It combines current ideas, package defaults, proxy-only settings, obsolete random-source tuning, a localhost certificate, and an obsolete Internet Explorer workaround. Treat it as a review inventory, not a deployable virtual host.

# Review-only inventory: do not deploy unchanged.
Listen 443 https
SSLPassPhraseDialog exec:/usr/libexec/httpd-ssl-pass-dialog
SSLSessionCache shmcb:/run/httpd/sslcache(512000)
SSLSessionCacheTimeout 300
SSLRandomSeed startup file:/dev/urandom 256
SSLRandomSeed connect builtin
SSLCryptoDevice builtin

<VirtualHost _default_:443>
    ErrorLog logs/ssl_error_log
    TransferLog logs/ssl_access_log
    LogLevel warn
    SSLEngine on
    Protocols h2 http/1.1
    SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
    SSLProxyProtocol all -SSLv3
    SSLHonorCipherOrder on
    SSLCompression off
    SSLSessionTickets off
    SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
    SSLProxyCipherSuite HIGH:MEDIUM:!MD5:!RC4
    SSLCertificateFile /etc/pki/tls/certs/localhost.crt
    SSLCertificateKeyFile /etc/pki/tls/private/localhost.key

    <FilesMatch "\.(cgi|shtml|phtml|php)$">
        SSLOptions +StdEnvVars
    </FilesMatch>
    <Directory "/var/www/cgi-bin">
        SSLOptions +StdEnvVars
    </Directory>

    BrowserMatch "MSIE [2-5]" \
        nokeepalive ssl-unclean-shutdown \
        downgrade-1.0 force-response-1.0
    CustomLog logs/ssl_request_log \
        "%t %h %{SSL_PROTOCOL}x %{SSL_CIPHER}x \"%r\" %b"
</VirtualHost>

# A second commonly copied TLS block:
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256
SSLHonorCipherOrder off
SSLSessionTickets off
SSLUseStapling On
SSLStaplingCache "shmcb:logs/ssl_stapling(32768)"
TLS itemKeep, replace, or scopeCurrent action
Listen 443 https, SSLEngine on, Protocols h2 http/1.1Keep when Apache terminates HTTPS and HTTP/2 is supportedConfirm listener ownership, ALPN negotiation, and virtual-host mapping.
SSLPassPhraseDialogDeployment-specificUse only if the private-key and unattended-start design requires it; do not embed a passphrase in a script.
Session cache and timeoutKeep or tune from supported package defaultsVerify cache path ownership, capacity, restart behavior, and performance.
SSLRandomSeed, SSLCryptoDevice builtinRemove as obsolete platform-era tuningUse the operating system random source and supported TLS library.
Relative log paths, LogLevel warn, and TLS request logKeep only with known ServerRoot and privacy policyUse controlled absolute or package-standard destinations, centralize events, and avoid sensitive request data.
SSLProxyProtocol, SSLProxyCipherSuiteProxy scope onlyRemove unless Apache initiates outbound TLS proxy connections; then validate upstream identity.
Long TLS 1.2 cipher list and conflicting cipher order valuesReplace with one reviewed policyUse supported distribution crypto policy and test required TLS 1.2/1.3 clients.
Localhost certificate pathsReplace for productionInstall the approved full chain and key for every served hostname, with renewal monitoring.
SSLOptions +StdEnvVars for CGI/PHP and the CGI directoryScope to an actual dependencyRemove if CGI and environment export are unused; do not broaden exposure for PHP-FPM.
MSIE 2-5 BrowserMatchRemoveObsolete workaround for unsupported clients.
Stapling and cacheOptional current featureTest chain, responder and DNS reachability, cache path, renewal, response status, and failure behavior.

Complete configuration coverage: file locations

Configuration pathDistribution contextCurrent use
/etc/php.iniCommon RHEL-family main PHP configurationUse a controlled included override when packaging supports it; verify the FPM-loaded path, not only CLI.
/etc/httpd/conf/httpd.confRHEL-family Apache root configurationPrefer small root-owned included files for site policy and minimize vendor-file changes.
/etc/httpd/conf.d/autoindex.confPackaged autoindex configurationDisable unused autoindex content or constrain it explicitly.
/etc/httpd/conf.d/tracetrack.confOptional locally created method-denial fileUsually replace with TraceEnable Off; retain only a justified rewrite exception.
/etc/httpd/conf.d/userdir.confPackaged per-user site configurationKeep UserDir disabled unless per-user hosting is an approved service.
/etc/httpd/conf.d/ssl.confPackaged RHEL-family TLS configurationKeep package integration, but move hostname-specific certificate and virtual-host policy into controlled site configuration.
/etc/my.cnfMariaDB/MySQL root option file on many systemsPrefer a late-sorting custom include such as /etc/my.cnf.d/z-custom-my.cnf; verify include order with the installed server.

Hardening snippets: current status and replacements

Configuration areaKeep or replaceReason and current action
expose_php = OffKeep as disclosure reductionUseful but not a patch or security boundary. Verify headers after FPM reload.
Large global disable_functions listReplace with per-application decisionIt can break applications and is not isolation. Use least-privilege pools, filesystem/network controls, confinement, and tested minimal exceptions.
allow_url_fopen inside disable_functionsCorrect the syntax and threat modelIt is an INI directive. Decide separately; also implement SSRF validation and egress controls.
ServerSignature Off, ServerTokens ProdKeepReduce easy disclosure while continuing to patch and inventory the actual server.
TraceEnable OffKeep when TRACE is not requiredDirect Apache control; a rewrite rule for TRACE/TRACK is usually unnecessary.
Options -Indexes -MultiViewsKeep where the application does not require themAdd explicit directory scope and test routing. Do not grant access to an unused icons directory.
UserDir disabledKeep when per-user sites are not requiredReduces exposure and user enumeration; do not publish the example enablement block.
X-XSS-ProtectionRemoveObsolete browser behavior. Use application output safety and CSP.
X-Frame-Options: SAMEORIGINOptional compatibility layerUse CSP frame-ancestors as the primary current framing policy.
Permissive wildcard CSPReplaceBuild and test an application-specific policy, beginning in report-only mode.
HSTS with one-year, subdomains, preload immediatelyStage graduallyStart short, prove all HTTPS and renewal paths, then expand scope and duration deliberately.
Static legacy TLS cipher list and MSIE workaroundReplaceUse supported crypto policy, TLS 1.2/1.3, correct certificates, renewal, and representative testing.

Production acceptance checklist

  • The OS, Apache, PHP, MariaDB, application, and dependencies are supported, inventoried, and assigned owners.
  • Administrator access is attributable, MFA-protected where supported, restricted by network path, and recoverable through a tested emergency method.
  • Only intended listeners are reachable; MariaDB uses a local socket, loopback, or restricted private TLS path.
  • Apache denies filesystem access by default, exposes only named document roots, disables unused options and modules, and passes syntax tests.
  • Each application uses a least-privilege FPM identity and socket, controlled runtime directories, and non-executable upload storage.
  • PHP does not expose errors to users; sessions, limits, extensions, URL wrappers, and execution functions match tested application requirements.
  • Code is not writable by the web process, secrets are outside the public tree and repository, and no application directory is world-writable.
  • TLS presents the correct hostname and chain, supports approved protocols, renews automatically, and has a monitored failure path.
  • Headers are application-specific; CSP is tested, HSTS is staged, and obsolete X-XSS-Protection is absent.
  • MariaDB has no unnecessary anonymous or remote administrative accounts; application and migration identities have separate minimum grants.
  • SELinux or AppArmor remains enforcing with reviewed labels and policy rather than broad bypasses.
  • Central logs, control health, disk capacity, certificates, authentication, backups, and application security events have owners and alerts.
  • A representative restore has proven data, configuration, identity, dependencies, security, service function, and recovery time.
  • Every high-impact change has a verified target, maintenance boundary, test, health check, and rollback.

For repeatable shell checks and failure-safe automation, use the Bash features guide. For secure remote administration, host identity, keys, bastions, and tunnels, use the SSH client operations guide. For application services packaged in containers, use the Docker step-by-step guide.

Official LAMP security references

Related technical notes

Mail queue investigation separating deferred traffic, SMTP responses, route health, and safe diagnostic evidenceLinux, MTA & Security · Jul 20, 2026 · 3 min read

Postfix Queue Backlog: A Safe Diagnostic Runbook

Diagnose a Postfix backlog from queue age, destination groups, SMTP responses, host health, DNS, and route evidence before changing retries or concurrency.

Technical review

Need this checked against your own sending system?

Share the domain, headers, bounces, provider warning, logs, or infrastructure symptom and NitWings will identify the practical next step.

Schedule a Technical Review
Advertisement