A Galera Cluster for MariaDB is a synchronous multi-master database solution - to break it down:
INSERT, UPDATE, or DELETE) on one node, it doesn't complete until it has been successfully replicated and confirmed by all other nodes in the cluster. This guarantees that all nodes have the exact same data at the exact same time, preventing data inconsistencies.In this guide, we will focus on the home set up but can cover a multi-site set up in the near future.
# Start the LXC if not already running
pct start 120
# Log into its shell
pct enter 120
# Perform a system update & upgrade
apt update && apt upgrade
apt upgrade apt -y
# Install prerequisite packages
sudo apt install -y dirmngr ca-certificates apt-transport-https curl
# Download and install a combo of MariaDB server + client + galera with a backup client + firewall + ssh + cron
apt install mariadb-server mariadb-client mariadb-backup galera-4 rsync nano ssh ufw cron fail2ban ncdu -y
# Create a user
adduser <username>
# Add the user into the list of admins
usermod -aG sudo jan
# Switch into that user
su jan
rsync was the method used for syncing between nodes. The challenge with this method is that while the update is being sent by the ‘donor’ node, the receiving ‘joiner’ node is not accepting other traffic. socat utility, which must be installed on both ‘donor’ and the ‘joiner’ nodes. gcache).wsrep_sst_method script (mariabackup or rsync).# Log into your MariaDB instance
mariadb -u root -p
CREATE USER 'sst_user'@'localhost' IDENTIFIED BY 'YourSSTPassword';
GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO 'sst_user'@'localhost';
FLUSH PRIVILEGES;
EXIT;
To modify the default SSH port, edit the following file (do not accidentally swap it with ‘ssh_config’, as that one will not lead to the desired change).
nano /etc/ssh/sshd_config # In case you do not have sudoers (above) installed, use 'su -' and then run this command without sudo
sudo systemctl restart ssh
ss -tulpn | grep 22 # You should see a line that includes [::]:2222
# Create a folder for logs and set up permissions
mkdir /var/log/mysql
chown mysql:mysql /var/log/mysql
chmod 2750 /var/log/mysql
nano /etc/mysql/mariadb.conf.d/50-server.cnf
# Change the bind address from 127.0.0.1
bind-address = 0.0.0.0
# Un-comment these two lines
general_log_file = /var/log/mysql/mysql.log
log_error = /var/log/mysql/error.log
# Restart the mariadb service
systemctl restart mariadb
# Simulate an error:
mysql -u someuser
# Check that an error was produced - this is important for fail2ban
tail -n 5 /var/log/mysql/error.log
Set up fail2ban for repeated failed login attempts (just in case).
# Create your own jail file
nano /etc/fail2ban/jail.local
# Copy paste the following (adjust as you see fit)
[mariadb]
enabled = true
port = 3306
filter = mysqld-auth
logpath = /var/log/mysql/error.log
findtime = 3600
maxretry = 5
bantime = 360
ignoreip = 127.0.0.1/8 ::1
systemctl reload fail2ban
systemctl restart mysql
# Allow temporary access to your instance using user 'root' - the password
mariadb -u root -p
# Replace the IP below with your testing host ip
GRANT ALL PRIVILEGES ON *.* TO 'randomuser'@'192.168.8.73'
IDENTIFIED BY 'some-long-random-password' WITH GRANT OPTION;
FLUSH PRIVILEGES;
EXIT;
# Now grab another host that has mariadb-client package installed and run a few of these:
mysql -h <your_galera_template_host_ip> -u blablah -plalala
# You should expect an error saying 'Access denied for user ...'
# Now hop on the galera template container and run the following to see the stats:
fail2ban-client status mariadb
fail2ban-client banned fail2ban-client set mariadb unbanip <offending_ip_address>
mysql -u root -p
DROP USER 'root'@'192.168.8.73';
FLUSH PRIVILEGES;
EXIT;
ufw allow proto tcp from 192.168.8.0/24 to any port 2222 # Allow comms for SSH from your subnet
ufw allow proto tcp from 192.168.8.0/24 to any port 3306 # Allow HAProxy to reach out for health checks and forward traffic
ufw allow proto tcp from 192.168.8.0/24 to any port 4567 # Handles galera replication traffic (TCP)
ufw allow proto udp from 192.168.8.0/24 to any port 4567 # Handles galera replication traffic (UDP)
ufw allow proto tcp from 192.168.8.0/24 to any port 4568 # Used for Incremental State Transfer (IST)
ufw allow proto tcp from 192.168.8.0/24 to any port 4444 # Rsyncd to transfer data for SST (mysqldump)
ufw deny http # We are not running a web server here so no need
ufw deny https # Same as above
ufw deny ftp # No FTP server needed here
ufw reload # Reload firewall rules
ufw enable # Enforce the firewall
ufw allow proto tcp from 192.168.8.0/24 to any port 10050 # Port for Zabbix no.1
ufw allow proto tcp from 192.168.8.0/24 to any port 10051 # Port for Zabbix no.2
ufw allow proto tcp from 192.168.8.0/24 to any port 3001 # Port for UptimeKuma
# Display all rules by number
ufw status numbered
# See the output of all the rules and choose which one you want to delete. Example:
# ufw delete 1
Let’s run the pre-packaged ‘mariadb-secure-installation’ script from the shell of any of the MariaDB container instances:
For more explanation and screenshots of each step, check out Linuxteck's guide.
This should be all for security hardening.
# Leave the container
exit
# Shut the container down (if you are connecting from Proxmox - if not, switch it off from the GUI).
pct stop <CT_ID>
So now we have a container prepared and need to turn it into a template and then create the actual MariaDB galera nodes from the template.
In the future, we can keep this template updated by creating an instance from it, updating it and then turning it back into a template.
Just a little stop in case you have not had to deal with this one before, and why I would strongly recommend ‘Full Clone’ for a DB container.
Let’s say that you created your first container. Good! Let’s assign it a static IP.
systemctl stop mariadb
nano /etc/mysql/mariadb.conf.d/60-galera.cnf
[mysqld]
# MySQL-related settings
bind-address = 0.0.0.0
binlog_format = ROW
default_storage_engine = InnoDB
innodb_autoinc_lock_mode = 2
# This setting provides better write performance at a small risk of data
# loss on OS crash (not just DB crash). In a multi-node cluster, this is an acceptable
# and common setting, as data exists on other nodes.
innodb_flush_log_at_trx_commit = 2
innodb_log_file_size = 256M
innodb_log_buffer_size = 64M
# A shorter lock wait timeout fails faster in a cluster environment,
# which is generally preferred over long waits.
innodb_lock_wait_timeout = 60
# The InnoDB buffer pool is the most critical performance setting.
# Set this to ~70% of the server's available RAM if it's a dedicated database server.
innodb_buffer_pool_size = 800M
# Skip trying to resolve names since we are using IP addresses
skip-name-resolve
# Allow for more attempts to prevent HAProxy from being blocked
max_connect_errors = 1000
# Logs not only errors but warnings, too (older more compatible method from log_error_verbosity)):
log_warnings = 1
wait_timeout = 1800
interactive_timeout = 1800
max_allowed_packet = 1G
[galera]
# Galera Provider Configuration
wsrep_on=ON
wsrep_provider=/usr/lib/galera/libgalera_smm.so
# Note: The gcache stores writesets for nodes that briefly disconnect.
# A larger gcache allows for a longer disconnect time before a full SST is required.
wsrep_provider_options="gcache.size=512M;gcs.fc_limit=128;gcs.fc_factor=0.8"
# Galera Cluster Configuration (ADJUST - same on each node)
wsrep_cluster_name="clusterA"
# Info about each node (ADJUST - same on each node + add a witness if you have one)
wsrep_cluster_address="gcomm://192.168.8.71,192.168.8.72,192.168.8.73,192.168.8.74"
# The newer non-blocking method is mariadb, the older one is rsync (ports need to be opened!)
wsrep_sst_method = mariabackup
# SST user for syncinging, same for each node. UPDATE!
wsrep_sst_auth = sst_user:YourSSTPassword
# Galera Node Configuration (ADJUST FOR EACH NODE)
wsrep_node_address = "192.168.8.71" # <-- CHANGE THIS ON EACH NODE
wsrep_node_name = "galera-a1" # <-- CHANGE THIS ON EACH NODE
wsrep_sst_receive_address = "192.168.8.71" # <-- CHANGE THIS ON EACH NODE
# Initiate the galera cluster from the first node:
galera_new_cluster
# Check that the output shows '1'
mysql -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
# Now start the MariaDB service!
systemctl start mariadb
# This ensures the user can connect from the subnet used for the Galera nodes:
CREATE USER IF NOT EXISTS 'sst_user'@'192.168.%' IDENTIFIED BY 'YourPassword';
# These are the standard "Donor" permissions for mariabackup
GRANT RELOAD, PROCESS, LOCK TABLES, REPLICATION CLIENT ON *.* TO 'sst_user'@'192.168.%';
# MariaDB 11.x also likes BINLOG MONITOR
GRANT BINLOG MONITOR ON *.* TO 'sst_user'@'192.168.%';
FLUSH PRIVILEGES;
systemctl stop mariadb
nano /etc/mysql/mariadb.conf.d/60-galera.cnf
# Copy the same config file as above with the difference in the last two rows:
# Galera Node Configuration (ADJUST FOR EACH NODE)
wsrep_node_address = "192.168.8.72" # <-- CHANGE THIS ON EACH NODE
wsrep_node_name = "galera-a2" # <-- CHANGE THIS ON EACH NODE
wsrep_sst_receive_address = "192.168.8.72" # <-- CHANGE THIS ON EACH NODE
# Exit with save. Do not start a new cluster by running 'galera_new_cluster'.
# Start the MariaDB service:
systemctl start mariadb
# Verify that the service started and the node joined the cluster
systemctl status mariadb
# Check how many nodes are in the cluster
mysql -e "SHOW STATUS LIKE 'wsrep_cluster_size';"
Rinse and repeat with ‘galera-A3’ and ‘galera-A4’, each time:
60-galera.conf filesst_user to ensure snapshot-based backups work during short outages.Once all four nodes have been added to the cluster, you can run mysql -e "SHOW STATUS LIKE 'wsrep_cluster_size';" on any of them and should get the value of 4.
mariadb -u root -p
-- Create the user for the first and second OPNsense nodes (or if you have more)
CREATE USER 'haproxy_check'@'opnsense_node1_ip_here' IDENTIFIED BY 'your_password_here';
CREATE USER 'haproxy_check'@'opnsense_node2_ip_here' IDENTIFIED BY 'your_password_here';
-- Grant the ability to connect to the server and run commands for health checks
GRANT USAGE ON *.* TO 'haproxy_check'@'opnsense_node1_ip_here';
GRANT REPLICATION CLIENT ON *.* TO 'haproxy_check'@'opnsense_node1_ip';
GRANT USAGE ON *.* TO 'haproxy_check'@'opnsense_node2_ip_here';
GRANT REPLICATION CLIENT ON *.* TO 'haproxy_check'@'opnsense_node2_ip';
-- Apply the new permissions
FLUSH PRIVILEGES;
SELECT User, Host FROM mysql.user;
mariadb -u root -p
DROP USER 'haproxy_check'@'opnsense_node1_ip_here';
DROP USER 'haproxy_check'@'opnsense_node2_ip_here';
FLUSH PRIVILEGES;
EXIT;
With all four nodes up and talking to each other, the next part puts HAProxy in front of them and confirms the cluster actually survives a failure.