Итак, если вы счастливый владелец nginx, знатный параноик и за каким-то чертом решили поставить wordpress, то… Первое, что пришло в голову — это «надо ограничить сему творению свободу!».
Настройки учетной записи, как и настройки php5-fpm, я опущу, так как у каждого свои тараканы, а кто-то вообще на apache запускает. Но вот общие для WordPress я опишу в этой части. Напишу о том, что сделал, что получилось и почему.
Папки
- wp-admin
- wp-content
- wp-includes
Файлы php
- wp-activate.php
- wp-blog-header.php
- wp-comments-post.php
- wp-config.php
- wp-config-sample.php
- wp-cron.php
- wp-links-opml.php
- wp-load.php
- wp-login.php
- wp-mail.php
- wp-postpass.php (об этом ниже)
- wp-settings.php
- wp-signup.php
- wp-trackback.php
- xmlrpc.php
- xmlrpc.txt (об этом тоже ниже)
Это типичный набор для WordPress 4.0.
Что же нам нужно? Нам нужно ограничить доступ к php файлам и админке, вынести статику, закрыть xmlrpc.
Ограничиваем доступ в админку и к php файлам
В моем варианте wordpress я не сохраняю комментарии пользователей и не использую xmlrpc. Как безопасно дать доступ на комментарии, как и ряд других насущных вопросов по nginx и wordpress будет расмотрен во второй части этой статьи, которая, естественно, будет создана при наличии оных. Так как тут нет apache, то файл .htaccess бесполезен.
Следовательно, закрываем указанные выше свистоперделки:
location ~* ^/(.htaccess|xmlrpc.php)$ { return 404; }
После этого при запросах xmlrpc.php и .htaccess у нас будет 404 ошибка. Хотя можно выдать и 403 и 200 «trololo», но это уже дело вкуса.
Далее ограничиваем доступ к оставшимся. Под ограничением я имею в виду запрос авторизации, а именно auth_basic.
location ~* ^/wp-admin/(.*(?<!(.php)))$ { auth_basic "protected by password"; auth_basic_user_file users/somefile; root /path/to/site/root; #еще параметры }
*данный код заставит nginx запрашивать авторизацию при запросе статики из /wp-admin/, статику выдает nginx.
Далее ограничиваем доступ к файлам:
location ~* (/wp-admin/|/wp-cron.php|/wp-config.php|/wp-config-sample.php|/wp-mail.php|/wp-settings.php|/wp-signup.php|/wp-trackback.php|/wp-activate.php|/wp-links-opml.php|/wp-load.php|/wp-comments-post.php|/wp-blog-header.php|/wp-login.php|/wp-includes/.*?.php|/wp-content/.*?.php) { auth_basic "protected by password"; auth_basic_user_file users/somefile; root /path/to/site/root; #еще параметры }
Запись вида /wp-includes/.*?.php включает в себя все php файлы в wp-includes и ниже.
Готово, доступ мы закрыли. Теперь будем выборочно включать нужные нам элементы для паблик версии, а это дальше по тексту.
Включаем защищенные записи в нашем защищенном WordPress
Так как мы закрыли wp-login.php авторизацией, то, написав защищенный пост и скинув ссылку и пароль (от поста) нужному пользователю, пользователь… испугается неведанного окна. Так как пароль передается файлу wp-login.php как post запрос с GET параметрами ?action=postpass.
nginx накладывает ряд ограничений:
- в location от nginx мы не можем описать параметры запроса;
- в if выражении нельзя использовать auth_basic;
- игра с переменной в конфиге, которое передается 1 в случае удачной авторизации ничего не принесет, так как переменная живет только в текущем запросе.
Что же делать?
Решение есть! Создать символическую ссылку на wp-login.php в той же папке. У меня это wp-postpass.php. Символическая ссылка нужна для того, что если мы обновим wordpress, то wp-login.php тоже обновится и обновится файл по ссылке… Вот за что я люблю linux.
Следом в конфиге nginx прописываем:
location ~* (/wp-postpass.php) { if ($args ~ "^action=postpass$") { set $wppostpass 1; } if ($wppostpass ~ 0) { return 403; } #еще параметры }
В таком случае при запросе /wp-postpass.php?action=postpass переменная wppostpass примет значение 1, и location отработает до конца. В случае голого запроса wp-postpass.php или с другими параметрами (как видит тут проверяется от начала ^ до конца$ строки) будет ошибка 403, что означает доступ закрыт.
Для работы такой схемы нам нужен ngx_http_substitutions_filter_module . В конфиге следует прописать
subs_filter 'https://example.com/wp-login.php?action=postpass' 'https://example.com/wp-postpass.php?action=postpass' gi;
Тогда nginx автоматом изменит ссылку wp-login.php?action-postpass на wp-postpass.php?action-postpass, и пользователь сможет авторизоватся паролем для просмотра защищенной записи.
Выносим статику на отдельный сервер и подключаем CDN
В нагрузке js, css и мелкие gif'ки роли не играют, так как при наличии памяти nginx хранит их в своем кеше, а при наличии достаточного количества памяти всю статику сайта можно вынести на tmpfs раздел (3.8 Гб чтение-запись и 745к iops'ов к примеру).
Но в случае одного сервера кто-то получит файл раньше, кто-то позже, и если у нас много клиентов, то при раздаче 1000 файлов по 1Мб канал нехило просядет, если не вводить rate.
Вот для этих случаев и придуманы кеширующие CDN провайдеры. Для примера — cloudflare.
Принцип работа замечательно проиллюстрирован на их картинке:
Без CDN все запросы идут на конечный сайт, а с CDN запросы идут на CDN провайдера, который выступает как промежуточное звено. И в этом случае если 1000 пользователей запросят файл размеров 1 Мб, то этот файл будет запрошен CDN провайдером 1 раз для своего кеша, и затем раздан той 1000 пользователей. Варианты DDoS'а в стиле à la google docs, когда запросили big_photo.jpg?ver=1, затем big_photo.jpg?ver=2, и т.д. не сработает, если выбран режим умеренного кеширования (у cloudflare он есть) и кеширование только статики, то при запросе big_photo.jpg, big_photo.jpg?ver=1 или big_photo.jpg?ver=123 с сервера запрашивается big_photo.jpg и затем раздается он и только он, даже если клиент запрашивает файл с аргументами (они просто игнорируются). Это решает проблему ддоса cdn провайдером, который по сути должен и от ддоса защищать.
Я не сильно лазил, но нашел, что дефолтовая статика хранится в:
- /wp-content/uploads/
- /wp-content/themes/
- /wp-content/plugins/
- /wp-includes/js/
- /wp-includes/css/
- /wp-includes/certificates/
- /wp-includes/fonts/
- /wp-includes/images/
Соответственно для них мы и сделаем новые правила в location и будем использовать nginx c ngx_http_substitutions_filter_module.
Ставить этот модуль не обязательно, можно обойтись одними лишь rewrite'ами, но сам по себе он полезный и через него можно улучшить тот или иной вывод с backend'а.
В конфиг добавляем:
subs_filter_types text/html; subs_filter_types text/xml;
Чтобы фильтровать вывод html и xml документов.
Затем:
subs_filter 'https://example.com/wp-content/uploads/' 'https://static.example.com/uploads/' gi; subs_filter 'https://example.com/wp-content/themes/' 'https://static.example.com/themes/' gi; subs_filter 'https://example.com/wp-content/plugins/' 'https://static.example.com/plugins/' gi; subs_filter 'https://example.com/wp-includes/js/' 'https://static.example.com/js/' gi; subs_filter 'https://example.com/wp-includes/css/' 'https://static.example.com/css/' gi; subs_filter 'https://example.com/wp-includes/certificates/' 'https://static.example.com/certificates/' gi; subs_filter 'https://example.com/wp-includes/fonts/' 'https://static.example.com/fonts/' gi; subs_filter 'https://example.com/wp-includes/images/' 'https://static.example.com/images/' gi;
Тем самым ссылки в html и xml будут переписаны. Теперь осталось сделать так, чтобы конечный пользователь, зная оригинальную ссылку, не абузил сервер, а был направлен на CDN.
location ~* ^/wp-content/themes/(.*(?<!(.php)))$ { rewrite ^/wp-content/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-content/plugins/(.*(?<!(.php)))$ { rewrite ^/wp-content/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-content/uploads/(.*(?<!(.php)))$ { rewrite ^/wp-content/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-includes/js/(.*(?<!(.php)))$ { rewrite ^/wp-includes/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-includes/css/(.*(?<!(.php)))$ { rewrite ^/wp-includes/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-includes/certificates/(.*(?<!(.php)))$ { rewrite ^/wp-includes/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-includes/fonts/(.*(?<!(.php)))$ { rewrite ^/wp-includes/(.*)$ https://static.example.com/$1 permanent; } location ~* ^/wp-includes/images/(.*(?<!(.php)))$ { rewrite ^/wp-includes/(.*)$ https://static.example.com/$1 permanent; }
В итоге, при запросе любого php файла ничего не будет. А при запросе статики (все что не php в случае с WP, что логично) пользователь будет перенаправлен на сервер статики.
Настройка профиля nginx для сервера статики будет рассмотрена ниже.
Затем остается лишь сделать аккаунт на cloudflare (или любом) другой cdn провайдере, который вы собираетесь использовать, прописать их DNS у себя и включить кеширование домена static.example.com, без кеширование example.com, где работает wordpress.
Настройка сервера статики
Так как мы перенесли отдачу на сервер статики, то необходимо его правильно настроить.
allow 127.0.0.1; allow IPv4 сервера; allow IPv6 сервера; allow IP/подсеть серверов CDN; ... allow IP/подсеть серверов CDN; deny all;
Требуется разрешить доступ локалхосту, доступ самому серверу с внешнего IP (например какой скрипт) и серверам CDN провайдера. Например, подсети CloudFlare можно найти вот по этой ссылке. И, конечно же, закрыть доступ всем остальным. Так как если CDN внезапно решит пустить траффик на прямую… оставить свободный канал.
Так же надо сделать dummy директорию как root для всего сервера статики.
root /path/to/site/dummy;
Чтобы запросы, которые пришли на сервер статики на location / или =/ и которые не соответствуют прописанным там location пошли в ту самую dummy директорию. Эта директория прописывается внутри server{}.
Далее location приветствие:
location =/ { default_type text/html; return 200 "c'est static, c'est simple :P"; }
Это текст, который увидит пользователь, запросивший корень. Писать можно что угодно, главное при использовании внутри " экранировать кавычки как ".
Затем следует прописать location'ы на статику:
location ~* ^/uploads/.*(?<!(.php))$ { root /path/to/site/root/wp-content; autoindex off; index index.html; } location ~* ^/themes/.*(?<!(.php))$ { root /path/to/site/root/wp-content; autoindex off; index index.html; } location ~* ^/plugins/.*(?<!(.php))$ { root /path/to/site/root/wp-content; autoindex off; index index.html; } location ~* ^/js/.*(?<!(.php))$ { root /path/to/site/root/wp-includes; autoindex off; index index.html; } location ~* ^/css/.*(?<!(.php))$ { root /path/to/site/root/wp-includes; autoindex off; index index.html; } location ~* ^/certificates/.*(?<!(.php))$ { root /path/to/site/root/wp-includes; autoindex off; index index.html; } location ~* ^/fonts/.*(?<!(.php))$ { root /path/to/site/root/wp-includes; autoindex off; index index.html; } location ~* ^/images/.*(?<!(.php))$ { root /path/to/site/root/wp-includes; autoindex off; index index.html; }
При запросе static.example.com/images/pic.png сервер отдаст файл из директории /wp-includes/images/ файл pic.png, но при запросе static.example.com/images/pic.php location прощелкает и в итоге пользователю отдадут файл из dummy/images/pic.php, которого нет и как итог ошибка 404.
Еще надо добавить рейты на скорость.
limit_rate_after 16m; limit_rate 2m;
После 16 мегабайт скорость уменьшается до 2 Мб в секунду на поток. Это чтобы CDN при кешировании огромного файла не забил весь канал.
В случае с cloudflare максимальный размер файла (на момент написания этого материала) составляет 512 мегабайт, а поддерживаемые форматы на бесплатном тарифном плане включают: css, js, jpg, jpeg, gif, ico, png, bmp, pict, csv, doc, pdf, pls, ppt, tif, tiff, eps, ejs, swf, midi, mid, ttf, eot, woff, otf, svg, svgz, webp, docx, xlsx, xls, pptx, ps, class, jar.
Фильтрация запросов
Тут сразу два случая:
- При загрузке медиафайлов они получают ссылку вида example.com/?attachment_id=XX, где XX это id странички для этого медиафайла. Соотвественно перебирая 1, 2, 3… пользователь может выкачать весь контент, причем и ту его часть, которая ему не предназначается;
- php так и пестрит болячками. Наверное, тут не столько архитектура языка, сколько скилы программистов и настройки среды, в которое сие творение вертится. Но раз поставили wordpress, то будем готовится к будущим багам.
Для этого пропишем в server {} нашего конфига для nginx код:
if ($args ~* "(attachment_id|eval|duplicate|base64|substring|preg_replace|create_function)") { return 403; }
Тогда если в аргументах запроса встретятся attachment_id, eval, duplicate, base64, substring, preg_replace, create_function nginx вернет ошибку 403, причем запрос не будет передан на динамику для исполнения потенциальной уязвимости.
Плюшки через subs_filter от nginx
Предназначение этого модуля было рассмотрено здесь.
Задача: wordpress по умолчанию открывает ссылки на медиафайл в текущем окне. А нужно, чтобы в новом.
Решение: добавить небольшой код в когфиг nginx.
subs_filter '<a href='https://static.example.com/uploads/(.*?)'>' '<a href='https://static.example.com/uploads/$1' target='_blank'>' gi; subs_filter '<a href="https://static.example.com/uploads/(.*?)">' '<a href="https://static.example.com/uploads/$1" target="_blank">' gi;
После этого к ссылкам на медиафайлы средствами frontend'a будет добавлен target="_blank".
Задача: повсюду xmlrpc.php ссылки… надо убрать.
Решение: добавить небольшой код в когфиг nginx.
subs_filter 'https://example.com/xmlrpc.php' 'https://example.com/xmlrpc.txt' gi;
Ну а в xmlrpc.txt можно засунуть пасхалочку.
Послесловие
- example.com как и static.example.com замените на ваши сервера. Примеры будут работать не только с php5-fpm, но и с apache;
- В статье рассмотрено только кеширование статики в CDN, без кеширование динамики. Причина очень простая: на бесплатных тарифах время обновления кеша составляет 30 минут. То есть ваша страница будет в кеше полчаса. Хотя теперь можно переключатся в development mode каждый раз при обновлении сайта (вместо выборочного сброса кеша каждый раз);
- В location указаны основные моменты. Я ориентируюсь на то, что читатель не ставит первый раз в жизни nginx и wordpress. Воспринимайте статью как hint к действиям и не более;
- Описания регулярок можно найти тут и прочитать вот эту статью. Я использую negative look behind конструкцию при раздаче статики, то есть регулярка берет значение location, смотрит с конца в начало и если .php не обнаружено, то файл отдается. Если же в запросе обнаружен php, то location неверный, а так как все location исключают php, то будет выбрат root для dummy, в котором php файла нету (просто пустая папка) и который вернет 404;
- CloudFlare.com замечательный CDN провайдер и уже на бесплатном плане ты получаешь нехилый функционал, которого за глаза хватает почти для любого проекта.
Удачи вам!
Автор: nikitasius