HestiaCP

Автоматический вход администратора в Roundcube из HestiaCP

В HestiaCP удобно управлять почтовыми доменами и ящиками, но штатной кнопки “войти в Roundcube под этим ящиком” в панели нет. Официально Hestia позволяет управлять почтовыми доменами и аккаунтами через раздел Mail, а webmail включается для домена отдельно. Также в CLI Hestia есть команда v-add-sys-roundcube для установки Roundcube и v-add-mail-domain-webmail для включения webmail-клиента на почтовом домене.

Разработчики HestiaCP обсуждали автологин в Roundcube и указывали, что Roundcube в такой связке фактически является прослойкой к Dovecot, то есть авторизация всё равно идёт через IMAP-сервер. Штатно Hestia не реализует безопасный автологин в Roundcube.

Тем не менее, сделать админский вход можно правильно: не передавать пароль ящика в ссылке, не менять пароль клиента, не хранить пароли всех ящиков, а использовать штатный механизм Dovecot Master User и одноразовый токен.

Что мы хотим получить

В панели HestiaCP у администратора рядом с каждым почтовым ящиком появляется иконка:

Войти в Roundcube

Админ нажимает иконку, открывается Roundcube, и админ сразу попадает в нужный ящик:

info@example.com

При этом:

пароль ящика не передаётся в URL;
пароль ящика не сохраняется в Hestia;
пароль клиента не меняется;
токен одноразовый;
токен живёт 30–60 секунд;
каждый вход логируется.

Общая схема работы

HestiaCP admin
    ↓
кнопка у почтового ящика
    ↓
создаётся одноразовый токен
    ↓
браузер открывает Roundcube:
https://webmail.example.com/?_hestia_sso=TOKEN
    ↓
плагин Roundcube проверяет токен
    ↓
Roundcube входит в IMAP как:
info@example.com*hestia-master
    ↓
Dovecot пускает администратора в ящик info@example.com

Dovecot официально поддерживает master users: они настраиваются отдельной passdb с параметром master=yes. Такой master-пользователь не обязан существовать как обычный почтовый пользователь; он используется для входа в чужие ящики через разделитель, например user@example.com*masteruser.

Roundcube поддерживает расширение через плагины и хуки. Для автологина обычно используют hook authenticate; в официальном репозитории Roundcube есть пример плагина autologon, а в документации хуков описаны механизмы подключения к IMAP перед авторизацией.

Важное предупреждение

Эта функция должна быть только для администратора сервера. Это не обычный SSO для клиентов.

Master-пароль Dovecot даёт доступ ко всем почтовым ящикам. Поэтому обязательно:

включить 2FA для admin в Hestia;
использовать HTTPS;
делать одноразовые токены;
ограничить срок токена до 30–60 секунд;
логировать все входы;
не показывать кнопку обычным пользователям;
не передавать пароль ящика в URL;
не вставлять пароль в HTML-форму Roundcube.

Hestia в своих рекомендациях отдельно подчёркивает важность 2FA для admin-пользователя, потому что этот пользователь имеет полный контроль над сервером.

Условия перед началом

Инструкция рассчитана на сервер с HestiaCP, где уже установлены:

Hestia Control Panel;
Dovecot;
Exim;
Roundcube;
Nginx/Apache;
PHP.

Команды выполнять от root.

Проверим базовые вещи:

systemctl status dovecot --no-pager
systemctl status exim4 --no-pager
systemctl status nginx --no-pager

Если Roundcube ещё не установлен, можно установить его штатной командой Hestia:

v-add-sys-roundcube

Эта команда присутствует в официальной CLI-документации HestiaCP как команда установки Roundcube webmail client.

Для конкретного почтового домена можно включить Roundcube так:

v-add-mail-domain-webmail admin example.com roundcube

Где:

admin       — пользователь Hestia;
example.com — почтовый домен;
roundcube   — webmail-клиент.

Шаг 1. Проверяем обычную почту

Сначала убедитесь, что обычный вход в Roundcube работает.

Создадим тестовый ящик, если его ещё нет:

v-add-mail-domain admin example.com yes no yes
v-add-mail-account admin example.com info 'СЛОЖНЫЙ_ПАРОЛЬ'

Теперь откройте webmail:

https://webmail.example.com

И попробуйте войти:

Логин: info@example.com
Пароль: пароль ящика

Если обычный вход не работает, автологин настраивать рано. Сначала нужно исправить обычную связку Roundcube → Dovecot.

Проверить IMAP вручную можно так:

openssl s_client -connect 127.0.0.1:993 -crlf

Или через doveadm:

doveadm auth test info@example.com 'ПАРОЛЬ_ЯЩИКА'

Ожидаемый результат:

passdb: info@example.com auth succeeded

Шаг 2. Создаём Dovecot master user

Создадим отдельный master-пароль:

MASTER_PASS="$(openssl rand -base64 48)"
mkdir -p /root/hestia-roundcube-sso
echo "$MASTER_PASS" > /root/hestia-roundcube-sso/master.pass
chmod 600 /root/hestia-roundcube-sso/master.pass

Создаём хеш пароля для Dovecot:

HASH="$(doveadm pw -s SHA512-CRYPT -p "$MASTER_PASS")"

Создаём файл master-пользователей:

cat > /etc/dovecot/passwd.masterusers <<EOF
hestia-master:$HASH
EOF

chown root:dovecot /etc/dovecot/passwd.masterusers
chmod 640 /etc/dovecot/passwd.masterusers

Теперь добавляем отдельный конфиг Dovecot:

cat > /etc/dovecot/conf.d/90-hestia-master-user.conf <<'EOF'
auth_master_user_separator = *

passdb {
  driver = passwd-file
  args = /etc/dovecot/passwd.masterusers
  master = yes
  result_success = continue
}
EOF

Проверяем конфигурацию:

doveconf -n > /tmp/doveconf-check.txt

Если команда прошла без ошибок, перезапускаем Dovecot:

systemctl restart dovecot
systemctl status dovecot --no-pager

Проверяем вход через master user:

MASTER_PASS="$(cat /root/hestia-roundcube-sso/master.pass)"
doveadm auth test 'info@example.com*hestia-master' "$MASTER_PASS"

Ожидаемый результат:

passdb: info@example.com*hestia-master auth succeeded

Если Dovecot ругается на символ *, можно использовать другой разделитель, например +:

auth_master_user_separator = +

Тогда логин будет таким:

info@example.com+hestia-master

Но если * работает, лучше оставить его: он наглядно отделяет реальный ящик от master-пользователя.

Шаг 3. Делаем место для одноразовых токенов

Создадим каталог, где Hestia будет создавать токены, а Roundcube будет их читать:

mkdir -p /var/lib/hestia-roundcube-sso/tokens
mkdir -p /var/log/hestia-roundcube-sso

touch /var/log/hestia-roundcube-sso/access.log
touch /var/log/hestia-roundcube-sso/error.log

Права зависят от того, под каким пользователем у вас работает webmail и веб-интерфейс Hestia.

Сначала посмотрим процессы:

ps aux | grep -E 'roundcube|php-fpm|hestia|nginx|apache' | grep -v grep

Чаще всего Roundcube работает от www-data. Для Hestia пользователь может отличаться. Надёжный вариант — использовать ACL.

Установим ACL:

apt update
apt install -y acl

Дадим доступ Roundcube-пользователю. Если у вас Roundcube работает от www-data:

chown -R root:www-data /var/lib/hestia-roundcube-sso
chmod 770 /var/lib/hestia-roundcube-sso
chmod 770 /var/lib/hestia-roundcube-sso/tokens

setfacl -m u:www-data:rwx /var/lib/hestia-roundcube-sso/tokens
setfacl -d -m u:www-data:rwx /var/lib/hestia-roundcube-sso/tokens

chown -R root:www-data /var/log/hestia-roundcube-sso
chmod 770 /var/log/hestia-roundcube-sso
chmod 660 /var/log/hestia-roundcube-sso/*.log

Если Hestia-панель работает не от www-data, а от другого пользователя, например hestiaweb, добавьте ACL и для него:

setfacl -m u:hestiaweb:rwx /var/lib/hestia-roundcube-sso/tokens
setfacl -d -m u:hestiaweb:rwx /var/lib/hestia-roundcube-sso/tokens

Если такого пользователя нет — команда выдаст ошибку. Тогда используйте фактического пользователя, которого увидели через ps aux.

Шаг 4. Передаём master-пароль Roundcube

Roundcube-плагину нужен master-пароль, чтобы подключиться к Dovecot как master user. Не кладите этот пароль в web-директорию.

Создадим защищённый каталог:

mkdir -p /etc/roundcube/hestia_sso
cp /root/hestia-roundcube-sso/master.pass /etc/roundcube/hestia_sso/master.pass

chown -R root:www-data /etc/roundcube/hestia_sso
chmod 750 /etc/roundcube/hestia_sso
chmod 640 /etc/roundcube/hestia_sso/master.pass

Если Roundcube работает не от www-data, замените www-data на нужного пользователя или группу.

Проверка:

sudo -u www-data cat /etc/roundcube/hestia_sso/master.pass >/dev/null && echo OK

Если видите OK, Roundcube сможет прочитать master-пароль.

Шаг 5. Устанавливаем Roundcube-плагин hestia_sso

Найдём каталог Roundcube:

find / -type d -path '*roundcube*plugins' 2>/dev/null | head

Часто это один из вариантов:

/var/lib/roundcube/plugins
/usr/share/roundcube/plugins

Допустим, у вас каталог:

/var/lib/roundcube/plugins

Создаём плагин:

mkdir -p /var/lib/roundcube/plugins/hestia_sso
nano /var/lib/roundcube/plugins/hestia_sso/hestia_sso.php

Вставляем код:

<?php

class hestia_sso extends rcube_plugin
{
    public $task = 'login';

    private string $tokenDir = '/var/lib/hestia-roundcube-sso/tokens';
    private string $masterPassFile = '/etc/roundcube/hestia_sso/master.pass';
    private string $accessLog = '/var/log/hestia-roundcube-sso/access.log';
    private string $errorLog = '/var/log/hestia-roundcube-sso/error.log';

    public function init()
    {
        $this->add_hook('startup', [$this, 'startup']);
        $this->add_hook('authenticate', [$this, 'authenticate']);
    }

    public function startup($args)
    {
        if (!empty($_GET['_hestia_sso'])) {
            $args['task'] = 'login';
            $args['action'] = 'login';
        }

        return $args;
    }

    public function authenticate($args)
    {
        $token = $_GET['_hestia_sso'] ?? '';

        if (!$token || !preg_match('/^[a-f0-9]{64}$/', $token)) {
            return $args;
        }

        $hash = hash('sha256', $token);
        $file = $this->tokenDir . '/' . $hash . '.json';

        if (!is_file($file)) {
            $this->logError('Token file not found');
            return $args;
        }

        $json = file_get_contents($file);

        // Токен одноразовый: удаляем сразу после чтения.
        @unlink($file);

        $data = json_decode($json, true);

        if (!is_array($data)) {
            $this->logError('Invalid token json');
            return $args;
        }

        $email = $data['email'] ?? '';
        $expires = (int)($data['expires'] ?? 0);
        $issuedBy = $data['by'] ?? 'unknown';
        $tokenIp = $data['ip'] ?? '';
        $currentIp = $_SERVER['REMOTE_ADDR'] ?? '';

        if ($expires < time()) {
            $this->logError('Expired token for ' . $email);
            return $args;
        }

        if ($tokenIp && $currentIp && $tokenIp !== $currentIp) {
            $this->logError('IP mismatch for ' . $email . ': token=' . $tokenIp . ', current=' . $currentIp);
            return $args;
        }

        if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
            $this->logError('Invalid email in token');
            return $args;
        }

        if (!is_readable($this->masterPassFile)) {
            $this->logError('Master password file is not readable');
            return $args;
        }

        $masterPass = trim(file_get_contents($this->masterPassFile));

        if ($masterPass === '') {
            $this->logError('Empty master password');
            return $args;
        }

        $this->logAccess($issuedBy, $email, $currentIp);

        $args['user'] = $email . '*hestia-master';
        $args['pass'] = $masterPass;
        $args['host'] = 'localhost';
        $args['cookiecheck'] = false;
        $args['valid'] = true;

        return $args;
    }

    private function logAccess(string $admin, string $email, string $ip): void
    {
        $line = sprintf(
            "[%s] admin=%s email=%s ip=%s\n",
            date('Y-m-d H:i:s'),
            $admin,
            $email,
            $ip
        );

        @file_put_contents($this->accessLog, $line, FILE_APPEND | LOCK_EX);
    }

    private function logError(string $message): void
    {
        $line = sprintf(
            "[%s] %s ip=%s\n",
            date('Y-m-d H:i:s'),
            $message,
            $_SERVER['REMOTE_ADDR'] ?? '-'
        );

        @file_put_contents($this->errorLog, $line, FILE_APPEND | LOCK_EX);
    }
}

Права:

chown -R root:www-data /var/lib/roundcube/plugins/hestia_sso
chmod -R 755 /var/lib/roundcube/plugins/hestia_sso

Если путь к плагинам другой — замените /var/lib/roundcube/plugins.

Шаг 6. Включаем плагин в Roundcube

Найдём конфиг Roundcube:

find /etc/roundcube /var/lib/roundcube /usr/share/roundcube -name 'config.inc.php' 2>/dev/null

Часто это:

/etc/roundcube/config.inc.php

Открываем:

nano /etc/roundcube/config.inc.php

Находим строку:

$config['plugins'] = [];

И добавляем hestia_sso.

Например:

$config['plugins'] = [
    'archive',
    'zipdownload',
    'hestia_sso',
];

Если строка уже есть и в ней много плагинов, просто добавьте hestia_sso в массив.

Также желательно явно указать локальный IMAP. В новых версиях Roundcube чаще используется:

$config['imap_host'] = 'localhost:143';

В старых конфигурациях может встречаться:

$config['default_host'] = 'localhost';

Не меняйте оба параметра вслепую. Посмотрите, какой параметр уже используется в вашем файле.

После изменения проверьте PHP-синтаксис:

php -l /etc/roundcube/config.inc.php
php -l /var/lib/roundcube/plugins/hestia_sso/hestia_sso.php

Шаг 7. Ручная проверка токена без Hestia

Перед встраиванием кнопки в панель проверим саму схему.

Создадим тестовый токен вручную:

TOKEN="$(openssl rand -hex 32)"
HASH="$(printf '%s' "$TOKEN" | sha256sum | awk '{print $1}')"

cat > "/var/lib/hestia-roundcube-sso/tokens/$HASH.json" <<EOF
{
  "email": "info@example.com",
  "created": $(date +%s),
  "expires": $(($(date +%s)+60)),
  "by": "manual-test",
  "ip": ""
}
EOF

chown www-data:www-data "/var/lib/hestia-roundcube-sso/tokens/$HASH.json"
chmod 660 "/var/lib/hestia-roundcube-sso/tokens/$HASH.json"

echo "https://webmail.example.com/?_hestia_sso=$TOKEN"

Откройте ссылку в браузере.

Если всё правильно, Roundcube должен войти в ящик info@example.com.

После входа проверьте лог:

cat /var/log/hestia-roundcube-sso/access.log

Там должна быть строка примерно такого вида:

[2026-07-05 10:25:11] admin=manual-test email=info@example.com ip=1.2.3.4

Если не вошло, смотрим ошибки:

cat /var/log/hestia-roundcube-sso/error.log
tail -n 100 /var/log/roundcube/errors.log 2>/dev/null
journalctl -u dovecot -n 100 --no-pager

Шаг 8. Встраиваем кнопку в HestiaCP

Самый аккуратный вариант — сделать отдельный endpoint в Hestia, который создаёт токен только по клику. Но для блога и первого рабочего варианта проще показать схему через правку шаблона списка почтовых аккаунтов.

Важно: файлы интерфейса Hestia могут перезаписаться при обновлении панели. Поэтому перед правкой делаем backup:

mkdir -p /root/hestia-ui-backups
cp -a /usr/local/hestia/web /root/hestia-ui-backups/web-$(date +%F-%H%M)

Найдём файл списка почтовых аккаунтов:

grep -R "list_mail_acc" -n /usr/local/hestia/web 2>/dev/null | head -20
grep -R "mail account" -n /usr/local/hestia/web/templates 2>/dev/null | head -20
grep -R "v-list-mail-accounts" -n /usr/local/hestia/web 2>/dev/null | head -20

В разных версиях путь может немного отличаться, но обычно нужный шаблон находится где-то в:

/usr/local/hestia/web/templates/pages/

Откройте найденный файл и найдите место, где выводятся действия для почтового ящика: edit, suspend, delete и т.д.

Добавьте PHP-функцию генерации токена. Её можно разместить в начале шаблона:

<?php

function hestia_roundcube_sso_url(string $email): string
{
    // Разрешаем только admin. В разных версиях Hestia переменная сессии может отличаться.
    // Проверьте, как в вашей версии хранится логин пользователя.
    $currentUser = $_SESSION['user'] ?? $_SESSION['look'] ?? '';

    if ($currentUser !== 'admin') {
        return '';
    }

    $token = bin2hex(random_bytes(32));
    $hash = hash('sha256', $token);

    $data = [
        'email' => $email,
        'created' => time(),
        'expires' => time() + 60,
        'by' => $currentUser,
        'ip' => $_SERVER['REMOTE_ADDR'] ?? '',
    ];

    $dir = '/var/lib/hestia-roundcube-sso/tokens';

    if (!is_dir($dir) || !is_writable($dir)) {
        return '';
    }

    file_put_contents(
        $dir . '/' . $hash . '.json',
        json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
        LOCK_EX
    );

    // Замените webmail.hsdns.ru на ваш универсальный webmail-домен.
    return 'https://webmail.hsdns.ru/?_hestia_sso=' . urlencode($token);
}

Теперь в строке каждого почтового аккаунта надо собрать полный email:

$email = $account . '@' . $domain;

И вывести иконку:

<?php $ssoUrl = hestia_roundcube_sso_url($email); ?>

<?php if ($ssoUrl): ?>
    <a href="<?= htmlspecialchars($ssoUrl, ENT_QUOTES, 'UTF-8') ?>"
       target="_blank"
       rel="noopener noreferrer"
       title="Войти в Roundcube как администратор">
        🔐
    </a>
<?php endif; ?>

Если в шаблоне переменные называются не $account и $domain, найдите фактические названия переменных. Обычно в шаблоне рядом уже выводится адрес ящика, и можно использовать те же переменные.

Шаг 9. Проверяем кнопку в Hestia

Зайдите в Hestia под admin.

Откройте:

Mail → example.com → список ящиков

Рядом с ящиком должна появиться иконка.

Нажмите её.

Ожидаемый результат:

открылся Roundcube;
вы сразу попали в нужный ящик;
в access.log появилась запись;
токен удалился из /var/lib/hestia-roundcube-sso/tokens.

Проверка токенов:

ls -lah /var/lib/hestia-roundcube-sso/tokens

После использования токена файл должен исчезать.

Проверка логов:

tail -n 50 /var/log/hestia-roundcube-sso/access.log
tail -n 50 /var/log/hestia-roundcube-sso/error.log
journalctl -u dovecot -n 100 --no-pager

Шаг 10. Чистка старых токенов

На всякий случай добавим cron, который удаляет старые токены:

cat > /etc/cron.d/hestia-roundcube-sso-clean <<'EOF'
* * * * * root find /var/lib/hestia-roundcube-sso/tokens -type f -mmin +5 -delete
EOF

Проверка:

cat /etc/cron.d/hestia-roundcube-sso-clean

Шаг 11. Защита админского доступа

Обязательно включите 2FA для admin:

v-add-user-2fa admin

Проверьте, что панель Hestia работает только по HTTPS.

Для панели:

v-add-letsencrypt-host

Hestia в документации по SSL указывает, что для панели можно сгенерировать Let’s Encrypt-сертификат после корректной настройки hostname.

Также желательно ограничить доступ к порту панели Hestia по IP через firewall, если панелью пользуетесь только вы.

Что не надо делать

Не надо делать так:

https://webmail.example.com/?user=info@example.com&pass=PASSWORD

Это плохая идея, потому что пароль может попасть:

в историю браузера;
в логи nginx/apache;
в referer;
в прокси;
в скриншоты;
в сторонние системы мониторинга.

Не надо временно менять пароль ящика клиента. Это ломает доверие, может сбить почтовые клиенты пользователя и создаёт лишние риски.

Не надо хранить пароли всех ящиков в отдельной базе. Это опаснее, чем master user с ограниченным доступом к файлу.

Диагностика проблем

Ошибка: Roundcube открывается, но не входит

Проверяем master user:

MASTER_PASS="$(cat /root/hestia-roundcube-sso/master.pass)"
doveadm auth test 'info@example.com*hestia-master' "$MASTER_PASS"

Если не работает — проблема в Dovecot.

Ошибка: токен не найден

Проверяем каталог:

ls -lah /var/lib/hestia-roundcube-sso/tokens
getfacl /var/lib/hestia-roundcube-sso/tokens

Проверяем, создаёт ли Hestia файл токена.

Ошибка: master password file is not readable

Проверяем:

ls -lah /etc/roundcube/hestia_sso/master.pass
sudo -u www-data cat /etc/roundcube/hestia_sso/master.pass >/dev/null && echo OK

Если Roundcube работает не от www-data, замените пользователя в команде.

Ошибка в PHP-плагине

Проверяем синтаксис:

php -l /var/lib/roundcube/plugins/hestia_sso/hestia_sso.php

Смотрим логи:

tail -n 100 /var/log/roundcube/errors.log 2>/dev/null
tail -n 100 /var/log/nginx/error.log
journalctl -xe --no-pager

После обновления Hestia кнопка пропала

Такое возможно, потому что обновление панели может перезаписать шаблоны интерфейса.

Поэтому храните patch:

diff -ruN /root/hestia-ui-backups/web-YYYY-MM-DD-HHMM /usr/local/hestia/web > /root/hestia-roundcube-sso-ui.patch

После обновления можно попробовать применить:

patch -p0 < /root/hestia-roundcube-sso-ui.patch

Но лучше проверять вручную, потому что структура шаблонов может измениться.

Более правильная архитектура для продакшена

Вариант выше рабочий, но токены создаются при открытии списка ящиков. Для небольшого личного хостинга это терпимо. Для более серьёзного хостинга лучше сделать отдельный endpoint:

/admin/mail-sso?email=info@example.com

Он должен:

проверить, что пользователь Hestia — admin;
проверить CSRF-токен;
проверить, что ящик реально существует;
создать одноразовый токен;
записать лог;
сделать redirect в Roundcube.

Это аккуратнее, потому что токен создаётся только по клику, а не при каждом открытии списка ящиков.

Итог

В HestiaCP нет штатного безопасного автологина в Roundcube, но его можно реализовать правильно:

Dovecot Master User
+ Roundcube plugin
+ одноразовые токены
+ кнопка в Hestia только для admin
+ обязательное логирование
+ HTTPS и 2FA

Главная идея — не знать и не передавать пароль почтового ящика. Администратор входит через специальный master user Dovecot, а Roundcube получает доступ только после проверки короткоживущего одноразового токена.

Такая схема намного безопаснее, чем передавать пароль в ссылке или временно менять пароль ящика клиента.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *