Secure Your LAMP Stack: Step-by-Step Hardening Guide
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.
| Layer | Primary boundary | Minimum evidence | Common failure |
|---|---|---|---|
| Linux host | Supported packages, administrator access, firewall, MAC policy, time, storage | OS release, package origin, enabled services, listeners, firewall, SELinux/AppArmor, updates | Unsupported OS, exposed administration, world-writable files, disabled confinement |
| Apache | Virtual hosts, modules, request limits, TLS, headers, logs, PHP handoff | Loaded modules, parsed virtual hosts, syntax test, listener map, representative responses | Default site exposed, broad overrides, directory listing, unsafe proxy or handler mapping |
| PHP-FPM | Pool identity, socket, extensions, runtime limits, sessions, secrets | Loaded INI files, pool configuration test, process identity, socket permissions, application tests | One shared overprivileged pool, secrets in web root, executable upload directory |
| MariaDB | Bind address, account hosts and grants, TLS, data files, backup and recovery | Listeners, users, grants, TLS variables, backup result, restore test | Remote root, wildcard-host account, application using administrator credentials |
| Application | Authentication, authorization, input/output handling, dependencies, uploads | Threat model, tests, dependency inventory, deploy record, security events | Assuming 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.
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 idea | Status | Current treatment |
|---|---|---|
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1 | Intent is still useful, but the allow-list form is clearer | Use SSLProtocol -all +TLSv1.2 +TLSv1.3 when supported and required. |
Long hand-maintained SSLCipherSuite | Ages and can conflict with system policy | Start with supported distribution crypto policy; add a reviewed TLS 1.2 suite only for a documented need. |
SSLHonorCipherOrder on/off | Relevant mainly to pre-TLS 1.3 negotiation | Follow current platform policy and test negotiated TLS 1.2 and 1.3 behavior. |
SSLCompression off | Often already disabled or unsupported in current libraries | Verify effective behavior; keeping an accepted directive is harmless but not the primary TLS control. |
SSLRandomSeed and SSLCryptoDevice builtin | Obsolete platform-era tuning | Use the supported OS random source and cryptographic library. |
BrowserMatch "MSIE [2-5]" ... | Obsolete browser workaround | Remove after configuration validation; do not design current TLS for those clients. |
| Default localhost certificate in production | Wrong identity for the public hostname | Install an approved certificate containing every served name and automate renewal. |
SSLUseStapling On without monitoring | May be useful, but deployment depends on chain and server behavior | Test 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.
- Select a recovery point that meets the service's recovery point objective and incident timeline.
- Restore into an isolated environment using documented emergency access.
- Validate database consistency, application files, owners, modes, ACLs, labels, secrets, certificates, and dependencies.
- Run application, authentication, authorization, upload, queue, cron, email, and health checks.
- Measure elapsed time against the recovery time objective.
- 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
| Symptom | Read-only checks | Likely boundary | Unsafe shortcut to avoid |
|---|---|---|---|
| Connection refused | ss -lntup, service status, local curl, firewall and load balancer health | Listener, address, port, service start, route | Opening every port or disabling the firewall |
| Wrong site or certificate | apachectl -S, SNI with openssl s_client, DNS, virtual-host logs | DNS, SNI, default vhost, certificate mapping | Copying one certificate to every site |
| 403 Forbidden | Apache error log, directory sections, Unix traversal modes, ACLs, SELinux/AppArmor | Authorization, filesystem, MAC policy | chmod -R 777 or disabling SELinux |
| 502/503 from PHP | FPM service, socket existence and mode, pool logs, worker capacity, handler mapping | Apache-to-FPM handoff or exhausted pool | Making the socket world-writable |
| PHP file downloads as text | FilesMatch/handler, enabled modules, FPM socket, virtual-host selection | Missing PHP handler | Leaving source code public while experimenting |
| Database access denied | Exact User@Host, grants, socket/TCP selection, secret source, server log | Account match, privilege, credential | Granting global privileges to the runtime user |
| Database connection timeout | Listener, bind address, route, firewall, TLS, connector timeout | Network or TLS path | Binding MariaDB to every interface without a source restriction |
| Changes not active | Loaded config paths, syntax test, include order, worker start time, effective response | Wrong file, service, pool, or reload behavior | Repeated blind restarts |
| Disk suddenly full | df -hT, df -ih, logs, database growth, temp and deleted-open files | Logs, uploads, database, inode exhaustion | Deleting 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"
| Directive | Current decision | Replacement or validation |
|---|---|---|
| HSTS one year, subdomains, preload | Unsafe as a first deployment | Start with max-age=300, prove HTTPS and renewal, then deliberately increase duration and scope. |
X-XSS-Protection | Obsolete | Remove it. Use contextual output encoding, safe DOM APIs, framework controls, and a tested CSP. |
X-Frame-Options SAMEORIGIN | Useful compatibility control | Also express the intended framing policy with CSP frame-ancestors. |
X-Content-Type-Options nosniff | Keep | Send accurate Content-Type values and test downloads and static assets. |
| Wildcard font, image, script, and style sources | Replace | Build an application-specific allowlist; avoid script-src * and style-src *. |
| Permissions Policy feature list | Retain only relevant features | Unknown or retired features may be ignored. Test the current application and browser behavior. |
Referrer-Policy strict-origin | Valid policy | Keep 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 item | Keep, replace, or scope | Current action |
|---|---|---|
Listen 443 https, SSLEngine on, Protocols h2 http/1.1 | Keep when Apache terminates HTTPS and HTTP/2 is supported | Confirm listener ownership, ALPN negotiation, and virtual-host mapping. |
SSLPassPhraseDialog | Deployment-specific | Use only if the private-key and unattended-start design requires it; do not embed a passphrase in a script. |
| Session cache and timeout | Keep or tune from supported package defaults | Verify cache path ownership, capacity, restart behavior, and performance. |
SSLRandomSeed, SSLCryptoDevice builtin | Remove as obsolete platform-era tuning | Use the operating system random source and supported TLS library. |
Relative log paths, LogLevel warn, and TLS request log | Keep only with known ServerRoot and privacy policy | Use controlled absolute or package-standard destinations, centralize events, and avoid sensitive request data. |
SSLProxyProtocol, SSLProxyCipherSuite | Proxy scope only | Remove unless Apache initiates outbound TLS proxy connections; then validate upstream identity. |
| Long TLS 1.2 cipher list and conflicting cipher order values | Replace with one reviewed policy | Use supported distribution crypto policy and test required TLS 1.2/1.3 clients. |
| Localhost certificate paths | Replace for production | Install the approved full chain and key for every served hostname, with renewal monitoring. |
SSLOptions +StdEnvVars for CGI/PHP and the CGI directory | Scope to an actual dependency | Remove if CGI and environment export are unused; do not broaden exposure for PHP-FPM. |
MSIE 2-5 BrowserMatch | Remove | Obsolete workaround for unsupported clients. |
| Stapling and cache | Optional current feature | Test chain, responder and DNS reachability, cache path, renewal, response status, and failure behavior. |
Complete configuration coverage: file locations
| Configuration path | Distribution context | Current use |
|---|---|---|
/etc/php.ini | Common RHEL-family main PHP configuration | Use a controlled included override when packaging supports it; verify the FPM-loaded path, not only CLI. |
/etc/httpd/conf/httpd.conf | RHEL-family Apache root configuration | Prefer small root-owned included files for site policy and minimize vendor-file changes. |
/etc/httpd/conf.d/autoindex.conf | Packaged autoindex configuration | Disable unused autoindex content or constrain it explicitly. |
/etc/httpd/conf.d/tracetrack.conf | Optional locally created method-denial file | Usually replace with TraceEnable Off; retain only a justified rewrite exception. |
/etc/httpd/conf.d/userdir.conf | Packaged per-user site configuration | Keep UserDir disabled unless per-user hosting is an approved service. |
/etc/httpd/conf.d/ssl.conf | Packaged RHEL-family TLS configuration | Keep package integration, but move hostname-specific certificate and virtual-host policy into controlled site configuration. |
/etc/my.cnf | MariaDB/MySQL root option file on many systems | Prefer 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 area | Keep or replace | Reason and current action |
|---|---|---|
expose_php = Off | Keep as disclosure reduction | Useful but not a patch or security boundary. Verify headers after FPM reload. |
Large global disable_functions list | Replace with per-application decision | It can break applications and is not isolation. Use least-privilege pools, filesystem/network controls, confinement, and tested minimal exceptions. |
allow_url_fopen inside disable_functions | Correct the syntax and threat model | It is an INI directive. Decide separately; also implement SSRF validation and egress controls. |
ServerSignature Off, ServerTokens Prod | Keep | Reduce easy disclosure while continuing to patch and inventory the actual server. |
TraceEnable Off | Keep when TRACE is not required | Direct Apache control; a rewrite rule for TRACE/TRACK is usually unnecessary. |
Options -Indexes -MultiViews | Keep where the application does not require them | Add explicit directory scope and test routing. Do not grant access to an unused icons directory. |
UserDir disabled | Keep when per-user sites are not required | Reduces exposure and user enumeration; do not publish the example enablement block. |
X-XSS-Protection | Remove | Obsolete browser behavior. Use application output safety and CSP. |
X-Frame-Options: SAMEORIGIN | Optional compatibility layer | Use CSP frame-ancestors as the primary current framing policy. |
| Permissive wildcard CSP | Replace | Build and test an application-specific policy, beginning in report-only mode. |
| HSTS with one-year, subdomains, preload immediately | Stage gradually | Start short, prove all HTTPS and renewal paths, then expand scope and duration deliberately. |
| Static legacy TLS cipher list and MSIE workaround | Replace | Use 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-Protectionis 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
- Apache HTTP Server 2.4 Security Tips
- Apache core directive reference
- Apache SSL/TLS encryption documentation
- PHP core php.ini directives
- PHP-FPM configuration reference
- PHP secure session INI settings
- MariaDB secure-installation documentation
- MariaDB requiring TLS documentation
- OWASP Application Security Verification Standard
- OWASP Secure Headers Project


