1. Control of the module set (reduce the attack surface)
Every loaded module is a new potential attack vector (a vulnerability in the module itself, or its misconfiguration). The fewer modules — the fewer holes.
Historically, mod_status, mod_autoindex, CGI, and others “got hit”. With extra modules you can: reveal server internals, bypass authorization, get RCE in a chain with a vulnerable app/interpreter, or simply expand the DoS surface.
Principle of minimizing the Trusted Computing Base (TCB). The less code runs with the web server’s privileges, the lower the chance of a vulnerability and the shorter the exploitation chain.
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule actions_module modules/mod_actions.so
LoadModule alias_module modules/mod_alias.so
LoadModule allowmethods_module modules/mod_allowmethods.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule include_module modules/mod_include.so
LoadModule isapi_module modules/mod_isapi.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule mime_module modules/mod_mime.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.soKeep the list short. Anything you don’t need — remove. “Might come in handy” is a poor security strategy.
2. “Deny by default” policy at the root
Lock everything down, then open only the required directories selectively.
Unobtrusive directories/files (backups, temporary ones, forgotten dev folders) can suddenly become anonymously accessible.
Default-deny > Default-allow. It’s easier to mistakenly add an extra Require all granted than to remember to close something that shouldn’t be exposed.
<Directory />
AllowOverride none
Require all denied
</Directory>First forbid everything, then grant exactly as much access as the site needs.
3. Harden the site directory: indexes, .ht*, and managed access
Disable auto-indexing, “shield” .ht*, and don’t let .htaccess spoil your policy.
Options Indexes→ leakage of project structure and static files.- Allowed
.htaccess→ local RCE/rule bypass via a single committed file. - Hidden/service files → direct reading of secrets.
.htaccess is convenient but unsafe: scattered policy, slower I/O, higher risk of config injection if the deployment is compromised.
<Directory "${SRVROOT}/htdocs">
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
<FilesMatch "^\.(?!well-known/).+">
Require all denied
</FilesMatch>
<FilesMatch "\.(env|ini|cfg|conf|bak|swp)$">
Require all denied
</FilesMatch>
</Directory>
<Files ".ht*">
Require all denied
</Files>Close indexes, forbid .htaccess and “invisible” files, and leave only what users actually need.
4. Complete ban on CGI
CGI is a classic source of vulnerabilities: executing external interpreters via an HTTP request.
A buggy CGI script == RCE. Even if you don’t use CGI, a directory left “just in case” is a RISK.
The “disable interpreters on the web” principle minimizes the threat model.
<Directory "${SRVROOT}/cgi-bin">
AllowOverride None
Options None
Require all denied
</Directory>
<LocationMatch "^/cgi-bin/?">
Require all denied
</LocationMatch>No CGI — no class of attacks that rely on it.
5. Minimizing metadata leaks and tracing vectors
Hide version info, disable TRACE, don’t give scanners hints.
Simple banner-grabbing matches an exploit to your version. TRACE helps with XST and header tricks.
Security by configuration: don’t give attackers extra telemetry; remove rarely needed methods.
TraceEnable off
ServerTokens Prod
ServerSignature Off
HostnameLookups OffThe less a scanner knows about you, the harder it is to aim.
6. Strict security headers (XFO, XCTO, XSSP, Referrer, Permissions)
Restrict framing, MIME sniffing, referrers, and access to devices.
- Clickjacking via
<iframe>withoutX-Frame-Options. - MIME sniffing → uploading JS as “text” turns into executable script.
- Excess referrers → leakage of tokens/URLs with secrets.
- Permissions-Policy → limit potential API abuse.
Frontend security is browser policy. Headers are your compact CSP-lite.
<IfModule headers_module>
RequestHeader unset Proxy early
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>Add a full CSP in production. X-XSS-Protection is deprecated but harmless — the real value comes from CSP.
7. Blocking encoded path bypasses
Forbid decoding of encoded slashes and catch traversal/exploit attempts via suspicious URIs.
%2f, %5c, double/triple encoding and “..” can punch through route checks and pull files outside DocumentRoot or land in an unexpected location.
URI normalization is painful. It’s simpler to explicitly cut off non-standard forms and double encodings.
AllowEncodedSlashes NoDecode
SetEnvIfNoCase Request_URI "(?i)(?:/cgi-bin\b|/bin/sh\b)" BAD_URI=1
SetEnvIfNoCase Request_URI "(?i)(?:\.\.|%2e%2e|%25%32%65%25%32%65|%252e%252e|%2f%2e%2e|%5c%2e%2e)" BAD_URI=1
SetEnvIfNoCase Request_URI "(?i)(?:%2f|%5c)" BAD_URI=1
<Location "/">
<RequireAll>
Require all granted
Require not env BAD_URI
</RequireAll>
<LimitExcept GET POST HEAD>
Require all denied
</LimitExcept>
</Location>This is a lightweight “WAF-lite”. It doesn’t replace a full WAF, but it will cut a lot of junk and dangerous requests.
8. Whitelist of methods
Limit methods to the safe minimum (reads/simple forms).
Methods like PUT, DELETE, OPTIONS, PROPFIND, and exotics can “light up” through a module/app bug and grant write/delete.
Least functionality: enable only what routes truly need.
<LimitExcept GET POST HEAD>
Require all denied
</LimitExcept>If the app needs more, you know where to open it — selectively, not “everywhere”.
9. Limiting request sizes (anti-DoS/anti-smuggling)
Protect the header parser and upstream from huge/dirty requests.
Large headers/many fields are classic low-rate DoS. Also fewer chances for clever overflow/offset attacks (HTTP Request Smuggling loves non-standard headers).
Rate-limiting != size-limiting. You need both. This is the size limiter.
LimitRequestLine 8190
LimitRequestFieldSize 8190
LimitRequestFields 100Values are reasonable for most frameworks. If you have unusually large JWTs in cookies, reconsider the design — not the boundaries.
10. TLS: sessions and OCSP stapling
More stable and faster TLS sessions and certificate revocation checking (stapling).
Technically harder to “break in,” but without stapling → clients query the OCSP server themselves (leaks, instability). Without a session cache, it’s easier to choke the CPU with spikes of TLS handshakes.
OCSP stapling — the server attaches the certificate’s fresh status to the handshake; caches — reduce the cost of repeat connections.
<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLSessionCache "shmcb:${SRVROOT}/logs/ssl_scache(512000)"
SSLSessionCacheTimeout 300
SSLUseStapling On
SSLStaplingCache "shmcb:${SRVROOT}/logs/stapling_cache(128000)"
</IfModule>This is the basic hygiene minimum for TLS.
11. Enforcing HTTPS
Any HTTP request → redirect to HTTPS. Eliminate mixed traffic.
MitM, cookie interception, content tampering, downgrade attacks. HTTP is a postcard without an envelope.
HTTPS-everywhere + HSTS is the industry standard. First the redirect, then HSTS (with preload — as appropriate).
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
ErrorLog "logs/redirect_error.log"
CustomLog "logs/redirect_access.log" combined
</VirtualHost>We’ll dive deeper into this later.
12. Logs as a security tool (detection and incident analysis)
Without logs you’re blind: you won’t see scans, brute-force attempts, LFI/RCE, or unexpected methods.
The attacker “fades into the shadows”: without correct format and separate logs, the investigation will drag on or become impossible.
Observability is part of security. Access + error logs, separate files for redirects/SSL/application.
ErrorLog "logs/error.log"
LogLevel warn
<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
<IfModule logio_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
</IfModule>
CustomLog "logs/access.log" combined
</IfModule>Personally, I periodically feed the logs to AI for checks. Yes, a crutch — I’ll automate it in the future.
13. Anti-proxy junk in requests
Strip the suspicious Proxy header early — sometimes helpful against crooked proxy chains/bugs.
Vulnerabilities at proxy/backend boundaries often flare up from unexpected service headers in raw form.
Header hygiene: anything the application doesn’t need is better cleaned/reserved.
RequestHeader unset Proxy earlyA small thing, but nice. Especially in a zoo of CDNs, layered proxies, and backends.
Summary
Here’s roughly the code you should end up with:
Define SRVROOT "C:/Apache24"
ServerRoot "${SRVROOT}"
Listen 80
LoadModule access_compat_module modules/mod_access_compat.so
LoadModule actions_module modules/mod_actions.so
LoadModule alias_module modules/mod_alias.so
LoadModule allowmethods_module modules/mod_allowmethods.so
LoadModule auth_basic_module modules/mod_auth_basic.so
LoadModule authn_core_module modules/mod_authn_core.so
LoadModule authn_file_module modules/mod_authn_file.so
LoadModule authz_core_module modules/mod_authz_core.so
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
LoadModule authz_host_module modules/mod_authz_host.so
LoadModule authz_user_module modules/mod_authz_user.so
LoadModule autoindex_module modules/mod_autoindex.so
LoadModule dir_module modules/mod_dir.so
LoadModule env_module modules/mod_env.so
LoadModule headers_module modules/mod_headers.so
LoadModule include_module modules/mod_include.so
LoadModule isapi_module modules/mod_isapi.so
LoadModule log_config_module modules/mod_log_config.so
LoadModule mime_module modules/mod_mime.so
LoadModule negotiation_module modules/mod_negotiation.so
LoadModule setenvif_module modules/mod_setenvif.so
LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
LoadModule ssl_module modules/mod_ssl.so
LoadFile "C:/Program Files/Python313/python313.dll"
LoadModule wsgi_module "C:/Program Files/Python313/Lib/site-packages/mod_wsgi/server/mod_wsgi.cp313-win_amd64.pyd"
WSGIPythonHome "C:/Apache24/htdocs/venv"
WSGIPythonPath "C:/Apache24/htdocs/our_project"
<IfModule unixd_module>
User daemon
Group daemon
</IfModule>
ServerAdmin admin@example.com
ServerName example.com
<Directory />
AllowOverride none
Require all denied
</Directory>
DocumentRoot "${SRVROOT}/htdocs"
<Directory "${SRVROOT}/htdocs">
Options -Indexes +FollowSymLinks
AllowOverride None
Require all granted
<FilesMatch "^\.(?!well-known/).+">
Require all denied
</FilesMatch>
<FilesMatch "\.(env|ini|cfg|conf|bak|swp)$">
Require all denied
</FilesMatch>
</Directory>
<IfModule dir_module>
DirectoryIndex index.html
</IfModule>
<Files ".ht*">
Require all denied
</Files>
ErrorLog "logs/error.log"
LogLevel warn
<IfModule log_config_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
LogFormat "%h %l %u %t \"%r\" %>s %b" common
<IfModule logio_module>
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
</IfModule>
CustomLog "logs/access.log" combined
</IfModule>
<Directory "${SRVROOT}/cgi-bin">
AllowOverride None
Options None
Require all denied
</Directory>
<LocationMatch "^/cgi-bin/?">
Require all denied
</LocationMatch>
<IfModule mime_module>
TypesConfig conf/mime.types
AddType application/x-compress .Z
AddType application/x-gzip .gz .tgz
</IfModule>
<IfModule proxy_html_module>
Include conf/extra/proxy-html.conf
</IfModule>
TraceEnable off
ServerTokens Prod
ServerSignature Off
HostnameLookups Off
<IfModule headers_module>
RequestHeader unset Proxy early
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
Header always set Referrer-Policy "strict-origin-when-cross-origin"
Header always set Permissions-Policy "camera=(), microphone=(), geolocation=()"
</IfModule>
AllowEncodedSlashes NoDecode
SetEnvIfNoCase Request_URI "(?i)(?:/cgi-bin\b|/bin/sh\b)" BAD_URI=1
SetEnvIfNoCase Request_URI "(?i)(?:\.\.|%2e%2e|%25%32%65%25%32%65|%252e%252e|%2f%2e%2e|%5c%2e%2e)" BAD_URI=1
SetEnvIfNoCase Request_URI "(?i)(?:%2f|%5c)" BAD_URI=1
<Location "/">
<RequireAll>
Require all granted
Require not env BAD_URI
</RequireAll>
<LimitExcept GET POST HEAD>
Require all denied
</LimitExcept>
</Location>
LimitRequestLine 8190
LimitRequestFieldSize 8190
LimitRequestFields 100
<IfModule ssl_module>
SSLRandomSeed startup builtin
SSLRandomSeed connect builtin
SSLSessionCache "shmcb:${SRVROOT}/logs/ssl_scache(512000)"
SSLSessionCacheTimeout 300
SSLUseStapling On
SSLStaplingCache "shmcb:${SRVROOT}/logs/stapling_cache(128000)"
</IfModule>
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
ErrorLog "logs/redirect_error.log"
CustomLog "logs/redirect_access.log" combined
</VirtualHost>
Include conf/extra/httpd-ssl.conf