MariaDB Core Security Model
How MariaDB authenticates users, stores privileges, activates roles, encrypts data at rest, and protects connections with TLS. Read this skill before creating any user, before enabling encryption, and before assuming MySQL security commands map one-to-one onto MariaDB.
Quick Reference
- ed25519 is the modern default authentication plug-in. mysql_native_password (SHA-1) is legacy ; use it only for clients that cannot speak ed25519.
- Privileges live in
mysql.global_priv since 10.4+. mysql.user is now a backward-compatibility view over global_priv, NOT the authoritative table.
- Encryption at rest needs ALL of :
innodb_encrypt_tables, innodb_encrypt_log, encrypt_binlog, encrypt_tmp_files, aria_encrypt_tables. Encrypting only tables leaks data through the binlog and temp files.
- Roles exist since 10.0.5. ALWAYS pair
GRANT <role> TO <user> with SET DEFAULT ROLE <role> FOR <user> so the user gets the role on connect.
- TLS requires BOTH
REQUIRE SSL (or stricter) on the GRANT AND ssl_cert / ssl_key / ssl_ca server variables. One without the other fails open or fails closed.
- ALWAYS run
mariadb-upgrade exactly once after a major version upgrade to migrate mysql.user rows into mysql.global_priv. NEVER run it twice in a row.
- NEVER
GRANT ALL ON *.* TO 'app_user'@'%'. ALL includes SUPER, FILE, PROCESS, RELOAD, SHUTDOWN. An SQL-injection in any code path escalates to server-level.
- File-key-management gained
file_key_management_use_pbkdf2 and file_key_management_digest in 12.0.1+ for key-derivation hardening.
- ed25519 password hashing requires the
ed25519_password() UDF or USING PASSWORD('secret') shortcut. The legacy PASSWORD() function does NOT work with ed25519.
Authentication Plug-ins
MariaDB authentication is plug-in based. The server consults the plugin column of mysql.global_priv (the JSON Priv.plugin field, since 10.4+) to pick a verification routine per user.
| Plug-in |
Algorithm / Mechanism |
Use when |
Avoid when |
mysql_native_password |
SHA-1 challenge-response |
Legacy clients that cannot upgrade |
New accounts, security-sensitive deployments |
ed25519 |
Elliptic Curve DSA (same as OpenSSH) |
New accounts on 10.1.21+ |
Clients on a very old MariaDB client library |
unix_socket |
Maps OS uid to DB identity |
Local root access on Debian / Ubuntu, automation scripts running as a specific OS user |
Network access ; the plug-in fails closed on TCP |
gssapi |
Kerberos / SSPI |
Active Directory / enterprise SSO |
Standalone servers without an AD domain |
pam |
Pluggable Authentication Modules |
Centralised Linux PAM (LDAP, OTP, etc.) |
Environments without PAM tooling |
Decision rule :
- ALWAYS use
ed25519 for new application users on 10.1.21+.
- ALWAYS use
unix_socket for local root access on Debian / Ubuntu installs (this is the package default).
- NEVER create new accounts with
mysql_native_password on a greenfield deployment.
- NEVER mix
unix_socket with 'root'@'%' ; unix_socket rejects TCP connections.
Install and use ed25519
-- 10.1.21+ : dynamic install, no restart needed
INSTALL SONAME 'auth_ed25519';
-- Or persistent via my.cnf
-- [mariadb]
-- plugin_load_add = auth_ed25519
-- Create a user with ed25519 (server hashes 'secret' for you)
CREATE USER 'alice'@'%' IDENTIFIED VIA ed25519 USING PASSWORD('secret');
-- Or supply a pre-computed hash (computed via ed25519_password() UDF)
CREATE USER 'bob'@'%' IDENTIFIED VIA ed25519
USING 'ZIgUREUg5PVgQ6LskhXmO+eZLS0nC8be6HPjYWR4YJY';
User Schema : mysql.user vs mysql.global_priv
| Version |
Authoritative table |
Backward-compat view |
Notes |
| 10.3 and earlier |
mysql.user (column-per-privilege) |
none |
Legacy schema, one BOOLEAN column per global priv |
| 10.4+ |
mysql.global_priv (JSON in Priv) |
mysql.user (read mostly) |
All global privs stored in JSON ; mysql.user updates partly compatible |
| 10.5.2+ |
mysql.global_priv with version_id field |
mysql.user view |
Priv.version_id tracks the server that last wrote the row |
The mysql.global_priv table has three columns : Host CHAR(60), User CHAR(80), Priv LONGTEXT (a JSON document). The Priv JSON holds at minimum access (numeric bitmask), plugin (auth plug-in name), authentication_string (password hash), and optionally password_last_changed, account_locked, version_id.
Decision rule :
- ALWAYS query
mysql.global_priv directly when inspecting accounts on 10.4+. The mysql.user view drops fields and can hide account state (account_locked, password_last_changed).
- ALWAYS run
mariadb-upgrade once after upgrading from 10.3 to 10.4+ to migrate rows ; the server boots without this but new privileges may not work as expected.
- NEVER hand-edit
mysql.global_priv.Priv JSON to set access ; use GRANT / REVOKE so the in-memory grant cache is refreshed.
- NEVER assume third-party tools that grep
mysql.user see the full picture on 10.4+.
GRANT Model
Full grant syntax (per MariaDB KB grant) :
GRANT priv_type [(column_list)] [, priv_type ...]
ON [object_type] priv_level
TO account_or_role [, account_or_role ...]
[REQUIRE {NONE | tls_option ...}]
[WITH grant_option_list]
| Element |
Possible values |
Notes |
priv_type |
SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, INDEX, REFERENCES, EXECUTE, EVENT, TRIGGER, SUPER, FILE, PROCESS, RELOAD, SHUTDOWN, REPLICATION CLIENT, REPLICATION SLAVE, GRANT OPTION, etc. |
ALL [PRIVILEGES] is a shortcut for all of them ; avoid in production |
priv_level |
*.* (global), db_name.* (db), db_name.tbl_name (table), tbl_name, function or procedure scope |
Smaller scope is always preferable |
REQUIRE |
NONE, SSL, X509, SUBJECT 'subject', ISSUER 'issuer', CIPHER 'cipher', combinable with AND |
SUBJECT and ISSUER imply X509 ; CIPHER implies SSL |
WITH grant_option_list |
GRANT OPTION, MAX_QUERIES_PER_HOUR n, MAX_UPDATES_PER_HOUR n, MAX_CONNECTIONS_PER_HOUR n, MAX_USER_CONNECTIONS n, MAX_STATEMENT_TIME seconds |
Resource limits enforced server-side, reset hourly |
Decision rule :
- ALWAYS scope to the smallest necessary
priv_level : db_name.* for an application user, db_name.tbl_name for a service-specific user.
- ALWAYS prefer a role with the right privileges over a long per-user
GRANT list.
- NEVER include
WITH GRANT OPTION on application users ; reserve it for admin accounts.
- NEVER use
'%' host on a privileged account ; pin to a known host or subnet.
Roles (10.0.5+)
Roles are named privilege bundles. They activate explicitly with SET ROLE or implicitly on connect via SET DEFAULT ROLE.
-- 10.0.5+ : create a role, grant privileges to it, assign to a user
CREATE ROLE app_read;
GRANT SELECT ON app.* TO app_read;
GRANT app_read TO 'alice'@'%';
-- Make the role active automatically on connect
SET DEFAULT ROLE app_read FOR 'alice'@'%';
-- Switch role inside a session
SET ROLE app_read;
SET ROLE NONE; -- deactivate all roles
Decision rule :
- ALWAYS set a default role for any user that you grant roles to. Without
SET DEFAULT ROLE, the user connects with zero role-derived privileges and has to call SET ROLE manually.
- ALWAYS grant privileges TO the role, then grant the role TO the user. Mixing direct user grants with role grants makes audit harder.
- NEVER assume nested roles propagate on activation : only roles granted directly to a user can be set with
SET ROLE. Privileges from nested roles still cascade, but the role name itself does not.
- NEVER grant
WITH ADMIN OPTION to ordinary users : it lets them re-grant the role to anyone.
Encryption at Rest
Three plug-ins are available : file_key_management (local key file, default and most common), aws_key_management (AWS KMS), and HashiCorp Vault via third-party plug-in.
Minimum file-key-management config
# my.cnf : MariaDB 10.6+, all encryption surfaces ON
[mariadb]
plugin_load_add = file_key_management
loose_file_key_management_filename = /etc/mysql/encryption/keyfile.enc
loose_file_key_management_filekey = FILE:/etc/mysql/encryption/keyfile.key
loose_file_key_management_encryption_algorithm = AES_CTR
# 12.0.1+ : harden key derivation
loose_file_key_management_use_pbkdf2 = 1000
loose_file_key_management_digest = sha256
# Encrypt every surface : tables, redo log, binlog, temp files, aria
innodb_encrypt_tables = ON
innodb_encrypt_log = ON
innodb_encrypt_temporary_tables = ON
encrypt_binlog = ON
encrypt_tmp_files = ON
encrypt_tmp_disk_tables = ON
aria_encrypt_tables = ON
| Surface |
Variable |
Effect if OFF |
| InnoDB tablespaces |
innodb_encrypt_tables (ON, OFF, FORCE) |
Unencrypted .ibd files on disk |
| InnoDB redo log |
innodb_encrypt_log |
Plaintext redo log entries reveal recent row changes |
| InnoDB internal temp tables |
innodb_encrypt_temporary_tables |
Plaintext intermediate join / sort results |
| Binary log |
encrypt_binlog |
Plaintext row images in binlog ; key data leak for replicas |
| MyISAM / temp files |
encrypt_tmp_files |
Plaintext sort / aggregate spill files |
| Disk-based temp tables |
encrypt_tmp_disk_tables |
Plaintext temp tables once they spill to disk |
| Aria engine |
aria_encrypt_tables |
Plaintext Aria tablespaces (used by system tables) |
Key file format :
# /etc/mysql/encryption/keyfile.enc (community / enterprise <11.8)
<key_id>;<hex_encoded_key>
# Example
1;a7addd9adea9978fda19f21e6be987880e68ac92632ca052e5bb42b1a506939a
# Enterprise 11.8+ adds a version column
<key_id>;<version>;<hex_encoded_key>
Decision rule :
- ALWAYS encrypt every surface listed above when enabling at-rest encryption. Partial encryption leaks via the unprotected surfaces.
- ALWAYS chmod the key file to
0400 mysql:mysql. World-readable key files defeat the purpose.
- ALWAYS enable
use_pbkdf2 and digest=sha256 on 12.0.1+ for new deployments. PBKDF2 raises the cost of an offline key-file attack.
- NEVER store the key file on the same disk as the encrypted data without a separate access boundary (preferably a different volume mount with its own ACL).
- NEVER set
innodb_encrypt_tables=FORCE without first re-encrypting existing tables ; new writes will succeed but the existing tables remain plaintext.
TLS for Client Connections
Server-side TLS requires three variables in my.cnf, plus per-user REQUIRE on every account that should be TLS-only.
# my.cnf : enable server-side TLS (MariaDB 10.6+)
[mariadb]
ssl_ca = /etc/mysql/certs/ca.pem
ssl_cert = /etc/mysql/certs/server-cert.pem
ssl_key = /etc/mysql/certs/server-key.pem
tls_version = TLSv1.2,TLSv1.3
-- Per-user TLS enforcement (combine with the server config above)
GRANT SELECT ON app.* TO 'alice'@'%' REQUIRE SSL;
-- Strict X509 with cert-pinning
GRANT SELECT ON app.* TO 'bob'@'%'
REQUIRE SUBJECT '/CN=bob/O=Acme/C=NL'
AND ISSUER '/C=FI/O=Acme CA/CN=Root'
AND CIPHER 'ECDHE-RSA-AES256-GCM-SHA384';
Decision rule :
- ALWAYS set
ssl_ca / ssl_cert / ssl_key on the server AND REQUIRE SSL (or stricter) on the user. The server config alone makes TLS available ; the GRANT clause makes it mandatory.
- ALWAYS pin
tls_version to TLSv1.2,TLSv1.3. Older TLS (1.0, 1.1) is broken.
- NEVER ship
ssl_cert without the full certificate chain ; TLS handshake fails silently with intermediate-cert clients.
- NEVER use
REQUIRE SSL without checking that the client driver actually validates the server cert. Many drivers default to no verification.
TLS for Replication
-- On the replica
STOP SLAVE;
CHANGE MASTER TO
MASTER_SSL = 1,
MASTER_SSL_CA = '/etc/mysql/certs/ca.pem',
MASTER_SSL_CERT = '/etc/mysql/certs/replica-cert.pem',
MASTER_SSL_KEY = '/etc/mysql/certs/replica-key.pem',
MASTER_SSL_VERIFY_SERVER_CERT = 1;
START SLAVE;
-- 12.3+ shortcut : inherit server-level TLS config
CHANGE MASTER TO
MASTER_SSL = 1,
MASTER_SSL_CA = DEFAULT,
MASTER_SSL_CERT = DEFAULT,
MASTER_SSL_KEY = DEFAULT,
MASTER_SSL_VERIFY_SERVER_CERT = 1;
Decision rule :
- ALWAYS set
MASTER_SSL_VERIFY_SERVER_CERT = 1 ; without it, the connection is encrypted but MITM-vulnerable.
- ALWAYS use the
DEFAULT keyword on 12.3+ to reuse server-level TLS config and avoid per-channel cert duplication.
- NEVER omit
MASTER_SSL_CA ; without a CA the replica trusts any presented cert.
Hardening Checklist
Run through this checklist after every fresh install :
DROP USER ''@'localhost' and any other anonymous accounts.
DROP USER 'root'@'%' ; keep 'root'@'localhost' only.
- Set
'root'@'localhost' IDENTIFIED VIA unix_socket on Debian / Ubuntu.
- Create one application user per database with
ed25519 and REQUIRE SSL.
- Create roles per access tier (read-only, app-write, admin) and grant roles to users.
- Enable file-key-management with PBKDF2 (12.0.1+) and chmod the key file
0400 mysql:mysql.
- Enable encryption on tables, log, binlog, tmp files, aria.
- Configure
ssl_ca / ssl_cert / ssl_key server-side and tls_version=TLSv1.2,TLSv1.3.
- Grant minimum privileges per role ; NEVER
GRANT ALL ON *.*.
- Run
mariadb-upgrade once after the install or upgrade to migrate any stale mysql.user rows.
Reference Links
1---2name: mariadb-core-security-model3description: Use when creating users, granting privileges, choosing an authentication plug-in, enabling roles, setting up encryption at rest or TLS, or migrating user schema from MySQL. Prevents the common mistake of using mysql_native_password instead of ed25519, forgetting to enable encryption on binlog / tmp / redo as well as tables, or losing access after upgrading mysql.user to mysql.global_priv. Covers authentication plug-ins (mysql_native_password, ed25519, gssapi, pam, unix_socket), CREATE USER and GRANT model, roles since 10.0.5, encryption at rest via file-key-management plug-in, TLS for client and replication, mysql.user vs mysql.global_priv shift in 10.4+. Keywords: mariadb security, CREATE USER, GRANT, REVOKE, roles, SET DEFAULT ROLE, ed25519, mysql_native_password, gssapi, pam, unix_socket, encryption at rest, file-key-management, TLS, SSL, REQUIRE SSL, mysql.user, mysql.global_priv, access denied, authentication failure, how do I add a user, secure mariadb, file_key_management_use_pbkdf2, file_key_managemen4license: MIT5---67# MariaDB Core Security Model89How MariaDB authenticates users, stores privileges, activates roles, encrypts data at rest, and protects connections with TLS. Read this skill before creating any user, before enabling encryption, and before assuming MySQL security commands map one-to-one onto MariaDB.1011## Quick Reference1213- ed25519 is the modern default authentication plug-in. mysql_native_password (SHA-1) is legacy ; use it only for clients that cannot speak ed25519.14- Privileges live in `mysql.global_priv` since 10.4+. `mysql.user` is now a backward-compatibility view over `global_priv`, NOT the authoritative table.15- Encryption at rest needs ALL of : `innodb_encrypt_tables`, `innodb_encrypt_log`, `encrypt_binlog`, `encrypt_tmp_files`, `aria_encrypt_tables`. Encrypting only tables leaks data through the binlog and temp files.16- Roles exist since 10.0.5. ALWAYS pair `GRANT <role> TO <user>` with `SET DEFAULT ROLE <role> FOR <user>` so the user gets the role on connect.17- TLS requires BOTH `REQUIRE SSL` (or stricter) on the `GRANT` AND `ssl_cert` / `ssl_key` / `ssl_ca` server variables. One without the other fails open or fails closed.18- ALWAYS run `mariadb-upgrade` exactly once after a major version upgrade to migrate `mysql.user` rows into `mysql.global_priv`. NEVER run it twice in a row.19- NEVER `GRANT ALL ON *.* TO 'app_user'@'%'`. `ALL` includes `SUPER`, `FILE`, `PROCESS`, `RELOAD`, `SHUTDOWN`. An SQL-injection in any code path escalates to server-level.20- File-key-management gained `file_key_management_use_pbkdf2` and `file_key_management_digest` in 12.0.1+ for key-derivation hardening.21- ed25519 password hashing requires the `ed25519_password()` UDF or `USING PASSWORD('secret')` shortcut. The legacy `PASSWORD()` function does NOT work with ed25519.2223## Authentication Plug-ins2425MariaDB authentication is plug-in based. The server consults the `plugin` column of `mysql.global_priv` (the JSON `Priv.plugin` field, since 10.4+) to pick a verification routine per user.2627| Plug-in | Algorithm / Mechanism | Use when | Avoid when |28|---------|-----------------------|----------|------------|29| `mysql_native_password` | SHA-1 challenge-response | Legacy clients that cannot upgrade | New accounts, security-sensitive deployments |30| `ed25519` | Elliptic Curve DSA (same as OpenSSH) | New accounts on 10.1.21+ | Clients on a very old MariaDB client library |31| `unix_socket` | Maps OS uid to DB identity | Local `root` access on Debian / Ubuntu, automation scripts running as a specific OS user | Network access ; the plug-in fails closed on TCP |32| `gssapi` | Kerberos / SSPI | Active Directory / enterprise SSO | Standalone servers without an AD domain |33| `pam` | Pluggable Authentication Modules | Centralised Linux PAM (LDAP, OTP, etc.) | Environments without PAM tooling |3435**Decision rule** :36- ALWAYS use `ed25519` for new application users on 10.1.21+.37- ALWAYS use `unix_socket` for local `root` access on Debian / Ubuntu installs (this is the package default).38- NEVER create new accounts with `mysql_native_password` on a greenfield deployment.39- NEVER mix `unix_socket` with `'root'@'%'` ; `unix_socket` rejects TCP connections.4041### Install and use ed255194243```sql44-- 10.1.21+ : dynamic install, no restart needed45INSTALL SONAME 'auth_ed25519';4647-- Or persistent via my.cnf48-- [mariadb]49-- plugin_load_add = auth_ed255195051-- Create a user with ed25519 (server hashes 'secret' for you)52CREATE USER 'alice'@'%' IDENTIFIED VIA ed25519 USING PASSWORD('secret');5354-- Or supply a pre-computed hash (computed via ed25519_password() UDF)55CREATE USER 'bob'@'%' IDENTIFIED VIA ed2551956 USING 'ZIgUREUg5PVgQ6LskhXmO+eZLS0nC8be6HPjYWR4YJY';57```5859## User Schema : mysql.user vs mysql.global_priv6061| Version | Authoritative table | Backward-compat view | Notes |62|---------|---------------------|----------------------|-------|63| 10.3 and earlier | `mysql.user` (column-per-privilege) | none | Legacy schema, one BOOLEAN column per global priv |64| 10.4+ | `mysql.global_priv` (JSON in `Priv`) | `mysql.user` (read mostly) | All global privs stored in JSON ; `mysql.user` updates partly compatible |65| 10.5.2+ | `mysql.global_priv` with `version_id` field | `mysql.user` view | `Priv.version_id` tracks the server that last wrote the row |6667The `mysql.global_priv` table has three columns : `Host CHAR(60)`, `User CHAR(80)`, `Priv LONGTEXT` (a JSON document). The `Priv` JSON holds at minimum `access` (numeric bitmask), `plugin` (auth plug-in name), `authentication_string` (password hash), and optionally `password_last_changed`, `account_locked`, `version_id`.6869**Decision rule** :70- ALWAYS query `mysql.global_priv` directly when inspecting accounts on 10.4+. The `mysql.user` view drops fields and can hide account state (`account_locked`, `password_last_changed`).71- ALWAYS run `mariadb-upgrade` once after upgrading from 10.3 to 10.4+ to migrate rows ; the server boots without this but new privileges may not work as expected.72- NEVER hand-edit `mysql.global_priv.Priv` JSON to set `access` ; use `GRANT` / `REVOKE` so the in-memory grant cache is refreshed.73- NEVER assume third-party tools that grep `mysql.user` see the full picture on 10.4+.7475## GRANT Model7677Full grant syntax (per MariaDB KB `grant`) :7879```sql80GRANT priv_type [(column_list)] [, priv_type ...]81 ON [object_type] priv_level82 TO account_or_role [, account_or_role ...]83 [REQUIRE {NONE | tls_option ...}]84 [WITH grant_option_list]85```8687| Element | Possible values | Notes |88|---------|-----------------|-------|89| `priv_type` | `SELECT`, `INSERT`, `UPDATE`, `DELETE`, `CREATE`, `DROP`, `ALTER`, `INDEX`, `REFERENCES`, `EXECUTE`, `EVENT`, `TRIGGER`, `SUPER`, `FILE`, `PROCESS`, `RELOAD`, `SHUTDOWN`, `REPLICATION CLIENT`, `REPLICATION SLAVE`, `GRANT OPTION`, etc. | `ALL [PRIVILEGES]` is a shortcut for all of them ; avoid in production |90| `priv_level` | `*.*` (global), `db_name.*` (db), `db_name.tbl_name` (table), `tbl_name`, function or procedure scope | Smaller scope is always preferable |91| `REQUIRE` | `NONE`, `SSL`, `X509`, `SUBJECT 'subject'`, `ISSUER 'issuer'`, `CIPHER 'cipher'`, combinable with `AND` | `SUBJECT` and `ISSUER` imply `X509` ; `CIPHER` implies `SSL` |92| `WITH grant_option_list` | `GRANT OPTION`, `MAX_QUERIES_PER_HOUR n`, `MAX_UPDATES_PER_HOUR n`, `MAX_CONNECTIONS_PER_HOUR n`, `MAX_USER_CONNECTIONS n`, `MAX_STATEMENT_TIME seconds` | Resource limits enforced server-side, reset hourly |9394**Decision rule** :95- ALWAYS scope to the smallest necessary `priv_level` : `db_name.*` for an application user, `db_name.tbl_name` for a service-specific user.96- ALWAYS prefer a role with the right privileges over a long per-user `GRANT` list.97- NEVER include `WITH GRANT OPTION` on application users ; reserve it for admin accounts.98- NEVER use `'%'` host on a privileged account ; pin to a known host or subnet.99100## Roles (10.0.5+)101102Roles are named privilege bundles. They activate explicitly with `SET ROLE` or implicitly on connect via `SET DEFAULT ROLE`.103104```sql105-- 10.0.5+ : create a role, grant privileges to it, assign to a user106CREATE ROLE app_read;107GRANT SELECT ON app.* TO app_read;108GRANT app_read TO 'alice'@'%';109110-- Make the role active automatically on connect111SET DEFAULT ROLE app_read FOR 'alice'@'%';112113-- Switch role inside a session114SET ROLE app_read;115SET ROLE NONE; -- deactivate all roles116```117118**Decision rule** :119- ALWAYS set a default role for any user that you grant roles to. Without `SET DEFAULT ROLE`, the user connects with zero role-derived privileges and has to call `SET ROLE` manually.120- ALWAYS grant privileges TO the role, then grant the role TO the user. Mixing direct user grants with role grants makes audit harder.121- NEVER assume nested roles propagate on activation : only roles granted directly to a user can be set with `SET ROLE`. Privileges from nested roles still cascade, but the role name itself does not.122- NEVER grant `WITH ADMIN OPTION` to ordinary users : it lets them re-grant the role to anyone.123124## Encryption at Rest125126Three plug-ins are available : `file_key_management` (local key file, default and most common), `aws_key_management` (AWS KMS), and HashiCorp Vault via third-party plug-in.127128### Minimum file-key-management config129130```ini131# my.cnf : MariaDB 10.6+, all encryption surfaces ON132[mariadb]133plugin_load_add = file_key_management134loose_file_key_management_filename = /etc/mysql/encryption/keyfile.enc135loose_file_key_management_filekey = FILE:/etc/mysql/encryption/keyfile.key136loose_file_key_management_encryption_algorithm = AES_CTR137138# 12.0.1+ : harden key derivation139loose_file_key_management_use_pbkdf2 = 1000140loose_file_key_management_digest = sha256141142# Encrypt every surface : tables, redo log, binlog, temp files, aria143innodb_encrypt_tables = ON144innodb_encrypt_log = ON145innodb_encrypt_temporary_tables = ON146encrypt_binlog = ON147encrypt_tmp_files = ON148encrypt_tmp_disk_tables = ON149aria_encrypt_tables = ON150```151152| Surface | Variable | Effect if OFF |153|---------|----------|---------------|154| InnoDB tablespaces | `innodb_encrypt_tables` (`ON`, `OFF`, `FORCE`) | Unencrypted `.ibd` files on disk |155| InnoDB redo log | `innodb_encrypt_log` | Plaintext redo log entries reveal recent row changes |156| InnoDB internal temp tables | `innodb_encrypt_temporary_tables` | Plaintext intermediate join / sort results |157| Binary log | `encrypt_binlog` | Plaintext row images in binlog ; key data leak for replicas |158| MyISAM / temp files | `encrypt_tmp_files` | Plaintext sort / aggregate spill files |159| Disk-based temp tables | `encrypt_tmp_disk_tables` | Plaintext temp tables once they spill to disk |160| Aria engine | `aria_encrypt_tables` | Plaintext Aria tablespaces (used by system tables) |161162**Key file format** :163164```165# /etc/mysql/encryption/keyfile.enc (community / enterprise <11.8)166<key_id>;<hex_encoded_key>167168# Example1691;a7addd9adea9978fda19f21e6be987880e68ac92632ca052e5bb42b1a506939a170171# Enterprise 11.8+ adds a version column172<key_id>;<version>;<hex_encoded_key>173```174175**Decision rule** :176- ALWAYS encrypt every surface listed above when enabling at-rest encryption. Partial encryption leaks via the unprotected surfaces.177- ALWAYS chmod the key file to `0400 mysql:mysql`. World-readable key files defeat the purpose.178- ALWAYS enable `use_pbkdf2` and `digest=sha256` on 12.0.1+ for new deployments. PBKDF2 raises the cost of an offline key-file attack.179- NEVER store the key file on the same disk as the encrypted data without a separate access boundary (preferably a different volume mount with its own ACL).180- NEVER set `innodb_encrypt_tables=FORCE` without first re-encrypting existing tables ; new writes will succeed but the existing tables remain plaintext.181182## TLS for Client Connections183184Server-side TLS requires three variables in `my.cnf`, plus per-user `REQUIRE` on every account that should be TLS-only.185186```ini187# my.cnf : enable server-side TLS (MariaDB 10.6+)188[mariadb]189ssl_ca = /etc/mysql/certs/ca.pem190ssl_cert = /etc/mysql/certs/server-cert.pem191ssl_key = /etc/mysql/certs/server-key.pem192tls_version = TLSv1.2,TLSv1.3193```194195```sql196-- Per-user TLS enforcement (combine with the server config above)197GRANT SELECT ON app.* TO 'alice'@'%' REQUIRE SSL;198199-- Strict X509 with cert-pinning200GRANT SELECT ON app.* TO 'bob'@'%'201 REQUIRE SUBJECT '/CN=bob/O=Acme/C=NL'202 AND ISSUER '/C=FI/O=Acme CA/CN=Root'203 AND CIPHER 'ECDHE-RSA-AES256-GCM-SHA384';204```205206**Decision rule** :207- ALWAYS set `ssl_ca` / `ssl_cert` / `ssl_key` on the server AND `REQUIRE SSL` (or stricter) on the user. The server config alone makes TLS available ; the GRANT clause makes it mandatory.208- ALWAYS pin `tls_version` to `TLSv1.2,TLSv1.3`. Older TLS (1.0, 1.1) is broken.209- NEVER ship `ssl_cert` without the full certificate chain ; TLS handshake fails silently with intermediate-cert clients.210- NEVER use `REQUIRE SSL` without checking that the client driver actually validates the server cert. Many drivers default to no verification.211212## TLS for Replication213214```sql215-- On the replica216STOP SLAVE;217CHANGE MASTER TO218 MASTER_SSL = 1,219 MASTER_SSL_CA = '/etc/mysql/certs/ca.pem',220 MASTER_SSL_CERT = '/etc/mysql/certs/replica-cert.pem',221 MASTER_SSL_KEY = '/etc/mysql/certs/replica-key.pem',222 MASTER_SSL_VERIFY_SERVER_CERT = 1;223START SLAVE;224225-- 12.3+ shortcut : inherit server-level TLS config226CHANGE MASTER TO227 MASTER_SSL = 1,228 MASTER_SSL_CA = DEFAULT,229 MASTER_SSL_CERT = DEFAULT,230 MASTER_SSL_KEY = DEFAULT,231 MASTER_SSL_VERIFY_SERVER_CERT = 1;232```233234**Decision rule** :235- ALWAYS set `MASTER_SSL_VERIFY_SERVER_CERT = 1` ; without it, the connection is encrypted but MITM-vulnerable.236- ALWAYS use the `DEFAULT` keyword on 12.3+ to reuse server-level TLS config and avoid per-channel cert duplication.237- NEVER omit `MASTER_SSL_CA` ; without a CA the replica trusts any presented cert.238239## Hardening Checklist240241Run through this checklist after every fresh install :2422431. `DROP USER ''@'localhost'` and any other anonymous accounts.2442. `DROP USER 'root'@'%'` ; keep `'root'@'localhost'` only.2453. Set `'root'@'localhost' IDENTIFIED VIA unix_socket` on Debian / Ubuntu.2464. Create one application user per database with `ed25519` and `REQUIRE SSL`.2475. Create roles per access tier (read-only, app-write, admin) and grant roles to users.2486. Enable file-key-management with PBKDF2 (12.0.1+) and chmod the key file `0400 mysql:mysql`.2497. Enable encryption on tables, log, binlog, tmp files, aria.2508. Configure `ssl_ca` / `ssl_cert` / `ssl_key` server-side and `tls_version=TLSv1.2,TLSv1.3`.2519. Grant minimum privileges per role ; NEVER `GRANT ALL ON *.*`.25210. Run `mariadb-upgrade` once after the install or upgrade to migrate any stale `mysql.user` rows.253254## Reference Links255256- `references/methods.md` : per-plug-in install + syntax, full GRANT privilege table, role lifecycle, encryption variable matrix per version.257- `references/examples.md` : 8 complete working examples (user creation, GRANT patterns, role workflow, encryption config, TLS server + client, audit-log plug-in).258- `references/anti-patterns.md` : 8 documented anti-patterns with WHY-it-fails and the correct alternative.259- KB ed25519 plug-in : https://mariadb.com/kb/en/authentication-plugin-ed25519/260- KB GRANT statement : https://mariadb.com/kb/en/grant/261- KB roles overview : https://mariadb.com/kb/en/roles_overview/262- Docs file-key-management plug-in : https://mariadb.com/docs/server/security/encryption/data-at-rest-encryption/key-management-and-encryption-plugins/file-key-management-encryption-plugin.md263- Docs mysql.global_priv table : https://mariadb.com/docs/server/reference/system-tables/the-mysql-database-tables/mysql-global_priv-table.md264- KB CHANGE MASTER TO : https://mariadb.com/kb/en/change-master-to/