Lighttpd - fly light.

How to whitelist IP addresses ?

Both methods are functionally identical and only differ in readability. Just don't forget :

Lighttpd conditional configuration

Lighttpd has no AND / OR logical operators, but it's possible to obtain the same behavior by nesting / chaining conditions.

Logical AND :

condition1 {
	condition2 {
		doSomething
		}
	}
nested if blocks are equivalent to a logical AND

Logical OR :

condition1 {
	doSomething
	}
condition2 {
	doSomething
	}
chained if blocks are equivalent to a logical OR

if-then-elif-else construct :

condition1 {
	doSomething1	then block
	}
else condition2 {
	doSomething2	else if block
	}
else {
	doSomething3	else block
	}

How to defeat hotlinking with Lighttpd ?

The procedure below works better when Lighttpd is directly facing HTTP clients. If there is an extra layer (web cache, load balancing, ...) :

  1. open /etc/lighttpd/lighttpd.conf
  2. add something like (source) :
    $HTTP["referer"] =~ "BADDOMAIN\.com|IMAGESUCKERDOMAIN\.com" {
    	url.rewrite = ("(?i)(/.*\.(jpe?g|png))$" => "/path/to/hotlink.png" )
    	}
  3. On my side, since I already configured caches for the images, I had :
    $HTTP["url"] =~ "(gif|jpg|png|svg)$" {
    	expire.url = ( "" => "access plus 1 weeks" ),
    	setenv.add-response-header = ( "Cache-Control" => "public, max-age=604800" )
    	}
    which becomes :
    $HTTP["url"] =~ "(gif|jpg|png|svg)$" {
    	$HTTP["referer"] =~ "BADDOMAIN\.com|IMAGESUCKERDOMAIN\.com" {
    		url.rewrite = ("(?i)(/.*\.(gif|jpe?g|png|svg))$" => "pictures/hotlinking.png" )
    		}
    
    	expire.url = ( "" => "access plus 1 weeks" ),
    	setenv.add-response-header = ( "Cache-Control" => "public, max-age=604800" )
    	}
    This works on a blacklisting principle, meaning only the pages from the listed domains will actually be affected. This also implies analyzing the webserver logs in search of such hotlinkers. So far, I've not found any better solution .
  4. check the syntax of the configuration file
  5. restart Lighttpd :
    systemctl restart lighttpd.service
  6. restart / purge web caches, if any
  7. check it :
  8. That's it. Enjoy

How to password-protect a subtree of a website ?

Situation :

Some webservers may expose content publicly, which is not always desirable. Several solutions exist to workaround this :
  • As a non-security expert, I can NOT ensure what follows is safe / good practice / well implemented. Use at your own risks.
  • While reading on this topic, I've found blogs / articles stating that this is buggy in Lighttpd (not perfectly standard-compliant / has some bugs)...

Solution :

Read : What's the difference between Basic and Digest authentication ?

The basic method :

  1. Create the password file :
    htpasswd -c /path/to/.htpasswd kevin
    New password: PaSsWoRd
    Re-type new password: PaSsWoRd
    Adding password for user kevin
    There are no strong restrictions regarding /path/to/.htpasswd (location or name (.htpasswd is just an example)), but :
    • it shouldn't be stored anywhere under the document root of the corresponding website / virtualhost : it might be disclosed in the event of a web server misconfiguration
    • don't forget to make it readable by the user executing Lighttpd
  2. Configure Lighttpd :
    1. server modules :
      server.modules = (
      	
      	"mod_auth",
      	
      	)
      mod_auth must be loaded before mod_fastcgi (source).
    2. virtualhost (details) :
      $HTTP["host"] =~ "" {
      	
      	auth.backend = "htpasswd"
      	auth.backend.htpasswd.userfile = "/path/to/.htpasswd"
      	auth.require = ( "/url/to/protect" => (		this is what comes after "http://www.example.com/"
      			"method"  => "basic",
      			"realm"   => "prompt",		will be displayed on the login/password pop-up window
      			"require" => "valid-user"	valid-user to allow any valid user, or a list of |-separated user=username
      			)
      		)
      	
      	}
  3. Check the syntax of the configuration file
  4. restart Lighttpd :
    systemctl restart lighttpd.service
  5. Test it with
    • a web browser
    • a command line tool such as wget :
      wget -S -O /dev/null http://www.example.com/url/to/protect
      HTTP request sent, awaiting response...
      	HTTP/1.1 401 Unauthorized
      	WWW-Authenticate: Basic realm="prompt", charset="UTF-8"
      	Content-Type: text/html
      	
      
      Username/Password Authentication Failed.
      wget -S -O /dev/null --http-user=bob --http-password=PaSsWoRd http://www.example.com/url/to/protect
      HTTP request sent, awaiting response...
      	HTTP/1.1 401 Unauthorized
      	WWW-Authenticate: Basic realm="prompt", charset="UTF-8"
      	Content-Type: text/html
      	
      Authentication selected: Basic realm="prompt", charset="UTF-8"
      Reusing existing connection to www.example.com:80.
      HTTP request sent, awaiting response...
      	HTTP/1.1 200 OK
      	Content-type: text/html; charset=utf-8
      	

The digest method (source) :

  1. Create the password file :
    htdigest -c /path/to/.htdigest 'prompt' kevin
    Adding password for kevin in realm prompt.
    New password: PaSsWoRd
    Re-type new password: PaSsWoRd
    Same remark as above regarding the password file.
  2. Check it :
    cat /path/to/.htdigest
    kevin:prompt:222b05e09cf0635131ab4f0a44bd5d59
  3. Configure Lighttpd :
    1. server modules :
      server.modules = (
      	
      	"mod_auth",
      	
      	)
      mod_auth must be loaded before mod_fastcgi (source).
    2. virtualhost (details) :
      $HTTP["host"] =~ "" {
      	
      	auth.backend = "htdigest"
      	auth.backend.htdigest.userfile = "/path/to/.htdigest"
      	auth.require = ( "/url/to/protect" => (		this is what comes after "http://www.example.com/"
      			"method"  => "digest",
      			"realm"   => "prompt",		will be displayed on the login/password pop-up window
      			"require" => "valid-user"	valid-user to allow any valid user, or a list of |-separated user=username
      			)
      		)
      	
      	}
    3. The construct in the snippet above applies when :
      • the corresponding virtualhost has both public (i.e. passwordless) and private (password-protected) URLs
      • there is a known and limited set of private URLs (example)
      If all URLs are private, you may use :
      $HTTP["host"] =~ "" {
      	server.document-root    = "/var/www/myVirtualhost"
      	accesslog.filename      = "/var/log/myVirtualhost.log"
      
      	$HTTP["url"] =~ "^/" {		match everything
      		auth.backend = "htdigest"
      		auth.backend.htdigest.userfile = "/path/to/.htdigest"
      		auth.require = ( "" => (			empty string, on purpose
      				"method"  => "digest",
      				"realm"   => "prompt",
      				"require" => "valid-user"
      				)
      			)
      		}
      	}
  4. Check the syntax of the configuration file
  5. restart Lighttpd :
    systemctl restart lighttpd.service
  6. Test it with wget :
    wget -S -O /dev/null http://www.example.com/url/to/protect
    HTTP request sent, awaiting response...
    	HTTP/1.1 401 Unauthorized
    	WWW-Authenticate: Digest realm="prompt", charset="UTF-8", nonce="5bf7111e:b193aaf497484f867e4c02793a0ff9fd", qop="auth"
    	Content-Type: text/html
    	
    
    Username/Password Authentication Failed.
    wget -S -O /dev/null --http-user=bob --http-password=PaSsWoRd http://www.example.com/url/to/protect
    HTTP request sent, awaiting response...
    	HTTP/1.1 401 Unauthorized
    	WWW-Authenticate: Digest realm="prompt", charset="UTF-8", nonce="5bf7120c:89e30ae25bad5914828b14a0d99319f7", qop="auth"
    	Content-Type: text/html
    	
    Authentication selected: Digest realm="prompt", charset="UTF-8", nonce="5bf7120c:89e30ae25bad5914828b14a0d99319f7", qop="auth"
    Reusing existing connection to www.example.com:80.
    HTTP request sent, awaiting response...
    	HTTP/1.1 200 OK
    	Content-type: text/html; charset=utf-8
    	

How to check the syntax of the configuration file ?

Usage :

lighttpd -tt -f /etc/lighttpd/lighttpd.conf
will output :

Flags :

Flag Usage
-f configurationFile Load file configurationFile
-t test the configuration file for syntax errors and exit
-tt Test the configuration file for syntax errors, load and initialize modules, and exit
this mode may be over-pernickety —especially while the daemon is running— reporting that modules can not be loaded twice. Try with -t

How to send all requests to a single URL ?

Situation :

Let's say you have a single page website (http://www.mysite.tld), and you want ALL requests (whatever they are) to be "sent" to that unique page. It is tempting to create a rule for anything not matching /index.html, and effectively redirect to that URL.

Details :

Actually, when I tried to do so, I've not been able to make it work : the /whatever requests were effectively redirected to /index.html, matched again and looped . I don't know if it's even possible to redirect rather than rewrite.

Solution :

The magic is rewriting instead of redirecting, with a rule such as :

url.rewrite-once = ( "^/(.*)" => "/index.html" )

How to make a negative match in a RegExp for mod_redirect or mod_rewrite ?

Situation :

We're configuring redirects / rewrites, which is done with Regular Expressions and nothing much exotic so far... until we want to create a rule for requests not matching a RegExp.

Solution :

Lighttpd has its own syntax to do so :

(?!expression)

expression itself can also have some ( ), be careful .

How to configure caches ?

Add Expires headers (source) :

  1. Load the mod_expire module :
    server.modules = (
    	,
    	"mod_expire",
    	
    	)
  2. Define per-URL rules :
    $HTTP["url"] =~ "html$" {
    	expire.url = ( "" => "access plus 15 minutes" )
    	}
    
    $HTTP["url"] =~ "(gif|jpg|png)$" {
    	expire.url = ( "" => "access plus 2 hours" )
    	}
    Syntax : [access|modification] plus n [years|months|days|hours|minutes|seconds]
    Don't forget to define a rule for the home page :
    $HTTP["url"] =~ "^/$" {  }
  3. Reload. Validate changes with Firebug or wget. Enjoy.

Add Cache-Control headers :

  1. Load the mod_setenv module :
    server.modules += ( "mod_setenv" )
  2. Define rules :
    • server-wide :
      setenv.add-response-header += (
      	"Cache-Control" => "public, max-age=86400"
      	)
    • per URL :
      $HTTP["url"] =~ "^/$" {
      	setenv.add-response-header = ( "Cache-Control" => "public, max-age=86400" )
      	}
  3. check the syntax of the configuration file
  4. Reload :
    systemctl restart lighttpd.service
  5. Validate changes with Firebug or wget. Enjoy.

How to redirect requests made to my.site.tld/document.xml to my.site.tld/document.html ?

  1. Make sure the configuration includes mod_redirect :
    server.modules = (
    	,
    	"mod_redirect",
    	
    	)
  2. Then edit the corresponding virtualhost :
    $HTTP["host"] =~ "^(my\.site\.tld)" {
    	server.document-root	= "/var/www/my.site.tld"
    	
    	url.redirect = (
    		"^/(.*)\.xml$"	=>	"http://%1/$1.html"
    		)
    	}
    $n
    the substring that was matched by the nth (regExp) from the current regexp match (i.e. the url.redirect directive). Here $1 matches "document" (since the HTTP request is for /document.xml).
    %n
    the substring that was matched by the nth (regExp) from the previous regexp match (i.e. the $HTTP["host"] directive). Here %1 matches "my.site.tld".
  3. By default, url.redirect returns a 301 HTTP status. You can specify another status with :
    	url.redirect-code = 302
  4. Check your configuration
  5. Reload the configuration and voilà !

Lighttpd config tips

Debug rewrite rules and other stuff (sources : 1, 2) :

Edit etc/lighttpd/lighttpd.conf :
server.errorlog			= "/var/log/lighttpd/error.log"
debug.log-condition-handling	= "enable"	# nice but VERY verbose
debug.log-response-header	= "enable"
debug.log-request-handling	= "enable"

The logs complain WARNING: unknown config-key: accesslog.filename :

Looks like an expected server module has not been declared (hence not loaded) but is expected in the configuration. To fix this :
  1. Have a look at the available server modules and find the one matching the error message : accesslog.filename sounds related to mod_accesslog, isn't it ?
  2. Edit the configuration (either /etc/lighttpd/lighttpd.conf or any other file) :
    • server.modules += ( "mod_accesslog" )
    • server.modules = (
      	...
      	"mod_setenv",
      	"mod_accesslog",
      	...
      	)

Create and install a self-signed SSL certificate

Create the self-signed certificate :

  1. siteName='example.com'; lighttpdCertificateFolder="/etc/lighttpd/ssl/$siteName"; lighttpdUser='userName'; lighttpdGroup='groupName'
  2. mkdir -p $lighttpdCertificateFolder; cd $lighttpdCertificateFolder
  3. openssl req -new -x509 -keyout $siteName.pem -out $siteName.pem -days 365 -nodes
    At prompt "Common Name (eg, YOUR name) []:", enter the domain name that will be served through SSL. This can be the "$siteName" or *.example.com
  4. chown ${lighttpdUser}:${lighttpdGroup} /etc/lighttpd/ssl -R
  5. chmod 0600 /etc/lighttpd/ssl/$siteName

Configure Lighttpd :

  1. documentRootFolder='/var/www/ssl'; mkdir -p $documentRootFolder; echo "HTTPS: HELLO WORLD" > $documentRootFolder/index.html
  2. cp /etc/lighttpd/lighttpd.conf{,_BITI}; vim /etc/lighttpd/lighttpd.conf
  3. # TESTING (SSL)
    $SERVER["socket"] == "192.168.144.114:443" {
    	server.document-root = "/var/www/ssl"
    	ssl.engine = "enable"
    	ssl.pemfile = "/etc/lighttpd/ssl/example.com/example.com.pem"
    	}
    # /TESTING (SSL)
  4. Then, open in a browser : https://192.168.144.114/
  5. Enjoy !

General information on sites served via SSL/TLS :

A web page such as http://www.example.com/index.php can legally link some resources from a different domain such as http://static.example.com/style.css. As far as HTTPS is concerned, if https://www.example.com/index.php links http://static.example.com/style.css, this is called mixed content and it's blocked by Firefox and other browsers.