## Configuring h2o/php-fpm

I first started using h2o because I wanted something that supported HTTP/2 (shortly after the standard was approved) and neither nginx nor apache offered anything that was not “development”. I very much like the simplicity of its configuration, light resource footprint, and responsive behavior. Documentation for h2o.

Getting this setup for serving up our mail related sites is really easy. We need to add 2 bits to the h2o.conf file (if you want to read about security for h2o, Calomel h2o has a great writeup). The config file is yaml, and sensitive to spacing. If you get errors on startup, don’t forget to check the spacing of things.

The first section is:

# php-fpm
file.custom-handler:
extension: .php
fastcgi.connect:
host: 127.0.0.1
port: 9000
type: tcp

# Directory Index
file.index: [ 'index.php', 'index.html' ]


This takes care of not only handing all the php(1) files to php-fpm, but also defining index.php as an optional index file.

The second section is the paths we want to make available to run our applications. There are several: phpPgAdmin, PostfixAdmin, policyd, and Roundcube.

I’ve defined 2 “hosts” here. The first accepts traffic on port 80, sends back a 301 redirect to the same host, but port 443 (which is SSL encrypted). You might be wondering why not include the HSTS header with the redirect, so the browser would know to only use HTTPS. The browser won’t trust an HSTS header unless its sent over HTTP. Otherwise it could be altered in transit. The section looks like this:

# A+ on https://securityheaders.io/

# per-host configuration
hosts:
"mx.cryptomonkeys.com:80":
listen:
port: 80
paths:
/:
redirect:
status: 301
url: https://mx.cryptomonkeys.com/
"mx.cryptomonkeys.com:443":
listen:
port: 443
ssl:
certificate-file: /usr/local/etc/ssl/server.crt
key-file: /usr/local/etc/ssl/server.key
dh-file: /usr/local/etc/ssl/dh2048.pem
cipher-preference: server
cipher-suite: ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK
minimum-version: TLSv1.2
paths:
"/ppa":
"/pfa":
"/policyd":
mruby.handler: |
require "#{$H2O_ROOT}/share/h2o/mruby/htpasswd.rb" Htpasswd.new("/usr/local/www/policyd/.htpasswd", "realm-name") file.dir: "/usr/local/www/policyd" "/webmail": file.dir: "/usr/local/www/roundcube"  I’ve set the minimum version of TLS to be 1.2. As long as you have a relatively recently version of a popular browser, you should be fine with this. I’ve also restricted the ciphers to what can be found on Mozilla’s Security/Server Side TLS under the moniker ‘modern’. Now we need to create the .htpasswd file in the policyd directory. If you have a system with apache, you can use the included utility. If you are just testing things, you can use this web based htpasswd generator. Now we can create a php.ini file. In /usr/local/etc/, copy the production one. cp php.ini-production php.ini  Now edit the php.ini and set the date/timezone. If you run lots of servers, I’d suggest UTC. Also, here are some security best practices: open_basedir = /usr/local/www expose_php = Off memory_limit = 8M error_log = syslog post_max_size = 256K sys_temp_dir = "/var/php_tmp" upload_tmp_dir = /var/php_tmp upload_max_filesize = 20M allow_url_fopen = Off date.timezone = UTC sql.safe_mode = On session.save_path = "/var/php_tmp"  I’ve left the upload at 20M because I want people to be able to attach things in webmail. If its larger than 20M, it doesn’t belong in email. Don’t forget to create /var/php_tmp and set the proper permissions: mkdir /var/php_tmp chmod 1777 /var/php_tmp  Now we can run: sudo sysrc h2o_enable=YES sudo sysrc php_fpm_enable=YES  to /etc/rc.conf. Once this is done, you can run: sudo service php-fpm start && sudo service h2o start  You should be able to see both in the ps(1) output. It should look something like this: [louisk@mx louisk 44 ]$ ps ax | egrep 'php|h2o'
758  -  I      0:00.03 /usr/local/bin/perl -x /usr/local/share/h2o/start_server --pid-file=/var/run/h2o.pid --log-f
759  -  I      0:01.45 /usr/local/bin/h2o -c /usr/local/etc/h2o/h2o.conf
1037  -  Ss     0:00.62 php-fpm: master process (/usr/local/etc/php-fpm.conf) (php-fpm)
2210  -  I      0:04.44 php-fpm: pool www (php-fpm)
5623  -  I      0:01.32 php-fpm: pool www (php-fpm)
6304  -  I      0:00.26 php-fpm: pool www (php-fpm)
8356  3  S+     0:00.00 egrep php|h2o
[louisk@mx louisk 45 ]$ You can also check for open sockets with: [louisk@mx louisk 48 ]$ sockstat -46l | egrep 'php|h2o'
www      php-fpm    6304  0  tcp4   127.0.0.1:9000        *:*
www      php-fpm    5623  0  tcp4   127.0.0.1:9000        *:*
www      php-fpm    2210  0  tcp4   127.0.0.1:9000        *:*
root     php-fpm    1037  8  tcp4   127.0.0.1:9000        *:*
www      h2o        759   5  tcp6   *:80                  *:*
www      h2o        759   6  tcp4   *:80                  *:*
www      h2o        759   7  tcp6   *:443                 *:*
www      h2o        759   8  tcp4   *:443                 *:*
www      h2o        759   15 tcp6   *:80                  *:*
www      h2o        759   16 tcp4   *:80                  *:*
www      h2o        759   17 tcp6   *:443                 *:*
www      h2o        759   18 tcp4   *:443                 *:*
[louisk@mx louisk 49 ]$ In case you’re wondering, the config below is HTTP/2 compliant and modern browsers will access the site via HTTP/2. # vi: ft=yaml # to find out the configuration commands, run: h2o --help user: www pid-file: /var/run/h2o.pid access-log: /var/log/h2o/h2o-access.log error-log: /var/log/h2o/h2o-error.log # php-fpm file.custom-handler: extension: .php fastcgi.connect: host: 127.0.0.1 port: 9000 type: tcp # Directory Index file.index: [ 'index.php', 'index.html' ] file.dirlisting: off # per-host configuration hosts: "mx.cryptomonkeys.com:80": listen: port: 80 paths: /: redirect: status: 301 url: https://mx.cryptomonkeys.com/ "mx.cryptomonkeys.com:443": header.add: "strict-transport-security: max-age=31556926; preload" listen: port: 443 ssl: certificate-file: /usr/local/etc/ssl/server.crt key-file: /usr/local/etc/ssl/server.key dh-file: /usr/local/etc/ssl/dh2048.pem cipher-preference: server cipher-suite: ECDHE-RSA-CHACHA20-POLY1305:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK minimum-version: TLSv1.2 paths: "/ppa": file.dir: "/usr/local/www/phpPgAdmin" "/pfa": file.dir: "/usr/local/www/postfixadmin" "/webmail": file.dir: "/usr/local/www/roundcube" "/policyd": file.dir: "/usr/local/www/policyd"  ## Configuring phpPgAdmin This component is optional. If you are comfortable using psql(1) to manipulate the databases, there is no need to install this. If you choose to skip it, don’t forget to remove the appropriate lines from the h2o.conf and restart h2o. You can find documentation for phpPgAdmin here. By default, phpPgAdmin installs bits into /usr/local/www/phpPgAdmin (web root). The config is pretty basic. I only had to modify things in the top dozen or two lines. $conf['servers'][0]['host'] = 'localhost';
$conf['servers'][0]['port'] = 5432;$conf['servers'][0]['sslmode'] = 'require';


Now you should be able to point your browser at your webserver (https://my-ip/ppa/), and get something that looks similar to this:

PostfixAdmin is the easy way for people to control virtual users and domains. Privlidges are assignable by domain so you can give somebody free reign over their own domain(s) if you wish. Documentation for PostfixAdmin.

PostfixAdmin defaults the install to /usr/local/www/postfixadmin. We need to edit the config.inc.php file first. I’ve made the following changes (to existing lines/entries):

...
$CONF['configured'] = true;$CONF['setup_password'] = 'winkle-snicker';
$CONF['database_type'] = 'pgsql';$CONF['database_host'] = 'localhost';
$CONF['database_user'] = 'postfix_admin';$CONF['database_password'] = 'password';
$CONF['database_name'] = 'postfix_admin'; ...$CONF['admin_email'] = 'admins@cryptomonkeys.com';
...
$CONF['default_aliases'] = array ( 'abuse' => 'abuse@cryptomonkeys.com', 'hostmaster' => 'hostmaster@cryptomonkeys.com', 'postmaster' => 'postmaster@cryptomonkeys.com', 'webmaster' => 'webmaster@cryptomonkeys.com' ); ...$CONF['vacation'] = 'YES';


Now, its time to add a postfix database and user to Postgres. Connect with psql and type in:

CREATE ROLE postfix_admin WITH LOGIN ENCRYPTED PASSWORD 'winkle-snicker';


Once you have these bits set, you should be able to point your browser at https://my-ip/pfa/setup.php and get something that looks similar to this:

Come up with a “setup password” and admin credentials. Then you can re-point your browser at https://my-ip/pfa/, and get something that looks similar to this:

You should be able to login to PostfixAdmin and create domains, users, and aliases. They take effect immediately.

NOTE: If you wish to convert from MySQL to PostgreSQL (insert plenty of comments about the one true database), you will need to do a little dirty work. Its not terribly complicated, but it is a manual process.

Start by dumping the postfix database from mysql(1).

mysqldump --compatible=postgresql dbname > export.sql


You will need to make some edits to this (I creatively called mine postfix.sql) before you can import it into PostgreSQL.

• At the top of the document, I inserted the following lines so I could run the script more than once as I was working through it.
TRUNCATE TABLE admin CASCADE;

• All of the stanzas that are in the category of “table structure” get deleted
• For each line that starts with ‘INSERT INTO’, remove all of the backquotes () and single quotes (')
• All of the boolean values need to be converted from ‘0’ or ‘1’, into ‘true’ or ‘false’
• Reordering of the tables that we insert into (because we have foreign key dependancies that must be met to succeed. The order is as follows:

Mailbox table is slightly reordered. Must be changed for every insert-entry. Order matters here for foreign keys to work properly.

1. admin
2. config
3. domain
`