Three loose ends turn a hardened application into a hardened deployment: keep secrets out of the code, send the right headers, and stop the server advertising itself. By default expose_php is On, adding an X-Powered-By header that names the exact version.
Serving a page under php -d expose_php=1 versus =0 and reading the header with curl 3,008 :
== expose_php=On == X-Powered-By: PHP/8.5.4 == expose_php=Off == (no X-Powered-By header)
Set expose_php = Off for production. Secrets, database passwords, API keys, encryption keys, must never live in committed source. Read them from the environment with getenv() (populated by the web server, an orchestrator, or a .env loaded by vlucas/phpdotenv and kept out of git), and make sure the server refuses to serve .env, .git and backups (Subsections 2.8.6 and 2.10.1). Set the headers once in the front controller.
foreach ([
'X-Content-Type-Options: nosniff',
'Referrer-Policy: strict-origin-when-cross-origin',
'Cross-Origin-Opener-Policy: same-origin',
'Strict-Transport-Security: max-age=31536000; includeSubDomains',
] as $h) header($h);
header_remove('X-Powered-By'); // belt and suspenders if expose_php is still OnX-Content-Type-Options: nosniff ... Strict-Transport-Security: max-age=31536000; includeSubDomains
These four, plus the CSP of Subsection 4.17.7, are the baseline; Security Headers and CORS explains each and adds CORS. Set errors to log rather than display in production (display_errors = Off, log_errors = On, Subsection 4.13.7) so a stack trace never reaches a visitor, covering misconfiguration, the second OWASP risk.