A standby PostgreSQL server keeps a copy of the API database. While the primary API is unavailable, the standby API serves read requests from that copy: settings and behavioral models remain available the API rejects changes such as new attack records or setting updates.
The paths and pg_ctlcluster commands below are for Debian and Ubuntu with PostgreSQL 17. 10.0.0.1 is the primary server and 10.0.0.2 is the standby server. Replace them with the addresses of the installation.
5432 unless it was changed:iptables -A INPUT -p tcp --dport 5432 -j ACCEPT
Replication clients are also restricted in pg_hba.conf below.
su - postgres -c "createuser --replication -P repluser"
The command asks for the password of repluser.
/etc/postgresql/17/main/postgresql.conf:listen_addresses = '*'
checkpoint_timeout = 30s
max_wal_size = 1GB
min_wal_size = 80MB
wal_level = replica
max_wal_senders = 2
max_replication_slots = 2
hot_standby = on
hot_standby_feedback = on
max_wal_senders and max_replication_slots are the number of standby servers plus one. The value 2 is for one standby.
repluser to connect for replication. Add the following lines to /etc/postgresql/17/main/pg_hba.conf:host replication repluser 127.0.0.1/32 md5
host replication repluser 10.0.0.2/32 md5
slave01 is the name used by the standby in primary_slot_name:su - postgres -c "psql -c \"SELECT pg_create_physical_replication_slot('slave01');\""
systemctl restart postgresql
The standby keeps API available for reading while the primary API is down. The API on this server can read settings and behavioral models. It cannot write attack records or change Mikiri WAF settings.
The standby uses its own PostgreSQL cluster named slave, so its configuration and data directory are not the main cluster of a normal installation.
5433:pg_createcluster 17 slave --port=5433
pg_ctlcluster 17 slave stop
/etc/postgresql/17/slave/postgresql.conf:port = 5433
hot_standby = on
primary_conninfo = 'user=repluser password=YOUR_PASSWORD host=10.0.0.1 port=5432 sslmode=prefer'
primary_slot_name = 'slave01'
Replace YOUR_PASSWORD with the password of repluser.
rm -rf /var/lib/postgresql/17/slave/*
su - postgres -c "pg_basebackup -h 10.0.0.1 -U repluser -D /var/lib/postgresql/17/slave --write-recovery-conf"
--write-recovery-conf marks the data directory as a standby. pg_basebackup asks for the password of repluser.
pg_ctlcluster 17 slave start
pg_ctlcluster 17 slave status
postgres user:su - postgres -c "psql -d postgres -c \"CREATE TABLE test_table (id INT, name TEXT);\""
su - postgres -c "psql -d postgres -c \"INSERT INTO test_table (id, name) VALUES (1, 'test');\""
5433:su - postgres -c "psql -p 5433 -d postgres -c \"SELECT * FROM test_table;\""
The row (1, test) is present when replication is running. Drop test_table on the primary after the check. The standby cannot drop it.
On the API server that uses the standby database, turn on read-only mode and point the API at that database. The configuration file is /var/www/mikiri-waf-api/settings.py. The option is described in Configuring the agent.
DB_HOST = '127.0.0.1'
DB_PORT = '5433'
RO_MODE = true
DB_PORT is 5433 because the standby cluster listens on that port. Restart the services:
systemctl restart nginx memcached mikiri-waf-api mikiri-waf-api-aux
A request that changes data receives the response code 502.
Filter resolves the name of the API and receives the addresses of two Nginx servers, one in each circuit. Round-robin DNS chooses which of those Nginx servers receives the request. Each circuit contains Nginx, API PostgreSQL. The Nginx configuration is the same in both circuits: the request goes to the primary API to the read-only API only when the primary does not answer.
upstream api.example.com {
server 10.0.0.1 max_fails=3 fail_timeout=30s;
server 10.0.0.2 backup;
}
server {
location / {
proxy_pass http://api.example.com;
}
}
10.0.0.1 is the primary API. It writes to the PostgreSQL server in the same circuit. That PostgreSQL server replicates to the PostgreSQL server in the standby circuit. 10.0.0.2 is the read-only API. It reads the replica in its own circuit.
When 10.0.0.1 does not answer, Nginx in either circuit sends the request to 10.0.0.2.
A solid arrow is the path used while the primary API answers. A dashed arrow is the path used when it does not.
10.0.0.1 when that server answers.10.0.0.1 does not answer, Nginx in either circuit sends the request to the read-only API at 10.0.0.2.When the standby has fallen behind and a new copy is required, repeat the base backup. Run these commands on the standby server.
pg_ctlcluster 17 slave stop
pg_hba.conf and replace the data directory:cp /etc/postgresql/17/slave/pg_hba.conf /etc/postgresql/17/slave/pg_hba.conf.bak
rm -rf /var/lib/postgresql/17/slave/*
su - postgres -c "pg_basebackup -h 10.0.0.1 -U repluser -D /var/lib/postgresql/17/slave --write-recovery-conf"
pg_hba.conf was not replaced:diff /etc/postgresql/17/slave/pg_hba.conf /etc/postgresql/17/slave/pg_hba.conf.bak
No output means the files are identical.
pg_ctlcluster 17 slave start
pg_ctlcluster 17 slave status