Proteger o MariaDB e o MySQL: os passos a dar depois da instalação
Depois de instalar o pacote, uma base de dados ainda não está segura. Este guia percorre o mysql_secure_installation, esclarece a questão do unix_socket e mostra o utilizador próprio por aplicação.
Uma base de dados acabada de instalar num servidor root raramente é tão insegura como os guias mais antigos afirmam, e raramente é tão segura como o sistema de pacotes sugere. Entre esses dois extremos ficam exatamente os passos que este artigo descreve: o que o mysql_secure_installation faz de facto, porque é que a questão do unix_socket se responde hoje de forma diferente da de 2015, como se define um utilizador de aplicação com privilégios mínimos e como manter as palavras-passe fora da lista de processos.
Todos os comandos correm como root. Se trabalhar com um utilizador normal, anteponha sudo. As saídas apresentadas provêm do Debian 13 e do Debian 12, bem como do Ubuntu 24.04 e do Ubuntu 22.04.
Ponto de partida: que pacote corre em que distribuição
O primeiro tropeção surge antes do primeiro passo de segurança. Há anos que o Debian já não distribui um pacote próprio chamado mysql-server. Ali o nome existe apenas como pacote virtual sem versão própria, visível somente através das dependências de mariadb-server:
apt-cache policy mysql-server
mysql-server:
Installed: (none)
Candidate: (none)
Version table:
Esta comparação de versões só devolve resultado no Ubuntu, e aí trata-se da 8.0.46, tanto na 24.04 como na 22.04. No Debian usa-se antes apt-cache showpkg mysql-server, que torna visível o carácter virtual do nome.
No Debian a base de dados é, portanto, sempre o MariaDB. No Ubuntu existem as duas e é preciso escolher. As versões das distribuições atuais:
| Distribuição | mariadb-server | mysql-server |
| Debian 13 | 11.8 | apenas nome virtual, sem versão |
| Debian 12 | 10.11 | apenas nome virtual, sem versão |
| Ubuntu 24.04 | 10.11 | 8.0.46 |
| Ubuntu 22.04 | 10.6 | 8.0.46 |
A instalação faz-se como de costume:
apt update
apt install -y mariadb-server
Saber se apanhou mesmo a versão esperada não depende só da versão do pacote, mas do servidor em execução:
mariadb -e "SELECT @@version, @@version_comment;"
O segundo tropeção é o nome do programa. A partir do MariaDB 11.0, os nomes compatíveis com MySQL deixaram de estar no pacote principal e passaram para mariadb-client-compat e mariadb-server-compat. No Debian 13, um mysql --version pode por isso responder com um aviso ou simplesmente não existir:
mysql: Deprecated program name. It will be removed in a future release, use '/usr/bin/mariadb' instead
bash: mysql: command not found
Quem escreve scripts que devem correr nos quatro sistemas usa no MariaDB sempre mariadb, mariadb-dump e mariadb-secure-installation. Estes nomes existem desde o MariaDB 10.5, ou seja, também no Ubuntu 22.04.
mysql_secure_installation passo a passo
A ferramenta tem nomes diferentes consoante o servidor. No MariaDB o nome canónico é mariadb-secure-installation, no MySQL 8.0 continua a ser mysql_secure_installation. No MariaDB, o nome antigo mantém-se como ligação simbólica no Debian 12 e no Ubuntu 24.04 e 22.04, mas já não no Debian 13: aí existe exclusivamente mariadb-secure-installation.
Vale a pena olhar para as opções, porque o programa aceita, entre outras, --defaults-file, --socket e --protocol. Esta panorâmica, porém, só consta do manual:
man mariadb-secure-installation
É que um --help não existe. O script não interpreta argumentos de todo, engole a opção desconhecida em silêncio e arranca de imediato com o processo interativo. Se faltar uma consola verdadeira, por exemplo numa pipeline ou num container sem terminal, repete a pergunta pela palavra-passe indefinidamente e nunca regressa por si próprio. Por isso invoca-se sem argumentos e a partir de uma shell interativa:
mariadb-secure-installation
No Debian 13 o script antepõe ao processo um aviso bem claro:
NOTE: MariaDB is secure by default in Debian. Running this script is useless at best,
and misleading at worst. This script will be removed in a future MariaDB release in Debian.
O Debian considera portanto que a execução é dispensável e vai retirar o script numa versão futura, como se lê em /usr/share/doc/mariadb-server/README.Debian.gz. Ainda assim, vale a pena percorrer os passos seguintes, porque mostram o que é verificado e porque é que num sistema atual quase não há nada a alterar.
O processo começa com a pergunta pela palavra-passe do root. A formulação depende da versão do script:
Enter current password for root (enter for none):
As versões mais recentes perguntam antes:
Enter root user password or leave blank:
Nos dois casos trata-se do mesmo. Numa instalação acabada de fazer não existe palavra-passe nenhuma, basta premir Enter.
Seguem-se depois as decisões propriamente ditas. A ordem e a formulação diferem entre MariaDB e MySQL, mas em conteúdo trata-se dos mesmos cinco pontos.
A questão do unix_socket
No MariaDB surge de seguida:
Switch to unix_socket authentication [Y/n]
Esta pergunta confunde, porque em todos os sistemas aqui tratados já está respondida. Desde o MariaDB 10.4 que root@localhost está protegido de origem pelo plugin unix_socket, e isso vale tanto para a 10.6 no Ubuntu 22.04 como para a 11.8 no Debian 13. No MySQL 8.0 o equivalente chama-se auth_socket, e o assistente di-lo abertamente:
Skipping password set for root as authentication with auth_socket is used by default.
Na prática significa o seguinte: quem tiver sessão iniciada como utilizador de sistema root entra na base de dados com mariadb sem palavra-passe. Quem não for esse utilizador não entra de todo, nem sequer com a palavra-passe certa. Isto não é uma lacuna, é a variante mais forte. Não existe nenhuma palavra-passe que possa escapar por um backup, por um ficheiro de configuração ou por uma captura de ecrã. A resposta à pergunta é, portanto, manter o Sim, e uma palavra-passe adicional para o root é dispensável num servidor de aplicação isolado.
O estado só se verifica na tabela verdadeira. A consulta óbvia SELECT user, host, plugin FROM mysql.user induz aqui em erro: no root mostra o valor mysql_native_password, embora na realidade seja o unix_socket que atua. No MariaDB a partir da 10.4, mysql.user é apenas uma vista sobre mysql.global_priv, e essa vista só conhece um único método de autenticação por conta. Quem confiar nela julga erradamente que o servidor se autentica por palavra-passe. A regra completa está no campo JSON Priv da tabela verdadeira:
mariadb -e "SELECT User, Host, JSON_DETAILED(Priv) FROM mysql.global_priv;"
Para o root em localhost, o MariaDB tem ali, no essencial, o seguinte:
{"plugin":"mysql_native_password","authentication_string":"invalid","auth_or":[{},{"plugin":"unix_socket"}]}
A primeira entrada é o método por palavra-passe contra o hash inutilizável da cadeia invalid, que ninguém consegue acertar. A segunda entrada, sob auth_or, é a autenticação por socket que realmente atua. De forma mais compacta, consultam-se as duas assim:
mariadb -e "SELECT User, Host, JSON_VALUE(Priv,'$.plugin') AS plugin, JSON_QUERY(Priv,'$.auth_or') AS auth_or FROM mysql.global_priv;"
Para root e localhost espera-se um unix_socket, se não na coluna plugin, então sob auth_or. Se ali constar apenas um método por palavra-passe e nada sob auth_or, a conta trabalha puramente com palavra-passe. No MySQL 8.0 não existe mysql.global_priv, ali mysql.user é uma tabela verdadeira e a coluna plugin tem de mostrar auth_socket. Da vista do MariaDB continua, aliás, a poder ler-se, mas já não escrever.
Só se uma ferramenta precisar mesmo de uma palavra-passe, por exemplo um sistema de monitorização que não corre como root, é que se muda. Nesse caso faz mais sentido criar um segundo utilizador de administração do que reconverter o root. No MariaDB a partir da 11.6, ou seja, também na 11.8 do Debian 13, é possível ligar adicionalmente uma conta a um utilizador de sistema concreto:
CREATE USER 'dbadmin'@'localhost' IDENTIFIED VIA unix_socket AS 'deploy';
Assim o utilizador de sistema deploy pode iniciar sessão como utilizador de base de dados dbadmin, sem que os nomes tenham de coincidir. No Debian 12 e nas versões do Ubuntu, a cadeia depois de AS continua a ser ignorada; ali o utilizador de sistema e o utilizador de base de dados têm de ter o mesmo nome.
As quatro perguntas restantes
O resto é pacífico e responde-se sempre com Sim: remover utilizadores anónimos, proibir o início de sessão remoto do root, remover a base de dados test e os respetivos privilégios, recarregar as tabelas de privilégios. Num Debian ou Ubuntu atual, os utilizadores anónimos e a base de dados de teste nem sequer costumam existir, e o script limita-se então a informar que não havia nada a fazer.
No MySQL 8.0 acresce uma pergunta que o MariaDB não conhece:
Would you like to setup VALIDATE PASSWORD component?
Este componente impõe requisitos mínimos a todas as palavras-passe definidas daí em diante. É útil quando várias pessoas criam utilizadores. É incómodo quando um script de aprovisionamento gera palavras-passe aleatórias que, por acaso, não contêm nenhum carácter especial. Nesse caso o script aborta com:
ERROR 1819 (HY000): Your password does not satisfy the current policy requirements
Quem o ativar deve ajustar antes o gerador de palavras-passe. O nível predefinido é MEDIUM e exige pelo menos oito caracteres, maiúsculas e minúsculas, um algarismo e um carácter especial.
Quando corre mal: voltar a entrar na base de dados
A forma mais comum de ficar fechado de fora é a mudança bem intencionada de unix_socket para uma palavra-passe que depois se perde. Ou o contrário: um script antigo volta a criar /etc/mysql/debian.cnf e de repente nada bate certo. As mensagens de erro que se procuram nessa altura são estas:
ERROR 1698 (28000): Access denied for user 'root'@'localhost'
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: YES)
ERROR 1524 (HY000): Plugin 'unix_socket' is not loaded
O erro 1698 significa: a conta espera unix_socket, mas não é esse o utilizador de sistema que está a executar o comando. Um sudo à frente resolve muitas vezes o caso. O erro 1045 significa: é esperada uma palavra-passe e a que indicou não está certa.
Se já não conseguir entrar de todo, arranque o servidor sem verificação de privilégios. No MariaDB isso faz-se de forma limpa através da variável de ambiente que a unidade systemd fornecida com o pacote avalia, sem tocar em nenhum ficheiro do pacote:
systemctl stop mariadb
systemctl set-environment MYSQLD_OPTS="--skip-grant-tables --skip-networking"
systemctl start mariadb
Agora liga-se com mariadb -u root e repõe-se o estado. É importante o FLUSH PRIVILEGES logo à partida, porque sem as tabelas de privilégios carregadas o ALTER USER falha:
FLUSH PRIVILEGES;
ALTER USER 'root'@'localhost' IDENTIFIED VIA unix_socket;
Depois é imprescindível limpar, caso contrário o servidor arranca desprotegido a cada reinício:
systemctl stop mariadb
systemctl unset-environment MYSQLD_OPTS
systemctl start mariadb
No MySQL 8.0 do Ubuntu este caminho não funciona, porque a unidade não avalia nenhuma variável desse tipo e porque o ALTER USER tem armadilhas adicionais sob --skip-grant-tables. Ali recorre-se a um ficheiro de arranque. Tem de ficar num diretório que o AppArmor permita ao processo do servidor, caso contrário o arranque falha com Can't open file. /var/lib/mysql-files é permitido, /tmp não:
systemctl stop mysql
echo "ALTER USER 'root'@'localhost' IDENTIFIED WITH auth_socket;" > /var/lib/mysql-files/reset.sql
chown mysql:mysql /var/lib/mysql-files/reset.sql
systemctl edit mysql
No editor entra uma sobreposição do comando de arranque; a primeira linha vazia é necessária para apagar o valor original:
[Service]
ExecStart=
ExecStart=/usr/sbin/mysqld --init-file=/var/lib/mysql-files/reset.sql
Depois de systemctl daemon-reload e de um arranque, a conta fica reposta. A seguir remova a sobreposição com systemctl revert mysql e apague o ficheiro. Quem opera bases de dados em sistemas separados encontra indicações complementares sobre a proteção do acesso no artigo Proteger o servidor SSH.
Um utilizador próprio por aplicação em vez do root
O passo mais eficaz não aparece em nenhum assistente. As aplicações não se devem ligar como root e também não precisam de nenhum GRANT ALL. Uma aplicação web típica lê e escreve linhas, não cria bases de dados e não lê ficheiros do servidor.
CREATE DATABASE shopdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'shopapp'@'localhost' IDENTIFIED BY 'AquiUmaPalavraPasseLongaAleatoria';
GRANT SELECT, INSERT, UPDATE, DELETE ON shopdb.* TO 'shopapp'@'localhost';
Três pontos são aqui importantes. Primeiro, o ponto em shopdb.* em vez de *.*: os privilégios sobre *.* são privilégios globais e valem também para mysql e information_schema. Segundo, a indicação @'localhost' em vez de @'%': assim a conta só é utilizável localmente, mesmo que a porta venha um dia a ficar aberta. Terceiro, falta o WITH GRANT OPTION, porque uma conta que pode passar privilégios adiante é, na prática, um administrador.
As alterações de esquema passam depois por uma segunda conta, usada apenas no deploy:
CREATE USER 'shopmigrate'@'localhost' IDENTIFIED BY 'OutraPalavraPasseLongaAleatoria';
GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, DROP, INDEX, REFERENCES ON shopdb.* TO 'shopmigrate'@'localhost';
Parece trabalho a mais e é exatamente o ponto em que uma injeção de SQL na aplicação se transforma numa fuga de dados aborrecida em vez de uma perda total. Uma conta sem DROP não consegue apagar nenhuma tabela.
A verificação não se faz pelo texto do GRANT, mas pelo resultado:
SHOW GRANTS FOR 'shopapp'@'localhost';
Esperam-se exatamente duas linhas: um GRANT USAGE ON *.*, que representa apenas o direito de iniciar sessão e não significa acesso nenhum a dados, e a linha com os quatro privilégios sobre shopdb.*. Se ali aparecer ALL PRIVILEGES ON *.*, o ponto estava no sítio errado. A segunda prova, mais reveladora, é iniciar sessão com a conta nova e executar um SHOW DATABASES;. Só devem ficar visíveis information_schema e shopdb.
Um erro frequente na criação:
ERROR 1396 (HY000): Operation CREATE USER failed for 'shopapp'@'localhost'
Isto significa quase sempre que a conta já existe, muitas vezes como resto de uma tentativa anterior. DROP USER 'shopapp'@'localhost'; e começar de novo.
bind-address: onde a base de dados escuta afinal
Nas quatro distribuições, depois da instalação do pacote, a base de dados escuta apenas em 127.0.0.1. No MariaDB a linha está em /etc/mysql/mariadb.conf.d/50-server.cnf e no MySQL 8.0 em /etc/mysql/mysql.conf.d/mysqld.cnf. Consultar em vez de supor:
grep -R "bind-address" /etc/mysql/
No MySQL 8.0 existe uma segunda linha que passa facilmente despercebida: mysqlx-bind-address controla o protocolo X na porta 33060. Quem altera apenas bind-address pode acabar por fechar só metade do acesso ou por deixar a segunda porta aberta.
A verificação mais fiável não é o ficheiro de configuração, é o kernel:
ss -lntp
LISTEN 0 80 127.0.0.1:3306 0.0.0.0:* users:(("mariadbd",pid=712,fd=22))
Se ali constar 0.0.0.0:3306 ou *:3306, o serviço está acessível na rede. A título complementar, o servidor fornece a sua própria perspetiva:
mariadb -e "SELECT @@bind_address, @@port, @@skip_networking;"
Há aqui duas armadilhas frequentes. A primeira: os ficheiros em mariadb.conf.d são lidos por ordem alfabética, e ganha a indicação lida em último lugar. Por isso, quem escreve a sua alteração num ficheiro próprio dá-lhe o nome 99-eigene.cnf e não 10-eigene.cnf. É exatamente nisto que falha a maioria dos relatos em que o MariaDB alegadamente ignora bind-address. A vantagem de um ficheiro próprio: as atualizações de pacotes não pedem resolução de conflitos, porque o ficheiro fornecido fica intacto.
A segunda armadilha: quem não precisa mesmo do acesso pela rede vai um passo além do bind-address e define skip-networking. Deixa então de existir porta TCP, resta apenas o socket Unix. É a definição correta para o caso normal de aplicação web e base de dados no mesmo servidor, mas custa nervos se uma aplicação tiver na configuração 127.0.0.1 em vez de localhost: com 127.0.0.1 as bibliotecas de cliente forçam TCP.
Acesso a partir do exterior só quando for mesmo necessário
Uma porta de base de dados exposta na rede aberta é encontrada em poucas horas e testada sem parar. A melhor resposta à questão do acesso externo é, por isso, evitá-lo. Para manutenção ocasional chega um túnel SSH, que coloca no seu próprio computador uma porta local sobre o socket de base de dados da máquina remota. A ferramenta de base de dados liga-se então a 127.0.0.1, sem que o servidor abra o que quer que seja na rede.
Para ligações permanentes entre vários servidores, um túnel WireGuard é a solução limpa. A base de dados passa então a ligar-se exclusivamente ao endereço do túnel, não ao endereço IP público.
Se ainda assim tiver de ser uma porta aberta, quatro medidas atuam em conjunto. O servidor liga-se a exatamente um endereço interno. A firewall deixa passar apenas o endereço de origem conhecido. A conta de base de dados está presa ao mesmo endereço, ou seja, 'shopapp'@'10.0.0.5' e nunca 'shopapp'@'%'. E a ligação é obrigatoriamente cifrada:
ALTER USER 'shopapp'@'10.0.0.5' REQUIRE SSL;
Ao testar a partir do exterior surgem dois erros que muitas vezes se confundem. O primeiro significa que ninguém responde, ou seja, firewall ou bind-address:
ERROR 2003 (HY000): Can't connect to MySQL server on '203.0.113.10:3306' (110)
O segundo significa que o servidor responde e recusa a ligação de forma deliberada, ou seja, falta a conta adequada para essa proveniência:
ERROR 1130 (HY000): Host '203.0.113.55' is not allowed to connect to this MariaDB server
Localmente, por sua vez, o clássico é o serviço simplesmente não estar a correr:
ERROR 2002 (HY000): Can't connect to local server through socket '/run/mysqld/mysqld.sock' (2)
Palavras-passe fora da linha de comandos
A invocação mariadb -u shopapp -pGeheim123 funciona e continua a ser um erro. O próprio servidor o diz:
Warning: Using a password on the command line interface can be insecure.
Há duas razões por trás disso. Primeiro, a linha vai parar ao histórico da shell. Segundo, a linha de comandos de um processo é, num sistema normal, visível através de ps para qualquer utilizador com sessão iniciada. Num servidor com vários clientes ou vários serviços, isso equivale a entregar a palavra-passe sem esforço nenhum.
O caminho correto para pessoas é -p sem nada a seguir. A palavra-passe é então pedida de forma interativa e nada fica no histórico:
mariadb -u shopapp -p shopdb
O caminho correto para scripts e cronjobs é um ficheiro de opções com permissões restritas. Criar com o modo correto num único passo:
install -m 600 /dev/null /root/.my.cnf
Conteúdo:
[client]
user=backup
password=AquiUmaPalavraPasseLongaAleatoria
A partir daí, qualquer cliente encontra sozinho as credenciais. Para tarefas separadas criam-se vários ficheiros e aponta-se especificamente para cada um. Vale aqui uma regra na qual muitos falham: --defaults-extra-file e --defaults-file têm de ser a primeira opção da invocação, caso contrário são ignorados sem qualquer aviso.
mariadb-dump --defaults-extra-file=/root/.my-backup.cnf --single-transaction shopdb
A variável de ambiente MYSQL_PWD não é solução. Fica em /proc e é, por isso, tão visível como a linha de comandos. No MySQL 8.0 existe ainda o mysql_config_editor, que escreve um ficheiro ~/.mylogin.cnf. O conteúdo está dissimulado, mas não cifrado, e o MariaDB não conhece esta ferramenta. Em ambientes mistos, o simples ficheiro de opções com modo 600 é a escolha mais fiável.
Um último ponto diz respeito aos backups. Uma exportação contém tudo aquilo que a aplicação vê, e uma conta de backup não precisa de privilégios de escrita para isso. Para o mariadb-dump com --single-transaction costuma bastar:
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES ON shopdb.* TO 'backup'@'localhost';
GRANT PROCESS ON *.* TO 'backup'@'localhost';
Aceitação: como reconhecer que ficou bem feito
Um comando que corre sem erros não prova nada. Estas seis provas provam alguma coisa:
- A conta root usa autenticação por socket:
mariadb -e "SELECT User, Host, JSON_VALUE(Priv,'$.plugin') AS plugin, JSON_QUERY(Priv,'$.auth_or') AS auth_or FROM mysql.global_priv;"mostra norootumunix_socket, seja na colunaplugin, seja sobauth_or. A vistamysql.usernão serve para isto, devolve apenas o primeiro método. No MySQL 8.0 vale o contrário: aí é decisiva a colunapluginemmysql.user, e nela tem de constarauth_socket. - Não existem contas anónimas:
mariadb -e "SELECT user, host FROM mysql.user WHERE user = '';"devolve um conjunto de resultados vazio. - A base de dados de teste desapareceu:
mariadb -e "SHOW DATABASES LIKE 'test';"não devolve nada. - A porta está fechada:
ss -lntpnão mostra nada para a 3306 ou mostra exclusivamente127.0.0.1. - A conta da aplicação está limitada: ao iniciar sessão com ela, um
SHOW DATABASES;mostra apenas a própria base de dados, e umDROP TABLEfalha. - Nenhuma palavra-passe em texto simples pelo caminho:
grep -rs "password" /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.weekly/ /etc/cron.monthly/não encontra nada, e o mesmo vale paracrontab -ldo root e para uma olhada em/var/spool/cron/crontabs/; além disso, os ficheiros de opções têm modo 600. A opção-sfaz parte do comando, porque um diretório em falta abortaria a invocação com o código de saída 2: no Debian 13, depois de uma instalação apenas da base de dados,/etc/cron.dnão existe, porque ainda não está instalado nenhum pacote de cron.
Quem cumprir estes seis pontos num servidor novo já excluiu a esmagadora maioria dos ataques contra bases de dados, sem instalar um único software adicional. O resto é disciplina nas atualizações e um backup que funcione, cujo restauro tenha sido ensaiado pelo menos uma vez.
Perguntas frequentes
Ainda preciso de uma palavra-passe de root no MariaDB?
Porque é que o meu Debian diz que o pacote mysql-server não existe?
Como volto a entrar na base de dados depois de me ter fechado de fora?
Qual é a diferença entre o ERROR 1698 e o ERROR 1045?
Basta bind-address = 127.0.0.1 para proteger a base de dados?
Porque é que a aplicação não deve aceder à base de dados como root?
Como passo uma palavra-passe de base de dados a um cronjob em segurança?
2026 KernelHost GmbH. Todos os direitos reservados. Este guia está protegido por direitos de autor. A sua republicação noutros sites, na íntegra, em parte ou de forma editada, não é permitida sem o nosso consentimento por escrito. Citações com indicação da fonte e ligação são expressamente bem-vindas.

