🛡️ Hardening Lighttpd: A Proactive Security Guide

Site Building, creating the production stack

Company: Personal Project

Lighttpd is legendary for being lightweight, but its "secure by default" nature requires a specific touch when transitioning from Apache. Here is a guide to hardening your instance based on a production-ready configuration.

Transitioning from Apache to Lighttpd often reveals that Lighttpd is "too efficient"—it does exactly what you tell it, which can lead to information leakage if you aren't careful. This guide covers how to transform a default Lighttpd instance into a hardened, stealthy web server.


1. The Hardened Configuration (lighttpd.conf)

This sample configuration focuses on Surface Area Reduction and Strict URL Normalization.

Code snippet
 
server.modules = (
    "mod_indexfile",
    "mod_access",
    "mod_alias",
    "mod_redirect",
    "mod_auth",
    "mod_fastcgi",
    "mod_status",
    "mod_authn_file",
    "mod_rewrite",
    "mod_magnet",
)

# --- Identity & Stealth ---
server.tag = "Webserver" # Obscure version info
server.username = "www-data"
server.groupname = "www-data"

# --- Directory & Metadata Protection ---

# 1. Block Publii metadata leakage
$HTTP["url"] =~ "^/files\.publii\.json$" {
    url.redirect = ( "" => "/" )
    url.redirect-code = 301
}

# 2. Prevent pattern-based file discovery
$HTTP["url"] =~ "\.(it|env|htaccess)$" {
    url.access-deny = ("")
}
url.access-deny = ( "~", ".inc" )

# 3. Stealth Directory Handling (The Lua Gate)
$HTTP["url"] =~ "/$" {
    magnet.attract-physical-path-to = ( "/etc/lighttpd/redirect.lua" )
}

# --- Security & Normalization ---
server.http-parseopts = (
  "header-strict"           => "enable",
  "host-strict"             => "enable",
  "host-normalize"          => "enable",
  "url-normalize-unreserved"=> "enable",
  "url-normalize-required"  => "enable",
  "url-ctrls-reject"        => "enable",
  "url-path-2f-decode"      => "enable",
  "url-path-dotseg-remove"  => "enable",
)

# --- Performance & Resource Limits ---
server.use-noatime = "enable"
server.max-read-idle = 60
server.max-write-idle = 60

# --- Access Control ---
$HTTP["remoteip"] == "192.168.16.0/24" {
    status.status-url = "/server-status"
}

2. The "Silent Guard" Lua Script

Standard hardening usually disables directory listings, resulting in a 403 Forbidden error. To a scanner, a 403 confirms a directory exists. Our Lua script provides a "stealth" response by redirecting empty directories back to the root.

File: /etc/lighttpd/redirect.lua

Lua
 
-- LIGHTTPD DIRECTORY HARDENING SCRIPT
-- Purpose: Prevent Directory Discovery & Information Leakage

-- Step 1: Capture the resolved physical path on the disk
local path = lighty.env["physical.path"]

-- Step 2: Define allowed entry points. 
local has_index_html = lighty.stat(path .. "index.html")
local has_index_php  = lighty.stat(path .. "index.php")

-- Step 3: Enforcement Logic
-- If no index exists, we 'black-hole' the request to the homepage.
if (not has_index_html and not has_index_php) then
    lighty.header["Location"] = "/"
    return 302
end

3. Key Hardening Concepts Explored

Obscuring Server Identity

By setting server.tag = "Webserver", we stop broadcasting our specific version number. This prevents attackers from easily identifying if your server is susceptible to version-specific CVEs.

HTTP Normalization

The server.http-parseopts section is vital. By enabling url-path-dotseg-remove, you mitigate Directory Traversal attacks (e.g., ../../etc/passwd). The server cleans the URL before processing it, ensuring an attacker cannot "climb" out of your web root.

The "Silent Guard" Logic

Using mod_magnet to check for file existence before the response is sent allows for complex logic that static configuration cannot handle. This ensures that valid subdirectories (with an index.html) work perfectly, while everything else is seamlessly redirected.


4. Verification & Log Auditing

To ensure your hardening is active, monitor your access logs:

Bash
 
tail -f /var/log/lighttpd/access.log

How to read your logs:

  • Status 200: A legitimate file was found and served.

  • Status 301: A blocked metadata file (like files.publii.json) was caught and redirected.

  • Status 302: The Lua script caught a directory without an index and bounced the user home.

Detecting Scanners:

If you see an IP address generating multiple 302 entries in rapid succession, you have identified a bot attempting to map your directory structure. Because of your hardening, the bot has gained zero information about your server's layout


Additional Hardening Suggestions

  1. Disable Follow-Symlinks: If you currently have server.follow-symlink = "enable". Unless your site specifically relies on symlinks, setting this to "disable" prevents an attacker from tricking the server into reading files outside the web root (like /etc/shadow) if they find a way to create a link.

  2. MIME-Type Sniffing: Add a header to prevent browsers from guessing file types, which can lead to XSS:

    Code snippet
    setenv.add-response-header = ( "X-Content-Type-Options" => "nosniff" )
    

    (Requires mod_setenv)

  Publii Sitemap Tools