Enabling MariaDB remote access safely requires four controls to agree: the server must listen on an intended interface, the network must permit only approved sources, MariaDB must match the connection to the correct 'user'@'host' account, and the client must authenticate over a protected transport. Changing only bind-address is incomplete. Granting 'user'@'%' does not repair a firewall. Opening port 3306 to the internet does not repair an account mismatch.
This guide configures a private application host to reach MariaDB, verifies TLS and account matching, and provides a troubleshooting sequence for timeouts, refused connections, certificate failures, and Access denied. It also shows how to roll back remote access cleanly.
Examples use Ubuntu 24.04, UFW, a MariaDB server at 10.20.30.10, and an application host at 10.20.30.25. Replace them with addresses you control. Do not copy example passwords. Store credentials in an approved secret manager.
Decide whether remote TCP access is necessary
If the application runs on the database host, prefer the Unix socket. It avoids a network listener, firewall path, packet interception, DNS dependency, and TCP overhead. A local client can connect with:
mariadb --protocol=socket --user=appuser --password --database=appdb
Remote access is justified when application and database tiers are separated, backup or monitoring tools run elsewhere, replication connects nodes, or a controlled administration path is required. Even then, a private network, VPN, bastion, or database proxy is preferable to a public listener.
Document:
- MariaDB server addresses and interfaces.
- Each client source address after NAT.
- Routing and firewall ownership.
- DNS names used for TLS verification.
- Application connection pool size and timeout.
- Account, database, and required operations.
- Certificate issuer, renewal, and trust distribution.
- Rollback path if clients fail after the change.
Understand the connection path
A remote client crosses layers in order:
DNS -> route -> firewall/NAT -> TCP listener -> TLS -> account match -> authentication -> authorization
The error identifies the layer:
| Symptom | Layer most likely reached | Useful next check |
|---|---|---|
| DNS name not found | DNS | Resolver and record |
| TCP timeout | Route or firewall | Packet path and ACL |
| Connection refused | Target TCP stack | Listener address and port |
| TLS certificate error | TLS handshake | CA, SAN, hostname, clock |
Access denied |
MariaDB authentication | Exact user@host, plugin, password |
| Permission denied for SQL | Authorization | CURRENT_USER() and grants |
Do not change database grants for a timeout. Do not open a firewall for an SQL privilege error.
Capture the existing server state
Before editing configuration:
ip -br address
ip route
sudo ss -lntp | grep ':3306' || true
sudo mariadb -e "SHOW VARIABLES WHERE Variable_name IN (
'bind_address','port','skip_networking','skip_name_resolve'
);"
sudo ufw status verbose
Find option-file order and duplicate definitions:
mariadbd --help --verbose 2>/dev/null |
sed -n '/Default options are read from/,/Variables and options/p' | head -n 30
sudo grep -R --line-number -E
'^s*(bind-address|port|skip-networking|skip-name-resolve)'
/etc/mysql 2>/dev/null
my_print_defaults mariadb mariadbd server mysqld
List remote-capable accounts without displaying password hashes:
SELECT User, Host, plugin
FROM mysql.user
ORDER BY User, Host;
Protect this inventory. It reveals account names, trust boundaries, and authentication methods.
Choose a precise bind address
Ubuntu packages commonly bind MariaDB to 127.0.0.1, which blocks remote TCP access by design. For the example private interface, create or edit a small local override:
sudoedit /etc/mysql/mariadb.conf.d/60-remote-access.cnf
[mariadb]
bind-address = 10.20.30.10
port = 3306
The bind address must exist on the server. MariaDB will fail to start if asked to bind an unavailable address. Avoid 0.0.0.0; it listens on every IPv4 interface, including interfaces added later.
Starting with MariaDB 10.11, bind_address can accept comma-separated addresses. Use that only after verifying the installed version and syntax. IPv6 has separate semantics: from MariaDB 10.6, binding :: does not implicitly mean all IPv4 addresses. Test both address families deliberately.
If skip-networking is enabled, MariaDB does not create a TCP listener regardless of bind-address. Remove it or set it according to the exact version's documented syntax when remote TCP is required.
Validate the parsed options:
my_print_defaults mariadb mariadbd server mysqld
sudo mariadbd --validate-config
If --validate-config is unavailable, validate on staging and inspect the journal immediately after a controlled restart.
sudo systemctl restart mariadb
systemctl status mariadb --no-pager
sudo journalctl -u mariadb --since "5 minutes ago" --no-pager
sudo ss -lntp | grep ':3306'
sudo mariadb -e "SHOW VARIABLES LIKE 'bind_address';"
Expected evidence is a listener on 10.20.30.10:3306, not on public or unrelated interfaces.
Create the exact MariaDB account
MariaDB identities include the host. Create the database and an account that matches the application source:
CREATE DATABASE IF NOT EXISTS appdb
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
CREATE USER 'app_runtime'@'10.20.30.25'
IDENTIFIED BY 'REPLACE_WITH_SECRET';
GRANT SELECT, INSERT, UPDATE, DELETE
ON appdb.* TO 'app_runtime'@'10.20.30.25';
SHOW GRANTS FOR 'app_runtime'@'10.20.30.25';
Separate schema migration privileges from runtime access. A migration account can be enabled for controlled deployments and disabled afterward. Runtime code rarely needs global privileges, user administration, file access, or grant delegation.
Avoid 'app_runtime'@'%'. It can match unintended sources and makes firewall errors more dangerous. When clients use ephemeral addresses, use a controlled private subnet only after understanding wildcard or netmask matching and NAT behavior.
MariaDB evaluates candidate accounts by specificity. Anonymous localhost users or a more specific host entry can match before the account an operator expected. Test:
SELECT USER(), CURRENT_USER();
USER() is the presented client identity; CURRENT_USER() is the matched MariaDB account used for privileges.
Handle DNS and skip_name_resolve
By default, MariaDB may resolve client hostnames. Reverse DNS latency or inconsistency can delay connections. skip_name_resolve=ON disables hostname resolution and requires grant-table hosts to be IP addresses or localhost.
Before enabling it, audit accounts:
SELECT User, Host
FROM mysql.user
WHERE Host NOT IN ('localhost', '127.0.0.1', '::1')
ORDER BY User, Host;
Do not enable it if active accounts depend on DNS names until they are migrated and tested. After enabling, restart is required because the variable is not dynamic:
[mariadb]
skip-name-resolve = 1
Verify:
sudo systemctl restart mariadb
sudo mariadb -e "SHOW VARIABLES LIKE 'skip_name_resolve';"
This setting does not change client DNS resolution for the server hostname. Applications can still resolve db.internal.example; MariaDB simply avoids resolving the client's address when matching accounts.
Add a source-limited firewall rule
On the database host:
sudo ufw allow from 10.20.30.25 to 10.20.30.10 port 3306 proto tcp
sudo ufw status numbered
Before enabling UFW remotely, ensure SSH is allowed and console recovery exists. If UFW is not the authoritative firewall, configure the correct cloud security group, network firewall, Kubernetes policy, or host framework.
Apply defense in depth:
- Network ACL permits only approved source and destination.
- Host firewall repeats that restriction.
- MariaDB account matches the source.
- Grants limit data and operations.
- TLS protects the session.
Avoid broad rules such as “allow 3306 from anywhere.” Changing the default port is not a security boundary; scanners find alternate ports, and legitimate clients still need authentication and encryption.
Understand NAT and observed source addresses
MariaDB matches the source address it sees, which may be a NAT gateway rather than the application's local address. Inspect network architecture before creating grants. A shared NAT source reduces identity precision because many clients appear from one address.
During an approved test, observe connections:
sudo tcpdump -ni any tcp port 3306 and host 10.20.30.25
Capturing database traffic can expose metadata or unencrypted content. Use a narrow filter, short window, restricted output, and an authorized incident process. Prefer firewall connection logs where they provide enough evidence.
Within MariaDB, inspect active sessions:
SHOW FULL PROCESSLIST;
Process lists can include SQL text. Restrict access and redact before sharing.
Configure TLS for remote sessions
Network restriction does not encrypt traffic. MariaDB 11.4 and newer add automatic secure-connection behavior in supported server and Connector/C combinations, while older releases and other connectors require explicit TLS configuration. Never assume encryption from the server version alone; verify the actual session.
For CA-managed TLS, configure the server certificate, private key, and CA in the correct option group according to the installed version:
[mariadb]
ssl_ca = /etc/mysql/tls/ca.pem
ssl_cert = /etc/mysql/tls/server-cert.pem
ssl_key = /etc/mysql/tls/server-key.pem
Protect the key:
sudo chown mysql:mysql /etc/mysql/tls/server-key.pem
sudo chmod 0600 /etc/mysql/tls/server-key.pem
sudo openssl x509 -in /etc/mysql/tls/server-cert.pem
-noout -subject -issuer -dates -ext subjectAltName
Ensure the certificate SAN contains the DNS name or IP clients use. Verify key/certificate matching through your PKI procedure. Test config and restart or use the version-supported certificate reload workflow.
Require TLS for the account:
ALTER USER 'app_runtime'@'10.20.30.25' REQUIRE SSL;
SHOW CREATE USER 'app_runtime'@'10.20.30.25';
Test with server verification:
mariadb --host=db.internal.example
--user=app_runtime --password --database=appdb
--ssl-ca=/etc/ssl/certs/internal-db-ca.pem
--ssl-verify-server-cert
-e "SHOW SESSION STATUS LIKE 'Ssl_version'; SHOW SESSION STATUS LIKE 'Ssl_cipher';"
Client flags and defaults differ by connector version. Consult the exact connector documentation. Before MariaDB 11.3, some command-line client certificate-verification checks were disabled unless explicitly enabled. Disabling verification can produce an encrypted connection to an attacker, so it is not a production fix.
For mutual TLS, provide client certificates and use REQUIRE X509, issuer, or subject constraints. This increases PKI and rotation complexity; automate renewal and test overlapping certificate windows.
Store client credentials safely
Do not pass passwords inline:
mariadb --password=REAL_SECRET
It can expose the secret through shell history or process inspection. Use an interactive prompt, a protected option file, or the application's secret integration.
Example option file:
[client]
host = db.internal.example
user = app_runtime
password = REPLACE_WITH_SECRET
ssl_ca = /etc/ssl/certs/internal-db-ca.pem
ssl-verify-server-cert
Protect it:
chmod 0600 /secure/path/appdb.cnf
mariadb --defaults-extra-file=/secure/path/appdb.cnf appdb
An option file is still a secret at rest. Limit its owner, exclude it from images and backups where inappropriate, rotate it, and prefer a secret manager when the platform supports one.
Test the path in layers
From the application host, confirm DNS and routing:
getent ahosts db.internal.example
ip route get 10.20.30.10
Confirm TCP:
nc -vz -w 3 10.20.30.10 3306
Then test MariaDB with TLS and a prompt:
mariadb --host=db.internal.example --port=3306
--user=app_runtime --password --database=appdb
--ssl-ca=/etc/ssl/certs/internal-db-ca.pem
--ssl-verify-server-cert
-e "SELECT USER(), CURRENT_USER(), DATABASE(), @@hostname;"
Finally test least privilege:
SELECT 1;
CREATE TABLE remote_access_test (id INT PRIMARY KEY);
DROP TABLE remote_access_test;
Run only operations appropriate to a disposable test schema. A runtime user that should not perform DDL must receive permission errors for CREATE and DROP. Negative tests prove privilege boundaries.
Configure application timeouts and pools
Remote access adds network failure modes. Use finite connect, read, write, and acquisition timeouts. Configure pool size from measured application concurrency and MariaDB capacity, not CPU count alone.
Compare server limits and peaks:
SHOW VARIABLES LIKE 'max_connections';
SHOW GLOBAL STATUS LIKE 'Max_used_connections';
SHOW GLOBAL STATUS LIKE 'Threads_connected';
SHOW GLOBAL STATUS LIKE 'Threads_running';
If many application replicas each create a large pool, aggregate connections can exhaust MariaDB during deployment or failover. Use backoff with jitter for retries and avoid retrying nontransient authentication or authorization errors.
Verify remote access from the server side
A client-side success proves one route and one identity. Add server-side evidence so later incidents have a baseline. During a controlled test, inspect the matched account, source, database, command, and session encryption:
SHOW FULL PROCESSLIST;
SELECT USER(), CURRENT_USER();
SHOW SESSION STATUS LIKE 'Ssl_version';
SHOW SESSION STATUS LIKE 'Ssl_cipher';
Use a dedicated monitoring query rather than granting the application process-list privileges solely for diagnostics. Process-list output may include SQL text and must be handled as sensitive operational data.
Record connection establishment time, TLS version, certificate expiry, pool occupancy, rejected sessions, and MariaDB Threads_connected during a normal deployment and a replica surge. A health check that only opens TCP proves much less than one that authenticates with a restricted monitoring identity and executes a cheap query. Conversely, do not make every orchestrator probe perform application writes: a transient database stall can trigger synchronized restarts and amplify the incident.
Test fail-closed behavior as well. Temporarily use an unauthorized source in a lab, an account without the required grant, and an invalid CA. The firewall, MariaDB identity, and TLS verification should reject each case at its intended layer. Document the expected error so operators can distinguish a working control from an outage.
Troubleshoot remote connection failures
DNS lookup fails or returns the wrong address
getent ahosts db.internal.example
resolvectl query db.internal.example
Check split-horizon DNS, stale records, search domains, and IPv4/IPv6 ordering. The certificate SAN must match the hostname used.
Connection times out
On the client:
ip route get 10.20.30.10
nc -vz -w 3 10.20.30.10 3306
On the server:
sudo ss -lntp | grep ':3306'
sudo ufw status verbose
sudo tcpdump -ni any host 10.20.30.25 and tcp port 3306
No SYN at the server points upstream. SYN without a completed handshake points to host firewall, return path, or listener. Use packet capture only with authorization.
Connection is refused
The host responded but no process accepted that address/port. Check bind_address, skip_networking, port, service state, and IPv4 versus IPv6.
systemctl status mariadb --no-pager
sudo journalctl -u mariadb -n 100 --no-pager
sudo mariadb -e "SHOW VARIABLES WHERE Variable_name IN ('bind_address','port','skip_networking');"
Access denied for user
Network connectivity succeeded. Inspect exact accounts:
SELECT User, Host, plugin
FROM mysql.user
WHERE User = 'app_runtime'
ORDER BY Host;
Verify password source, plugin compatibility, account lock/expiration, source after NAT, and TLS requirements. Do not add a wildcard superuser.
Authentication succeeds but SQL is denied
SELECT USER(), CURRENT_USER();
SHOW GRANTS;
Grant the minimum missing operation on the required schema. Determine whether a separate migration identity should own DDL.
TLS certificate verification fails
Check:
date -Is
openssl x509 -in /etc/ssl/certs/internal-db-ca.pem -noout -subject -issuer -dates
openssl s_client -starttls mysql -connect db.internal.example:3306
-servername db.internal.example -CAfile /etc/ssl/certs/internal-db-ca.pem </dev/null
Support for -starttls mysql depends on the installed OpenSSL. Prefer the MariaDB client as the authoritative application-path test. Fix CA trust, SAN, hostname, chain, or clock; do not disable verification permanently.
Connections work by IP but not hostname
This can be client DNS, certificate hostname validation, or MariaDB host-account resolution. Separate them. skip_name_resolve affects how MariaDB matches client hosts; it does not stop the client resolving the database server name.
Roll back remote access
Prepare rollback before the change:
- Keep an active local socket administrative session.
- Preserve the previous option file.
- Record firewall rule numbers.
- Know which remote account and grants were added.
Restore loopback binding:
[mariadb]
bind-address = 127.0.0.1
Validate, restart, and verify:
sudo systemctl restart mariadb
sudo ss -lntp | grep ':3306'
sudo mariadb --protocol=socket -e "SELECT 1;"
Remove the firewall rule by its reviewed UFW number:
sudo ufw status numbered
sudo ufw delete RULE_NUMBER
Dropping the account is destructive to that identity. Lock it first if a rollback window is required:
ALTER USER 'app_runtime'@'10.20.30.25' ACCOUNT LOCK;
After dependencies and retention are confirmed:
DROP USER 'app_runtime'@'10.20.30.25';
Production best practices
- Prefer Unix sockets for same-host applications.
- Use private addressing or controlled proxies, never public port 3306.
- Bind only intended interfaces.
- Restrict sources at network and host firewalls.
- Use exact
user@hostaccounts and least privilege. - Require verified TLS for cross-host sessions.
- Separate runtime, migration, backup, monitoring, and operator users.
- Store credentials in a secret manager and rotate them.
- Monitor listener, firewall, grants, TLS expiry, failed logins, and connection pressure.
- Test both successful operations and expected permission failures.
- Maintain a rollback path through local socket administration.
FAQ
What is required for MariaDB remote access?
MariaDB needs a TCP listener on the intended interface, a network path and firewall rule, a matching user@host account, valid authentication, sufficient grants, and secure transport.
Should I set bind-address = 0.0.0.0?
Avoid it when a specific private address is available. 0.0.0.0 listens on every IPv4 interface and increases exposure to future network changes.
Why does MariaDB work locally but not remotely?
The server may bind only to loopback, skip-networking may disable TCP, a firewall may block port 3306, or no account may match the remote source.
Is 'user'@'%' safe behind a firewall?
It still broadens database identity matching and increases the impact of firewall mistakes. Prefer exact hosts or controlled subnets plus TLS and least privilege.
Does changing MariaDB to a nonstandard port make it secure?
No. It may reduce noise but does not provide authentication, authorization, encryption, or meaningful protection from discovery.
Should remote MariaDB connections use TLS?
Yes, unless an equally reviewed encrypted transport protects the entire path. Verify the server certificate; encryption without identity verification can permit interception.
What does skip_name_resolve change?
It stops MariaDB resolving client hostnames and requires IP-based or localhost grant entries. It does not disable DNS resolution performed by clients for the database hostname.
How can I tell which MariaDB account matched?
Run SELECT USER(), CURRENT_USER();. CURRENT_USER() identifies the account whose privileges are active.
Conclusion
Safe MariaDB remote access is a controlled path, not an open port. Bind MariaDB to a private interface, allow only approved sources, create exact host-specific users, grant the minimum operations, require verified TLS, and test each layer independently. This design makes failures diagnosable and limits the impact of a leaked credential or network mistake.
Keep local socket administration available for rollback. Treat timeout, refused connection, TLS error, Access denied, and SQL permission denial as different failures. Avoid wildcard accounts, public listeners, inline passwords, and disabled certificate verification. Once remote access works, monitor the controls that keep it safe: listener drift, firewall drift, account changes, certificate expiry, failed logins, pool growth, and backup connectivity.
Suggested Internal Links
- Install MariaDB on Ubuntu 24.04: Production Setup Guide —
/install-mariadb-ubuntu-24-04/ - Secure a New MariaDB Server: Production Hardening Guide —
/secure-mariadb-server/ - Create MariaDB Databases, Users, and Least-Privilege Grants —
/mariadb-users-grants/ - Configure TLS Encryption for MariaDB Client Connections —
/configure-mariadb-tls/ - Fix MariaDB Access Denied Errors Without Broad Grants —
/mariadb-access-denied/
Suggested External Sources
- MariaDB remote client access — https://mariadb.com/docs/server/server-usage/connecting/mariadb-remote-connection-guide-1
- Configuring MariaDB remote access — https://mariadb.com/docs/server/mariadb-quickstart-guides/mariadb-remote-connection-guide
- Secure connections overview — https://mariadb.com/docs/server/security/encryption/data-in-transit-encryption/secure-connections-overview
- Securing client and server connections — https://mariadb.com/docs/server/security/securing-mariadb/securing-mariadb-encryption/data-in-transit-encryption/securing-connections-for-client-and-server
- MariaDB server system variables — https://mariadb.com/docs/server/server-management/variables-and-modes/server-system-variables
- Connecting to MariaDB — https://mariadb.com/docs/server/mariadb-quickstart-guides/mariadb-connecting-guide
CREATE USERreference — https://mariadb.com/docs/server/reference/sql-statements/account-management-sql-statements/create-userGRANTreference — https://mariadb.com/docs/server/reference/sql-statements/account-management-sql-statements/grant