Type something to search...
How to secure a MySQL database server?

How to secure a MySQL database server?

To secure a MySQL database server, you keep it off the public internet, remove default accounts and test databases, give each application its own user with only the privileges it needs, require strong authentication and encrypted connections, harden the server configuration, and keep the software patched. You then add backups, logging, and monitoring so you can spot and recover from problems. Most of this takes less than an hour on a fresh server and pays off for years.

Your database holds the most valuable data on your site: user accounts, orders, content, and often personal information. This guide covers the practical steps for hardening MySQL 8.x (and, where they differ, MariaDB) on a Linux web server, from the first run of mysql_secure_installation through network binding, user privileges, TLS, configuration options, and backups. The commands assume Ubuntu or Debian, with RHEL equivalents noted where they're easy to give.

Why MySQL Security Matters

A web application talks to its database constantly, and attackers know it. The most common database-related risks are:

  • Exposed ports: A MySQL server listening on the public internet on port 3306 gets probed by automated scanners constantly, and weak passwords get guessed.
  • Over-privileged accounts: If your website connects as root, an SQL injection flaw in one plugin can give an attacker control of every database on the server.
  • Stolen credentials: Database passwords stored in readable config files or leaked in backups can be reused by anyone who finds them.
  • Unencrypted traffic: When the application and database run on different machines, credentials and data can be intercepted if connections aren't encrypted.
  • Unpatched software: Like any server software, MySQL and MariaDB receive security fixes that only protect you if you install them.

Securing the server doesn't fix vulnerable application code, but it drastically limits what an attacker can do if your application is ever compromised.

Step 1: Keep MySQL Updated

Start with a supported, patched version. On Ubuntu or Debian, MySQL and MariaDB updates arrive through the normal package manager:

sudo apt update
sudo apt upgrade
mysql --version

On RHEL, Rocky Linux, or AlmaLinux, use sudo dnf upgrade. If you installed MySQL from Oracle's official APT or YUM repository, updates come from there instead. Check that your major version is still supported: older branches such as MySQL 5.7 have reached end of life and no longer receive security fixes, so plan a migration to MySQL 8.x (an LTS release where possible) or a current MariaDB release.

Before any major upgrade, take a full backup and test the upgrade on a copy of your data.

Step 2: Run mysql_secure_installation

Both MySQL and MariaDB include a script that handles the most important first steps:

sudo mysql_secure_installation

The script walks you through:

  1. Password validation: MySQL can install the validate_password component to enforce minimum password strength. Choose at least the MEDIUM policy.
  2. Root password or authentication: On Ubuntu and Debian, the MySQL root account uses the auth_socket plugin by default, meaning only the Linux root user can log in as MySQL root, without a password. That's a secure default, so you can keep it.
  3. Removing anonymous users: Answer yes. Anonymous accounts let anyone connect without a username.
  4. Disallowing remote root login: Answer yes. Root should only ever connect from localhost.
  5. Removing the test database: Answer yes. The test database is accessible to any user by default.
  6. Reloading privilege tables: Answer yes so the changes apply immediately.

On MariaDB, the equivalent script is mariadb-secure-installation, and the root account typically uses unix_socket authentication.

Step 3: Keep MySQL Off the Public Internet

The single most effective protection is making sure nobody on the internet can connect to your database at all. If your website and database run on the same server, MySQL should only listen on localhost.

Edit the server configuration. On Ubuntu with MySQL, that's usually /etc/mysql/mysql.conf.d/mysqld.cnf; on MariaDB it's /etc/mysql/mariadb.conf.d/50-server.cnf; on RHEL-based systems it's /etc/my.cnf or a file in /etc/my.cnf.d/.

[mysqld]
# Only accept connections from the local machine
bind-address = 127.0.0.1

# Disable the X Protocol listener if you don't use it (MySQL 8 only)
mysqlx = OFF

If you don't need TCP connections at all, because every application connects through the Unix socket, you can go further:

[mysqld]
skip-networking

Only do this if you're sure nothing connects over TCP, including monitoring tools and applications configured with 127.0.0.1 rather than localhost.

Restart MySQL and confirm what it's listening on:

sudo systemctl restart mysql
sudo ss -tlnp | grep -E 'mysqld|mariadbd'

You want to see 127.0.0.1:3306 (and possibly 127.0.0.1:33060 if X Protocol is on), never 0.0.0.0:3306 or *:3306. On MariaDB the service name is mariadb.

When the Database Is on a Separate Server

If your application runs on a different server, bind MySQL to a private network address and use a firewall to allow only your application servers. Never open 3306 to the world.

With UFW on Ubuntu, allowing a single application server looks like this:

sudo ufw allow from 10.0.0.5 to any port 3306 proto tcp

If your hosting provider offers private networking or a VPC, use it so database traffic never crosses the public internet. Double-check that UFW is enabled and that you've already allowed SSH before enabling it, so you don't lock yourself out. On RHEL systems, the equivalent is a firewalld rich rule or a zone restricted to the application server's IP.

Step 4: Create Least-Privilege Users for Each Application

Every application should have its own MySQL user with access to only its own database. Never let a website connect as root.

Log in as the administrative user:

sudo mysql

Then create a database and a dedicated user:

CREATE DATABASE wordpress_site CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE USER 'wp_site'@'localhost' IDENTIFIED BY 'use-a-long-random-password-here';

GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, ALTER, INDEX, DROP,
      CREATE TEMPORARY TABLES, LOCK TABLES
  ON wordpress_site.* TO 'wp_site'@'localhost';

Notice a few things:

  • The host is localhost: The account can only connect from the same machine. For a remote app server, use its specific IP, like 'wp_site'@'10.0.0.5', never '%' unless you have a very good reason and strong network controls.
  • Privileges are scoped to one database: wordpress_site.* means the user can't touch other databases on the server.
  • No global privileges: The user doesn't get FILE, SUPER, PROCESS, GRANT OPTION, or other server-wide rights.

WordPress needs CREATE, ALTER, and DROP for core and plugin updates that change tables. For simpler applications that never change their schema, you can grant only SELECT, INSERT, UPDATE, DELETE and run migrations with a separate, more privileged account.

Generate a strong password with a tool like:

openssl rand -base64 32

Review Existing Users and Privileges

On an older server, audit what already exists:

SELECT user, host, plugin FROM mysql.user ORDER BY user;

SHOW GRANTS FOR 'wp_site'@'localhost';

Look for accounts with a host of %, accounts you don't recognise, and users with ALL PRIVILEGES ON *.*. Remove anything that isn't needed:

DROP USER 'old_app'@'%';

Read-Only Users for Reporting

If someone needs to run reports or connect a dashboard, give them a separate read-only account instead of sharing the application's credentials:

CREATE USER 'reporting'@'10.0.0.8' IDENTIFIED BY 'another-long-random-password';
GRANT SELECT ON wordpress_site.* TO 'reporting'@'10.0.0.8';

Step 5: Use Strong Authentication

MySQL 8.0 and later use the caching_sha2_password plugin by default, which is much stronger than the old mysql_native_password. Legacy native password authentication is deprecated and disabled by default in recent MySQL releases, so check whether any of your accounts still rely on it:

SELECT user, host, plugin FROM mysql.user WHERE plugin = 'mysql_native_password';

If you find any, switch them to the modern plugin (after confirming your application's client library supports it, which current PHP versions do):

ALTER USER 'wp_site'@'localhost'
  IDENTIFIED WITH caching_sha2_password BY 'use-a-long-random-password-here';

MariaDB handles authentication differently. It supports mysql_native_password, unix_socket, and ed25519 among others; ed25519 is a good choice for password accounts where your client libraries support it.

You can also make MySQL lock accounts after repeated failed logins, which slows down password guessing:

ALTER USER 'wp_site'@'localhost'
  FAILED_LOGIN_ATTEMPTS 5 PASSWORD_LOCK_TIME 1;

This locks the account for one day after five consecutive failures. Be careful with this on application accounts, because a misconfigured app could lock itself out and take your site down. It's most useful on human accounts.

Step 6: Encrypt Connections With TLS

If your application and database are on the same machine and connect over the socket or localhost, TLS adds little. But as soon as traffic crosses a network, even a "private" one, you should encrypt it.

MySQL 8 generates self-signed certificates automatically on first start, so TLS is often already available. Check with:

SHOW VARIABLES LIKE 'have_ssl';
SHOW VARIABLES LIKE 'tls_version';

To require encrypted connections for all TCP clients, add this to the server configuration:

[mysqld]
require_secure_transport = ON
tls_version = TLSv1.2,TLSv1.3

You can also require TLS for specific accounts:

ALTER USER 'wp_site'@'10.0.0.5' REQUIRE SSL;

For production setups spanning multiple servers, consider issuing certificates from your own internal certificate authority so clients can verify the server's identity rather than simply trusting a self-signed certificate. On the WordPress side, you can enable TLS with the MYSQL_CLIENT_FLAGS constant in wp-config.php:

define( 'MYSQL_CLIENT_FLAGS', MYSQLI_CLIENT_SSL );

Step 7: Harden the Server Configuration

Several configuration options reduce the attack surface further. Add these to the [mysqld] section of your configuration file:

[mysqld]
# Block LOAD DATA LOCAL INFILE, which lets clients read local files
local_infile = 0

# Restrict file import/export to a single directory (or disable with NULL)
secure_file_priv = /var/lib/mysql-files

# Don't resolve client hostnames (faster and avoids DNS-based tricks)
skip_name_resolve = ON

# Don't reveal databases the user has no access to
skip_show_database

Here's what each does:

  1. local_infile = 0: Prevents LOAD DATA LOCAL INFILE, a feature that can be abused to read files from the client machine. Most web applications never need it.
  2. secure_file_priv: Limits SELECT ... INTO OUTFILE and LOAD DATA INFILE to one directory. This matters because an SQL injection with the FILE privilege could otherwise write files, including web shells, into your web root. Setting it to an empty string removes the restriction, so never do that.
  3. skip_name_resolve: Tells MySQL to match users by IP address only. If you use this, make sure your user accounts use IP addresses or localhost, not hostnames.
  4. skip_show_database: Stops users without the SHOW DATABASES privilege from listing databases.

Restart MySQL after making changes and check the error log if it fails to start:

sudo systemctl restart mysql
sudo journalctl -u mysql --since "10 minutes ago"

Step 8: Protect Files and Credentials on Disk

Database security extends to the files on the server:

  • Data directory permissions: The data directory (usually /var/lib/mysql) should be owned by the mysql user and not readable by others. Package installs set this correctly, so check you haven't loosened it.
  • Application config files: wp-config.php or .env files containing database passwords should be readable only by the site's user. Permissions of 600 or 640 are typical.
  • Client option files: If you store credentials in ~/.my.cnf for scripts, set its permissions to 600.
  • Command history: Avoid typing passwords directly on the command line with -p'password', since they can end up in your shell history and be visible in the process list. Let mysql prompt you, or use an option file.

For automated scripts, MySQL's mysql_config_editor stores credentials in an obfuscated login path file, which is better than plain text in scripts:

mysql_config_editor set --login-path=backup --host=localhost --user=backup_user --password

Consider encryption at rest if your data is sensitive or you're subject to compliance requirements. MySQL 8 supports InnoDB tablespace encryption with a keyring component, and full-disk encryption at the infrastructure level is another option many cloud providers offer.

Step 9: Back Up Securely

Backups protect you from ransomware, bad updates, and accidental deletions, but they can also leak your entire database if handled carelessly.

A simple, consistent backup with mysqldump for InnoDB tables:

mysqldump --single-transaction --routines --triggers \
  --login-path=backup wordpress_site | gzip > /var/backups/mysql/wordpress_site-$(date +%F).sql.gz
chmod 600 /var/backups/mysql/*.sql.gz

Create a dedicated backup user with only the privileges it needs:

CREATE USER 'backup_user'@'localhost' IDENTIFIED BY 'long-random-backup-password';
GRANT SELECT, SHOW VIEW, TRIGGER, LOCK TABLES, EVENT, PROCESS ON *.* TO 'backup_user'@'localhost';

Then follow a few rules:

  • Never store backups in the web root: A .sql file in public_html can be downloaded by anyone who guesses its name.
  • Copy backups off the server: Keep at least one copy in a separate location, ideally with versioning or immutability so an attacker who gets into the server can't delete them.
  • Encrypt off-site copies: Tools like gpg or age, or your backup service's built-in encryption, protect data if storage is ever exposed.
  • Test restores: A backup you've never restored is a guess. Restore to a test server regularly.

For larger databases, look at physical backup tools such as Percona XtraBackup for MySQL or Mariabackup for MariaDB.

Step 10: Enable Logging and Monitoring

Logs help you notice problems early and investigate them afterwards.

  • Error log: Always on. Check it after restarts and when anything behaves strangely.
  • Slow query log: Useful for performance, and sometimes reveals unusual queries from injection attempts.
  • General query log: Records every query. It's useful for short debugging sessions but grows very quickly and can capture sensitive data, so don't leave it on in production.
  • Audit logging: For compliance needs, MySQL Enterprise Audit, the Percona audit log plugin, or MariaDB's server_audit plugin can record logins and queries.

Enable the slow query log in your config:

[mysqld]
slow_query_log = ON
slow_query_log_file = /var/log/mysql/mysql-slow.log
long_query_time = 2

Also watch for failed login attempts in the error log. If your server must be reachable from other machines, Fail2Ban can be configured to watch that log and block repeat offenders, although restricting access by firewall is far more effective.

Step 11: Protect Against SQL Injection at the Application Layer

Server hardening limits the damage, but the real defence against SQL injection is in your code. Classic injection happens when user input like ' OR 1=1 -- is pasted directly into a query. Always use prepared statements.

In plain PHP with PDO:

<?php
$pdo = new PDO(
    'mysql:host=localhost;dbname=wordpress_site;charset=utf8mb4',
    'wp_site',
    getenv( 'DB_PASSWORD' ),
    array(
        PDO::ATTR_ERRMODE          => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_EMULATE_PREPARES => false,
    )
);

$stmt = $pdo->prepare( 'SELECT id, email FROM customers WHERE email = :email' );
$stmt->execute( array( ':email' => $_POST['email'] ?? '' ) );
$customer = $stmt->fetch( PDO::FETCH_ASSOC );

In WordPress, use $wpdb->prepare() for any query that includes variable data:

global $wpdb;

$results = $wpdb->get_results(
    $wpdb->prepare(
        "SELECT ID, post_title FROM {$wpdb->posts} WHERE post_author = %d AND post_status = %s",
        absint( $author_id ),
        'publish'
    )
);

Combined with a least-privilege database user, prepared statements mean that even if something slips through, the attacker's options are extremely limited.

A Quick MySQL Security Checklist

  1. Patched and supported: MySQL or MariaDB is on a maintained release with updates applied.
  2. Secure installation done: No anonymous users, no test database, no remote root.
  3. Not publicly reachable: Bound to localhost or a private IP, with a firewall in front.
  4. One user per application: Scoped to a single database with minimal privileges.
  5. Modern authentication: caching_sha2_password (MySQL) or ed25519/unix_socket (MariaDB).
  6. TLS for network traffic: Required whenever connections leave the machine.
  7. Hardened config: local_infile off, secure_file_priv set.
  8. Credentials protected: Config files locked down, no passwords in shell history.
  9. Backups encrypted, off-site, and tested.
  10. Logs reviewed: Error log monitored, audit logging where required.

FAQ: Securing MySQL

Almost never. Bind MySQL to 127.0.0.1 when the application runs on the same server, or to a private network address with a firewall rule that only allows your application servers. Publicly exposed database ports are scanned and attacked constantly.

No. Create a dedicated user for each WordPress site with privileges limited to that site's database. If a plugin has an SQL injection flaw, a scoped user keeps the attacker away from every other database on the server.

It sets up password validation, secures the root account, removes anonymous users, disables remote root login, deletes the test database, and reloads the privilege tables. On MariaDB the equivalent command is mariadb-secure-installation.

Usually not. Connections over the local Unix socket or 127.0.0.1 never leave the machine. TLS becomes important as soon as the application and database are on different servers, even on a private network.

Mostly, yes. Network binding, least-privilege users, local_infile, secure_file_priv, backups, and logging all work the same way. The main differences are authentication plugins, some variable names, and service and script names such as mariadb and mariadb-secure-installation.

It depends on how often your data changes. A busy store may need hourly or continuous backups, while a small blog may be fine with daily ones. Whatever the schedule, keep copies off the server, encrypt them, and test restores regularly.

Not on its own. SQL injection is an application flaw, fixed with prepared statements such as PDO or $wpdb->prepare(). Server hardening and least-privilege users limit the damage an injection can do.


Conclusion

Securing a MySQL server comes down to a few principles applied consistently: keep it off the public internet, give every application only the access it needs, use strong authentication and encryption, tighten risky configuration options, and keep the software patched. Each layer covers gaps in the others, so an attacker who gets past one still has to deal with the rest.

Work through the steps in order on a new server, or use the checklist to audit an existing one, and always take a backup before changing users or configuration. Once the basics are in place, schedule a quick review every few months to catch forgotten accounts, outdated versions, and backups that haven't been tested in a while.

Tags :
Share :

Related Posts

What are the best WordPress security plugins?

What are the best WordPress security plugins?

The best WordPress security plugins for most sites are Wordfence, Sucuri Security, Solid Security, MalCare, All-In-One Security (AIOS), Patchstack, a

Dive Deeper
What are the most common website security threats?

What are the most common website security threats?

The most common website security threats are vulnerable or outdated software, weak and stolen passwords, malware infections, injection attacks like S

Dive Deeper
How does GDPR affect website security?

How does GDPR affect website security?

GDPR affects website security by turning it from a good habit into a legal obligation. If your website collects personal data from people in the EU (

Dive Deeper