Uzix | микроблог

https

В связи с наметившейся тенденцией всяческого ущемления (понижение позиции в выдаче поисковиков, предупреждения о небезопасности в браузерах) сайтов, доступных только по http, а так же по причине необходимости повышения безопасности передаваемых данных, передо мной возникла задача внедрения https с минимальными материально-техническими затратами на внедрение и сопровождение.

Итак, возможны несколько подходов при внедрении https. 1. Использование самоподписанных сертификатов. Этот подход хорошо подходит для небольших частных сайтов, различных админок и подобной мелочи. Плюсы такого подхода:

  • Ни от кого не зависим - сертификат можно самостоятельно сделать на любой домен, на любой срок. Обычный центр сертификации по очевидным причинам не станет подписывать сертификат для внутренних доменов (*.lan и т.п.).
  • Всё довольно просто - получить самоподписанный сертификат можно буквально в одну команду. Минусы:
  • При первом посещении сайта пользователю будет выдано предупреждение о недоверенном сертификате и необходимо будет добавить сертификат в список доверенных. Для некоторых утилит часто неясно как или вовсе невозможно это сделать.

howto:

openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365

2. Использование собственного центра сертификации. Оптимальный вариант для внутренних ресурсов организаций. К плюсам и минусам подхода №1 добавляется:

  • Опять же ни от кого не зависим - сертификат можно самостоятельно сделать на любой домен, на любой срок.
  • Централизованное управление выданными/отозванными сертификатами. Как бонусом - удобно мониторить истекающие сертификаты.
  • Для доверия к любому внутреннему ресурсу достаточно добавить всего один сертификат на всех клиентах. При системы центрального управления конфигурациями (active directory, ansible и т.п.) сделать это крайне просто.
  • При компрометации сертификата центра сертификации автоматически компрометируются все серверы и клиенты, на которых он установлен. Т.к. x509 не предусматривает ограничений на выдаваемые центром сертификации сертификаты, то компрометируются в том числе и любые https соединения к любым серверам. Единственное что остаётся в безопасности - клиенты, использующие привязку к сертификату (certificate pinning).
  • Собственный центр сертификации относительно сложно настроить. Нельзя просто скопировать первый рецепт с serverfault - нужно понимать что и зачем делать.

howto: Не существует быстрого и универсального пути, но для себя я написал небольшой скрипт, обладающий минимальным функционалам по созданию ЦС, созданию и подписи ключей и сертификатов. Скрипт далеко не идеальный, но длительное время он служил мне верой и правдой.

#!/bin/bash -xe

export OPENSSL_CONF=cnf

a="$1"
shift || true
n="$1"
o="$2"

cd "`dirname $0`"
umask 0007

case "$a" in
  gen)
    test -n "$n"
    openssl genrsa 2048 >private/$n

    cp "$OPENSSL_CONF"{,.tmp}
    export OPENSSL_CONF="$OPENSSL_CONF.tmp"
    echo '[ alt_names ]' >> "$OPENSSL_CONF"
    cnt=1
    while (( "$#" )); do
        echo "DNS.$cnt = $1" >> "$OPENSSL_CONF"
        (( cnt++ ))
        shift
    done
    
    openssl req -new -key private/$n -subj "/C=RU/ST=Belgorod/O=IVTBSU/OU=CA/CN=$n" >csr.$n
    openssl req -text -noout <csr.$n
    openssl ca -batch -notext -extensions server -in csr.$n >cert/$n
    rm csr.$n
  ;;
  genca)
    mkdir ca cert private dh || true
    openssl genrsa 4096 >ca/key
    openssl req -new -key ca/key -subj "/C=RU/ST=Belgorod/O=IVTBSU/OU=CA/CN=ivt.su" >ca/csr
    openssl x509 -req -days 10000 -in ca/csr -signkey ca/key >ca/crt
    rm ca/csr
    echo 100001 >ca/srl
    echo 100001 >ca/crlnum
    touch ca/index
    openssl ca -gencrl >ca/crl
  ;;
  gendh)
    test -r cert/$n
    openssl dhparam -in cert/$n 1024 >dh/$n
  ;;
  revoke)
    test -r cert/$n
    openssl ca -revoke cert/$n
    openssl ca -gencrl >ca/crl
    rm cert/$n private/$n
  ;;
  public)
    openssl rsa -in cert/$n -pubout
  ;;
  info)
    openssl x509 -in cert/$n -text
  ;;
  chown)
    test -n "$o"
    for i in dh cert private; do
      test -r "$i/$n" || continue
      chmod 0400 "$i/$n"
      chown $o:wheel "$i/$n"
    done  
  ;;
  *)
    echo "Usage: $0 "
    exit 1
  ;;
esac

Так же понадобится файл конфигурации openssl (cnf):

[ ca ]
default_ca = myca

[ crl_ext ]
# issuerAltName=issuer:copy  #this would copy the issuer name to altname
authorityKeyIdentifier = keyid:always

[ myca ]
dir              = ./ca
new_certs_dir    = $dir
unique_subject   = no
certificate      = $dir/crt
database         = $dir/index
private_key      = $dir/key
serial           = $dir/srl
default_days     = 730
default_md       = sha256
policy           = myca_policy
x509_extensions  = myca_extensions
crlnumber        = $dir/crlnum
default_crl_days = 730

[ myca_policy ]
commonName             = supplied
stateOrProvinceName    = supplied
countryName            = optional
emailAddress           = optional
organizationName       = supplied
organizationalUnitName = optional

[ myca_extensions ]
basicConstraints       = CA:false
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always
keyUsage               = digitalSignature,keyEncipherment
extendedKeyUsage       = serverAuth
crlDistributionPoints  = URI:http://CHANGE.ME/root.crl
nsComment              = "OpenSSL Generated Certificate"
subjectAltName         = @alt_names

[ req ]
default_md             = sha256
distinguished_name     = req_distinguished_name
#attributes            = req_attributes
x509_extensions        = v3_ca
req_extensions         = v3_req
[ v3_req ]
subjectAltName         = @alt_names
[ v3_ca ]
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid:always,issuer
basicConstraints       = CA:true

[ req_distinguished_name ]
countryName                  = Country?
countryName_default          = RU
stateOrProvinceName          = State?
stateOrProvinceName_default  = Belgorod obl.
localityName                 = City?
localityName_default         = Belgorod
0.organizationName           = Organization?
0.organizationName_default   = Home
commonName                   = Domain?
commonName_default           = change.me
emailAddress                 = Email?
emailAddress_default         = ca@change.me

[ server ]
basicConstraints       = CA:FALSE
nsCertType             = server
nsComment              = "OpenSSL Generated Server Certificate"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
extendedKeyUsage       = serverAuth
keyUsage               = digitalSignature, keyEncipherment
subjectAltName         = @alt_names

[ client ]
basicConstraints       = CA:FALSE
nsComment              = "OpenSSL Generated Client Certificate"
subjectKeyIdentifier   = hash
authorityKeyIdentifier = keyid,issuer:always
extendedKeyUsage       = clientAuth
keyUsage               = digitalSignature

3. Использование публичного центра сертификации. Фактически единственный приемлемый вариант для публичных сайтов.

  • Все клиенты по умолчанию будет доверять вашему сертификату.
  • Нужно проходить процедуру верификации для подтверждения владения доменом, при чём домен, очевидно, должен быть публичным.
  • Центр сертификации может накладывать собственные ограничения на параметры сертификата. Например, Let’s Encrypt выдаёт сертификаты не более чем на ~3 месяца и не допускает использования widcard в доменном имени.
  • Большинство центров сертификации хотят денег.

howto: Я буду использовать let’s encrypt (LE) т.к.

  1. Он бесплатный;
  2. Процедура получения и обновления сертификатов автоматизирована. Всю грязную работу будет выполнять утилита acme-tiny:
 pkg install acme-tiny

Вряд ли кому-то доставит удовольствие поддерживать сертификаты для нескольких десятков доменов, особенно с учётом срока годности сертификатов LE (~3 месяца). Поэтому я набросал небольшой скрипт. Он автоматически создаёт ключи и сертификаты для указанных доменов и, при близости срока их истекания, проводит обновление. Предварительная подготовка:

# mkdir -p /var/www/le
# mkdir /etc/ssl/le
# cd /etc/ssl/le
# openssl genrsa 4096 >LE.key

Собственно, сам скрипт:

#!/bin/sh -e

cd "$(dirname $0)"

isok() {
  name="$1"; domains="$4"
  test -r "$name.key" || return 1
  test -r "$name.crt" || return 1
  openssl x509 -in "$name.crt" -checkend 2592000 -noout || return 1
  f1="`mktemp`"; echo "$domains" |tr : '\n' |sort >"$f1"
  f2="`mktemp`"; openssl x509 -in "$name.crt" -text |egrep -m1 '^ +DNS:' |sed -e 's/ *DNS://g' |tr , '\n' |sort >"$f2"
  set +e
  diff -q "$f1" "$f2" >/dev/null
  ret=$?
  set -e
  rm "$f1" "$f2"
  test "$ret" = 0 || return 1
  return 0
}

upd() {
  name="$1"; group="$2"; service="$3"; domains="$4"
  echo "Generating $name..." >&2
  cnff="`mktemp`"
  test -r "$name.key" || openssl genrsa 4096 >"$name.key"
  cat /etc/ssl/openssl.cnf >"$cnff"
  echo "$domains" | awk -vRS=: 'BEGIN{printf "[SAN]\nsubjectAltName="}notfirst==1{printf ","}{printf "DNS:"$0; notfirst=1} ' >>"$cnff"
  openssl req -new -sha256 -key $name.key -subj "/" -reqexts SAN -config "$cnff" > "$name.csr"
  rm "$cnff"
  acme_tiny --account-key LE.key --csr "$name.csr" --acme-dir /var/www/le/ > "$name.crt"
  rm "$name.csr"
  curl -s https://letsencrypt.org/certs/lets-encrypt-x3-cross-signed.pem >> "$name.crt"
  cat "$name.crt" "$name.key" >"$name.pem"
  chgrp "$group" "$name.key" "$name.crt" "$name.pem"
  chmod o-rwx    "$name.key" "$name.crt" "$name.pem"
  test "$service" = '-' || service "$service" reload
}

cat << EOF |grep -v -e^# -e^$ |while read i; do isok $i || upd $i; done

# name    group    service    dns1.example.com:dns2.example.com
web       www      nginx      err200.net:www.err200.net:ftp.err200.net
imap      dovecot  dovecot    imap.err200.net:pop3.err200.net:pop.err200.net
irc       ircd     inspircd   irc.err200.net
EOF

Добавляем в cron:

@monthly /etc/ssl/le/upd.sh

Самое время настроить веб-сервер. Я сделаю это на примере nginx. Let’s encrypt верифицирует домен путём загрузки через http файла со случайно сгенерированным именем, нужно это учесть. Создаём /etc/nginx/lessl.conf:

listen 443 ssl http2;

add_header Strict-Transport-Security "max-age=31536000";

location ^~ /.well-known/acme-challenge/ {
    alias /var/www/le/;
    try_files $uri =404;
    auth_basic off;
}

Теперь в конфиг нужных server’ов просто пишем

include lessl.conf;

… делаем service nginx reload и запускаем скрипт. Если всё сделано правильно - скрипт отрапортует “Certificate signed!” столько раз, сколько в нём указано сертификатов. Всё, можно прописывать в server’ах:

ssl_certificate /etc/ssl/le/web.crt;
ssl_certificate_key /etc/ssl/le/web.key;

… и сайт должен заработать по https.

При использовании реверс прокси может возникнуть проблема редиректов с https на http внутри сайта. Решается она очень просто:

proxy_redirect default;
proxy_redirect http:// $scheme://;

Или для haproxy:

http-response replace-value Location http://(.*) https://\1 if { dst_port 443 }

На этом всё.

Комментарии