- Overview
- Module Description
- What nginxpack affects
- Usage
- Common Use Cases
- Limitations (only Debian-likes)
- Development
This module installs and configures Nginx (lightweight and robust webserver). It's a pack because you can optionally install and configure PHP7 at the same time. There are three types of vhost available (basic, proxy and redirection) and some smart options for Nginx and PHP. This module is full IPv6 compliant because we are in the 21st century.
Features available:
- Install and configure Nginx
- Optionally: install and configure PHP7-FPM (FastCGI)
- Optionally: install PHP-MySQL connector and/or others PHP7 modules
- Basic vhosts
- Proxy vhosts
- 301 Redirection vhosts
- SSL support
- SNI support
- AcceptPathInfo support
- Full IPv6 compliant (and still IPv4...) including IPv6-Only
- Automatic blackhole for non-existent domains
- Several options (upload limits with Nginx/PHP, timezone, logrotate, default SSL certificate, htpasswd, listing, XSS injection protection, etc.)
- Custom configuration option for non-supported features
Recipes validated with +230 rspec tests.
Installed packages:
- nginx
- With
enable_php
: php5-fpm - With
php_mysql
: php5-mysql - With
logrotate
: logrotate, psmisc (if not already present)
logrotate is used with a configuration file in /etc/logrotate.d/nginx allowing it to daily rotate vhost logs. The configuration uses killall from psmisc in order to force nginx to update his inodes (this is the classic way).
Use nginxpack::php::mod { 'foo': }
involves installing php5-foo (e.g. nginxpack::php::mod { [ 'mcrypt', 'gd' ]: }
).
Added services:
- Use
/etc/init.d/nginx
- With
enable_php
: use/etc/init.d/php5-fpm
Added files:
- Vhosts: /etc/nginx/sites-{available,enabled}/* and /etc/nginx/include/*
- Logs: /var/log/nginx/<vhostname>/{access,error}.log
- Certificates: /etc/nginx/ssl/*
- Script for automatic blackholes: /etc/nginx/find_default_listen.sh
Use php_timezone
, php_upload_max_filesize
and/or php_upload_max_files
affects /etc/php5/cgi/php.ini (but not overrides it).
If you just want a webserver installing with the default options and without PHP you can run:
include 'nginxpack'
And with PHP:
class { 'nginxpack':
enable_php => true,
}
With PHP-MySQL connector:
class { 'nginxpack':
enable_php => true,
php_mysql => true,
}
Others options for PHP:
class { 'nginxpack':
enable_php => true,
php_timezone => 'Antarctica/Vostok',
php_upload_max_filesize => '1G',
php_upload_max_files => 5,
}
With this example, you will be able to propose uploads of 5 files of 1G max each together. In this case, the POST-data size limit (from PHP) will automatically be configured to accept until 5G.
With LogRotate (enabled by default):
class { 'nginxpack':
logrotate => true,
logrotate_frequency => 'weekly',
logrotate_rotate => 52,
}
LogRotate values given in this example are the default ones. logrotate_rotate
corresponds to the number of rotating before being removed (zero means that old versions are removed rather than rotated).
You can also configure default https configuration here. See the first common use case.
Standard vhost.
Listen on all IPv6/IPv4 available with port 80, no PHP and no SSL:
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
}
Using aliases:
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com', 'www.foobar.example.com' ]
}
With PHP:
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
use_php => true,
}
Since you use use_php
for at least one vhost, you have to use enable_php
with the webserver. For activating AcceptPathInfo, add php_acceptpathinfo => true
to the vhost (e.g. /foo/index.php/bar/ with PATH_INFO=/bar/). You can also replace the default PHP index index.php thanks to php_index
.
With legacy CGI (binaries are executed individually for each request):
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
use_legacycgi => true,
legacycgi_path => '/mailman',
}
Since you use use_legacycgi
for at least one vhost, you have to use enable_legacycgi
with the webserver. legacycgi_path
must be the URL path of the cgi-bin directory (/cgi-bin by default).
Listen on a specific IPv6 and all IPv4 available:
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
ipv6 => '2001:db8::42',
}
You can use the ipv4
option to listen on a specific IPv4 address. And if you don't want listening on IPv4 you can set ipv6only => true
(and ipv4only
for not listening on IPv6, when it's strictly necessary).
Listen on a specific port:
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
port => 8080,
}
With SSL (https://):
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
https => true,
ssl_cert_source => 'puppet:///certificates/foobar.pem',
ssl_key_source => 'puppet:///certificates/foobar.key',
}
Generate pem (crt) and key files (put your full qualified domain name in Common Name):
$ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout foobar.key -out foobar.pem
You also could use ssl_cert_content
and ssl_key_content
to define the certificate from a string (useful if you use hiera to store your certificates: ssl_cert_content => hiera('foobar-cert')
). Or use ssl_cert_path
and ssl_key_path
to directly use a file already existing on the system (without copying its content).
Additional parameters ssl_ocsp_dns1
and ssl_ocsp_dns2
can be set in order to set the DNS resolvers used for obtaining the IP address of the OCSP responder (certificates revocation status). OCSP DNS can be IP addresses (IPv6 starting from Nginx 1.2.2) or names (resolving into IPv6 starting from Nginx 1.5.8). You can set optional ports with IP:port or name:port with Nginx version equal or greater than 1.2.2 (default 53).
Additionnal parameter ssl_dhparam_source
, ssl_dhparam_path
or ssl_dhparam_content
can be set in order to set a strong dhparam file to use for Diffie-Hellman challenges.
Generate dhparam file:
$ openssl dhparam -out dhparam.pem 4096
The default listening port becomes 443 but you still could force a different one with port
. The SSL configuration is compliant with Cipherli.st recommendations (ssl_stapling options are disabled with Debian Wheezy and Ubuntu Precise).
Other options:
nginxpack::vhost::basic { 'foobar':
domains => [ 'foobar.example.com' ],
enable => false,
files_dir => '/srv/websites/foobar/',
injectionsafe => true,
html_index => 'foobar.html',
upload_max_size => '5G',
htpasswd => 'user1:$apr1$EUoQVU1i$kcGRxeBAJaMuWYud6fxZL/',
htpasswd_msg => "Restricted Foobar's access",
forbidden => [ '^/logs/', '^/tmp/', '\.inc$' ],
add_config_content => 'location @barfoo { rewrite ^(.+)$ /files/$1; }',
try_files => '@barfoo =404',
listing => true
}
files_dir
(DocumentRoot) default value is /var/www/<name>/ (e.g. /var/www/foobar/).
injectionsafe
applies these protections against XSS injections. These restrictions might be incompatible with your applications.
upload_max_size
should be in line with php_upload_max_filesize
x php_upload_max_files
listing
corresponds to directory index enabled (auto files indexing)
htpasswd
's value can be generated from a command line tool (apache2-utils):
$ htpasswd -nb user1 secretpassword
If you want to use a specific configuration for a specific vhost, you can use add_config_source
or add_config_content
to inject custom Nginx instructions directly in server { }
.
Reverse-proxy vhost allowing you to seamlessly redirect the traffic to a remote webserver.
Default listen identical to basic vhosts and remote server reached on port 80 without using SSL:
nginxpack::vhost::proxy { 'foobarlan':
domains => [ 'foobar.example.com' ],
to_domain => 'foobar.lan',
}
Remote SSL and different remote port:
nginxpack::vhost::proxy { 'foobarlan':
domains => [ 'foobar.example.com' ],
to_domain => 'foobar.lan',
to_https => true,
to_port => 8080,
}
Default remote port is 80. In this case it would have been 443 due to to_https
.
SSL (https://) is usable in the same manner as basic vhosts.
Options ipv6
, ipv4
, ipv6only
, ipv4only
, port
, enable
, add_config_source
, add_config_content
and upload_max_size
are also available in the same way as basic vhosts.
General redirection (using 301 http code) allowing you to officially redirect any requests to a remote domain. In short: http://foobar.example.com/(.*) => http://foobar.com/$1.
Default listen identical to basic vhosts, and remote domain reached on port 80 without using SSL:
nginxpack::vhost::redirection { 'foobarlan':
domains => [ 'foobar.example.com' ],
to_domain => 'www.foobar.com',
}
Options to_https
and to_port
are available in the same way as proxy vhosts.
Options ipv6
, ipv4
, ipv6only
, ipv4only
, port
, enable
, add_config_source
and add_config_content
are available in the same way as basic vhosts.
The previous section should be clear enough to understand the possibilities of nginxpack.
If you want a detailed documentation of types and options, there is a full documentation in the headers of the Puppet files.
Have a determinist way to access to the vhosts is a good practice in web security. If you say that a vhost can be reached via my.example.com, any request using another domain should not success. If you do not have a default vhost with a listen line for each port used on the webserver, Nginx will use a doubful algorithm to determine which vhost is usable in the case of an unknown domain.
Good news! Nginxpack creates this default vhost for you and redirects any request out of your scopes to a blackhole.
You can disable the https blackhole with default_https_blackhole => false
(useful if you have no https vhosts and you don't want Nginx listening on 443).
The full circle is easy to understand:
- Nginx chooses the correct vhost (among those who are listening on the correct port and IP) thanks to the host field (HTTP 1.1).
- When a client initiates a SSL connection, this field is encrypted, until Nginx decrypts the request.
- Informations about decryption (e.g. certificate location) are in the correct vhost. Back to 1.
Thus, if you have several vhosts listening on the same port with the same IP (or all IP) and using SSL, you have a problem.
With Nginx >= 0.7.62 and OpenSSL >= 0.9.8j, you can use SNI, the modern way to solve this problem. You have nothing to do, but your visitors must have recent browsers:
- Opera 8.0;
- MSIE 7.0 (but only on Windows Vista or higher);
- Firefox 2.0 and other browsers using Mozilla Platform rv:1.8.1;
- Safari 3.2.1 (Windows version supports SNI on Vista or higher);
- or Chrome (Windows version supports SNI on Vista or higher, too).
The other constraint is that you cannot use specific addresses (ipv6
and ipv4
options) with SNI.
If you don't want to restrict compatible browsers or you want use specific addresses or you want to manage only one wildcard certificate, the good solution is to use a default vhost, listening on all ports and IP used by SSL vhosts on the webserver and containing the decryption informations. When Nginx will receive a SSL request, it will use this vhost, and so, will be able to decrypt it. Once the host field is readable, it can chose the correct vhost. The latter don't have to propose SSL but it absolutely must listen on port 443 (or another if you use SSL with another one).
Nginxpack creates this default vhost for you, with a default certificate. To replace the default certificate, you can use ssl_default_cert_source
(or ssl_default_cert_key
) and ssl_default_key_source
(or ssl_default_key_source
) options. This certificate should be valid for all domains used, so it will probably be at least a wildcard certificate. In a same manner, you can also define a ssl_default_dhparam_source
or ssl_default_dhparam_content
parameter.
The first common use case in the next section provides an example with this solution.
You are in charge of servers in the wrong decade: there is almost no more IPv4 but you still can't use only IPv6. Thus, your provider has provided you as many IPv6 addresses as there are grains of sand on earth, but only one poor IPv4.
If you have various webservers (on remote machines or in containers beside) behind this access, you need to have a reverse-proxy. In the following examples, we consider that your firewall redirects ports 80 and 443 to the server corresponding to your reverse-proxy for any incoming IPv4 flux (probably by configuring your NATPT on your CPE if you are at home) .
The first example considers that your ISP provides you IPv6 addresses and that you are able to use it. Second example considers that you have a crappy ISP and so no IPv6 addresses available. In both examples, we want a blog (http), a wiki (https) and a members panel (https). For the sake of cleanliness and security reasons, each website must have its own webserver on its own machine/container.
Goals:
- 1 webserver (1 machine/container) by website and 1 for the reverse-proxy.
- IPv6 clients reach websites directly and can't use the reverse-proxy.
- IPv4 clients reach websites only through the reverse-proxy and can't reach them directly.
- Therefore, communications between the reverse-proxy and the remote websites use IPv6, but it's seamless for (IPv4) clients and there is no problem with IPv6 over the internal network.
The certificat location is provided on the reverse-proxy for IPv4 clients (see previous section), and on the vhosts for IPv6 clients. Proxy vhosts listen on port 443 but not have SSL capabilities (see again the previous section).
Webserver hosting the reverse-proxy:
# Could be replaced by an internal DNS server
host {
'blog.lan':
ip => '2001:db8::a';
'wiki.lan':
ip => '2001:db8::b';
'members.lan':
ip => '2001:db8::c';
}
# Wildcard certificate (*.example.com)
class { 'nginxpack':
ssl_default_cert_source => 'puppet:///certificates/default.pem',
ssl_default_key_source => 'puppet:///certificates/default.key',
}
nginxpack::vhost::proxy { 'blog':
domains => [ 'blog.example.com' ],
ipv4only => true,
to_domain => 'blog.lan',
}
nginxpack::vhost::proxy { 'wiki':
domains => [ 'wiki.example.com' ],
port => 443,
ipv4only => true,
to_domain => 'wiki.lan',
to_https => true,
}
nginxpack::vhost::proxy { 'members':
domains => [ 'members.example.com' ],
port => 443,
ipv4only => true,
to_domain => 'members.lan',
to_https => true,
}
Webserver hosting blog.example.com:
nginxpack::vhost::basic { 'blog':
domains => [ 'blog.example.com' ],
ipv6only => true,
use_php => true,
}
Webserver hosting wiki.example.com:
nginxpack::vhost::basic { 'wiki':
domains => [ 'wiki.example.com' ],
https => true,
ssl_cert_source => 'puppet:///certificates/default.pem',
ssl_key_source => 'puppet:///certificates/default.key',
ipv6only => true,
use_php => true,
}
Webserver hosting members.example.com:
nginxpack::vhost::basic { 'members':
domains => [ 'members.example.com' ],
https => true,
ssl_cert_source => 'puppet:///certificates/default.pem',
ssl_key_source => 'puppet:///certificates/default.key',
ipv6only => true,
use_php => true,
}
Goals:
- 1 webserver (1 machine/container) by website and 1 for the reverse-proxy.
- Clients reach websites only through the reverse-proxy and can't reach them directly.
- We trust in the internal network so communications between the reverse-proxy and the websites are never encrypted.
Webserver hosting the reverse-proxy:
# Could be replaced by an internal DNS server
host {
'blog.lan':
ip => '10.0.0.10';
'wiki.lan':
ip => '10.0.0.20';
'members.lan':
ip => '10.0.0.30';
}
# Wildcard certificate (*.example.com)
class { 'nginxpack':
ssl_default_cert_source => 'puppet:///certificates/default.pem',
ssl_default_key_source => 'puppet:///certificates/default.key',
}
nginxpack::vhost::proxy { 'blog':
domains => [ 'blog.example.com' ],
to_domain => 'blog.lan',
}
nginxpack::vhost::proxy { 'wiki':
domains => [ 'wiki.example.com' ],
port => 443,
to_domain => 'wiki.lan',
}
nginxpack::vhost::proxy { 'members':
domains => [ 'members.example.com' ],
port => 443,
to_domain => 'members.lan',
}
Webserver hosting blog.example.com:
nginxpack::vhost::basic { 'blog':
domains => [ 'blog.example.com' ],
use_php => true,
}
Webserver hosting wiki.example.com:
nginxpack::vhost::basic { 'wiki':
domains => [ 'wiki.example.com' ],
use_php => true,
}
Webserver hosting members.example.com:
nginxpack::vhost::basic { 'members':
domains => [ 'members.example.com' ],
use_php => true,
}
Usage of www.
Using www.example.com is so 2005 and you want automatically redirect all requests from http://www.example.com/.* to http://example.com/$1.
nginxpack::vhost::basic { 'eatmytux':
domains => [ 'example.com' ],
use_php => true,
}
nginxpack::vhost::redirection { 'www-eatmytux':
domains => [ 'www.example.com' ],
to_domain => 'example.com',
}
Proxy and redirection vhosts use the first value of domains
when to_domain
is absent.
Your out-of-the-box webapp listens on port 8080 but you want use it on port 80 without modifying its configuration:
nginxpack::vhost::proxy { 'mywebapp':
domains => [ 'example.com' ],
to_port => 8080,
}
Visible location switching (the client will see his URL transformation: http://example.com/.* => http://example.com:8080/$1) means redirection:
nginxpack::vhost::redirection { 'mywebapp':
domains => [ 'example.com' ],
to_port => 8080,
}
Spontaneous switching from http to https:
nginxpack::vhost::basic { 'wiki':
domains => [ 'wiki.example.com' ],
https => true,
ssl_cert_source => 'puppet:///certificates/wiki.pem',
ssl_key_source => 'puppet:///certificates/wiki.key',
}
nginxpack::vhost::redirection { 'https-wiki':
domains => [ 'wiki.example.com' ],
to_https => true,
}
You own a website not available in IPv6 and you cannot have an IPv6 address on its machine. A way to solving this problem is to create a proxy on a dual-stack machine (listening on IPv6 to accept incoming requests and listening on IPv4 to contact the remote webserver):
nginxpack::vhost::proxy { 'foobar':
domains => [ 'example.com' ],
to_domain => 'ip4.example.com',
ipv6only => true,
}
DNS configuration:
example.com
AAAA proxy
A webserver
ip4.example.com
A webserver
This trick could also be used in the opposite case.
- puppetlabs/stdlib >= 3.x (
file_line
is used to edit php.ini,validate_re
to check some parameters andensure_packages
to install logrotate and psmisc)
This module is only available for Debian-likes.
I developed this module for my own needs but I think it's generic enough to be useful for someone else.
Feel free to contribute. I'm not a big fan of centralized services like GitHub but I used it to permit easy pull-requests, so show me that's a good idea!
- Lorraine Data Network for testing the module
- SĂ©bastien Badia for adding LogRotate options with comprehensive documention