SSD Nodes Learn Hosting plans →
Руководства Matt ConnorАвтор: Matt Connor

Веб-почта Roundcube для Stalwart: Docker, nginx, защита

Ставим Roundcube 1.7 перед Stalwart: IMAP и SMTP с проверкой TLS, ключ в env-файле, свой поддомен за nginx, русский интерфейс и план обновлений после CVE-2025-49113.

Как добавить Roundcube к Stalwart

Roundcube для Stalwart ставится отдельным контейнером рядом с почтовым сервером. Он читает почту из Stalwart по IMAP на порту 993, отправляет письма через порт 465 и открывается в браузере на своём поддомене, например webmail.example.com. Писем Roundcube не хранит. У него есть только своя небольшая база для адресной книги, подписей, настроек и сессий.

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

Зачем веб-почта, если есть почтовые клиенты

Многие переезжают на свой сервер с Яндекс 360 или Mail.ru. Там почта всегда жила во вкладке браузера, и люди к этому привыкли. Бухгалтеру или родителям проще открыть знакомый адрес в браузере, чем настраивать Thunderbird по инструкции с портами и типами шифрования.

Stalwart в ветке 0.16.x (на ней построено руководство по установке почтового сервера Stalwart на VPS) работает по протоколам IMAP, JMAP и SMTP. Его веб-интерфейс предназначен для администрирования и настроек учётных записей, а не для работы с письмами. Эту роль и берёт на себя Roundcube. Если вы ещё решаете, нужен ли вам свой почтовый сервер вообще, сначала прочитайте разбор стоит ли сегодня держать почту на своём сервере.

Перед началом нужно:

  • работающий Stalwart 0.16.x с действительным сертификатом для mail.example.com;
  • Docker с плагином Compose (если его ещё нет, начните с основ Docker Compose на VPS);
  • nginx, который занимает порты 80 и 443 на этом сервере;
  • DNS-запись A (или AAAA) для webmail.example.com, которая указывает на тот же VPS.

Ниже mail.example.com означает имя вашего Stalwart, а webmail.example.com новое имя для веб-почты. Замените оба имени на свои.

Какую версию Roundcube выбрать: 1.7.x или 1.6.x LTS

На сентябрь 2026 года текущий стабильный выпуск Roundcube 1.7.4. Он вышел 6 сентября 2026 года. Ветка 1.7 появилась 10 мая 2026 года, и с этого момента 1.6.x работает в режиме LTS (long-term support, долгосрочная поддержка). В LTS попадают только исправления безопасности и редкие исправления серьёзных ошибок. Последний выпуск LTS на сентябрь 2026 года 1.6.19. Ветка 1.5 больше не поддерживается. Для новой установки берите 1.7.

Официальный образ roundcube/roundcubemail выходит в вариантах apache, fpm и fpm-alpine, и у каждого есть версия с суффиксом -nonroot. Вариант apache самый простой: один контейнер сразу отдаёт HTTP, и отдельный веб-сервер для PHP не нужен. Тег latest-apache всегда указывает на последний стабильный выпуск. Тег 1.7.x-apache следует за последним патчем ветки. Тег 1.7.4-apache после выхода больше не меняется.

Закрепите точный тег 1.7.4-apache. Тогда обновление происходит в тот момент, когда вы его запускаете, и вы всегда знаете, какой код сейчас работает. Цена такого выбора: за выпусками придётся следить самому. Как это делать, описано в разделе про обновления.

Где Roundcube хранит своё состояние

Чтобы решить, что класть в тома, нужно знать, что делает контейнер при каждом старте. Скрипт запуска образа (docker-entrypoint.sh) выполняет по порядку четыре шага:

  1. Копирует код Roundcube в /var/www/html, если там пусто. Если код там уже есть, обновляет его скриптом bin/installto.sh.
  2. Создаёт config/config.inc.php со случайным ключом des_key, если этого файла ещё нет.
  3. Заново пишет config/config.docker.inc.php из переменных окружения ROUNDCUBEMAIL_* и подключает в его конце все файлы *.php из /var/roundcube/config.
  4. Создаёт или обновляет схему базы данных командой bin/initdb.sh --update.

Из этого следуют четыре решения.

Код не кладём в том. Каталог /var/www/html остаётся внутри контейнера, поэтому код всегда берётся из образа. Обновление сводится к смене тега. Если вынести /var/www/html в том, старый код будет лежать на диске, а обновлять его будет скрипт при старте. Это лишний шаг, который может сломаться.

База данных: SQLite в /var/roundcube/db. Если тип базы не задан, образ сам выбирает SQLite и создаёт файл /var/roundcube/db/sqlite.db. Для семьи или небольшой команды этого достаточно. MySQL или PostgreSQL имеют смысл, когда пользователей много и они работают одновременно.

Что лежит в этой базе. Адресные книги, подписи и отправители (identities), личные настройки, сессии и кэш. Самих писем там нет, они остаются в Stalwart. Потеря базы не уничтожит почту, но пользователи лишатся контактов и подписей. Поэтому каталог с базой входит в резервную копию.

Ключ des_key задаём сами. Этим ключом Roundcube шифрует пароль IMAP пользователя, который хранится в записи сессии. Код и config.inc.php у нас не в томе, значит при каждом пересоздании контейнера скрипт сгенерирует новый случайный ключ. Старые сессии после этого нельзя расшифровать, и после каждого обновления всех пользователей выбросит на страницу входа. Скрипт запуска умеет брать ключ из переменной ROUNDCUBEMAIL_DES_KEY. Эта строка попадает в конфигурацию после сгенерированной, поэтому действует именно она.

Как хранить секреты: ключ в env-файле, а не в compose.yaml

Ключ шифрования является секретом. В compose.yaml ему не место, потому что этот файл часто кладут в git и показывают коллегам. Положите ключ в отдельный файл с правами 600.

sudo mkdir -p /opt/roundcube/config /opt/roundcube/db
cd /opt/roundcube
sudo sh -c 'umask 077; printf "ROUNDCUBEMAIL_DES_KEY=%s\n" "$(openssl rand -base64 18)" > roundcube.env'
sudo ls -l roundcube.env
sudo wc -c roundcube.env

ls -l должен показать права -rw------- и владельца root. wc -c должен показать число 47: 22 символа имени переменной со знаком =, 24 символа ключа и перевод строки. Ключ должен быть ровно из 24 символов, потому что шифр Roundcube по умолчанию требует ключ такой длины. Восемнадцать случайных байт в base64 дают ровно 24 символа без знаков = в конце.

Файл называется roundcube.env, а не .env, и это сделано намеренно. Compose читает .env автоматически и подставляет его значения в сам compose.yaml. Файл из директивы env_file попадает только в окружение контейнера. Разница подробно разобрана в руководстве про env-файлы и секреты в Docker Compose.

Права 600 у root означают ещё одно: команды docker compose придётся запускать через sudo. Compose читает env_file на стороне клиента, и пользователь из группы docker без прав root получит ошибку доступа к этому файлу. Поэтому ниже все команды идут через sudo.

Как подключить Roundcube к IMAP и SMTP Stalwart с проверкой TLS

Создайте файл /opt/roundcube/compose.yaml:

services:
  roundcube:
    image: roundcube/roundcubemail:1.7.4-apache
    restart: unless-stopped
    env_file: roundcube.env
    environment:
      ROUNDCUBEMAIL_DEFAULT_HOST: ssl://mail.example.com
      ROUNDCUBEMAIL_DEFAULT_PORT: "993"
      ROUNDCUBEMAIL_SMTP_SERVER: ssl://mail.example.com
      ROUNDCUBEMAIL_SMTP_PORT: "465"
      ROUNDCUBEMAIL_PLUGINS: archive,zipdownload
      ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE: 20M
    ports:
      - "127.0.0.1:8081:80"
    volumes:
      - ./db:/var/roundcube/db
      - ./config:/var/roundcube/config:ro

Префикс адреса задаёт способ шифрования. ssl:// означает неявный TLS (implicit TLS, шифрование с первого байта соединения). Это порт 993 для IMAP и порт 465 для отправки. tls:// означает STARTTLS: соединение начинается открытым текстом и затем переходит на TLS (transport layer security). Если вы предпочитаете порт 587, напишите ROUNDCUBEMAIL_SMTP_SERVER: tls://mail.example.com и ROUNDCUBEMAIL_SMTP_PORT: "587". Stalwart принимает оба варианта. Неявный TLS на 465 проще, потому что у него нет открытого начала соединения, из которого посредник мог бы вырезать команду STARTTLS.

Roundcube входит в SMTP с теми же логином и паролем, которые пользователь ввёл на странице входа. Так он настроен по умолчанию (smtp_user = '%u' и smtp_pass = '%p'), поэтому отдельные учётные данные для отправки не нужны.

Почему в адресе стоит именно mail.example.com. PHP проверяет сертификат сервера и сверяет имя в нём с именем из адреса подключения. Сертификат Stalwart выписан на mail.example.com. Если указать имя сервиса Docker (ssl://stalwart) или ssl://127.0.0.1, имя не совпадёт, и подключение не пройдёт проверку. В интернете в такой ситуации часто советуют выключить verify_peer в imap_conn_options. Не делайте этого. Без проверки Roundcube отдаст пароль пользователя любому, кто сумеет встать между ним и почтовым сервером.

Контейнер находит mail.example.com через обычный DNS и подключается к публичному IP этого же VPS. Порты 993 и 465 и так открыты для почтовых клиентов, поэтому новые правила файрвола не нужны.

Проверьте сертификат с хоста так же, как его проверит PHP:

openssl s_client -connect mail.example.com:993 -servername mail.example.com -verify_hostname mail.example.com </dev/null 2>/dev/null | grep 'Verify return code'
openssl s_client -connect mail.example.com:465 -servername mail.example.com -verify_hostname mail.example.com </dev/null 2>/dev/null | grep 'Verify return code'

Обе команды должны вывести Verify return code: 0 (ok). Строка Verify return code: 62 (hostname mismatch) означает, что сертификат выписан на другое имя. Тогда Roundcube получит ту же ошибку при каждом входе.

Если пользователи входят по короткому имени (например, anna), а Stalwart ждёт полный адрес, добавьте переменную ROUNDCUBEMAIL_USERNAME_DOMAIN: example.com. Roundcube допишет домен к любому имени без @. Если пользователи и так вводят полный адрес, эта переменная не нужна.

Как включить русский интерфейс Roundcube по умолчанию

Языка нет среди переменных образа. Его задают обычным файлом настроек PHP в каталоге config. Скрипт запуска подключит этот файл последним, поэтому его значения перекрывают всё остальное.

sudo tee /opt/roundcube/config/ru.php > /dev/null <<'EOF'
<?php
$config['language'] = 'ru_RU';
$config['product_name'] = 'Почта example.com';
EOF

language задаёт язык страницы входа и язык по умолчанию для новых пользователей. Без этой строки Roundcube выбирает язык по настройкам браузера, и сотрудник с англоязычной системой получит английский интерфейс. Позже каждый пользователь может сменить язык для себя в настройках интерфейса. product_name меняет название во вкладке браузера и на странице входа.

Запуск и первая проверка

cd /opt/roundcube
sudo docker compose up -d
sudo docker compose ps
sudo docker compose logs --tail=50 roundcube

docker compose ps должен показать контейнер в состоянии Up. В этом образе Roundcube пишет свой журнал в стандартный вывод (log_driver = stdout), поэтому ошибки подключения к IMAP и SMTP вы найдёте в docker compose logs, а не в файлах внутри контейнера.

sudo ss -tlnp | grep 8081
sudo ls -l /opt/roundcube/db
curl -sI http://127.0.0.1:8081/ | head -n 1

ss должен показать адрес 127.0.0.1:8081. Если там 0.0.0.0:8081, в строке ports потерялся префикс 127.0.0.1:. Это важно, потому что Docker публикует порты в обход правил ufw (uncomplicated firewall), и порт без привязки к loopback откроется всему интернету. В каталоге db должен появиться файл sqlite.db. curl должен вернуть строку с кодом 200.

Как опубликовать Roundcube за nginx на своём поддомене

Отдельное имя webmail.example.com лучше, чем путь вроде example.com/mail. Cookies сессии Roundcube тогда не пересекаются с другими приложениями на том же имени. Кроме того, доступ к веб-почте можно ограничить в одном блоке server, не трогая остальные сайты. Как устроены proxy_pass и заголовки X-Forwarded-*, подробно объясняет разбор конфигурации обратного прокси nginx.

Если certbot ещё не установлен, поставьте его вместе с модулем для nginx:

sudo apt update && sudo apt install -y certbot python3-certbot-nginx

Создайте файл /etc/nginx/sites-available/webmail:

server {
    listen 80;
    server_name webmail.example.com;

    client_max_body_size 20m;

    location / {
        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

client_max_body_size совпадает с ROUNDCUBEMAIL_UPLOAD_MAX_FILESIZE. Если nginx пропускает меньше, чем разрешает PHP, крупное вложение остановится на nginx с ответом 413 Request Entity Too Large, и до Roundcube запрос не дойдёт.

Включите сайт и получите сертификат:

sudo ln -s /etc/nginx/sites-available/webmail /etc/nginx/sites-enabled/webmail
sudo nginx -t
sudo systemctl reload nginx
sudo certbot --nginx -d webmail.example.com

nginx -t должен закончиться строкой test is successful. Certbot получит сертификат Let's Encrypt по протоколу ACME (automatic certificate management environment) и сам добавит в этот блок server прослушивание порта 443 с TLS. Откройте https://webmail.example.com и войдите с адресом и паролем пользователя Stalwart. После входа в списке должны быть те же папки, которые почтовый клиент видит по IMAP.

Если Stalwart сам слушает порт 443. Stalwart умеет отдавать HTTPS без nginx, и в некоторых установках он занимает 443. Два процесса не могут слушать один порт. nginx -t при этом проходит, а запуск nginx завершается ошибкой bind() to 0.0.0.0:443 failed (98: Address already in use). Решение: перевести веб-интерфейс Stalwart на внутренний порт и проксировать его через тот же nginx по имени mail.example.com. Если Stalwart сам получает сертификаты через ACME на порту 443, до переноса проверьте, как он будет продлевать сертификат после него.

Почему веб-почту атакуют чаще остальных частей почтового сервера

У веб-почты есть две особенности, которых нет у IMAP и SMTP. Первая: она показывает в браузере HTML из писем, которые прислал кто угодно. Ошибка в фильтре HTML превращается в XSS (cross-site scripting, межсайтовый скриптинг): человек открывает письмо, и чужой JavaScript выполняется в его сессии. Вторая: это большое приложение на PHP с загрузкой файлов и сессиями, доступное по HTTPS с любого адреса. Поэтому в каталоге уязвимостей с подтверждённой эксплуатацией Roundcube встречается не один раз.

Хороший пример: CVE-2025-49113. Это удалённое выполнение кода после входа (post-auth RCE, remote code execution). Параметр _from в адресе запроса не проверялся в program/actions/settings/upload.php, и это приводило к десериализации объектов PHP. Уязвимы версии до 1.5.10 и версии 1.6.x до 1.6.11. Исправления вышли 1 июня 2025 года, оценка CVSS 9.9 из 10, нашёл ошибку исследователь Кирилл Фирсов. Рабочий эксплойт появился в открытом доступе через несколько дней после патча. В феврале 2026 года американское агентство CISA внесло эту уязвимость в каталог KEV (Known Exploited Vulnerabilities, уязвимости, которые уже используют в атаках). Вместе с ней туда попала XSS-уязвимость CVE-2025-68461.

Слова «после входа» успокаивают зря. Атакующему нужен пароль одного любого почтового ящика, а пароли утекают через фишинг и из старых чужих баз. Код атакующего выполнится внутри контейнера Roundcube. Там же лежат база сессий и ключ des_key, а значит, и зашифрованные пароли IMAP всех, кто сейчас вошёл. Один украденный пароль может превратиться в пароли всех пользователей с открытой сессией.

Отсюда два вывода. Обновления безопасности надо ставить в течение нескольких дней после выхода. А если веб-почтой пользуются несколько человек, страницу входа лучше вообще не показывать интернету.

Как обновлять Roundcube: короткий регламент

Ритм выпусков ветки 1.7 хорошо показывает, зачем нужен регламент. После 1.7.0 вышли 1.7.1 (24 мая 2026), 1.7.2 (5 июля), 1.7.3 (9 августа) и 1.7.4 (6 сентября). Все четыре вышли как обновления безопасности, то есть примерно раз в месяц.

  1. Подпишитесь на ленту выпусков Roundcube на GitHub в любой RSS-читалке. Сообщения о безопасности проект публикует в разделе новостей на roundcube.net с заголовком «Security updates».
  2. Когда выходит новый патч ветки 1.7, сделайте резервную копию (команды ниже).
  3. В compose.yaml замените номер в теге 1.7.4-apache на номер нового выпуска.
  4. Скачайте новый образ, пересоздайте контейнер и проверьте результат.
cd /opt/roundcube
sudo docker compose pull roundcube
sudo docker compose up -d roundcube
sudo docker compose images roundcube
sudo docker compose logs --tail=50 roundcube

В выводе docker compose images в колонке TAG должен стоять новый номер. При старте скрипт сам обновит схему базы (bin/initdb.sh --update), поэтому отдельного шага миграции нет. Ключ ROUNDCUBEMAIL_DES_KEY не меняется, поэтому пользователи останутся в своих сессиях.

Переход на следующую ветку делайте отдельно и осторожнее. Сначала прочитайте примечания к выпуску. Например, в 1.7.0 убрали поддержку баз MS SQL Server и Oracle, а драйвер кэша apc заменили на apcu.

Как закрыть вход в Roundcube от всего интернета

Если веб-почтой пользуются несколько человек из одного офиса или одна семья, страница входа не обязана быть доступна всем. Самый простой вариант: список разрешённых адресов в nginx. После certbot ваш блок location / обслуживает HTTPS. Добавьте в его начало строки allow и deny:

    location / {
        allow 198.51.100.0/24;
        allow 10.8.0.0/24;
        deny all;

        proxy_pass http://127.0.0.1:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

Первая строка пропускает сеть офиса, вторая пропускает подсеть VPN. Выполните sudo nginx -t и sudo systemctl reload nginx. С чужого адреса nginx теперь отвечает 403 Forbidden, и запрос вообще не доходит до PHP. Значит, ошибку в Roundcube нельзя использовать снаружи, даже если она срабатывает до входа.

Если адреса пользователей часто меняются, поставьте Roundcube за VPN. WireGuard на том же сервере и allow только для подсети туннеля дают тот же результат, а пользователю нужно лишь включить туннель в приложении. Для тех, кому VPN неудобен, второй барьер даёт HTTP Basic Auth в nginx: директива auth_basic и файл паролей, созданный утилитой htpasswd из пакета apache2-utils. Обновлений это не заменяет, но атакующему придётся узнать ещё один пароль, прежде чем он увидит Roundcube.

Проверьте автоматическую блокировку в Stalwart. Stalwart умеет блокировать IP-адреса после серии неудачных входов. Все подключения Roundcube к IMAP идут с одного адреса, поэтому ошибки с паролем у нескольких разных людей могут заблокировать веб-почту для всех. Если такая блокировка у вас включена, добавьте адрес, с которого подключается Roundcube, в исключения. Это безопасно только тогда, когда страница входа закрыта списком адресов или VPN. Иначе подбор паролей через Roundcube никто не остановит.

Как сделать резервную копию Roundcube

cd /opt/roundcube
sudo docker compose stop roundcube
sudo tar czf /root/roundcube-$(date +%F).tgz -C /opt roundcube
sudo chmod 600 /root/roundcube-$(date +%F).tgz
sudo docker compose start roundcube

Короткая остановка нужна, чтобы SQLite не записывал файл базы в момент копирования. В архив попадают база, compose.yaml, файлы настроек и roundcube.env с ключом. Поэтому храните архив как секрет и не только на этом сервере. Писем в архиве нет: их резервное копирование относится к Stalwart.

SnappyMail как более лёгкая альтернатива

Если Roundcube кажется тяжёлым, посмотрите на SnappyMail. Это форк RainLoop, развитие которого прекратилось. SnappyMail тоже написан на PHP, но не требует базы данных и хранит настройки и данные пользователей в файлах. Интерфейс у него легче и быстрее, а установка проще. Плата за лёгкость: меньше плагинов и меньше людей, которые сопровождают проект. Всё сказанное выше о безопасности относится и к нему. Это то же приложение на PHP, которое показывает в браузере чужой HTML, поэтому SnappyMail тоже стоит прятать за списком адресов или VPN и обновлять сразу после выпусков. Если же вы ещё не выбрали сам почтовый сервер, учтите, что почтовый сервер mailcow на VPS ставится сразу с собственной веб-почтой.

FAQ

Почему Roundcube не пускает пользователя, хотя Thunderbird с тем же паролем входит?

Чаще всего Roundcube не может проверить сертификат Stalwart. Это случается, когда в ROUNDCUBEMAIL_DEFAULT_HOST указано имя, на которое сертификат не выписан, например имя сервиса Docker или 127.0.0.1. Проверьте сертификат командой openssl s_client -connect mail.example.com:993 -servername mail.example.com -verify_hostname mail.example.com и ищите строку Verify return code: 0 (ok). Вторая частая причина: Stalwart ждёт полный адрес, а пользователь вводит короткое имя, и тогда помогает ROUNDCUBEMAIL_USERNAME_DOMAIN. Подробности ошибки Roundcube пишет в sudo docker compose logs roundcube.

Нужна ли Roundcube отдельная база MySQL или PostgreSQL?

Для семьи или небольшой команды нет. Без настроек базы официальный образ использует SQLite в /var/roundcube/db/sqlite.db, и этого достаточно. В базе Roundcube лежат только контакты, подписи, настройки и сессии, а письма остаются в Stalwart. Вынесите каталог базы в том и включите его в резервную копию.

Что выбрать для новой установки: Roundcube 1.7.x или 1.6.x LTS?

Берите 1.7.x. На сентябрь 2026 года текущий выпуск 1.7.4. Ветка 1.6.x с мая 2026 года работает как LTS и получает только исправления безопасности, последний её выпуск 1.6.19. LTS имеет смысл для уже работающих установок, которые вы пока не готовы переводить на новую ветку.

Почему после обновления контейнера Roundcube всех пользователей выбросило на страницу входа?

Скорее всего, ключ des_key сменился. Этим ключом Roundcube шифрует пароль IMAP в записи сессии. Если ключ не задан явно и код не лежит в томе, образ генерирует новый случайный ключ при каждом пересоздании контейнера, и старые сессии нельзя расшифровать. Задайте ключ из 24 символов в переменной ROUNDCUBEMAIL_DES_KEY в env-файле, и следующие обновления пройдут без выхода пользователей.

Можно ли оставить страницу входа Roundcube открытой для всего интернета?

Можно, но тогда обновления надо ставить в течение нескольких дней после выхода. Roundcube уже не раз попадал в каталог CISA KEV, например с уязвимостью CVE-2025-49113, которая позволяла выполнить код на сервере любому, кто знает пароль одного ящика. Если веб-почтой пользуются несколько человек, закройте её списком allow в nginx или пустите только через VPN.