Открыть MySQL или MariaDB наружу технически просто: поменять bind-address, перезапустить службу и разрешить порт 3306. Но именно в этой простоте прячется большинство проблем: база внезапно становится доступной всему интернету, пользователи заведены как 'app'@'%', пароль уходит по сети без проверки сертификата, а firewall «вроде включён», но Docker или облачные правила обходят ожидания администратора.
В этой инструкции разберём безопасный вариант для VDS: база слушает только нужный интерфейс, доступ разрешён только с доверенных IP-адресов, соединение защищено TLS/SSL, а права пользователя ограничены конкретной схемой и конкретным источником. Материал подходит для MySQL и MariaDB; где поведение отличается, это отмечено отдельно.
Главное правило: не используйте 0.0.0.0 как единственный механизм «удобства». Если MySQL или MariaDB должны принимать удалённые подключения, открывайте их только на нужном адресе и только для нужных клиентов.
Когда вообще стоит открывать MySQL или MariaDB по сети
Удалённый доступ к базе нужен не всегда. Если веб-приложение, cron-задачи и база живут на одном VDS, лучше оставить MySQL или MariaDB на 127.0.0.1 и подключаться через loopback или Unix-сокет. Это уменьшает поверхность атаки: порт не виден снаружи, firewall не нужно держать в сложной конфигурации, а случайная ошибка в правах пользователя не превращается в сетевую проблему.
Открывать сетевой доступ разумно в нескольких сценариях: приложение находится на другом сервере, есть отдельный VDS для базы, требуется подключение из CI/CD, репликации, аналитики или административного клиента. Даже в этих случаях не стоит делать базу публичным сервисом «для всех». MySQL и MariaDB должны быть доступны только по маршрутам, которые действительно нужны: частная сеть провайдера, VPN, bastion-сервер, фиксированные публичные IP или отдельный межсерверный сегмент.
Если у вас есть возможность использовать приватный адрес между VDS, выбирайте его. Публичный адрес допустим, но тогда особенно важны TLS, firewall, строгие grants и мониторинг неудачных подключений. Если нужно связать несколько серверов через защищённый туннель, полезно заранее продумать схему VPN; подробнее похожий подход разобран в материале про site-to-site WireGuard между VDS.
План безопасной настройки
Хорошая конфигурация складывается из нескольких независимых слоёв. Каждый слой не заменяет остальные, а страхует их:
bind-addressограничивает, на каких IP-адресах сервер базы слушает TCP-порт.- Firewall разрешает порт
3306только с доверенных адресов. - Пользователи MySQL/MariaDB создаются не для
%, а для конкретного IP или подсети. - TLS/SSL шифрует трафик и позволяет клиенту проверить, что он подключился именно к нужному серверу.
- Проверки после перезапуска показывают, что база слушает то, что вы ожидали, а не весь интернет.
Перед изменениями сделайте резервную копию конфигурационных файлов и убедитесь, что у вас есть доступ к VDS по SSH. Если база обслуживает production, запланируйте короткое окно: перезапуск MySQL или MariaDB обычно быстрый, но приложения могут получить временные ошибки подключения.

Где находится конфигурация MySQL и MariaDB
Файл зависит от дистрибутива и от того, установлен MySQL или MariaDB. Не редактируйте первый попавшийся файл вслепую: проверьте, какие конфиги реально читает сервер, и где уже задан bind-address.
Debian и Ubuntu
Для MySQL чаще используется файл /etc/mysql/mysql.conf.d/mysqld.cnf. Для MariaDB — /etc/mysql/mariadb.conf.d/50-server.cnf. Проверить упоминания параметра можно так:
sudo grep -R "bind-address\|skip-networking\|ssl-" /etc/mysql /etc/my.cnf /etc/my.cnf.d 2>/dev/null
Службы обычно называются mysql для MySQL и mariadb для MariaDB, но на некоторых системах алиасы могут совпадать. Проверяйте через systemctl status.
systemctl status mysql
systemctl status mariadb
AlmaLinux, Rocky Linux, CentOS Stream, Oracle Linux и Fedora
В RHEL-based системах конфиги обычно лежат в /etc/my.cnf и /etc/my.cnf.d/. Для MariaDB часто встречается /etc/my.cnf.d/mariadb-server.cnf, для MySQL из официальных пакетов — файлы внутри /etc/my.cnf.d/. Искать параметры лучше рекурсивно:
sudo grep -R "bind-address\|skip-networking\|ssl-" /etc/my.cnf /etc/my.cnf.d /etc/mysql 2>/dev/null
Если сервер установлен из репозитория дистрибутива, служба чаще называется mariadb. Если установлен MySQL Community Server, обычно используется mysqld.
systemctl status mariadb
systemctl status mysqld
Настраиваем bind-address без лишнего риска
Параметр bind-address определяет IP-адрес, на котором сервер принимает TCP-подключения. Самые частые значения:
127.0.0.1— только локальные подключения с этого же сервера.10.0.0.10или другой приватный адрес — подключения только через внутреннюю сеть.203.0.113.10— подключения на конкретный публичный адрес VDS.0.0.0.0— слушать все IPv4-интерфейсы; использовать только вместе с жёстким firewall.::— слушать IPv6; открывайте только если IPv6 действительно нужен и защищён правилами firewall.
Оптимальный вариант для межсерверного подключения — указать конкретный приватный IP базы. Например, если VDS с базой имеет внутренний адрес 10.10.0.5, конфигурация может выглядеть так:
[mysqld]
bind-address = 10.10.0.5
port = 3306
Если у вас уже есть строка skip-networking, она отключает TCP-сеть. Её нужно закомментировать или удалить, иначе bind-address не поможет. Не путайте это с Unix-сокетом: локальные подключения через сокет могут работать, даже когда TCP отключён.
После изменения перезапустите службу. Используйте имя, которое реально есть в вашей системе.
Debian и Ubuntu
sudo systemctl restart mysql
sudo systemctl restart mariadb
RHEL-based и Fedora
sudo systemctl restart mysqld
sudo systemctl restart mariadb
Проверить, где слушает база, можно командой ss:
sudo ss -ltnp | grep ':3306'
Хороший результат для приватного адреса выглядит примерно так: сервер слушает 10.10.0.5:3306, а не 0.0.0.0:3306. Если вы видите только 127.0.0.1:3306, значит конфиг не применился, параметр переопределён в другом файле или была перезапущена не та служба.
Создаём пользователя не шире, чем нужно
В MySQL и MariaDB пользователь — это не только имя, но и хост. 'app'@'10.10.0.20' и 'app'@'%' — разные учётные записи. Для удалённого приложения лучше создать пользователя, привязанного к IP сервера приложения или к узкой подсети.
CREATE DATABASE appdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'app'@'10.10.0.20' IDENTIFIED BY 'change_this_long_password';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX ON appdb.* TO 'app'@'10.10.0.20';
FLUSH PRIVILEGES;
Не выдавайте ALL PRIVILEGES по привычке. Для CMS или фреймворка на этапе установки могут понадобиться CREATE, ALTER и INDEX, но в штатной работе часть прав можно убрать. Для read-only аналитики достаточно SELECT. Для миграций можно завести отдельного пользователя, который используется только в CI/CD и не хранится в конфиге веб-приложения.
Если источник имеет динамический IP, не спешите ставить 'user'@'%'. Лучше использовать VPN, bastion или подсеть с понятными границами. Широкий host-шаблон в базе плюс открытый порт — типовая причина инцидентов.
Включаем TLS/SSL для MySQL и MariaDB
TLS нужен не только для шифрования пароля. Он защищает весь трафик: запросы, результаты выборок, служебные данные, токены сессий, персональные данные пользователей. Если приложение подключается к базе через публичную сеть или через инфраструктуру, которую вы не контролируете полностью, TLS должен быть обязательным.
Для production лучше использовать сертификат с корректным именем сервера, чтобы клиент мог проверить его по hostname. Если база доступна как db.internal.example, именно это имя должно быть в Subject Alternative Name сертификата. Для приватного IP можно добавить IP SAN, но удобнее использовать внутреннее DNS-имя.
Минимальный пример с собственной CA
Ниже — упрощённый пример для внутренней инфраструктуры. Он создаёт локальный центр сертификации и серверный сертификат. В реальном production храните ключ CA отдельно от VDS с базой, а на сервер базы копируйте только ca.pem, server-cert.pem и server-key.pem.
sudo install -d -m 0750 /etc/mysql/tls
sudo openssl genrsa -out /etc/mysql/tls/ca-key.pem 4096
sudo openssl req -new -x509 -days 3650 -key /etc/mysql/tls/ca-key.pem -out /etc/mysql/tls/ca.pem -subj '/CN=Internal MySQL CA'
sudo openssl genrsa -out /etc/mysql/tls/server-key.pem 4096
sudo openssl req -new -key /etc/mysql/tls/server-key.pem -out /etc/mysql/tls/server.csr -subj '/CN=db.internal.example' -addext 'subjectAltName=DNS:db.internal.example,IP:10.10.0.5'
sudo openssl x509 -req -days 825 -in /etc/mysql/tls/server.csr -CA /etc/mysql/tls/ca.pem -CAkey /etc/mysql/tls/ca-key.pem -CAcreateserial -out /etc/mysql/tls/server-cert.pem -copy_extensions copy
sudo chown -R mysql:mysql /etc/mysql/tls
sudo chmod 0640 /etc/mysql/tls/server-key.pem
sudo chmod 0644 /etc/mysql/tls/ca.pem /etc/mysql/tls/server-cert.pem
Затем добавьте TLS-параметры в секцию [mysqld]. В конфигурационных файлах MySQL и MariaDB обычно используются имена с дефисом: ssl-ca, ssl-cert, ssl-key. Параметр require_secure_transport запрещает незащищённые TCP-подключения. Он поддерживается современными версиями MySQL и MariaDB, но на старых релизах его наличие нужно проверить.
[mysqld]
ssl-ca = /etc/mysql/tls/ca.pem
ssl-cert = /etc/mysql/tls/server-cert.pem
ssl-key = /etc/mysql/tls/server-key.pem
tls-version = TLSv1.2,TLSv1.3
require_secure_transport = ON
После изменения перезапустите службу и проверьте переменные:
SHOW VARIABLES LIKE 'have_ssl';
SHOW VARIABLES LIKE 'ssl_ca';
SHOW VARIABLES LIKE 'require_secure_transport';
SHOW STATUS LIKE 'Ssl_cipher';
have_ssl должен быть YES. Значение Ssl_cipher в рамках текущего соединения покажет шифр, если вы подключились по TLS. Если оно пустое, клиентское соединение не использует TLS, даже если сервер умеет его принимать.
SELinux на RHEL-based системах
На AlmaLinux, Rocky Linux, CentOS Stream и Oracle Linux при включённом SELinux MySQL или MariaDB может не прочитать ключи из нестандартного каталога. Если в журнале есть отказы доступа, задайте контекст для каталога с TLS-файлами:
sudo dnf install -y policycoreutils-python-utils
sudo semanage fcontext -a -t mysqld_etc_t '/etc/mysql/tls(/.*)?'
sudo restorecon -Rv /etc/mysql/tls
sudo systemctl restart mariadb
Если служба называется mysqld, перезапускайте её. Не отключайте SELinux только ради того, чтобы «быстро заработало»: правильный контекст решает проблему аккуратнее.
Требуем TLS на уровне пользователя
Серверный require_secure_transport удобен, но полезно продублировать требование в правах пользователя. Так вы явно фиксируете политику доступа: этот пользователь не должен подключаться без шифрования.
ALTER USER 'app'@'10.10.0.20' REQUIRE SSL;
FLUSH PRIVILEGES;
Если вы используете клиентские сертификаты, можно усилить правило до REQUIRE X509 или указать требования к issuer/subject. Это полезно для межсерверных сервисов, но усложняет ротацию сертификатов. Для большинства веб-приложений достаточно серверного сертификата с проверкой CA и hostname на стороне клиента.
Настраиваем firewall: разрешаем только нужные источники
Firewall должен быть настроен до того, как вы делаете базу доступной на публичном или приватном сетевом интерфейсе. Идеальная схема: сначала добавить разрешающее правило для IP приложения, затем убедиться, что остальные подключения к 3306 не проходят, и только после этого менять bind-address.
UFW на Debian и Ubuntu
Если вы используете UFW, разрешите доступ только с IP сервера приложения. В примере база принимает подключения от 10.10.0.20:
sudo ufw allow from 10.10.0.20 to any port 3306 proto tcp
sudo ufw status numbered
Если раньше вы открывали порт всем, удалите широкое правило. Сначала посмотрите номера правил через ufw status numbered, затем удалите нужное:
sudo ufw delete 5
firewalld на RHEL-based и Fedora
В firewalld используйте rich rule с конкретным источником. Не добавляйте сервис или порт глобально, если база не должна быть доступна всем хостам в зоне.
sudo firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="10.10.0.20/32" port port="3306" protocol="tcp" accept'
sudo firewall-cmd --reload
sudo firewall-cmd --list-rich-rules
Для IPv6 создавайте отдельное правило с family="ipv6" и конкретным IPv6-адресом или подсетью. Не забывайте, что сервер может слушать IPv6 даже тогда, когда вы проверяете только IPv4.
nftables без UFW и firewalld
Если вы управляете правилами напрямую через nftables, логика та же: разрешить established-соединения, SSH, нужные web-порты и отдельное правило для MySQL/MariaDB от доверенного источника. Фрагмент таблицы может выглядеть так:
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
ct state established,related accept
iif lo accept
tcp dport 22 accept
ip saddr 10.10.0.20 tcp dport 3306 accept
}
}
Перед применением правил на удалённом VDS всегда оставляйте себе путь отката: активную SSH-сессию, консоль провайдера или временный таймер, который вернёт старую конфигурацию. Ошибка в firewall может быть неприятнее ошибки в MySQL.

Проверяем подключение с клиента
Проверять нужно с того сервера, с которого реально будет ходить приложение. Локальная проверка на VDS с базой не подтверждает, что firewall, маршруты и TLS работают для удалённого клиента.
Для MySQL-клиента удобен режим VERIFY_IDENTITY: он проверяет цепочку сертификатов и имя сервера. Подключайтесь по тому DNS-имени, которое указано в сертификате.
mysql -h db.internal.example -u app -p --ssl-mode=VERIFY_IDENTITY --ssl-ca=/etc/mysql/tls/ca.pem appdb
Для MariaDB-клиента часто используется такой вариант:
mariadb -h db.internal.example -u app -p --ssl --ssl-verify-server-cert --ssl-ca=/etc/mysql/tls/ca.pem appdb
После входа проверьте текущий TLS-шифр:
SHOW STATUS LIKE 'Ssl_cipher';
SELECT CURRENT_USER();
CURRENT_USER() полезен, потому что показывает, какая именно запись пользователя сработала. Если вы ожидали 'app'@'10.10.0.20', а видите другую запись, проверьте grants и порядок совпадения пользователей.
Как понять, что именно не работает
При настройке удалённого доступа ошибки обычно относятся к одному из четырёх слоёв: сервер не слушает адрес, сеть не пропускает порт, MySQL/MariaDB не пускает пользователя или TLS не проходит проверку.
Сервер не слушает нужный адрес
На VDS с базой выполните:
sudo ss -ltnp | grep ':3306'
sudo journalctl -u mysql -u mariadb -u mysqld -n 100 --no-pager
Если в журнале ошибка чтения ключа TLS, проверьте права на файл server-key.pem, владельца mysql и SELinux/AppArmor. Если служба стартует, но слушает 127.0.0.1, найдите все упоминания bind-address: параметр может быть задан в другом включаемом файле ниже по порядку.
Порт не проходит по сети
С клиента проверьте TCP-доступ. Если nc не установлен, поставьте пакет netcat-openbsd на Debian/Ubuntu или nmap-ncat на RHEL-based системах.
nc -vz db.internal.example 3306
Таймаут обычно указывает на firewall, маршрут или неверный IP. Мгновенный отказ соединения часто означает, что на адресе никто не слушает порт. Если соединение устанавливается с одного сервера, но не устанавливается с другого, сравните исходные IP: firewall мог быть настроен на старый адрес NAT или на публичный IP вместо приватного.
Доступ запрещён пользователю
Ошибка вида Access denied for user не всегда означает неверный пароль. Часто проблема в host-части пользователя. Посмотрите, какие записи есть:
SELECT User, Host, ssl_type FROM mysql.user WHERE User = 'app';
SHOW GRANTS FOR 'app'@'10.10.0.20';
Если приложение приходит не с 10.10.0.20, а с адреса шлюза, NAT или контейнерной сети, MySQL будет искать другую запись пользователя. В такой ситуации лучше исправить сетевую схему или создать точное правило для фактического источника, а не расширять доступ до %.
TLS не проходит проверку
Если клиент сообщает о проблеме сертификата, проверьте три вещи: доверяет ли он вашему ca.pem, совпадает ли hostname с SAN сертификата, не истёк ли срок действия. Подключение по IP к сертификату, выписанному только на DNS-имя, в режиме проверки имени завершится ошибкой — и это правильное поведение.
Убедиться в параметрах сертификата можно на сервере базы:
openssl x509 -in /etc/mysql/tls/server-cert.pem -noout -subject -issuer -dates -ext subjectAltName
Особые случаи: Docker, панели и несколько интерфейсов
Если MySQL или MariaDB запущены в Docker, опубликованный порт может вести себя не так, как ожидает администратор UFW. Docker создаёт собственные правила NAT и может сделать контейнер доступным извне, даже если вы думали, что порт закрыт. Безопаснее публиковать порт на конкретный адрес, например на приватный IP или localhost, и отдельно проверять итоговые правила nftables/iptables. Подробнее о таких ловушках — в разборе Docker, firewall, iptables и nftables.
Если на VDS установлена панель управления, она может перезаписывать часть конфигурации или создавать собственные firewall-правила. В таких системах особенно важно понимать, какой файл является источником истины: конфиг панели, include-файл MySQL/MariaDB или ручная настройка администратора.
На серверах с несколькими IP-адресами не указывайте 0.0.0.0 без необходимости. Лучше явно выбрать адрес внутренней сети. Это снижает риск, что база начнёт слушать дополнительный публичный интерфейс после добавления нового IP на VDS.
Короткий чек-лист перед production
bind-addressуказывает конкретный IP, а не все интерфейсы без причины.skip-networkingне мешает TCP-подключениям, если они действительно нужны.- Firewall разрешает
3306/tcpтолько с доверенных IP или подсетей. - Пользователь базы создан как
'user'@'source_ip', а не как'user'@'%'. - Для пользователя включено
REQUIRE SSL, а на сервере —require_secure_transport, если версия поддерживает параметр. - Клиент проверяет CA и hostname, а не просто включает шифрование «как получится».
- Проверены IPv4 и IPv6, если на VDS включён dual-stack.
- Есть понятный план отката: вернуть
bind-address = 127.0.0.1, закрыть firewall и удалить лишнего пользователя.
План отката, если что-то пошло не так
Если приложение не подключается после изменений, не расширяйте доступ хаотично. Безопасный откат простой: вернуть локальный bind-address, перезапустить службу, закрыть порт 3306 в firewall и проверить, что база снова доступна локальному приложению.
[mysqld]
bind-address = 127.0.0.1
Затем удалите или заблокируйте временного пользователя, если он больше не нужен:
DROP USER 'app'@'10.10.0.20';
FLUSH PRIVILEGES;
Такой откат лучше, чем временно открыть порт миру и «потом разобраться». Для базы данных краткий период избыточной открытости может быть достаточным, чтобы попасть в сканеры, получить brute-force по стандартному порту или оставить в логах поток мусорных подключений.
Итог
Безопасный удалённый доступ к MySQL или MariaDB на VDS — это не один параметр, а связка настроек. bind-address определяет, где сервер слушает, firewall определяет, кто вообще может дойти до порта, grants определяют, кто имеет право войти, а TLS/SSL защищает соединение и подтверждает подлинность сервера.
Самая практичная схема для большинства проектов: база слушает приватный IP, firewall пропускает только сервер приложения, пользователь создан под конкретный источник, а клиент подключается с проверкой сертификата. Такая настройка требует чуть больше внимания на старте, зато потом избавляет от неприятных сюрпризов: случайно открытого порта, широких прав, незашифрованного трафика и неочевидных ошибок при росте инфраструктуры.


