A production MariaDB firewall configuration should make the intended connection path obvious and every other path unavailable. Opening TCP port 3306 is not the objective. The objective is to allow specific application, backup, monitoring, replication, and administrative sources to reach a precise MariaDB listener while public networks, unrelated workloads, and unexpected routes remain blocked.
This guide designs that boundary across MariaDB listener settings, Linux host firewalls, cloud security controls, routing and NAT, Kubernetes NetworkPolicy, and database account host matching. It includes staged change procedures, verification from allowed and denied sources, packet-level troubleshooting, and rollback steps that avoid turning a firewall improvement into an outage.
Examples use Ubuntu 24.04, a database address of 10.20.30.10, an application host at 10.20.30.25, a backup host at 10.20.30.70, and MariaDB's conventional TCP port 3306. Replace every address with verified values from your environment. Keep an independent administrative session and console path while changing network policy.
Define the allowed flows first
Begin with a flow matrix, not firewall commands:
| Source identity | Observed source | Destination | Port | Purpose | Owner |
|---|---|---|---|---|---|
| Orders application | 10.20.30.25/32 |
10.20.30.10 |
TCP 3306 | Runtime SQL | App team |
| Backup service | 10.20.30.70/32 |
10.20.30.10 |
TCP 3306 | Physical backup | DBA team |
| Metrics collector | 10.20.30.80/32 |
10.20.30.10 |
TCP 3306 | Database metrics | SRE team |
| Replica | 10.20.31.10/32 |
10.20.30.10 |
TCP 3306 | Replication | DBA team |
| Administrator | VPN or bastion address | 10.20.30.10 |
TCP 3306 | Approved maintenance | Platform team |
For each flow, answer:
- Is the source address stable, or translated by NAT?
- Does a proxy terminate the client connection?
- Which interface receives the packet?
- Which cloud security group, ACL, host firewall, or CNI policy evaluates it?
- Does MariaDB see the original source or an intermediary?
- Which
'user'@'host'account should match? - Is verified TLS required?
- Who removes the flow when the workload is retired?
Avoid broad placeholders such as an entire corporate RFC1918 network. Private address space is not automatically trusted. A compromised development host on the same private network should not reach the production database.
Understand every enforcement layer
A remote connection can cross several independent controls:
Client DNS
-> client egress policy
-> route/VPN/NAT
-> cloud or perimeter firewall
-> host firewall
-> MariaDB TCP listener
-> TLS
-> MariaDB user@host authentication
-> grants and roles
Each layer answers a different question. A route decides where traffic travels. A firewall decides whether packets pass. bind_address decides where MariaDB listens. TLS protects and authenticates the channel. The host part of a MariaDB account participates in account matching. Grants authorize SQL.
Do not repair a TCP timeout with GRANT. The client has not reached authentication. Do not open a firewall because an authenticated session receives SELECT command denied; that is authorization. Troubleshooting becomes faster when evidence is assigned to the layer that can produce it.
Discover the current exposure
On the MariaDB host, record interfaces, routes, listeners, and firewall managers:
ip -br address
ip route show
ip -6 route show
sudo ss -lntp | grep ':3306' || true
sudo ufw status verbose
sudo nft list ruleset
Inspect MariaDB network variables:
sudo mariadb -e "SHOW GLOBAL VARIABLES WHERE Variable_name IN (
'bind_address','port','skip_networking','skip_name_resolve','require_secure_transport'
);"
Find option-file order and duplicate settings:
mariadbd --help --verbose 2>/dev/null |
sed -n '/Default options/,/Variables and options/p'
my_print_defaults mariadb server mysqld
Inventory cloud and upstream controls through the authoritative platform API or console. A host can show a restrictive UFW rule while a load balancer or Kubernetes NodePort exposes a different path. Conversely, a cloud firewall may block traffic before the host sees it.
From an authorized administrative workstation, resolve the service name:
getent ahosts db01.example.net
Confirm whether DNS returns private, public, IPv4, and IPv6 records. It is common to secure IPv4 while an unexpected IPv6 listener remains reachable.
Disable TCP entirely for local-only MariaDB
If every client runs on the database host and can use a Unix socket, the strongest network rule is no TCP listener. In a late-loading custom option file:
[mariadb]
skip_networking = ON
Restart during a planned window and verify:
sudo systemctl restart mariadb
sudo systemctl --no-pager --full status mariadb
sudo ss -lntp | grep ':3306' || true
sudo mariadb --protocol=socket -e 'SELECT 1;'
Before enabling skip_networking, inventory monitoring, backup, replication, and administrative scripts. A service located on the same machine may still force TCP through 127.0.0.1. Change it to the socket deliberately.
Rollback by removing the option and restoring the previous listener configuration. Do not combine this change with account or application migrations in the same untested window.
Bind MariaDB only to intended interfaces
When remote access is required, bind the server to a private database interface rather than every address. Create /etc/mysql/mariadb.conf.d/z-network.cnf on Ubuntu:
[mariadb]
bind_address = 10.20.30.10
port = 3306
MariaDB 10.11 and later support a comma-separated address list, useful when a server must listen on selected IPv4 and IPv6 addresses:
[mariadb]
bind_address = 10.20.30.10,fd20:30::10
Version-gate that syntax. Older releases accept only one address. Avoid 0.0.0.0 or * unless listening on every interface is an explicit, reviewed requirement backed by firewall policy.
Validate that the configured address exists locally:
ip address show
my_print_defaults mariadb server mysqld
Then restart and inspect actual listeners:
sudo systemctl restart mariadb
sudo systemctl --no-pager --full status mariadb
sudo journalctl -u mariadb -n 100 --no-pager
sudo ss -lntp '( sport = :3306 )'
MariaDB can fail to start when bind_address names an address not present on the host at startup. In cloud environments with dynamic interfaces, use stable private addressing or an architecture designed for service discovery rather than guessing at boot ordering.
Binding narrows exposure, but it is not a firewall. Any source able to route to that interface can attempt a connection unless another control denies it.
Stage UFW changes on Ubuntu
Ubuntu uses UFW as its common host-firewall frontend. Before enabling or reloading it on a remote server, preserve SSH or bastion access. Check the current status and defaults:
sudo ufw status verbose
sudo ufw status numbered
sudo ufw show raw
Preview the MariaDB allow rule:
sudo ufw --dry-run allow proto tcp
from 10.20.30.25
to 10.20.30.10 port 3306
comment 'orders-api to mariadb'
Apply exact application and backup sources:
sudo ufw allow proto tcp
from 10.20.30.25
to 10.20.30.10 port 3306
comment 'orders-api to mariadb'
sudo ufw allow proto tcp
from 10.20.30.70
to 10.20.30.10 port 3306
comment 'backup to mariadb'
Use destination address as well as port on a multi-homed host so a rule does not accidentally allow the same port on another interface. UFW maintains both IPv4 and IPv6 policy when IPv6 support is enabled; review both rendered rules.
If UFW is currently disabled, do not enable it until all required management and service flows are represented. At minimum, preserve the approved SSH path:
sudo ufw --dry-run allow proto tcp
from 10.20.10.15
to any port 22
comment 'management bastion ssh'
Apply the management rule, validate the complete ruleset, arrange console access, then enable UFW through the change procedure. Keep the existing SSH session open and start a second connection to prove the new rule.
Review after changes:
sudo ufw status numbered
sudo ufw status verbose
To remove a specific rule reliably, list numbered rules and delete the intended entry:
sudo ufw status numbered
sudo ufw delete 3
Rule numbers change after deletion. Re-list before every removal. A safer automation system should manage declared rules rather than depend on an interactive number.
Use nftables when the host policy requires it
Do not manage the same traffic independently through raw nftables and UFW without understanding how their generated chains interact. Choose the platform's declared firewall owner.
Inspect the active ruleset and handles:
sudo nft -n -a list ruleset
For a host managed directly with nftables, a minimal standalone example can define an inet table for both IPv4 and IPv6. Do not paste this over an existing ruleset; integrate it with the site's established policy:
table inet mariadb_filter {
set mariadb_ipv4_clients {
type ipv4_addr
elements = { 10.20.30.25, 10.20.30.70, 10.20.30.80 }
}
chain input {
type filter hook input priority filter; policy accept;
ct state established,related accept
ip daddr 10.20.30.10 ip saddr @mariadb_ipv4_clients
tcp dport 3306 ct state new accept
ip daddr 10.20.30.10 tcp dport 3306 drop
}
}
This table only demonstrates MariaDB-specific ordering. Its policy accept does not create a complete host firewall and does not protect SSH or other services. In a real default-deny ruleset, loopback, established traffic, ICMP/ICMPv6, management, monitoring, and required service rules must be designed together.
Validate a rules file without applying it:
sudo nft --check --file /etc/nftables.conf
Applying a full nftables file can replace or conflict with active policy and cut off remote access. Use an out-of-band console, a timed rollback mechanism approved by the organization, and atomic configuration management. Save the pre-change ruleset as evidence, not as an excuse to run an unreviewed restore script.
Align cloud firewalls and subnet controls
In cloud environments, apply least privilege at the security-group or virtual-firewall layer as well as the host:
- Database instances belong to private subnets without public addresses.
- Inbound 3306 references the application security identity or exact private CIDRs.
- Administrative access originates from a VPN, private bastion, or controlled access service.
- Network ACLs preserve return traffic and ephemeral ports according to their stateful or stateless behavior.
- Egress rules prevent unexpected database connections from unrelated workloads.
- Flow logs are enabled with appropriate retention and access controls.
Prefer security-group-to-security-group references where the platform preserves workload identity. CIDR rules become stale when autoscaling, replacement, or NAT changes addresses. Still verify what source the database host sees; a proxy or managed translation layer may make an identity reference at the cloud firewall differ from MariaDB's 'user'@'host' match.
Never assign a public IP to solve private routing. Fix route tables, peering, VPN, private DNS, or service endpoints. If a business requirement truly needs internet-facing database access, place it behind an architecture explicitly designed for that risk, with strong identity, TLS, rate control, monitoring, and vendor-supported controls. Direct global 3306 exposure is not a normal production baseline.
Handle NAT and proxies explicitly
The client believes its source is one address; the database may observe another after NAT. Capture evidence on the server during an approved test:
sudo tcpdump -ni any
'tcp dst port 3306 and (tcp[tcpflags] & tcp-syn != 0)'
tcpdump can expose addresses and traffic metadata. Restrict capture duration and file access. Do not use payload capture unless incident procedures explicitly authorize it.
After authentication, compare identities:
SELECT USER(), CURRENT_USER();
USER() helps show the observed client host, while CURRENT_USER() shows the matched MariaDB account. Configure firewall sources and 'user'@'host' entries based on observed topology, not a developer laptop's assumption.
A TCP proxy normally becomes the source seen by MariaDB. Restrict database access to the proxy, then enforce original client identity and authorization at an appropriate layer. Do not grant 'app'@'%' merely because the original address disappeared.
Design administrative access without public 3306
Administrators rarely need direct database exposure from arbitrary laptops. Use one of these controlled patterns:
- Corporate VPN into a private management network.
- Bastion with individual identity, MFA, session recording, and patch ownership.
- Short-lived access broker or approved database proxy.
- SSH local forwarding through a hardened bastion.
An SSH tunnel can bind a local port to the private database endpoint:
ssh -N
-L 127.0.0.1:13306:10.20.30.10:3306
dba@bastion.example.net
Then connect to the local endpoint:
mariadb
--host=127.0.0.1
--port=13306
--protocol=tcp
--user=alice
--password
The database sees the connection source associated with the tunnel's remote side, not the laptop. TLS should still be used where policy requires end-to-end database identity and encryption. The SSH tunnel protects its segment but does not automatically configure MariaDB certificate verification.
Avoid a shared bastion Unix account. Individual identity and narrowly scoped database accounts make revocation and attribution possible.
Apply Kubernetes NetworkPolicy correctly
Kubernetes NetworkPolicy controls pod traffic only when the cluster's networking implementation enforces it. Creating a policy object on a CNI without NetworkPolicy support changes nothing. Confirm CNI capability and test behavior.
Suppose the MariaDB pod in namespace data has label app: mariadb, and the application pod in namespace orders has app: orders-api. Label the application namespace explicitly:
kubectl label namespace orders access-to-mariadb=true
Apply a policy in the data namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: mariadb-ingress
namespace: data
spec:
podSelector:
matchLabels:
app: mariadb
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
access-to-mariadb: "true"
podSelector:
matchLabels:
app: orders-api
ports:
- protocol: TCP
port: 3306
Selectors within the same from item are combined: the source pod must match the pod selector in a namespace matching the namespace selector. Separate list items would mean logical alternatives and could allow more traffic.
Apply and inspect:
kubectl apply -f mariadb-networkpolicy.yaml
kubectl -n data get networkpolicy mariadb-ingress -o yaml
Test from an allowed application pod and a deliberately unapproved pod. Do not infer enforcement from the object existing. NetworkPolicy is additive: review every policy selecting the MariaDB pod because another policy can add an allowed path.
If MariaDB runs outside Kubernetes, control application egress too. A namespace policy can permit TCP 3306 only to the external database CIDR, but DNS and other required egress must also be allowed. Be cautious with ipBlock: behavior around pod IP translation and node NAT can differ by CNI and traffic path.
Do not expose MariaDB through Kubernetes Ingress. Ingress is for HTTP and HTTPS routing. A LoadBalancer or NodePort can expose arbitrary TCP but often broadens reach dramatically; prefer ClusterIP for in-cluster clients and private connectivity for external operators.
Align MariaDB host accounts with the network
Firewall rules decide whether packets reach MariaDB. Account host values decide which account definition can authenticate. Create a source-specific account:
CREATE USER 'orders_api'@'10.20.30.25'
IDENTIFIED BY 'replace-with-a-secret-manager-value'
REQUIRE SSL;
GRANT SELECT, INSERT, UPDATE, DELETE
ON orders.*
TO 'orders_api'@'10.20.30.25';
Avoid 'orders_api'@'%'. If Kubernetes or a proxy causes a stable set of node or proxy addresses to appear, use the narrowest truthful pattern and keep the firewall stricter still.
When enabling skip_name_resolve, MariaDB stops resolving hostnames for account matching; grant-table host values must use IP addresses or localhost. This can remove DNS dependency but requires a complete account inventory and restart. Do not enable it casually while accounts use DNS hostnames.
Verify from allowed and denied paths
From the approved application host, test DNS, TCP, TLS, authentication, and authorization separately:
getent ahosts db01.example.net
nc -vz -w 3 db01.example.net 3306
mariadb
--host=db01.example.net
--user=orders_api
--password
--ssl-ca=/etc/myapp/tls/mariadb-ca.pem
--ssl-verify-server-cert
--execute="SHOW SESSION STATUS LIKE 'Ssl_version'; SELECT CURRENT_USER();"
From an unapproved host, the TCP connection should fail. Use a controlled test source; do not scan networks without authorization.
On the database server, verify counters and logs at the enforcing layer:
sudo ufw status numbered
sudo nft -n -a list ruleset
sudo ss -tn state established '( sport = :3306 )'
sudo journalctl -k --since '-10 minutes' --no-pager
Firewall logging can create high volume and reveal network metadata. Rate-limit and retain it according to policy. A successful TCP handshake is not proof of TLS or correct grants; complete the full-layer test.
Troubleshoot connection failures by symptom
DNS resolution fails
Check the client resolver and intended private zone:
getent ahosts db01.example.net
resolvectl query db01.example.net
Do not replace a missing private DNS record with a public database address.
TCP connection times out
A timeout usually points to routing, a dropping firewall, security group, NetworkPolicy, or wrong destination. On the client:
ip route get 10.20.30.10
nc -vz -w 3 10.20.30.10 3306
On the server, watch for SYN packets during one test:
sudo tcpdump -ni any 'host 10.20.30.25 and tcp port 3306'
No packet at the server moves investigation upstream. An arriving SYN with no response points toward host policy or listener state.
Connection is refused
The target stack actively rejected the connection. Check listener address and port:
sudo ss -lntp '( sport = :3306 )'
sudo systemctl --no-pager --full status mariadb
sudo journalctl -u mariadb -n 100 --no-pager
Do not open more firewall ranges. MariaDB may be stopped or listening only on loopback.
Access denied
The client reached MariaDB, so focus on account matching, plugin, password, and TLS requirements:
SELECT User, Host, plugin
FROM mysql.user
WHERE User = 'orders_api';
Inspect SHOW CREATE USER for the exact host-qualified account. A firewall change cannot repair invalid credentials.
Only Kubernetes clients fail
Check Service endpoints, pod labels, namespace labels, NetworkPolicy selectors, and CNI enforcement:
kubectl -n data get service,endpoints,endpointslices
kubectl -n data get pods --show-labels
kubectl get namespace orders --show-labels
kubectl -n data get networkpolicy -o yaml
Test from a temporary approved diagnostic pod only if cluster policy permits it, then remove the pod.
IPv4 works but IPv6 fails or bypasses policy
Compare DNS records, listener families, routes, UFW IPv6 configuration, nftables family, and cloud IPv6 rules:
getent ahosts db01.example.net
sudo ss -lntp '( sport = :3306 )'
ip -6 route show
Secure both families or deliberately remove the unused one. Do not assume an IPv4-only rule governs IPv6.
Roll back network changes safely
Prepare rollback before application:
- Previous MariaDB option file and a local socket administrative path.
- Exact UFW rule numbers or configuration-management revision.
- Previous nftables ruleset and an out-of-band console.
- Cloud firewall change identifier and owner.
- Previous NetworkPolicy manifest.
- Test commands for management and application paths.
If a MariaDB bind change breaks startup, restore the previous address and restart through the normal procedure. If a new UFW rule blocks the application, add the verified exact source temporarily or revert the reviewed change; do not disable the whole firewall as the first reaction.
For Kubernetes, restore the known-good manifest:
kubectl apply -f mariadb-networkpolicy.previous.yaml
kubectl -n data get networkpolicy mariadb-ingress -o yaml
Deletion may remove isolation entirely when no other policy selects the pod. Applying the previous policy is usually safer than deleting the new one blindly.
After rollback, verify management access, allowed application traffic, denied-source behavior, TLS, authentication, and SQL operations. A recovered health check alone may miss an accidentally broad rule.
Production best practices
- Keep MariaDB on private addresses with no direct internet route.
- Disable TCP when all clients can use a local Unix socket.
- Bind only to intended interfaces and secure IPv4 plus IPv6.
- Allow exact workload sources instead of entire private networks.
- Use cloud identity references where available, then verify observed source IPs.
- Maintain host firewall policy even when cloud controls exist.
- Route human access through VPN, bastion, or an approved access broker.
- Require verified TLS after the network path is established.
- Match MariaDB accounts to truthful, narrow source hosts.
- Confirm Kubernetes CNI enforcement and test allowed plus denied pods.
- Avoid public
LoadBalancer,NodePort, and global port 3306 exposure. - Manage firewall changes declaratively with review and ownership.
- Keep console access and explicit rollback during network changes.
- Audit rules for retired workloads, stale IPs, and unexpected IPv6 paths.
FAQ
Should MariaDB port 3306 be open to the internet?
Normally no. Place MariaDB on a private network and provide controlled access through application subnets, VPN, bastion, or a database access service.
Is bind_address enough to secure MariaDB?
No. It limits listener interfaces. Use host and upstream firewalls, verified TLS, source-specific accounts, and least-privilege grants as separate controls.
Should UFW allow an entire application subnet?
Use exact sources when stable. A narrow subnet may be appropriate for autoscaling, but document its ownership and prevent unrelated workloads from joining it.
Why does MariaDB see a proxy address instead of the client?
The proxy establishes the server-side TCP connection, so its address becomes the source observed by MariaDB. Enforce original identity at the proxy or another trusted layer.
Does creating a Kubernetes NetworkPolicy automatically protect pods?
Only when the cluster CNI implements NetworkPolicy. Verify enforcement with positive and negative connection tests.
Can Kubernetes Ingress expose MariaDB?
Standard Ingress routes HTTP and HTTPS, not arbitrary MariaDB TCP. Do not use it as a database exposure mechanism.
Why does a connection timeout after adding the correct UFW rule?
Another layer may still block it: routing, cloud firewall, IPv6 policy, NAT, nftables ordering, or Kubernetes NetworkPolicy. Trace the packet path rather than widening the rule.
Does require_secure_transport replace a firewall?
No. It requires a secure transport after the client reaches MariaDB. A firewall controls which sources can reach the listener at all.
Conclusion
A strong MariaDB firewall configuration is a documented set of allowed flows enforced at several independent layers. Start with exact source, destination, purpose, and owner; bind MariaDB only where necessary; then implement host, cloud, and Kubernetes controls that reflect the real packet path.
Verification must include an allowed source, a denied source, TLS negotiation, account matching, and SQL authorization. When every change also has a tested rollback and a lifecycle owner, network security remains precise as applications scale, addresses change, and old workloads disappear.
Suggested Internal Links
- Configure MariaDB Remote Access Without Exposing It
- MariaDB Users and Grants: A Least-Privilege Production Guide
- Configure MariaDB TLS for Secure Client Connections
- Harden MariaDB Against SQL Injection and Credential Leaks
- Audit MariaDB Users, Privileges, and Security Events
- Troubleshoot MariaDB Connection Refused and Access Denied