Install MariaDB and MySQL on AlmaLinux 10 and Secure It

MySQL/MariaDB on AlmaLinux series, part 1. If you followed the PostgreSQL series, start there with Install PostgreSQL on AlmaLinux 10: PGDG vs AppStream, pg_hba.conf and Remote Access; this series follows the same layout for the MySQL family.

AlmaLinux 10 ships both MariaDB and MySQL in its AppStream repository, so you can have a supported database server running in a couple of minutes without adding any third-party repository. The part that takes a little more care is what comes right after dnf install: the default install has anonymous users and a test database, listens on every interface, and on MySQL the root account has no password at all. This guide installs MariaDB (the default choice on AlmaLinux), secures it with mariadb-secure-installation, applies a small hardening config, and verifies the result. A separate section does the same for MySQL 8.4 if your application needs Oracle MySQL specifically.

Applies to: AlmaLinux 10.x with MariaDB 10.11 (default stream) or MariaDB 11.8 (available from the 10.2 point release), or MySQL 8.4 from AppStream. The same package names and steps work on RHEL 10, Rocky Linux 10 and Oracle Linux 10. On AlmaLinux 9 the packages are modules instead (dnf module list mariadb mysql), but the securing and hardening steps are identical.

Prerequisites

  • An AlmaLinux 10 server with root or sudo access. The examples use db1.example.lan (192.168.1.40).
  • The BaseOS and AppStream repositories enabled (the default). Run sudo dnf -y update first so you start on current packages.
  • At least 1 GB of RAM for a small lab instance; more for anything real (InnoDB sizing is covered in part 6 of this series).
  • SELinux left in enforcing mode and firewalld running. Nothing in this guide requires turning either off.
  • A decision: MariaDB or MySQL, not both. Their RPMs conflict, so only one server family can be installed on a host. If you need both, run one of them in a container.

Step 1: See what AppStream offers

sudo dnf list --available 'mariadb*-server' 'mysql*-server'

On AlmaLinux 10 you will see output similar to this (exact build numbers change with updates):

mariadb-server.x86_64            3:10.11.x-y.el10_z     appstream
mariadb-gssapi-server.x86_64     3:10.11.x-y.el10_z     appstream
mariadb11.8-server.x86_64        3:11.8.x-y.el10_z      appstream
mysql8.4-server.x86_64           8.4.x-y.el10_z         appstream
PackageVersionService unitNotes
mariadb-serverMariaDB 10.11 LTSmariadb.serviceDefault stream, full AlmaLinux 10 lifecycle. Pick this unless you need something newer.
mariadb11.8-serverMariaDB 11.8 LTSmariadb.serviceNewer LTS, added in the 10.2 point release, shorter AppStream lifecycle.
mysql8.4-serverMySQL 8.4 LTSmysqld.serviceOracle MySQL, for applications that need MySQL-only features or exact MySQL behavior.

RHEL 10 and its rebuilds dropped modules for databases: the stream version is part of the package name instead. You install one stream per host; switching streams later is an upgrade with a dump/restore or an in-place upgrade, not a dnf module switch.

Step 2: Install MariaDB

# Default MariaDB 10.11
sudo dnf install -y mariadb-server

# ...or MariaDB 11.8 instead (AlmaLinux 10.2 and later)
# sudo dnf install -y mariadb11.8-server

This pulls in the client (mariadb), shared configuration (mariadb-common, which owns /etc/my.cnf and /etc/my.cnf.d/) and the server. Data lives in /var/lib/mysql, the server config is /etc/my.cnf.d/mariadb-server.cnf, and the error log is /var/log/mariadb/mariadb.log.

Step 3: Start the service and check it

sudo systemctl enable --now mariadb
systemctl status mariadb --no-pager
sudo ss -ltnp | grep -E ':3306\b'
sudo mariadb -e "SELECT VERSION(), @@datadir, @@socket;"

On first start the unit runs mariadb-prepare-db-dir, which creates the system tables in /var/lib/mysql. The last command should print the server version, /var/lib/mysql/ and /var/lib/mysql/mysql.sock. Notice that sudo mariadb logged you in as the database root user without a password: that is the unix_socket plugin at work, explained next.

Step 4: Understand root authentication on MariaDB

Since MariaDB 10.4 the root@localhost account is created with two authentication methods: unix_socket, which lets the operating system user root in over the local socket with no password, and mysql_native_password with an invalid hash, so no password can ever match until you set one. You can see it like this:

sudo mariadb -e "SHOW GRANTS FOR 'root'@'localhost';"
sudo mariadb -e "SELECT user, host, plugin FROM mysql.user;"

That default is already safe: only someone who is root on the server can be database root. For servers administered by people who already have sudo, keep it. You need a root password only if a tool must log in as database root over TCP, and even then a dedicated admin account (created in part 2) is the better choice.

Step 5: Run mariadb-secure-installation

mariadb-secure-installation (older name: mysql_secure_installation) removes the insecure leftovers from a fresh install. Run it as root so it can authenticate through the socket:

sudo mariadb-secure-installation

Recommended answers for a fresh MariaDB 10.11 or 11.8 install:

PromptAnswerWhy
Enter current password for rootPress EnterThere is no password yet; socket auth logs you in.
Switch to unix_socket authenticationnroot is already protected by unix_socket.
Change the root password?n (or Y if you need TCP root login)Not needed with socket auth.
Remove anonymous users?YAnonymous accounts let anyone connect without a user name.
Disallow root login remotely?YRoot should only connect from localhost.
Remove test database and access to it?YThe test database is writable by any user by default.
Reload privilege tables now?YApplies the changes immediately.

If you prefer to script it (for example in Ansible or a kickstart %post), these statements do the same thing:

sudo mariadb <<'SQL'
DELETE FROM mysql.global_priv WHERE User = '';
DELETE FROM mysql.global_priv WHERE User = 'root' AND Host NOT IN ('localhost', '127.0.0.1', '::1');
DROP DATABASE IF EXISTS test;
DELETE FROM mysql.db WHERE Db = 'test' OR Db = 'test\\_%';
FLUSH PRIVILEGES;
SQL

In MariaDB, mysql.user is a view over mysql.global_priv, so delete from global_priv. A cleaner modern alternative to the first two lines is DROP USER IF EXISTS ''@'localhost', ''@'db1.example.lan'; for each anonymous account listed by SELECT user, host FROM mysql.user;.

Step 6: Apply a small hardening config

Do not edit the packaged mariadb-server.cnf; drop your own file in /etc/my.cnf.d/ instead so package updates never overwrite it. Files are read in alphabetical order, so a zz- prefix makes yours win:

sudo tee /etc/my.cnf.d/zz-local.cnf <<'EOF'
[mariadb]
# Listen on localhost only until remote access is planned (part 2)
bind-address = 127.0.0.1

# Do not resolve client host names; grants use IPs or subnets
skip-name-resolve = ON

# Block LOAD DATA LOCAL INFILE (reads arbitrary client files)
local-infile = 0

# Sensible defaults for new schemas
character-set-server = utf8mb4
collation-server     = utf8mb4_unicode_ci
EOF

sudo systemctl restart mariadb
sudo mariadb -e "SHOW GLOBAL VARIABLES WHERE Variable_name IN
  ('bind_address','skip_name_resolve','local_infile','character_set_server','collation_server');"

The [mariadb] group is read only by MariaDB servers, which keeps the file harmless if you ever switch to MySQL. With skip-name-resolve, accounts must be defined with IP addresses or subnets (for example 'app'@'192.168.1.%'), never host names; part 2 covers that when it opens the server to the network.

Step 7: Firewall and SELinux (only if clients are remote)

With bind-address = 127.0.0.1 nothing outside the host can connect, which is right for an application on the same server. When you are ready for remote clients, change bind-address to the server’s IP (or 0.0.0.0) and open the port only to the client subnet:

sudo firewall-cmd --permanent --new-zone=dbclients
sudo firewall-cmd --permanent --zone=dbclients --add-source=192.168.1.0/24
sudo firewall-cmd --permanent --zone=dbclients --add-service=mysql
sudo firewall-cmd --reload
sudo firewall-cmd --zone=dbclients --list-all

SELinux already labels port 3306 as mysqld_port_t. If you run the server on a different port, label it first or the service will fail to bind: sudo semanage port -a -t mysqld_port_t -p tcp 3307 (from policycoreutils-python-utils).

Step 8: Verify with a test database and least-privilege user

sudo mariadb <<'SQL'
CREATE DATABASE appdb;
CREATE USER 'appuser'@'localhost' IDENTIFIED BY 'Use-A-Long-Random-Password';
GRANT SELECT, INSERT, UPDATE, DELETE ON appdb.* TO 'appuser'@'localhost';
SQL

mariadb -u appuser -p appdb -e "CREATE TABLE t (id INT);"   # should FAIL: no CREATE privilege
mariadb -u appuser -p appdb -e "SELECT CURRENT_USER(), DATABASE();"

The first client command must fail with ERROR 1142 (42000): CREATE command denied, which proves the grant is as narrow as intended; the second must succeed. Then confirm the cleanup from Step 5:

sudo mariadb -e "SELECT user, host FROM mysql.user;"   # no empty user names, root only on localhost
sudo mariadb -e "SHOW DATABASES;"                       # no 'test'
mariadb -u nobody -e "SELECT 1;"                        # must be refused: Access denied

Alternative: install and secure MySQL 8.4 instead

If you need Oracle MySQL, install it on a host that does not have MariaDB packages:

sudo dnf install -y mysql8.4-server
sudo systemctl enable --now mysqld
systemctl status mysqld --no-pager
mysql -u root -e "SELECT VERSION();"

The AlmaLinux/RHEL package initializes the data directory with an empty root password on root@localhost (the upstream Oracle RPMs instead generate a temporary password and print it in the error log). So the first thing to do is run the hardening script, which also offers to enable the password-strength component:

sudo mysql_secure_installation
  • VALIDATE PASSWORD component: y, then policy 1 (MEDIUM) or 2 (STRONG). It installs component_validate_password, which rejects weak passwords for every account from then on.
  • New root password: set a long one and store it in your password manager. MySQL 8.4 uses caching_sha2_password by default; mysql_native_password is disabled unless you explicitly load it, so old clients may need updating.
  • Remove anonymous users, disallow remote root, remove test database, reload privileges: y to all four.

For MySQL, put the hardening options in a [mysqld] section, for example /etc/my.cnf.d/zz-local.cnf with bind-address = 127.0.0.1, skip-name-resolve = ON and local_infile = 0. MySQL 8.4 already defaults to utf8mb4 with the utf8mb4_0900_ai_ci collation, so you do not need to set the character set. Then sudo systemctl restart mysqld and verify as in Step 8, using the mysql client. The server log is /var/log/mysql/mysqld.log.

To store the root password for your own shell sessions without typing it, use an obfuscated login path instead of a plain-text ~/.my.cnf: mysql_config_editor set --login-path=local --host=localhost --user=root --password, then mysql --login-path=local.

What about the upstream MariaDB and MySQL repositories?

Both projects publish their own repositories for EL10 if you need a release AppStream does not carry, such as a MariaDB rolling release or MySQL 9.x Innovation. MariaDB provides mariadb_repo_setup (for example curl -LsS https://r.mariadb.com/downloads/mariadb_repo_setup | sudo bash -s -- --mariadb-server-version=mariadb-11.8) and names its packages MariaDB-server; MySQL provides a release RPM from its Yum repository page. Before you use either, disable the matching AppStream packages so dnf does not mix vendors (exclude=mariadb* mysql* in the AppStream repo, or dnf install --repo), and remember that security updates then come from the vendor, not from AlmaLinux. For most servers AppStream is the simpler and better-supported option. For moving between versions later, the older post Performing a MySQL Upgrade shows the dump-and-upgrade flow.

Verification checklist

  • systemctl is-enabled mariadb (or mysqld) prints enabled, and the service survives a reboot.
  • ss -ltnp | grep 3306 shows 127.0.0.1:3306 only, until you deliberately open it.
  • SELECT user, host FROM mysql.user; shows no empty user names and no root account outside localhost.
  • SHOW DATABASES; has no test database.
  • SELECT @@local_infile, @@skip_name_resolve; returns 0 and 1.
  • An application account can do exactly what its grants say and nothing more.

Troubleshooting tips

  • file /usr/bin/mysql from install of mysql8.4 conflicts with file from package mariadb: MariaDB and MySQL cannot share a host. Remove one family completely (sudo dnf remove 'mariadb*') before installing the other, and move or remove /var/lib/mysql first, because the on-disk formats are not interchangeable.
  • ERROR 1698 (28000): Access denied for user 'root'@'localhost' on MariaDB: you ran mariadb -u root as a normal user. root uses unix_socket, so run sudo mariadb.
  • ERROR 1045 (28000): Access denied for an application user: the account host does not match how the client connects. 'appuser'@'localhost' covers the Unix socket; TCP to 127.0.0.1 also matches localhost when skip-name-resolve is off, but with it on you need 'appuser'@'127.0.0.1' too.
  • Service fails to start after editing the config: check journalctl -u mariadb -b and /var/log/mariadb/mariadb.log (MySQL: /var/log/mysql/mysqld.log). An unknown variable name aborts startup; mariadbd --help --verbose | grep -i bind lists the valid option spellings.
  • Custom data directory will not start, “Permission denied” with correct ownership: SELinux label. Run sudo semanage fcontext -a -t mysqld_db_t "/data/mysql(/.*)?" and sudo restorecon -Rv /data/mysql, and set datadir and socket in both the [mariadb] and [client] groups.
  • Can't connect to local server through socket '/var/lib/mysql/mysql.sock': the server is not running, or it uses a different socket path than the client. Compare @@socket with the [client] section.
  • Remote clients time out: confirm bind-address is not 127.0.0.1, the firewalld zone includes the client source and the mysql service, and test with nc -vz db1.example.lan 3306 from the client.
  • Old client fails with authentication plugin caching_sha2_password cannot be loaded against MySQL 8.4: upgrade the client library (MariaDB Connector/C 3.1+ supports it), or connect over TLS. Re-enabling mysql_native_password is a last resort.

Cheat sheet

TaskCommand
List available serversdnf list --available 'mariadb*-server' 'mysql*-server'
Install MariaDB 10.11 / 11.8dnf install -y mariadb-server / mariadb11.8-server
Install MySQL 8.4dnf install -y mysql8.4-server
Start at bootsystemctl enable --now mariadb (MySQL: mysqld)
Root shell (MariaDB)sudo mariadb
Secure the installsudo mariadb-secure-installation / sudo mysql_secure_installation
Server versionmariadb-admin version or SELECT VERSION();
Local config file/etc/my.cnf.d/zz-local.cnf
Error log/var/log/mariadb/mariadb.log / /var/log/mysql/mysqld.log
List accountsSELECT user, host, plugin FROM mysql.user;
Open firewall to a subnetfirewall-cmd --permanent --zone=dbclients --add-service=mysql
Non-default port SELinuxsemanage port -a -t mysqld_port_t -p tcp 3307

Next in the series

Part 2 covers users, grants and remote access: admin and application accounts, roles, GRANT/REVOKE, host patterns with skip-name-resolve, and opening the server safely to an application subnet. Later parts cover backups with mysqldump and mariadb-backup, GTID replication (the older MySQL Replication CentOS All Databases post shows the classic file-and-position method), Galera Cluster, tuning and TLS.

Official references: RHEL 10: Using MariaDB, RHEL 10: Using MySQL, MariaDB: mariadb-secure-installation, MariaDB: Authentication from 10.4 and MySQL 8.4: mysql_secure_installation.

Leave a Reply

Your email address will not be published. Required fields are marked *