Новый блог
Содержание
Сегодня я решил снова завести себе блог. В качестве платформы был взят org-page — статический генератор, основанный на Org mode.
Здесь я опишу процесс создания блога с помощью org-page (который был не совсем радужным) и проблемы, с которыми я столкнулся.
Первый блин
Сначала я установил org-page с помощью use-package:
(use-package org-page
:ensure t
:config
(setf op/site-preview-directory "/tmp/org-page-preview"
op/repository-directory "~/src/maximov.space"))
Следующий шаг — создание нового репозитория, в котором будет храниться код блога, с помощью
команды op/new-repository
. Тут меня поджидала неприятность: команда завершилась с ошибкой,
выдав сообщение о том, что не получилось создать git-репозиторий. Однако быстрая проверка
показала, что нужная директория всё-таки появилась, и в ней был инициализирован пустой репозиторий.
Решив выяснить, в чем же дело, я решил залезть в исходники. Как оказалось, виновата была функция
op/git-init-repo
:
(defun op/git-init-repo (repo-dir)
(unless (file-directory-p repo-dir)
(mkdir repo-dir t))
(unless (string-prefix-p "Initialized empty Git repository"
(op/shell-command repo-dir
"git init" nil))
(error "Fatal: Failed to initialize new git repository '%s'."
repo-dir)))
Проблема оказалась в том, что в локали ru_RU.UTF-8
сообщения git выдавал на русском языке.
Т.е. там, где функция op/git-init-repo
ожидала встретить строку Initialized empty Git repository
,
она получала Инициализирован пустой репозиторий Git
.
Я открыл баг в репозитории org-page (EDIT закрыт).
Исправляем ошибки
Чтобы не ждать, пока баг исправят, я решил сделать это сам, и переписал функции, работающие с git, с
помощью библиотеки git.el, также сделал PR (EDIT принят) в репозиторий org-page.
Вышеупомянутая функция op/git-init-repo
стала выглядеть так:
(defun op/git-init-repo (repo-dir)
(unless (file-directory-p repo-dir)
(mkdir repo-dir t))
(git-init repo-dir))
Весь результат можно увидеть здесь.
Настройка блога
Теперь, когда баг исправлен, можно настроить переменные, используемые при
генерации блога. Настройка выполняется в :config
-секции пакета use-package:
(use-package org-page
:load-path "lib/org-page"
:config
(setf op/site-preview-directory "/tmp/org-page-preview"
op/repository-directory "~/src/maximov.space"
op/site-domain "https://maximov.space"
op/site-main-title "Untitled"
op/site-sub-title "Emacs, Programming, an Anything"
op/personal-disqus-shortname "smaximov"
op/personal-github-link "https://github.com/smaximov"
op/personal-google-analytics-id "UA-74709646-1"))
Настройка VPS
Я решил хостить блог на VPS с CentOS 7.
Для начала нам нужен пользователь, под которым мы будем работать.
Создание нового пользователя
Добавим пользователя:
$ useradd nameless
$ passwd nameless
Создадим группу sudoers
, добавляем туда нашего пользователя:
$ groupadd -r sudoers
$ usermod -aG sudoers nameless
Чтобы дать возможность sudoers
использовать sudo
, воспольуемся EDITOR=emacs visudo
.
Завершим SSH-сессию и импортируем ssh-ключ:
[desktop] $ ssh-copy-id nameless@maximov.space
Залогинимся на maximov.space
снова, теперь ssh-клиент должен использовать ключ для логина.
Изменим параметр PasswordAuthentication
на no
в файле /etc/ssh/sshd_config
, чтобы запретить вход по паролю.
После этого нужно перезапустить sshd
:
$ sudo systemctl restart sshd
Теперь можно работать под новым пользователем.
Установка пакетов
Нам понадобятся следующие пакеты:
git
для работы с git-репозиториями;nginx
в качестве сервера нашего блога;letsencrypt
для генерации SSL-сертификатов.
Добавим репозиторий epel
и установим нужные пакеты:
$ sudo yum install epel-release
$ sudo yum update
$ sudo yum install nginx git letsencrypt
Настройка деплоя
Для начала создадим директории, которые нам понадобятся в дальнейшем:
$ sudo install -o nameless -g nameless -m 755 \
-d /var/www/maximov.space
$ sudo install -o nameless -g nameless -m 755 \
-d /var/repos/maximov.space.git
$ sudo install -o nameless -g nameless -m 755 \
-d /var/www/common/letsencrypt
В директории /var/www/maximov.space
будет храниться блог, в /var/repos/maximov.space.git
— git-репозиторий;
назначение директории /var/www/common/letsencrypt
будет рассмотрено в разделе Сертификаты.
Создадим в директории /var/repos/maximov.space.git
пустой репозиторий:
$ git init --bare /var/repos/maximov.space.git
Мы создаём bare
(«голый» или «пустой») репозиторий, он не содержит рабочего дерева, в нём хранятся только
служебные файлы (содержимое .git
). Это сделано для того, чтобы можно было пушить в него через ssh,
т.к. обновление текущей ветки для не-bare
репозиториев запрещено.
Так как bare
репозиторий не хранит рабочего дерева, нужно определить hook, который будет выполняться
после каждого пуша. Создадим в директории hooks
файл post-receive
:
#!/bin/sh
git --work-tree=/var/www/maximov.space \
--git-dir=/var/repos/maximov.space.git \
checkout -f
Сделаем его исполняемым:
chmod +x /var/repos/maximov.space.git/hooks/post-receive
Данный хук будет разворачивать рабочее дерево в /var/www/maximov.space
каждый раз, когда был выполнен push.
Укажем этот репозиторий в качестве remote
для локального репозитория:
$ git remote add maximov.space \
ssh://maximov.space/var/repos/maximov.space.git
Деплой осуществляется командой
$ git push maximov.space master
Также будем бекапить ветку source в Gitlab:
$ git remote add origin \
git@gitlab.com:smaximov/maximov.space.git
SELinux
Для того, чтобы веб-сервер мог отдавать статические файлы (точнее, чтобы SELinux разрешил серверу
отдавать эти файлы), необходимо, чтобы они были помечены типом SELinux httpd_sys_content_t
. Файлы,
помеченные этим типом, доступны в режиме чтения для веб-сервера и скриптов, которые он запускает.
Выполнив semanage fcontext -l | grep httpd_sys_content_t
, можно убедиться, что по умолчанию
данный тип применяется для всех файлов, которые соответствуют регулярному выражению /var/www(/.*)?
.
Чтобы восстановить контекст SELinux для директории, которая будет раздаваться веб-сервером, выполним команду:
$ restorecon -R /var/www/
Настройка Nginx
Для начала запустим Nginx, чтобы убедиться, что он работает:
$ sudo systemctl start nginx
Если всё прошло успешно, то по адресу http://maximov.space должна быть дефолтная страница Nginx:
Теперь приступим к генерации SSL-сертификата для наших доменов.
Сертификаты
Воспользуемся клиентом letsencrypt для генерации SSL-сертификата для доменов
maximov.space
и www.maximov.space
. Перед тем, как сгенерировать сертификат для домена,
letsencrypt должен удостовериться, что домен принадлежит именно нам.
Такая проверка осуществляется с помощью одного из поддерживаемых плагинов. Один из самых простых — это плагин standalone, который запускает локальный веб-сервер для коммуникации с проверяющим сервером Let's Encrypt. Сгенерировать сертификат для наших доменов с использованием этого плагина можно было бы так:
$ sudo letsencrypt certonly --standalone \
--email s.b.maximov@gmail.com --agree-tos \
--domains maximov.space,www.maximov.space
Сгенерированные сертификаты сохранены в директории /etc/letsencrypt/live/maximov.space/
вместе с
параметрами генерации, которые в дальнейшем будут использоваться для обновления сертификата.
Первоначально я и воспользовался данным методом, однако, несмотря на всю его простоту, он имеет серьёзный недостаток: для работы плагина standalone необходимо останавливать уже запущенные веб-серверы, такие как nginx, что, в свою очередь, создаст проблемы при обновлении сертификата. Поэтому вместо этого мы воспользуемся плагином webroot.
Этот плагин требует наличие существующего веб-сервера, способного отдавать контент из директории web root,
которая должна быть доступна для модификации пользователю, запускающему letsencrypt. Ранее мы создали директорию
/var/www/common/letsencrypt
(${webroot-path}
), которая и представляют web root для нашего сервера.
Плагин работает путём создания временного файла в ${webroot-path}/.well-known/acme-challenge
. Затем
удостоверяющий сервер Let's Encrypt делает HTTP-запрос на получение этого файла, который проверяет,
что DNS-запись для наших доменов резолвится на запущенный веб-сервер. При этом в логах можно увидеть
примерно такой запрос:
"GET /.well-known/acme-challenge/${some-random-string} HTTP/1.1"
Удалим дефолтный сервер из /etc/nginx/nginx.conf
, создадим /etc/nginx/conf.d/maximov.space.conf
.
server {
listen 80;
server_name www.maximov.space maximov.space;
location /.well-known/acme-challenge/ {
default_type "text/plain";
root /var/www/common/letsencrypt;
}
location / {
return 301 https://maximov.space$request_uri;
}
}
Данный сервер обрабатывает HTTP-запросы, отдавая по путям, начинающимся с /.well/known/acme-challenge/
,
файлы из /var/www/common/letsencrypt
(${webroot-path}
). Для всех остальных путей производится перенаправление
на https://maximov.space$request_uri
.
Перезагрузим конфигурацию nginx, чтобы внесённые изменения вступили в силу ($ sudo systemctl reload nginx
).
Теперь создадим скрипт, который будет использовать этот плагин. Для удобства мы воспользуемся файлом конфигурации
вместо опций командной строки. Создадим /etc/letsencrypt/maximov.space.ini
:
rsa-key-size=4096
server=https://acme-v01.api.letsencrypt.org/directory
email=s.b.maximov@gmail.com
domains=maximov.space,www.maximov.space
agree-tos=True
force-renew=True
Рассмотрим использованные опции:
- rsa-key-size
- размер ключа.
- server
- адрес удостоверяющего сервера Let's Encrypt. Значение по умолчанию —
https://acme-v01.api.letsencrypt.org/directory
. Для тестирования этот параметр можно изменить наhttps://acme-staging.api.letsencrypt.org/directory
либо указать в качестве альтернативыstaging=True
, также при этом может понадобиться указать параметрbreak-my-certs=True
, который позволит заменить валидный сертификат тестовым. - почтовый адрес, используемый при регистрации.
- domains
- список доменов, на которые распространяется сертификат.
- agree-tos
- согласиться с пользовательским соглашением.
- force-renew
- обновить сертификат, даже если это не требуется.
Создадим /etc/letsencrypt/create-renew-certificate.sh
и сделаем его исполняемым:
#!/bin/bash
set -e
WEBROOT=/var/www/common/letsencrypt
/bin/letsencrypt certonly --webroot --webroot-path=${WEBROOT} "$@"
systemctl reload nginx
Теперь мы можем сгенерировать сертификат командой
$ /etc/letsencrypt/create-renew-certificate.sh \
--config /etc/letsencrypt/maximov.space.ini
Вместо этого создадим отдельный unit /etc/systemd/system/cert-renew.service
, который нам ещё
пригодится в дальнейшем:
[Unit]
Description=Renew SSL certificates
[Service]
Type=oneshot
ExecStart=/etc/letsencrypt/create-renew-certificate.sh \
--config /etc/letsencrypt/maximov.space.ini
Запустим этот сервис, чтобы сгенерировать сертификат:
$ sudo systemctl start cert-renew
При этом в директории /etc/letsencrypt/live/maximov.space/
появятся следующие файлы:
- privkey.pem
- приватный ключ для сертификата. КО подсказывает, что его нужно хранить в секрете.
- cert.pem
- сертификат нашего сервера.
- chain.pem
- цепочка всех сертификатов, начиная с корневого сертификата, которая необходима браузеру для того, чтобы установить подлинность сервера, исключая cert.pem.
- fullchain.pem
- конкатенация chain.pem и cert.pem.
Время жизни сертификата — три месяца, поэтому необходимо настроить автоматическое обновление сертификата.
letsencrypt запоминает параметры создания последнего сертификата, поэтому для обновления достаточно
запустить letsencrypt без параметров или с последними использованными параметрами.
Воспользуемся созданным ранее сервисом /cert-renew.service
, напишем таймер
/etc/systemd/system/cert-renew.timer
, который будет запускать обновление сертификата каждый месяц:
[Unit]
Description=Run cert-renew.service every month
[Timer]
OnCalendar=monthly
Persistent=true
[Install]
WantedBy=timers.target
Запустим таймер и добавим его в автозагрузку:
$ sudo systemctl start cert-renew.timer
$ sudo systemctl enable cert-renew.timer
Теперь можно настроить HTTPS-сервер, который будет отдавать блог.
Сервер
Настройки SSL — сертификат, разрешённые протоколы и шифры, параметры сессии:
ssl_certificate /etc/letsencrypt/live/maximov.space/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/maximov.space/privkey.pem;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
Помимо уже настроенного перенаправления с HTTP на HTTPS сделаем перенаправление с www.maximov.space на maximov.space для SSL:
server {
listen 443 ssl;
server_name www.maximov.space;
return 301 https://maximov.space$request_uri;
}
Секция, которая будет обрабатывать запросы к блогу:
server {
listen 443 ssl;
server_name maximov.space;
root /var/www/maximov.space;
keepalive_timeout 70;
index index.html;
autoindex off;
location / {
try_files $uri $uri/ =404;
}
location ~ /\. {
access_log off;
log_not_found off;
deny all;
}
location ~ ~$ {
access_log off;
log_not_found off;
deny all;
}
}
Перезагрузим конфигурацию nginx, после этого всё должно работать, осталось только добавить unit Nginx в автозагрузку:
$ sudo systemctl enable nginx