/** * @file pgsql-ssl_keylog-t.cpp * @brief Integration test for PgSQL backend SSL keylog support. * * @details Validates that TLS secrets from PostgreSQL backend SSL connections * are written to the keylog file when admin-ssl_keylog_file is set. * * ProxySQL patches libpq with a global SSL keylog callback * (PQsetSSLKeyLogCallback). When admin-ssl_keylog_file is set, ALL * TLS connections write secrets to the keylog file in NSS Key Log Format. * * Test cases: * 1. Basic keylog creation — set admin-ssl_keylog_file, make PgSQL SSL connection, * verify file is non-empty * 2. NSS format validation — parse keylog lines, verify regex match for NSS Key Log Format * 3. TLS version label check — verify keylog contains TLS 1.2 or 1.3 secret labels * 4. Concurrent connections — 8 threads making PgSQL SSL connections, verify no corruption * 5. Disable keylog — set empty, make new connections, verify no new lines * 6. Log rotation — PROXYSQL FLUSH LOGS, verify new secrets appended * 7. Monitor SSL keylog — enable use_ssl=1, wait for monitor, verify keylog entries * 8. Non-SSL no keylog — use_ssl=0, verify no new entries * 9. Invalid keylog path — set /nonexistent/dir/file, verify graceful handling * * Test infrastructure: docker-pgsql16-single (PgSQL backend with SSL enabled) * Admin connection: PgSQL protocol on pgsql_admin_host:pgsql_admin_port */ #include #include #include #include #include #include #include #include #include #include #include "libpq-fe.h" #include "command_line.h" #include "tap.h" #include "utils.h" CommandLine cl; using PGConnPtr = std::unique_ptr; // The keylog file has to live in a path that is visible to BOTH the // ProxySQL process (which writes it) AND the test-runner process (which // reads it). In the isolated CI infra, ProxySQL and the test runner live // in different docker containers, each with its own /tmp/ - so /tmp/ is // not shared and the test-runner's file_size() / count_file_lines() calls // return -1 on a file that ProxySQL is happily writing to inside its own // container. /var/lib/proxysql/ IS bind-mounted on both containers (see // test/infra/control/run-tests-isolated.bash: the `-v ${PROXY_DATA_DIR_HOST} // :/var/lib/proxysql` mount appears on BOTH the proxysql. and // test-runner. containers), so writes on one side are readable on the // other. Use that path here. static const char* KEYLOG_PATH = "/var/lib/proxysql/pgsql_ssl_keylog_test.log"; // ============================================================ // Helper: open PgSQL admin connection // ============================================================ static PGConnPtr open_admin() { std::stringstream ss; ss << "host=" << cl.pgsql_admin_host << " port=" << cl.pgsql_admin_port << " user=" << cl.admin_username << " password=" << cl.admin_password << " sslmode=disable"; PGconn* conn = PQconnectdb(ss.str().c_str()); if (PQstatus(conn) != CONNECTION_OK) { fprintf(stderr, "Admin connection failed: %s", PQerrorMessage(conn)); PQfinish(conn); return PGConnPtr(nullptr, &PQfinish); } return PGConnPtr(conn, &PQfinish); } // ============================================================ // Helper: run admin SQL, return true on success // ============================================================ static bool admin_exec(PGconn* admin, const char* sql) { PGresult* res = PQexec(admin, sql); ExecStatusType st = PQresultStatus(res); bool ok = (st == PGRES_COMMAND_OK || st == PGRES_TUPLES_OK); if (!ok) { diag("Admin query failed: '%s' err='%s'", sql, PQerrorMessage(admin)); } PQclear(res); return ok; } // ============================================================ // Helper: run admin SQL and return first column of first row // ============================================================ static std::string admin_query_value(PGconn* admin, const char* sql) { std::string result; PGresult* res = PQexec(admin, sql); if (PQresultStatus(res) == PGRES_TUPLES_OK && PQntuples(res) > 0) { const char* val = PQgetvalue(res, 0, 0); if (val) result = val; } else { diag("Admin query failed: '%s' err='%s'", sql, PQerrorMessage(admin)); } PQclear(res); return result; } // ============================================================ // Helper: set admin-ssl_keylog_file and load to runtime // ============================================================ static bool set_keylog_file(PGconn* admin, const char* path) { std::string sql = "SET admin-ssl_keylog_file='"; if (path) sql += path; sql += "'"; if (!admin_exec(admin, sql.c_str())) return false; return admin_exec(admin, "LOAD ADMIN VARIABLES TO RUNTIME"); } // ============================================================ // Helper: make a PgSQL backend connection with SSL // ============================================================ static PGConnPtr make_pgsql_ssl_conn() { std::stringstream ss; ss << "host=" << cl.pgsql_host << " port=" << cl.pgsql_port << " user=" << cl.pgsql_root_username << " password=" << cl.pgsql_root_password << " sslmode=require"; PGconn* conn = PQconnectdb(ss.str().c_str()); return PGConnPtr(conn, &PQfinish); } // ============================================================ // Helper: make a PgSQL backend connection without SSL // ============================================================ static PGConnPtr make_pgsql_nossl_conn() { std::stringstream ss; ss << "host=" << cl.pgsql_host << " port=" << cl.pgsql_port << " user=" << cl.pgsql_root_username << " password=" << cl.pgsql_root_password << " sslmode=disable"; PGconn* conn = PQconnectdb(ss.str().c_str()); return PGConnPtr(conn, &PQfinish); } // ============================================================ // Helper: count non-empty lines in a file // ============================================================ static long count_file_lines(const char* path) { std::ifstream f(path); if (!f.is_open()) return -1; long n = 0; std::string line; while (std::getline(f, line)) { if (!line.empty()) ++n; } return n; } // ============================================================ // Helper: get file size in bytes, returns -1 if file doesn't exist // ============================================================ static long file_size(const char* path) { std::ifstream f(path, std::ios::ate | std::ios::binary); if (!f.is_open()) return -1; return (long)f.tellg(); } // ============================================================ // Helper: check whether any line in the file matches a regex // ============================================================ static bool file_has_regex_match(const char* path, const std::regex& re) { std::ifstream f(path); if (!f.is_open()) return false; std::string line; while (std::getline(f, line)) { if (std::regex_search(line, re)) return true; } return false; } // ============================================================ // Helper: set pgsql_servers use_ssl and reload // ============================================================ static bool set_pgsql_use_ssl(PGconn* admin, int value) { std::string sql = "UPDATE pgsql_servers SET use_ssl=" + std::to_string(value); if (!admin_exec(admin, sql.c_str())) return false; return admin_exec(admin, "LOAD PGSQL SERVERS TO RUNTIME"); } // ============================================================ // Helper: get pgsql-monitor_connect_interval (ms) // ============================================================ static long get_monitor_interval(PGconn* admin) { std::string v = admin_query_value(admin, "SELECT Variable_Value FROM global_variables " "WHERE Variable_Name='pgsql-monitor_connect_interval'"); return v.empty() ? 1000 : atol(v.c_str()); } int main(int argc, char** argv) { plan(9); if (cl.getEnv()) { diag("Failed to get required environment variables"); return exit_status(); } // Remove any leftover keylog file from a prior run ::remove(KEYLOG_PATH); // Open PgSQL admin connection auto admin_conn = open_admin(); if (!admin_conn) { BAIL_OUT("Admin connection failed — cannot continue"); return exit_status(); } PGconn* admin = admin_conn.get(); diag("PgSQL admin connected to %s:%d", cl.pgsql_admin_host, cl.pgsql_admin_port); // Save original keylog file setting so we can restore it at the end std::string original_keylog = admin_query_value(admin, "SELECT Variable_Value FROM global_variables " "WHERE Variable_Name='admin-ssl_keylog_file'"); diag("Original admin-ssl_keylog_file='%s'", original_keylog.c_str()); // ================================================================ // Test 1: Basic keylog creation // ================================================================ diag("---- Test 1: Basic keylog creation ----"); bool set_ok = set_keylog_file(admin, KEYLOG_PATH); diag("Set admin-ssl_keylog_file='%s' result=%s", KEYLOG_PATH, set_ok ? "ok" : "FAIL"); if (set_ok) { auto conn1 = make_pgsql_ssl_conn(); if (PQstatus(conn1.get()) != CONNECTION_OK) { diag("PgSQL SSL connection failed: %s", PQerrorMessage(conn1.get())); } usleep(100000); } long sz = file_size(KEYLOG_PATH); diag("Keylog file size after SSL PgSQL connection: %ld bytes", sz); ok(set_ok && sz > 0, "Test 1: Keylog file is non-empty after PgSQL SSL connection (size=%ld)", sz); // ================================================================ // Test 2: NSS format validation // ================================================================ diag("---- Test 2: NSS Key Log Format validation ----"); // NSS Key Log Format: //