Title: Переезд блога на HTTP/2 сервер H2O Content: Вероятно, постоянные читатели блога kostikov.co заметили, что он, как, впрочем, и ряд других поддерживаемых автором HTTP-серверов, работают на базе Lighttpd. Действительно, этот быстрый и лёгкий веб-сервер снискал широкую популярность и был широко распространён в качестве лучшей альтернативы стандартному Apache. Однако, с течением времени и неизбежным прогрессом в сетевых технологиях разработчики Lighttpd заметно отстали в их поддержке, а конкуренты, в лице, главным образом, Nginx, за тот же интервал существенно нарастил функционал и стал куда более распространённым решением.
Наиболее существенным недостатком Lighttpd представляется отсутствие в настоящее время поддержки им современной версии протокола HTTP/2, которая имеет большое количество преимуществ перед предыдущими версиями. Также минусом этого веб-сервера является отсутствие поддержки протокола Online Certificate Status Protocol (OCSP), который начинает приобретать важное значение в свете борьбы со злоупотреблениями при выпуске SSL-сертификатов.
В связи с вышеизложенным, после изучения возможных альтернатив, автор принял решение о переносе блога на HTTP/2 сервер H2O, которому ранее была посвещена обзорная статья, как на наиболее прогрессивный и перспективный в обозримом будущем вариант.
Блог kostikov.co работает на системе управления контентом (CMS) [Bludit](https://www.bludit.com, которая написана) PHP и организована по принципу flat file, то есть использует исключительно файловую систему для хранения своих данных, на операционной системе FreeBSD.
root@beta:~ # uname -v
FreeBSD 11.0-RELEASE-p9 #0: Tue Apr 11 08:48:40 UTC 2017 root@amd64-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC
Обработка производится PHP последней актуальной версии посредством службы PHP-FPM или PHP FastCGI Process Manager, которая запущена в качестве отдельного сервиса и отвечает на запросы по стандартному порту 9000 на локальном интерфейсе.
root@beta:~ # php -v
PHP 7.1.7 (cli) (built: Jul 8 2017 12:57:01) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.1.0, Copyright (c) 1998-2017 Zend Technologies
root@beta:~ # grep php /etc/rc.conf
php_fpm_enable="YES"
root@beta:~ # ps -aux | grep php-fpm
root 10876 0,0 0,4 153300 18640 - Ss 22:34 0:02,21 php-fpm: master process (/usr/local/etc/php-fpm.conf) (php-fpm)
www 10878 0,0 0,6 184380 24124 - I 22:34 0:10,73 php-fpm: pool www (php-fpm)
www 16019 0,0 0,6 188836 26900 - I 00:48 0:19,17 php-fpm: pool www (php-fpm)
www 16070 0,0 0,6 185172 25596 - I 00:52 0:09,38 php-fpm: pool www (php-fpm)
root 28217 0,0 0,1 14852 2568 0 S+ 11:58 0:00,01 grep php-fpm
root@beta:~ # sockstat -46 | grep 9000
www php-fpm 16070 0 tcp4 127.0.0.1:9000 *:*
www php-fpm 16019 0 tcp4 127.0.0.1:9000 *:*
www php-fpm 10878 0 tcp4 127.0.0.1:9000 *:*
root php-fpm 10876 7 tcp4 127.0.0.1:9000 *:*
Кроме того, сайт работает по защищённому протоколу HTTPS используя сертификаты выданные Let's Encrypt. Для дополнительной безопасности работы с сертификатами применяется технология HTTP Public Key Pining (HPKP), которой автор ранее уделил внимания в отдельной статье. Напомню, что HPKP реализуется посредством включения особого заголовка в HTTP-ответ сервера, содержимое которого генерируется специальным скриптом lerenew.sh автоматического обновления сертификатов Let's Encrypt. Ввиду того, что формат конфигурационного файла H2O, разумеется, отличается от ранее используемого в Lighttpd, потребуется модификация и скрипта обновления.
Кроме того, используется и набор других заголовков безопасности HTTP (см. "Заголовки HTTP и безопасность web-сервера"), что также требуется учесть при запуске сайта на базе нового веб-сервера.
Конфигурационный файл H2O h2o.conf расположен по стандартному пути /usr/local/etc/h2o. Модифицируем его для нужд работы CMS Bludit с учётом упомянутых в предыдущем разделе особенностей.
root@beta:/usr/local/etc/h2o # cd /usr/local/etc/h2o/
root@beta:/usr/local/etc/h2o # cat h2o.conf
# see https://h2o.examp1e.net/ for detailed documentation
# see h2o --help for command-line options and settings
# v.20170714 (c)2017 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co
user: www
pid-file: /var/run/h2o.pid
access-log: /var/log/h2o/h2o-access.log
error-log: /var/log/h2o/h2o-error.log
expires: off
file.dirlisting: off
file.send-compressed: on
file.index: [ 'index.html', 'index.php' ]
file.mime.addtypes:
"application/x-font-ttf": ".ttf"
"application/x-font-opentype": ".otf"
file.custom-handler:
extension: ".php"
fastcgi.connect:
host: 127.0.0.1
port: 9000
type: tcp
listen:
port: 80
listen:
port: 443
ssl: &default_ssl
cipher-suite: ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS
cipher-preference: server
dh-file: /etc/ssl/dhparams.pem
certificate-file: /usr/local/etc/letsencrypt/live/my.server/fullchain.cur.pem
key-file: /usr/local/etc/letsencrypt/live/my.server/privkey.cur.pem
...
# --- kostikov.co
"kostikov.co:80":
listen:
port: 80
paths:
"/":
redirect:
status: 301
url: https://kostikov.co/
"kostikov.co:443":
<<: !file /usr/local/etc/h2o/hdr/kostikov.co.full.h2o
listen:
port: 443
ssl:
<<: *default_ssl
certificate-file: /usr/local/etc/letsencrypt/live/kostikov.co/fullchain.cur.pem
key-file: /usr/local/etc/letsencrypt/live/kostikov.co/privkey.cur.pem
paths:
"/":
mruby.handler: |
acl {
deny { path.match("^bl-content/(.*)\.txt$") }
}
file.dir: /usr/local/www/kostikov.co
redirect:
url: /index.php
internal: YES
status: 307
...
Как видно, в глобальной секции помимо типовых параметров через опцию file.custom-handler добавлен обработчик расширений .php, который обращается к сервису php-fpm. Также в дополнение к стандартным для H2O MIME типам через директиву file.mime.addtypes добавлены ещё два, которые использует Bludit в шрифтовых оформлениях своих тем.
В описании имени хоста kostikov.co при обращении по незащищённом протоколу HTTP командой redirect производится переадресация на защищённую версию сайта с кодом ответа 301 Moved Permanently. При работе по HTTPS, во-первых, директивой <<: !file из файла kostikov.co.full.h2o расположенного в специально созданном подкаталоге /usr/local/etc/h2o/hdr добавляются дополнительные заголовки безопасности. Его содержимое таково.
root@beta:/usr/local/etc/h2o # cat hdr/kostikov.co.full.h2o
header.add: "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval' https://www.google-analytics.com https://mc.yandex.ru https://static.addtoany.com https://*.disqus.com https://*.disquscdn.com 'self'; style-src 'unsafe-inline' https://fonts.googleapis.com https://static.addtoany.com https://*.disquscdn.com 'self'; font-src https://fonts.gstatic.com 'self'; connect-src https://mc.yandex.ru https://links.services.disqus.com 'self'; img-src https://cex.io https://www.google-analytics.com https://mc.yandex.ru https://static.addtoany.com data: https://*.disquscdn.com https://referrer.disqus.com https://affiliates.purevpn.com 'self'; frame-src https://static.addtoany.com https://disqus.com 'self'"
header.add: "Strict-Transport-Security: max-age=15768000; includeSubDomains; preload"
header.add: "X-Content-Type-Options: nosniff"
header.add: "X-XSS-Protection: 1; mode=block"
header.add: "X-Frame-Options: DENY"
header.add: "Public-Key-Pins: pin-sha256=\"m5meRZL/rKw5ItGkshSqj3gJDwl9ghvL2PZIkiEuV1s=\"; pin-sha256=\"dIQH62dSELdTCHaxFmNPhAlZGKlLtaf1B3X3q5SsNAU=\"; max-age=3456000; includeSubDomains"
Выбор единого файла для хранения HTTP-заголовков определён тем, что сайт работает исключительно по защищённому протоколу и нет нужды в разделении заголовков по критерию их применимости (или наоборот) в незащищённом режиме.
Далее, в опции mruby.handler через встроенную в H2O систему контроля доступа (ACL) производится ограничение прямого доступа к внутреннему представлению содержимого сайта. И, наконец, ещё один redirect с кодом ответа 307 Temporary Redirect перенаправляет все обращения к сайту на единый обработчик расположенный в файле index.php.
Также, как уже было упомянуто, нам потребуется при каждом обновлении SSL-сертификатов обновлять их цифровые отпечатки используемые для реализации HPKP, которые передаются в заголовке Public-Key-Pins. Модернизированный для этого скрипт lerenew.sh приобрёл следующий вид.
#!/bin/sh
# Update "Let's Encrypt" certificates,
# make cert+key joined files, HPKP hashes and TLSA records
# v.20170713 (c)2016-17 by Max Kostikov http://kostikov.co e-mail: max@kostikov.co
#
# cat /etc/crontab | grep lerenew
# 0 0 * * 1 root /usr/local/etc/letsencrypt/lerenew.sh >/dev/null 2>&1
lepath="/usr/local/etc/letsencrypt"
log="/var/log/letsencrypt/lerenew.log"
pins="/usr/local/etc/h2o/hdr" # pins for H2O
nsddir="/usr/local/etc/nsd" # path to NSD dir
tlsapref="_tlsa" # prefix for TLSA records, use CNAME to point on
joincrt="privandcert" # joined file name
curpref="cur" # current set postfix
echo "`date '+%Y-%m-%d %H:%M:%S'` --- Starting SSL certs renew..." >>$log
certbot renew -n $* >>$log 2>&1
certs=`find ${lepath}/archive/*/cert*.pem -mtime -1h`
if [ "${certs}" ];
then
for i in $certs;
do
dir=`dirname $i` # path to certs
dom=`basename $dir` # domain name
ind=`basename $i | tr -d '[:alpha:].'` # latest cert index
cur=`expr $ind - 1` # previous cert index
# create new joined cert and symlink
cat ${dir}/privkey${ind}.pem $i > ${dir}/${joincrt}${ind}.pem
cd ${lepath}/live/${dom}
ln -fs ../../archive/${dom}/${joincrt}${ind}.pem ${joincrt}.pem
# create symlinks for previous certs set
for j in `ls -d ../../archive/${dom}/*${cur}.*`;
do
ln -fs $j `basename -s .pem $j | tr -d '[:digit:]'`.${curpref}.pem
done
# create cert pins
# H2O
conf="${pins}/${dom}.ssl.h2o" # pin config name
sed -i.bak '/^header.add: "Public-Key-Pins/d' $conf
echo 'header.add: "Public-Key-Pins: pin-sha256=\"'"`openssl x509 -in cert.${curpref}.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64`"'\"; pin-sha256=\"'"`openssl x509 -in cert.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64`"'\"; max-age=3456000; includeSubDomains"' >> $conf
cat ${pins}/hsts.h2o ${pins}/${dom}.h2o $conf 2>/dev/null > ${pins}/${dom}.full.h2o
# update DNS zone if DNSSEC enabled
zone="${nsddir}/zones/${dom}/${dom}.zone"
if [ -f ${zone}.signed ];
then
sed -i.bak "/TLSA/d" $zone
echo "${tlsapref}.${dom}. IN TLSA 3 0 1 `openssl x509 -in cert.${curpref}.pem -outform DER | sha256`" >>$zone
echo "${tlsapref}.${dom}. IN TLSA 3 0 1 `openssl x509 -in cert.pem -outform DER | sha256`" >>$zone
${nsddir}/dnsnewserial.sh $zone
${nsddir}/dnssignzone.sh $zone
fi
done
# reload services
nsd-control reload
service h2o restart
service dovecot restart
service exim restart
fi
echo "`date '+%Y-%m-%d %H:%M:%S'` --- SSL certs renew done!" >>$log
В процессе его работы, во-первых, модифицируется промежуточный файл с заголовками исключительно для режима HTTPS с наименованием kostikov.co.ssl.h2o. Далее из него, а также двух других файлов — hsts.h2o, содержащего стандартное значение для заголовка Strict-Transport-Security и kostikov.co.h2o с заголовками, которые могут использоваться в обоих режимах обмена данным по HTTP, создаётся единый файл с полным набором HTTP-заголовков для защищённого режима kostikov.co.full.h2o, который и включается в конфигурацию H2O.
root@beta:/usr/local/etc/h2o # cat hdr/hsts.h2o
header.add: "Strict-Transport-Security: max-age=15768000; includeSubDomains; preload"
root@beta:/usr/local/etc/h2o # cat hdr/kostikov.co.h2o
header.add: "Content-Security-Policy: upgrade-insecure-requests; default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval' https://www.google-analytics.com https://mc.yandex.ru https://static.addtoany.com https://*.disqus.com https://*.disquscdn.com 'self'; style-src 'unsafe-inline' https://fonts.googleapis.com https://static.addtoany.com https://*.disquscdn.com 'self'; font-src https://fonts.gstatic.com 'self'; connect-src https://mc.yandex.ru https://links.services.disqus.com 'self'; img-src https://cex.io https://www.google-analytics.com https://mc.yandex.ru https://static.addtoany.com data: https://*.disquscdn.com https://referrer.disqus.com https://affiliates.purevpn.com 'self'; frame-src https://static.addtoany.com https://disqus.com 'self'"
header.add: "X-Content-Type-Options: nosniff"
header.add: "X-XSS-Protection: 1; mode=block"
header.add: "X-Frame-Options: DENY"
root@beta:/usr/local/etc/h2o # cat hdr/kostikov.co.ssl.h2o
header.add: "Public-Key-Pins: pin-sha256=\"m5meRZL/rKw5ItGkshSqj3gJDwl9ghvL2PZIkiEuV1s=\"; pin-sha256=\"dIQH62dSELdTCHaxFmNPhAlZGKlLtaf1B3X3q5SsNAU=\"; max-age=3456000; includeSubDomains"
Такой подход по разбивке на несколько отдельных файлов является универсальным и позволяет включать различным сайтам, которые могут работать на том же веб-сервере, включать заголовки блоками в зависимости от режима и потребности.
Завершив конфигурирование можно запустить наш новый HTTP/2 сервер H2O, конечно не забыв предварительно остановить старый добрый Lighttpd и поправить содержимое файла /etc/rc.conf.
root@beta:~ # cat /etc/rc.conf
# -- sysinstall generated deltas -- # Wed Jan 12 19:24:42 2011
...
php_fpm_enable="YES"
h2o_enable="YES"
#lighttpd_enable="YES"
...
root@beta:~ # service lighttpd stop && service h2o start
Stopping lighttpd.
Waiting for PIDS: 30670.
Starting h2o.
start_server (pid:30687) starting now...
Проверить, что система работает должным образом можно из командной строки используя, к примеру, утилиту cURL.
root@beta:~ # curl -IL http://kostikov.co
HTTP/1.1 301 Redirected
Date: Fri, 14 Jul 2017 14:54:53 GMT
Connection: keep-alive
Server: h2o/2.2.2
location: https://kostikov.co/
content-type: text/html; charset=utf-8
HTTP/2 200
server: h2o/2.2.2
date: Fri, 14 Jul 2017 14:54:53 GMT
content-security-policy: upgrade-insecure-requests; default-src 'self'; script-src 'unsafe-inline' 'unsafe-eval' https://www.google-analytics.com https://mc.yandex.ru https://static.addtoany.com https://*.disqus.com https://*.disquscdn.com 'self'; style-src 'unsafe-inline' https://fonts.googleapis.com https://static.addtoany.com https://*.disquscdn.com 'self'; font-src https://fonts.gstatic.com 'self'; connect-src https://mc.yandex.ru https://links.services.disqus.com 'self'; img-src https://cex.io https://www.google-analytics.com https://mc.yandex.ru https://static.addtoany.com data: https://*.disquscdn.com https://referrer.disqus.com https://affiliates.purevpn.com 'self'; frame-src https://static.addtoany.com https://disqus.com 'self'
strict-transport-security: max-age=15768000; includeSubDomains; preload
x-content-type-options: nosniff
x-xss-protection: 1; mode=block
x-frame-options: DENY
public-key-pins: pin-sha256="m5meRZL/rKw5ItGkshSqj3gJDwl9ghvL2PZIkiEuV1s="; pin-sha256="dIQH62dSELdTCHaxFmNPhAlZGKlLtaf1B3X3q5SsNAU="; max-age=3456000; includeSubDomains
x-powered-by: PHP/7.1.7
set-cookie: Bludit-KEY=2c11b5d36c24f3ab375cff926dc7a2af; path=/; HttpOnly
expires: Thu, 19 Nov 1981 08:52:00 GMT
cache-control: no-store, no-cache, must-revalidate
pragma: no-cache
content-type: text/html; charset=UTF-8
Как можно видеть, при начальном обращении по незащищённому протоколу HTTP/1.1 произошла переадресация на защищённое соединение уже по протоколу HTTP/2. При этом были добавлены необходимые HTTP-заголовки.
Статья была полезной? Тогда прошу не стесняться и деньгами или биткоинами.