# Find vulnerabilities in web servers: 30+ hardening checks

> Online Apache and Nginx config linter free (.htaccess, vhost, server): 30+ checks on SSL/TLS, CSP, HSTS, Permissions-Policy, WAF. No upload.

Free in-browser tool (no upload): https://www.mauriziofonte.it/tools/apache-nginx-config-linter

Paste an **Apache** config block (`.htaccess`, `VirtualHost`, `Directory`, `Location`) or **Nginx** (`server`, `location`, `upstream`) and get a security-first context-aware audit: the tool runs native AST parsing (no regex grep) and applies 30+ checks on weak SSL/TLS, security headers with **value validators** for CSP (`unsafe-inline`, wildcard, `report-uri`), HSTS (`max-age`, `includeSubDomains`, `preload`), Permissions-Policy, forward secrecy, AEAD, HTTP/2, OCSP stapling, mod_security WAF, rate limiting on auth endpoints, HTTP->HTTPS redirect, Nginx `add_header` override semantics (the rule that wipes parent server headers when a location adds its own). Parser and rules run entirely in the browser: the config never leaves the page.

## How to use the linter

1. **Get the config** - **Apache**: typical paths `/etc/apache2/sites-enabled/*.conf`, `/etc/httpd/conf.d/*.conf`, or the `.htaccess` at the site root. **Nginx**: `/etc/nginx/sites-enabled/*`, `/etc/nginx/conf.d/*.conf`, or the main `nginx.conf`. For assisted analysis: save the block to a `.conf` (or `.htaccess`) file and use the dropzone to upload it directly.
2. **Paste in the tool** - The tool auto-detects the dialect by counting canonical pattern occurrences (Apache: XML-like `<...>` tags, ServerTokens, SSLEngine; Nginx: `server { }`, `location`, `add_header`). If no pattern matches, it fails with an explicit diagnostic. Input never leaves the browser: parser and rules run in local JavaScript.
3. **Read the report** - Three-tier output. **Detected dialect** at the top. **Findings ordered by severity**: critical, high, medium, low, info. **Aggregated score** 0-100. For each issue: problem description, normative reference (Mozilla SSL Configurator, OWASP Secure Headers Project, historical CVEs), copy-paste-ready remediation.
4. **Export or print** - **Copy report**: plain text for internal tickets or hardening documents. **Export for chatbot**: complete markdown with context, detailed findings, and prompt template, to paste into ChatGPT/Gemini/Claude for remediation prioritization or fix-effort estimates. Browser **Print** for PDF export (CSS print-optimized, hides nav and CTA).

## Why a context-aware linter is the right level of triage

**Apache and Nginx configs look simple, but they are compositional.** A directive can appear at top-level, inside an Apache `<VirtualHost>`, inside an Nginx `server { }`, inside a `<Location>` or a `location { }`: meaning changes with scope. Security headers declared in the wrong scope are formally present but do not apply to the page they were meant to protect. Modern ciphers declared at top-level get overridden by a legacy directive in a specific vhost. Regex grep does not see these subtleties: AST parsing does, and this version of the linter is based on a proper AST parser written in vanilla JS (~750 LOC total) producing a navigable tree of blocks and directives with line numbers.

**What it actually checks.** The linter applies 30+ checks across 5 families. **SSL/TLS**: obsolete protocols (SSLv2, SSLv3, TLSv1, TLSv1.1) and modern ones (TLSv1.2, TLSv1.3), weak ciphers (RC4, 3DES, EXPORT, NULL) vs strong ciphers, presence of **forward secrecy** (ECDHE/DHE), AEAD (GCM, ChaCha20-Poly1305), OCSP stapling. **Security headers with value validator**: this is the difference vs regex-based linters. CSP with `unsafe-inline` in `script-src` = effectively disabled, flagged medium; CSP with `*` wildcard, `unsafe-eval`, missing `report-uri`: separate findings each. HSTS with `max-age` < 6 months: medium. HSTS without `includeSubDomains`: low. HSTS without `preload`: info. **Information disclosure**: ServerTokens / server_tokens verbose, autoindex on, `/server-status` and `/nginx_status` not restricted. **Availability and hardening**: `listen 80` without HTTPS redirect, rate limiting on auth endpoints (`/login`, `/auth`, `/register`), HTTP/2 enabled, mod_security WAF present. **Nginx semantic overrides**: the classical case where an `add_header` inside a location silently wipes all parent server `add_header` for that location, leaving the page without HSTS/CSP/X-Frame.

**Difference between context-aware and regex-based linters.** The previous linter on this tool ran regex grep on the entire config text: `/Strict-Transport-Security/i.test(text)`. Result: HSTS declared inside `<Location /admin>` counted as HSTS present, even though the header was obviously not emitted outside /admin. Critical false negative. Same problem with Nginx `add_header`: presence in text does not imply application to the request. Same for protocols and ciphers: a directive valid in one vhost does not imply it applies to every other. The AST parser solves these problems at the root: every finding has precise context (which block, which line, with which arguments).

**Operational privacy.** A server config exposes the full structure of an infrastructure: hostnames, certificates, filesystem paths, internal backends, possible proxy caches, hidden admin paths. Uploading it to a third-party service for preliminary audit is a leak vector: the config leaves the perimeter, hits an external endpoint, gets processed and possibly logged. Here no network request leaves with config data: parser and rules run entirely in the browser. Verifiable by opening the DevTools Network panel during the lint.

**When static triage is the right level.** Typical scenarios. (a) Code review of a Pull Request modifying the Nginx config before merge: the linter covers 70-80% of hardening patterns and reports deterministically. (b) Pre-go-live audit of a new VPS: the freshly written DevOps config passes triage before production deploy. (c) Periodic self-check: the config running in production for two years is re-linted to catch regressions introduced by punctual changes. (d) Pre-engagement audit of a customer asking for a config review: thirty seconds of lint give the list of findings worth diving into with a serious hardening exercise.

**Scope limit.** The linter analyzes config text: it does not query the server runtime state, does not resolve `Include` / `include` against external files, does not validate X.509 certificates near expiry, does not verify the backend behind a `proxy_pass` actually answers HTTPS, does not parse logs for active attacks. For those aspects you need manual assessments, online scanners (SSLLabs, Mozilla Observatory) and continuous CVE monitoring. The linter is a fast triage, not a substitute.

## Glossary

- **VirtualHost (Apache)**: `<VirtualHost>` block that defines directives applied to a specific IP:port combination. An Apache config can have N VirtualHosts, each with different SSL, headers, certificate files. The linter analyzes every vhost separately.
- **server { } (Nginx)**: Nginx equivalent of Apache's VirtualHost. Defines directives for a listen + server_name combination. Nested inside `http { }` in the main config, or top-level in conf files included via `include`.
- **AST (Abstract Syntax Tree)**: Structured representation of the config after parsing. Every directive and block becomes a node with type, name, args, line, children. Allows answering scope and context questions that regex grep cannot see.
- **HSTS (Strict-Transport-Security)**: HTTP header instructing the browser to use HTTPS only for the domain for `max-age` seconds. Mitigation against TLS downgrade attacks. Best practice: `max-age=31536000; includeSubDomains; preload` and registration to Chrome's preload list (hstspreload.org).
- **CSP (Content-Security-Policy)**: HTTP header declaring which sources are authorized for script, style, img, connect, etc. Primary mitigation against reflected and DOM-based XSS. Main directives: `default-src`, `script-src`, `style-src`, `img-src`, `connect-src`, `frame-ancestors`, `report-uri`.
- **Forward secrecy**: TLS protocol property whereby the server's private key compromise does NOT allow decryption of past sessions. Guaranteed by ciphers with ephemeral key exchange (ECDHE, DHE). Static RSA ciphers lose it. Mozilla SSL Configurator Modern and Intermediate profiles both guarantee it.
- **AEAD (Authenticated Encryption with Associated Data)**: Cipher family combining encryption and authentication in a single operation, eliminating padding-oracle vulnerabilities. Examples: AES-128-GCM, AES-256-GCM, ChaCha20-Poly1305. Recommended starting from TLS 1.2.
- **OCSP stapling**: TLS extension where the server presents the client with a CA-signed OCSP response, avoiding the client's OCSP query (privacy + performance). Directives: Apache `SSLUseStapling on`, Nginx `ssl_stapling on; ssl_stapling_verify on;`.
- **HTTP/2 / HTTP/3**: Successor versions of HTTP with stream multiplexing, header compression (HPACK), server push (deprecated), QUIC over UDP for HTTP/3. Apache: `Protocols h2 http/1.1`. Nginx: `listen 443 ssl http2` (pre-1.25 syntax), or `http2 on;` (1.25+ syntax).
- **mod_security / ModSecurity**: WAF (Web Application Firewall) for Apache and Nginx, based on pattern-matching rules on request/response (CRS = OWASP Core Rule Set). Mitigation against SQLi, XSS, RCE, scanner detection. Apache: `<IfModule mod_security2.c>`. Nginx: build with `libmodsecurity`.
- **Permissions-Policy**: Header that disables unused browser APIs (camera, microphone, geolocation, payment, USB, ambient-light, accelerometer, gyroscope). Reduces attack surface. Successor of `Feature-Policy` (deprecated). Example: `camera=(), microphone=(), geolocation=()`.
- **X-Frame-Options / frame-ancestors**: X-Frame-Options (legacy): `DENY` | `SAMEORIGIN` | `ALLOW-FROM origin`. Mitigation against clickjacking. CSP `frame-ancestors` is the modern, more expressive version. Best practice: emit both, X-Frame as fallback for legacy browsers.
- **Referrer-Policy**: Header controlling what is sent in the Referer header during cross-origin navigation. Common values: `strict-origin-when-cross-origin` (modern default), `no-referrer` (maximum privacy). The variable browser default is an info leak.
- **rate limiting (Nginx)**: `limit_req_zone` defines rate-limit zones (rate-key + rate). `limit_req` inside a location applies the zone. Critical on authentication endpoints (login, signup, password reset) to resist brute force and credential stuffing.
- **add_header override (Nginx)**: Counterintuitive Nginx rule: if a `location` contains even a single `add_header`, the parent `server` `add_header` directives are ignored for that location. Workaround: repeat headers in every location, or use the `headers-more-nginx-module` (`more_set_headers`).
- **Mozilla SSL Configurator**: Official Mozilla tool generating ready SSL configs for Apache, Nginx, HAProxy, etc., choosing among three profiles: **Modern** (TLS 1.3 only, very recent browsers), **Intermediate** (TLS 1.2 + 1.3, broad compatibility, default recommended), **Old** (legacy, to avoid). URL: `ssl-config.mozilla.org`.

## Frequently asked questions about the Apache/Nginx linter

### I'm a sysadmin / DevOps with little specific experience on server hardening. Can I use this tool?

Yes. For each finding you get a problem description in operational language and copy-paste-ready remediation for the config. The tool's logic mirrors that of the official tools (Mozilla SSL Configurator, OWASP Secure Headers Project): the same checks a cyber expert would run, automated. For aspects the linter does not cover (cert near expiry, intermediate certificate validation, runtime monitoring) use SSLLabs and Mozilla Observatory in parallel.

### What does AST-based mean and why is it better than a regex linter?

AST = Abstract Syntax Tree: the config is parsed into a tree of blocks and directives with precise scope. The linter knows that `add_header X-Frame-Options DENY` inside `location /admin` does NOT apply to the rest of the site. A regex linter would see the string `X-Frame-Options` anywhere in the text and say "present". That false negative has practical consequences: the header is formally there but is not emitted on the responses it was meant to protect. Same reasoning for HSTS, CSP, and the Nginx `add_header` override rule.

### Can I test a .htaccess or do I need the full VirtualHost?

Both work. A `.htaccess` is a partial Apache config (inherited from the parent vhost): the linter accepts bare directives at top-level. A complete `VirtualHost` adds context (SSL port, certificates). For accurate audit prefer the full vhost because some checks (HSTS scoped to the SSL vhost, listen 80 redirect) require the block. If you paste only a .htaccess, the linter cannot decide whether you are on HTTPS or HTTP.

### Does the Apache sample pass with score 100? I want to understand it as baseline.

The Apache sample includes two vhosts: one HTTP->HTTPS redirect, one HTTPS with modern SSL/TLS, HSTS preload, CSP with report-uri, X-Frame, X-Content-Type, Referrer, Permissions, OCSP stapling, HTTP/2 (Protocols h2), AllowOverride None on Directory. Expected score: 90-100. Possible drop: mod_security not visible (info, -1), no explicit ServerTokens declared (info, -1).

### Nginx add_header override semantics: how do I really fix it?

Three approaches. (1) **Repeat headers in every location**: more boilerplate but deterministic, any sysadmin reading the config will understand it. (2) **Use `more_set_headers`** from `headers-more-nginx-module`: NOT subject to the override rule, global set that also applies to locations. Requires custom build or official package (Debian: `libnginx-mod-http-headers-more-filter`). (3) **Refactor the location structure**: collapse similar locations into one with `map` or `if`, reducing override surface.

### CSP with 'unsafe-inline': when can it be accepted?

Ideally never in new projects: nonce-based or hash-based CSP are the modern baseline. In legacy cases: (a) frontend with thousands of inline event handlers requiring long refactor, accept unsafe-inline for the moment but document it in the config commit message; (b) analytics tools injecting inline scripts (Google Tag Manager pre-2023, some A/B testing tools), unsafe-inline is the fastest way. Partial mitigation: add `'self'` + nonce for your scripts and accept unsafe-inline only as fallback whitelist.

### HSTS preload: is it worth signing up to Chrome's preload list?

Yes for any stable web-only domain. The preload list integrates the domain into Chromium code (and derived browsers): the first visit to the domain is already forced to HTTPS, eliminating the first-visit downgrade attack window. Trade-off: once enrolled, you only resi HTTPS (no quick rollback to HTTP). Procedure: `max-age=31536000` minimum, `includeSubDomains` mandatory, `preload` mandatory, submit at `hstspreload.org`.

### /server-status and /nginx_status: should they be disabled or just restricted?

Restricted, not disabled: useful for monitoring (Nagios, Zabbix, custom dashboards). Typical restriction: Apache `<Location /server-status> Require local </Location>` or `Require ip 10.0.0.0/8`. Nginx `location /nginx_status { stub_status on; allow 127.0.0.1; allow 10.0.0.0/8; deny all; }`. Publicly exposed they leak internal hostnames, per-vhost traffic, request rate: information leakage useful for fingerprinting and detecting attack waves.

### TLSv1 and TLSv1.1: should they be entirely disabled or is it OK to negotiate them for compatibility?

Entirely disabled for any new project. PCI DSS 4.0 requires TLSv1.2 minimum from mid-2024. Browser support: TLSv1.2 is supported by IE 11 onwards (Win 7+), Android 5+, iOS 5+. Remaining clients requiring TLSv1/TLSv1.1 are esoteric legacy clients (vintage enterprise printers, SCADA systems): if you have them, isolate them on a separate endpoint and declare TLSv1.2+ everywhere.

### Forward secrecy: how critical is it in practice?

Critical for long-life data. Without forward secrecy, future private key compromise (server breach, key theft, court order) allows decryption of ALL past intercepted TLS sessions (recorded by a passive adversary like ISP or nation-state actor). With forward secrecy, past sessions stay encrypted. ECDHE/DHE ciphers guarantee it, static RSA does not. Best practice: Mozilla Intermediate SSLCipherSuite guarantees it, migrate to that.

### HTTP/2 vs HTTP/3: should I enable both?

HTTP/2 is modern baseline (mature deploys since 2018, ~99% browser support). HTTP/3 (over QUIC) is next-gen but non-trivial deploy: requires UDP open at firewall, Apache only supports it via experimental mod_http2, Nginx requires `--with-http_v3_module` (1.25+). Pragmatic roadmap: HTTP/2 always, HTTP/3 when you have a real use case (mobile-heavy, devices on lossy connections).

### mod_security: is it worth enabling?

Yes, for any publicly exposed site. Cost: ~5-10% CPU overhead with standard CRS. Benefit: automatic block of SQLi, XSS, RCE, recognizable scanners (nikto, sqlmap, dirb), enumeration, plain-text attack patterns. Caveat: initial configuration requires tuning to reduce false positives on the specific app. Modern alternative: cloud WAF (Cloudflare, AWS WAF, Sucuri). Costs but eliminates tuning.

### Can I audit configs that use Include / include against external files?

The tool only lints what you paste. Apache `Include` and Nginx `include` point to external files: the browser cannot resolve them. Workaround: manually concatenate the files of interest and paste the resulting blob, or lint files one by one. Full audit requires server filesystem access, out of scope for a browser-only tool.

### Can I audit the main /etc/nginx/nginx.conf file?

Yes, the parser correctly supports `events { }`, `http { }`, nested `server { }`, `upstream`, top-level directives. Typical findings on main nginx.conf: `server_tokens off` at http level, global `limit_req_zone`, ssl session cache. Per-server findings will appear for every `server { }` included (also via manually resolved includes).

### Privacy: is the pasted config stored or tracked?

No. The tool runs entirely in the browser. No network request with the config: parser, context-aware rules, value validators, rendering, report generation - all in local JavaScript. Verifiable by opening the DevTools Network panel during the lint: zero outgoing requests with config data. The config is not saved in `localStorage` nor tracked in analytics.

### Can I integrate the linter into a CI/CD pipeline?

For CI/CD use dedicated tools: `nginx-config-formatter` + Nginx `configtest`, Apache `apachectl configtest`, `gixy` (Yandex, Python) for Nginx security audit, `apache-config-lint` Ruby. Our tool is browser-based: great for interactive reviews, sub-optimal for pipelines. Valid use case: PR code review of a config change, the developer pastes the diff and triages before push.

---
Tool by Maurizio Fonte, senior backend engineer. All tools: https://www.mauriziofonte.it/tools/
