diff --git a/doc/admin_tls.md b/doc/admin_tls.md new file mode 100644 index 0000000000..d75c3b31cd --- /dev/null +++ b/doc/admin_tls.md @@ -0,0 +1,65 @@ +# Dedicated TLS for the Admin interfaces + +ProxySQL can use a dedicated TLS context for its MySQL and PostgreSQL Admin +interfaces. The feature is disabled by default, so existing deployments retain +their previous behavior. + +When `admin-ssl_enabled` is enabled, all new connections to either Admin +interface must negotiate TLS. Existing connections are not disconnected. + +## Configuration + +| Variable | Default | Description | +|---|---|---| +| `admin-ssl_enabled` | `false` | Require TLS and activate the dedicated Admin TLS context. | +| `admin-ssl_key` | empty | PEM private key. Required when enabled. | +| `admin-ssl_cert` | empty | PEM certificate chain. Required when enabled. | +| `admin-ssl_ca` | empty | PEM CA bundle used to verify client certificates. | +| `admin-ssl_capath` | empty | OpenSSL hashed CA directory used to verify client certificates. | +| `admin-ssl_cipher` | empty | OpenSSL TLS 1.2 cipher list. The OpenSSL default is used when empty. | +| `admin-tls_version` | `TLSv1.2` | Minimum accepted version: `TLSv1.2` or `TLSv1.3`. | +| `admin-ssl_curves` | empty | OpenSSL groups/curves list. | +| `admin-ssl_verify_client` | `DISABLED` | Client certificate mode: `DISABLED`, `OPTIONAL`, or `REQUIRED`. Numeric values `0`, `1`, and `2` are also accepted. | +| `admin-ssl_crl` | empty | PEM certificate revocation list. | +| `admin-ssl_crlpath` | empty | OpenSSL hashed CRL directory. | + +Relative paths are resolved against ProxySQL's data directory. Client +verification requires `admin-ssl_ca` or `admin-ssl_capath`. + +Example using ProxySQL's existing default certificate: + +```sql +SELECT Variable_Name, Variable_Value +FROM stats.stats_proxysql_global +WHERE Variable_Name IN + ('TLS_Key_File', 'TLS_Server_Cert_File', 'TLS_CA_Cert_File'); + +SET admin-ssl_key='/var/lib/proxysql/proxysql-key.pem'; +SET admin-ssl_cert='/var/lib/proxysql/proxysql-cert.pem'; +SET admin-ssl_ca='/var/lib/proxysql/proxysql-ca.pem'; +SET admin-ssl_enabled='true'; +LOAD ADMIN VARIABLES TO RUNTIME; +SAVE ADMIN VARIABLES TO DISK; +``` + +To require a trusted client certificate: + +```sql +SET admin-ssl_verify_client='REQUIRED'; +LOAD ADMIN VARIABLES TO RUNTIME; +``` + +## Reload behavior + +`LOAD ADMIN VARIABLES TO RUNTIME` builds and validates a complete replacement +context before activating it. If validation fails, the previous runtime +configuration and context remain active and the command returns an error. + +Certificate files can be re-read without changing variables: + +```sql +PROXYSQL RELOAD ADMIN TLS; +``` + +The reload is atomic for new connections. Connections that are already using +TLS continue with the context under which they were established. diff --git a/include/MySQL_Protocol.h b/include/MySQL_Protocol.h index c6b2e3e2f6..43bb4fff68 100644 --- a/include/MySQL_Protocol.h +++ b/include/MySQL_Protocol.h @@ -159,7 +159,7 @@ class MySQL_Protocol { // - a pointer to unsigned int, used to return the size of the packet if not NULL // for now, they all return true bool generate_pkt_OK(bool send, void **ptr, unsigned int *len, uint8_t sequence_id, unsigned int affected_rows, uint64_t last_insert_id, uint16_t status, uint16_t warnings, char *msg, bool eof_identifier=false); - bool generate_pkt_ERR(bool send, void **ptr, unsigned int *len, uint8_t sequence_id, uint16_t error_code, char *sql_state, const char *sql_message, bool track=false); + bool generate_pkt_ERR(bool send, void **ptr, unsigned int *len, uint8_t sequence_id, uint16_t error_code, const char *sql_state, const char *sql_message, bool track=false); bool generate_pkt_EOF(bool send, void **ptr, unsigned int *len, uint8_t sequence_id, uint16_t warnings, uint16_t status, MySQL_ResultSet *myrs=NULL); // bool generate_COM_INIT_DB(bool send, void **ptr, unsigned int *len, char *schema); //bool generate_COM_PING(bool send, void **ptr, unsigned int *len); diff --git a/include/proxysql_admin.h b/include/proxysql_admin.h index 5ef4c980ca..c89a9d39fb 100644 --- a/include/proxysql_admin.h +++ b/include/proxysql_admin.h @@ -317,6 +317,7 @@ struct FlushVariableStats { int updated = 0; int rejected = 0; int unknown = 0; + std::string error; }; class ProxySQL_Admin { @@ -360,6 +361,7 @@ class ProxySQL_Admin { void wrlock(); void wrunlock(); + int reload_admin_tls_unlocked(std::string& msg); struct { char *admin_credentials; @@ -370,6 +372,25 @@ class ProxySQL_Admin { char *telnet_admin_ifaces; char *telnet_stats_ifaces; bool admin_read_only; + /** + * Require TLS and use the dedicated Admin TLS context for MySQL and + * PostgreSQL Admin interface connections. + */ + bool admin_ssl_enabled; + char *admin_ssl_key; + char *admin_ssl_cert; + char *admin_ssl_ca; + char *admin_ssl_capath; + char *admin_ssl_cipher; + char *admin_tls_version; + char *admin_ssl_curves; + /** + * Client certificate verification: 0=DISABLED, 1=OPTIONAL, + * 2=REQUIRED. + */ + int admin_ssl_verify_client; + char *admin_ssl_crl; + char *admin_ssl_crlpath; // bool hash_passwords; bool vacuum_stats; char * admin_version; @@ -644,6 +665,7 @@ class ProxySQL_Admin { void load_restapi_server(); bool get_read_only() { return variables.admin_read_only; } bool set_read_only(bool ro) { variables.admin_read_only=ro; return variables.admin_read_only; } + int reload_admin_tls(std::string& msg); bool has_variable(const char *name); void init_users(std::unique_ptr&& mysql_users_resultset = nullptr, const std::string& checksum = "", const time_t epoch = 0); void init_mysql_servers(); @@ -748,7 +770,9 @@ class ProxySQL_Admin { void load_scheduler_to_runtime(); void save_scheduler_runtime_to_database(bool); - void load_admin_variables_to_runtime(const std::string& checksum = "", const time_t epoch = 0, bool lock = true) { flush_admin_variables___database_to_runtime(admindb, true, checksum, epoch, lock); } + FlushVariableStats load_admin_variables_to_runtime(const std::string& checksum = "", const time_t epoch = 0, bool lock = true) { + return flush_admin_variables___database_to_runtime(admindb, true, checksum, epoch, lock); + } void save_admin_variables_from_runtime() { flush_admin_variables___runtime_to_database(admindb, true, true, false); } #ifdef PROXYSQLTSDB diff --git a/include/proxysql_glovars.hpp b/include/proxysql_glovars.hpp index 104c2613c1..6b52db17a9 100644 --- a/include/proxysql_glovars.hpp +++ b/include/proxysql_glovars.hpp @@ -109,6 +109,9 @@ class ProxySQL_GlobalVariables { #endif /* PROXYSQL40 */ SSL_CTX *get_SSL_ctx(); SSL *get_SSL_new(); + SSL *get_admin_SSL_new(); + bool is_admin_SSL_enabled(); + void set_admin_SSL_ctx(SSL_CTX *ctx, bool enabled); void get_SSL_pem_mem(char **key, char **cert); std::shared_ptr prometheus_registry { nullptr }; struct { @@ -138,6 +141,9 @@ class ProxySQL_GlobalVariables { std::mutex ssl_mutex; SSL_CTX *ssl_ctx; SSL_CTX *tmp_ssl_ctx; + std::mutex admin_ssl_mutex; + SSL_CTX *admin_ssl_ctx; + bool admin_ssl_enabled; // these two buffers are used for the web interface char * ssl_key_pem_mem; char * ssl_cert_pem_mem; diff --git a/lib/Admin_Bootstrap.cpp b/lib/Admin_Bootstrap.cpp index d1a0448ae1..430434b887 100644 --- a/lib/Admin_Bootstrap.cpp +++ b/lib/Admin_Bootstrap.cpp @@ -1172,7 +1172,12 @@ bool ProxySQL_Admin::init(const bootstrap_info_t& bootstrap_info) { // TODO-NOTE: This MUST go away; 'admin-hash_passwords' will be deprecated admindb->execute("UPDATE global_variables SET variable_value='false' WHERE variable_name='admin-hash_passwords'"); } - flush_admin_variables___database_to_runtime(admindb,true); + const FlushVariableStats admin_stats = + flush_admin_variables___database_to_runtime(admindb, true); + if (!admin_stats.error.empty()) { + proxy_error("Unable to load admin variables: %s\n", admin_stats.error.c_str()); + return false; + } if (GloVars.global.gr_bootstrap_mode) { flush_admin_variables___runtime_to_database(configdb, false, true, false); diff --git a/lib/Admin_FlushVariables.cpp b/lib/Admin_FlushVariables.cpp index 84db8e5e87..39e471b708 100644 --- a/lib/Admin_FlushVariables.cpp +++ b/lib/Admin_FlushVariables.cpp @@ -277,9 +277,57 @@ FlushVariableStats ProxySQL_Admin::flush_admin_variables___database_to_runtime( SQLite3_result *resultset=NULL; if (flush_GENERIC_variables__retrieve__database_to_runtime("admin", error, cols, affected_rows, resultset) == true) { wrlock(); + const bool old_ssl_enabled = variables.admin_ssl_enabled; + const int old_ssl_verify_client = variables.admin_ssl_verify_client; + const std::string old_ssl_key = variables.admin_ssl_key; + const std::string old_ssl_cert = variables.admin_ssl_cert; + const std::string old_ssl_ca = variables.admin_ssl_ca; + const std::string old_ssl_capath = variables.admin_ssl_capath; + const std::string old_ssl_cipher = variables.admin_ssl_cipher; + const std::string old_tls_version = variables.admin_tls_version; + const std::string old_ssl_curves = variables.admin_ssl_curves; + const std::string old_ssl_crl = variables.admin_ssl_crl; + const std::string old_ssl_crlpath = variables.admin_ssl_crlpath; + FlushVariableStats stats = flush_GENERIC_variables__process__database_to_runtime("admin", db, resultset, lock, replace, {"version"}, {"debug"}, {}, {}); //commit(); NOT IMPLEMENTED + const bool admin_tls_changed = + old_ssl_enabled != variables.admin_ssl_enabled + || old_ssl_verify_client != variables.admin_ssl_verify_client + || old_ssl_key != variables.admin_ssl_key + || old_ssl_cert != variables.admin_ssl_cert + || old_ssl_ca != variables.admin_ssl_ca + || old_ssl_capath != variables.admin_ssl_capath + || old_ssl_cipher != variables.admin_ssl_cipher + || old_tls_version != variables.admin_tls_version + || old_ssl_curves != variables.admin_ssl_curves + || old_ssl_crl != variables.admin_ssl_crl + || old_ssl_crlpath != variables.admin_ssl_crlpath; + + if (admin_tls_changed || variables.admin_ssl_enabled != GloVars.is_admin_SSL_enabled()) { + std::string tls_error; + if (reload_admin_tls_unlocked(tls_error) != 0) { + auto restore_string = [](char **target, const std::string& value) { + free_null(*target); + *target = strdup(value.c_str()); + }; + variables.admin_ssl_enabled = old_ssl_enabled; + variables.admin_ssl_verify_client = old_ssl_verify_client; + restore_string(&variables.admin_ssl_key, old_ssl_key); + restore_string(&variables.admin_ssl_cert, old_ssl_cert); + restore_string(&variables.admin_ssl_ca, old_ssl_ca); + restore_string(&variables.admin_ssl_capath, old_ssl_capath); + restore_string(&variables.admin_ssl_cipher, old_ssl_cipher); + restore_string(&variables.admin_tls_version, old_tls_version); + restore_string(&variables.admin_ssl_curves, old_ssl_curves); + restore_string(&variables.admin_ssl_crl, old_ssl_crl); + restore_string(&variables.admin_ssl_crlpath, old_ssl_crlpath); + stats.error = tls_error; + proxy_error("Rejected Admin TLS configuration: %s\n", tls_error.c_str()); + } + } + // Checksums are always generated - 'admin-checksum_*' deprecated { diff --git a/lib/Admin_Handler.cpp b/lib/Admin_Handler.cpp index 573e03382b..93c8d15bfd 100644 --- a/lib/Admin_Handler.cpp +++ b/lib/Admin_Handler.cpp @@ -1149,6 +1149,21 @@ bool admin_handler_command_proxysql(char *query_no_space, unsigned int query_no_ return false; } + if (strcasecmp("PROXYSQL RELOAD ADMIN TLS", query_no_space) == 0) { + proxy_info("Received %s command\n", query_no_space); + ProxySQL_Admin *SPA = (ProxySQL_Admin *)pa; + std::string msg; + const int rc = SPA->reload_admin_tls(msg); + if (rc == 0) { + SPA->send_ok_msg_to_client(sess, msg.empty() ? NULL : msg.c_str(), 0, query_no_space); + } else { + SPA->send_error_msg_to_client( + sess, msg.empty() ? "RELOAD ADMIN TLS failed" : msg.c_str() + ); + } + return false; + } + if (strncasecmp("PROXYSQL SET CONFIG FILE ", query_no_space, 25) == 0) { proxy_info("Received %s command\n", query_no_space); ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa; @@ -2752,9 +2767,17 @@ bool admin_handler_command_load_or_save(char *query_no_space, unsigned int query if ( is_admin_command_or_alias(LOAD_ADMIN_VARIABLES_FROM_MEMORY, query_no_space, query_no_space_length) ) { ProxySQL_Admin *SPA=(ProxySQL_Admin *)pa; - SPA->load_admin_variables_to_runtime(); - proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded admin variables to RUNTIME\n"); - SPA->send_ok_msg_to_client(sess, NULL, 0, query_no_space); + const FlushVariableStats stats = SPA->load_admin_variables_to_runtime(); + if (stats.error.empty()) { + proxy_debug(PROXY_DEBUG_ADMIN, 4, "Loaded admin variables to RUNTIME\n"); + SPA->send_ok_msg_to_client(sess, NULL, 0, query_no_space); + } else { + const std::string error = + "Admin variables loaded, but Admin TLS was rejected and rolled back: " + + stats.error; + proxy_error("%s\n", error.c_str()); + SPA->send_error_msg_to_client(sess, error.c_str()); + } return false; } diff --git a/lib/MySQL_Protocol.cpp b/lib/MySQL_Protocol.cpp index 9c1133b746..06ee7dc959 100644 --- a/lib/MySQL_Protocol.cpp +++ b/lib/MySQL_Protocol.cpp @@ -204,7 +204,7 @@ bool MySQL_Protocol::generate_pkt_EOF(bool send, void **ptr, unsigned int *len, return true; } -bool MySQL_Protocol::generate_pkt_ERR(bool send, void **ptr, unsigned int *len, uint8_t sequence_id, uint16_t error_code, char *sql_state, const char *sql_message, bool track) { +bool MySQL_Protocol::generate_pkt_ERR(bool send, void **ptr, unsigned int *len, uint8_t sequence_id, uint16_t error_code, const char *sql_state, const char *sql_message, bool track) { if ((*myds)->sess->mirror==true) { return true; } @@ -1080,7 +1080,12 @@ bool MySQL_Protocol::generate_pkt_initial_handshake(bool send, void **ptr, unsig mysql_thread___server_capabilities &= ~CLIENT_COMPRESS; mysql_thread___server_capabilities &= ~CLIENT_ZSTD_COMPRESSION_ALGORITHM; } - if (mysql_thread___have_ssl==true || mysql_thread___default_authentication_plugin_int==2) { + const bool admin_tls_enabled = + (*myds)->sess != NULL + && ((*myds)->sess->session_type == PROXYSQL_SESSION_ADMIN + || (*myds)->sess->session_type == PROXYSQL_SESSION_STATS) + && GloVars.is_admin_SSL_enabled(); + if (mysql_thread___have_ssl==true || mysql_thread___default_authentication_plugin_int==2 || admin_tls_enabled) { // we enable SSL for client connections for either of these 2 conditions: // - have_ssl is enabled // - default_authentication_plugin=caching_sha2_password diff --git a/lib/MySQL_Session.cpp b/lib/MySQL_Session.cpp index 707b3b5a9c..dcae4c0cb5 100644 --- a/lib/MySQL_Session.cpp +++ b/lib/MySQL_Session.cpp @@ -6529,6 +6529,35 @@ void MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE_ void MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE(PtrSize_t *pkt, bool *wrong_pass) { bool is_encrypted = client_myds->encrypted; + const bool admin_tls_required = + (session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) + && GloVars.is_admin_SSL_enabled(); + + // A plaintext MySQL HandshakeResponse starts with the client capability + // flags after the four-byte packet header. Reject it before credentials + // are parsed when the dedicated Admin TLS context is active. + if ( + admin_tls_required && is_encrypted == false && client_myds->DSS == STATE_SERVER_HANDSHAKE + && pkt->size >= 8 + ) { + const unsigned char *payload = (const unsigned char *)pkt->ptr + 4; + const uint32_t capabilities = + (uint32_t)payload[0] | ((uint32_t)payload[1] << 8) + | ((uint32_t)payload[2] << 16) | ((uint32_t)payload[3] << 24); + if ((capabilities & CLIENT_SSL) == 0) { + l_free(pkt->size, pkt->ptr); + *wrong_pass = true; + client_myds->setDSS_STATE_QUERY_SENT_NET(); + uint8_t packet_id = client_myds->pkt_sid; + packet_id++; + client_myds->myprot.generate_pkt_ERR( + true, NULL, NULL, packet_id, 1045, "28000", + "ProxySQL Admin interface requires SSL", true + ); + return; + } + } + bool handshake_response_return = client_myds->myprot.process_pkt_handshake_response((unsigned char *)pkt->ptr,pkt->size); bool handshake_err = true; @@ -6548,16 +6577,50 @@ void MySQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE( client_myds->DSS=STATE_SSL_INIT; client_myds->rbio_ssl = BIO_new(BIO_s_mem()); client_myds->wbio_ssl = BIO_new(BIO_s_mem()); - client_myds->ssl = GloVars.get_SSL_new(); + const bool admin_session = + session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS; + client_myds->ssl = + admin_session ? GloVars.get_admin_SSL_new() : GloVars.get_SSL_new(); + if (client_myds->ssl == NULL) { + proxy_error( + "Unable to create %s TLS connection: SSL_new() failed\n", + admin_session ? "MySQL Admin" : "MySQL frontend" + ); + BIO_free(client_myds->rbio_ssl); + BIO_free(client_myds->wbio_ssl); + client_myds->rbio_ssl = NULL; + client_myds->wbio_ssl = NULL; + l_free(pkt->size, pkt->ptr); + *wrong_pass = true; + client_myds->setDSS_STATE_QUERY_SENT_NET(); + return; + } SSL_set_fd(client_myds->ssl, client_myds->fd); SSL_set_accept_state(client_myds->ssl); SSL_set_bio(client_myds->ssl, client_myds->rbio_ssl, client_myds->wbio_ssl); l_free(pkt->size,pkt->ptr); - proxysql_keylog_attach_callback(GloVars.get_SSL_ctx()); + proxysql_keylog_attach_callback(SSL_get_SSL_CTX(client_myds->ssl)); return; } } + if ( + handshake_response_return == true + && admin_tls_required + && is_encrypted == false + ) { + l_free(pkt->size, pkt->ptr); + *wrong_pass = true; + client_myds->setDSS_STATE_QUERY_SENT_NET(); + uint8_t packet_id = client_myds->pkt_sid; + packet_id++; + client_myds->myprot.generate_pkt_ERR( + true, NULL, NULL, packet_id, 1045, "28000", + "ProxySQL Admin interface requires SSL", true + ); + return; + } + if ( //(client_myds->myprot.process_pkt_handshake_response((unsigned char *)pkt->ptr,pkt->size)==true) (handshake_response_return == true) diff --git a/lib/PgSQL_Protocol.cpp b/lib/PgSQL_Protocol.cpp index 08b9e93c74..3c5f46f87b 100644 --- a/lib/PgSQL_Protocol.cpp +++ b/lib/PgSQL_Protocol.cpp @@ -630,7 +630,12 @@ bool PgSQL_Protocol::process_startup_packet(unsigned char* pkt, unsigned int len } if (hdr.type == PG_PKT_SSLREQ) { - const bool have_ssl = pgsql_thread___have_ssl; + const bool admin_tls_enabled = + (*myds)->sess != NULL + && ((*myds)->sess->session_type == PROXYSQL_SESSION_ADMIN + || (*myds)->sess->session_type == PROXYSQL_SESSION_STATS) + && GloVars.is_admin_SSL_enabled(); + const bool have_ssl = pgsql_thread___have_ssl || admin_tls_enabled; char* ssl_supported = (char*)malloc(1); *ssl_supported = have_ssl ? 'S' : 'N'; (*myds)->PSarrayOUT->add((void*)ssl_supported, 1); diff --git a/lib/PgSQL_Session.cpp b/lib/PgSQL_Session.cpp index 35cdede51f..6ee4119e39 100644 --- a/lib/PgSQL_Session.cpp +++ b/lib/PgSQL_Session.cpp @@ -3855,6 +3855,18 @@ void PgSQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE( l_free(pkt->size, pkt->ptr); return; } + } else if ( + (session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) + && GloVars.is_admin_SSL_enabled() && is_encrypted == false + ) { + l_free(pkt->size, pkt->ptr); + *wrong_pass = true; + client_myds->setDSS_STATE_QUERY_SENT_NET(); + client_myds->myprot.generate_error_packet( + true, false, "ProxySQL Admin interface requires SSL", + PGSQL_ERROR_CODES::ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION, true, true + ); + return; } else if (client_myds->myprot.generate_pkt_initial_handshake(true, NULL, NULL, &thread_session_id, true) == true) { client_myds->auth_received_startup = true; l_free(pkt->size, pkt->ptr); @@ -3903,15 +3915,48 @@ void PgSQL_Session::handler___status_CONNECTING_CLIENT___STATE_SERVER_HANDSHAKE( client_myds->DSS = STATE_SSL_INIT; client_myds->rbio_ssl = BIO_new(BIO_s_mem()); client_myds->wbio_ssl = BIO_new(BIO_s_mem()); - client_myds->ssl = GloVars.get_SSL_new(); + const bool admin_session = + session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS; + client_myds->ssl = + admin_session ? GloVars.get_admin_SSL_new() : GloVars.get_SSL_new(); + if (client_myds->ssl == NULL) { + proxy_error( + "Unable to create %s TLS connection: SSL_new() failed\n", + admin_session ? "PostgreSQL Admin" : "PostgreSQL frontend" + ); + BIO_free(client_myds->rbio_ssl); + BIO_free(client_myds->wbio_ssl); + client_myds->rbio_ssl = NULL; + client_myds->wbio_ssl = NULL; + l_free(pkt->size, pkt->ptr); + *wrong_pass = true; + client_myds->setDSS_STATE_QUERY_SENT_NET(); + return; + } SSL_set_fd(client_myds->ssl, client_myds->fd); SSL_set_accept_state(client_myds->ssl); SSL_set_bio(client_myds->ssl, client_myds->rbio_ssl, client_myds->wbio_ssl); l_free(pkt->size, pkt->ptr); - proxysql_keylog_attach_callback(GloVars.get_SSL_ctx()); + proxysql_keylog_attach_callback(SSL_get_SSL_CTX(client_myds->ssl)); return; } + if ( + handshake_response_return == true + && (session_type == PROXYSQL_SESSION_ADMIN || session_type == PROXYSQL_SESSION_STATS) + && GloVars.is_admin_SSL_enabled() + && is_encrypted == false + ) { + l_free(pkt->size, pkt->ptr); + *wrong_pass = true; + client_myds->setDSS_STATE_QUERY_SENT_NET(); + client_myds->myprot.generate_error_packet( + true, false, "ProxySQL Admin interface requires SSL", + PGSQL_ERROR_CODES::ERRCODE_INVALID_AUTHORIZATION_SPECIFICATION, true, true + ); + return; + } + if ( //(client_myds->myprot.process_pkt_handshake_response((unsigned char *)pkt->ptr,pkt->size)==true) (handshake_response_return == true) diff --git a/lib/ProxySQL_Admin.cpp b/lib/ProxySQL_Admin.cpp index 81300650ee..886fd1c113 100644 --- a/lib/ProxySQL_Admin.cpp +++ b/lib/ProxySQL_Admin.cpp @@ -386,7 +386,7 @@ MHD_Result http_handler(void *cls, struct MHD_Connection *connection, const char #include "ProxySQL_Admin_Tables_Definitions.h" -static char * admin_variables_names[]= { +static const char * admin_variables_names[]= { (char *)"admin_credentials", (char *)"stats_credentials", (char *)"stats_mysql_connections", @@ -456,6 +456,17 @@ static char * admin_variables_names[]= { (char *)"coredump_generation_interval_ms", (char *)"coredump_generation_threshold", (char *)"ssl_keylog_file", + "ssl_enabled", + "ssl_key", + "ssl_cert", + "ssl_ca", + "ssl_capath", + "ssl_cipher", + "tls_version", + "ssl_curves", + "ssl_verify_client", + "ssl_crl", + "ssl_crlpath", NULL }; @@ -2969,6 +2980,17 @@ ProxySQL_Admin::ProxySQL_Admin() : variables.coredump_generation_interval_ms = 30000; variables.coredump_generation_threshold = 10; variables.ssl_keylog_file = strdup(""); + variables.admin_ssl_enabled = false; + variables.admin_ssl_key = strdup(""); + variables.admin_ssl_cert = strdup(""); + variables.admin_ssl_ca = strdup(""); + variables.admin_ssl_capath = strdup(""); + variables.admin_ssl_cipher = strdup(""); + variables.admin_tls_version = strdup("TLSv1.2"); + variables.admin_ssl_curves = strdup(""); + variables.admin_ssl_verify_client = 0; + variables.admin_ssl_crl = strdup(""); + variables.admin_ssl_crlpath = strdup(""); last_p_memory_metrics_ts = 0; // create the scheduler scheduler=new ProxySQL_External_Scheduler(); @@ -3279,6 +3301,15 @@ void ProxySQL_Admin::admin_shutdown() { if (variables.ssl_keylog_file) { free(variables.ssl_keylog_file); } + free_null(variables.admin_ssl_key); + free_null(variables.admin_ssl_cert); + free_null(variables.admin_ssl_ca); + free_null(variables.admin_ssl_capath); + free_null(variables.admin_ssl_cipher); + free_null(variables.admin_tls_version); + free_null(variables.admin_ssl_curves); + free_null(variables.admin_ssl_crl); + free_null(variables.admin_ssl_crlpath); }; ProxySQL_Admin::~ProxySQL_Admin() { @@ -3908,6 +3939,44 @@ char * ProxySQL_Admin::get_variable(char *name) { } return ssl_keylog_file; } + if (!strcasecmp(name, "ssl_enabled")) { + return strdup(variables.admin_ssl_enabled ? "true" : "false"); + } + if (!strcasecmp(name, "ssl_key")) { + return strdup(variables.admin_ssl_key); + } + if (!strcasecmp(name, "ssl_cert")) { + return strdup(variables.admin_ssl_cert); + } + if (!strcasecmp(name, "ssl_ca")) { + return strdup(variables.admin_ssl_ca); + } + if (!strcasecmp(name, "ssl_capath")) { + return strdup(variables.admin_ssl_capath); + } + if (!strcasecmp(name, "ssl_cipher")) { + return strdup(variables.admin_ssl_cipher); + } + if (!strcasecmp(name, "tls_version")) { + return strdup(variables.admin_tls_version); + } + if (!strcasecmp(name, "ssl_curves")) { + return strdup(variables.admin_ssl_curves); + } + if (!strcasecmp(name, "ssl_verify_client")) { + switch (variables.admin_ssl_verify_client) { + case 0: return strdup("DISABLED"); + case 1: return strdup("OPTIONAL"); + case 2: return strdup("REQUIRED"); + default: assert(0); + } + } + if (!strcasecmp(name, "ssl_crl")) { + return strdup(variables.admin_ssl_crl); + } + if (!strcasecmp(name, "ssl_crlpath")) { + return strdup(variables.admin_ssl_crlpath); + } return NULL; } @@ -4900,6 +4969,64 @@ bool ProxySQL_Admin::set_variable(char *name, char *value, bool lock) { // this } return true; } + if (!strcasecmp(name, "ssl_enabled")) { + if (!strcasecmp(value, "true") || !strcmp(value, "1")) { + variables.admin_ssl_enabled = true; + return true; + } + if (!strcasecmp(value, "false") || !strcmp(value, "0")) { + variables.admin_ssl_enabled = false; + return true; + } + return false; + } + if (!strcasecmp(name, "ssl_verify_client")) { + if (!strcasecmp(value, "DISABLED") || !strcmp(value, "0")) { + variables.admin_ssl_verify_client = 0; + return true; + } + if (!strcasecmp(value, "OPTIONAL") || !strcmp(value, "1")) { + variables.admin_ssl_verify_client = 1; + return true; + } + if (!strcasecmp(value, "REQUIRED") || !strcmp(value, "2")) { + variables.admin_ssl_verify_client = 2; + return true; + } + return false; + } + if (!strcasecmp(name, "tls_version")) { + if (strcasecmp(value, "TLSv1.2") && strcasecmp(value, "TLSv1.3")) { + return false; + } + free_null(variables.admin_tls_version); + variables.admin_tls_version = strdup(value); + return true; + } + + char **admin_ssl_string = NULL; + if (!strcasecmp(name, "ssl_key")) { + admin_ssl_string = &variables.admin_ssl_key; + } else if (!strcasecmp(name, "ssl_cert")) { + admin_ssl_string = &variables.admin_ssl_cert; + } else if (!strcasecmp(name, "ssl_ca")) { + admin_ssl_string = &variables.admin_ssl_ca; + } else if (!strcasecmp(name, "ssl_capath")) { + admin_ssl_string = &variables.admin_ssl_capath; + } else if (!strcasecmp(name, "ssl_cipher")) { + admin_ssl_string = &variables.admin_ssl_cipher; + } else if (!strcasecmp(name, "ssl_curves")) { + admin_ssl_string = &variables.admin_ssl_curves; + } else if (!strcasecmp(name, "ssl_crl")) { + admin_ssl_string = &variables.admin_ssl_crl; + } else if (!strcasecmp(name, "ssl_crlpath")) { + admin_ssl_string = &variables.admin_ssl_crlpath; + } + if (admin_ssl_string) { + free_null(*admin_ssl_string); + *admin_ssl_string = strdup(strcmp(value, "(null)") ? value : ""); + return true; + } return false; } diff --git a/lib/ProxySQL_GloVars.cpp b/lib/ProxySQL_GloVars.cpp index bcb00215ca..3d2dc8a9ee 100644 --- a/lib/ProxySQL_GloVars.cpp +++ b/lib/ProxySQL_GloVars.cpp @@ -206,6 +206,10 @@ ProxySQL_GlobalVariables::~ProxySQL_GlobalVariables() { free(global.tls_key_file); global.tls_key_file = nullptr; } + if (global.admin_ssl_ctx) { + SSL_CTX_free(global.admin_ssl_ctx); + global.admin_ssl_ctx = nullptr; + } }; ProxySQL_GlobalVariables::ProxySQL_GlobalVariables() : @@ -286,6 +290,8 @@ ProxySQL_GlobalVariables::ProxySQL_GlobalVariables() : global.gr_bootstrap_ssl_key = nullptr; global.gr_bootstrap_ssl_mode = nullptr; global.ssl_keylog_enabled = false; + global.admin_ssl_ctx = nullptr; + global.admin_ssl_enabled = false; global.tls_load_count = 0; global.tls_last_load_timestamp = 0; global.tls_last_load_ok = false; diff --git a/src/proxy_tls.cpp b/src/proxy_tls.cpp index 38acc553fa..15d65f8a1b 100644 --- a/src/proxy_tls.cpp +++ b/src/proxy_tls.cpp @@ -499,3 +499,238 @@ int ProxySQL_create_or_load_TLS(bool bootstrap, std::string& msg) { BIO_free(bio_err); return ret; } + +static std::string admin_tls_resolve_path(const char *path) { + if (path == NULL || path[0] == '\0') { + return ""; + } + if (path[0] == '/') { + return path; + } + return std::string(GloVars.datadir) + "/" + path; +} + +static std::string admin_tls_openssl_error(const char *operation) { + unsigned long error = 0; + unsigned long next_error = 0; + while ((next_error = ERR_get_error()) != 0) { + error = next_error; + } + if (error == 0) { + return operation; + } + char error_buf[256]; + ERR_error_string_n(error, error_buf, sizeof(error_buf)); + return std::string(operation) + ": " + error_buf; +} + +static bool admin_tls_load_crls( + SSL_CTX *ctx, const std::string& crl_file, const std::string& crl_path, std::string& msg +) { + if (crl_file.empty() && crl_path.empty()) { + return true; + } + + X509_STORE *store = SSL_CTX_get_cert_store(ctx); + if (store == NULL) { + msg = "Unable to get Admin TLS certificate store"; + return false; + } + if (!crl_file.empty()) { + X509_LOOKUP *lookup = X509_STORE_add_lookup(store, X509_LOOKUP_file()); + if (lookup == NULL || X509_load_crl_file(lookup, crl_file.c_str(), X509_FILETYPE_PEM) != 1) { + msg = admin_tls_openssl_error("Unable to load Admin TLS CRL file"); + return false; + } + } + if (!crl_path.empty()) { + X509_LOOKUP *lookup = X509_STORE_add_lookup(store, X509_LOOKUP_hash_dir()); + if (lookup == NULL || X509_LOOKUP_add_dir(lookup, crl_path.c_str(), X509_FILETYPE_PEM) != 1) { + msg = admin_tls_openssl_error("Unable to load Admin TLS CRL directory"); + return false; + } + } + if (X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL) != 1) { + msg = admin_tls_openssl_error("Unable to enable Admin TLS CRL checking"); + return false; + } + return true; +} + +static bool admin_tls_validate_paths( + const std::string& key, + const std::string& cert, + const std::string& ca, + const std::string& capath, + int verify_client, + std::string& msg +) { + if (key.empty()) { + msg = "admin-ssl_key is required when admin-ssl_enabled is true"; + return false; + } + if (cert.empty()) { + msg = "admin-ssl_cert is required when admin-ssl_enabled is true"; + return false; + } + if (verify_client != 0 && ca.empty() && capath.empty()) { + msg = "admin-ssl_ca or admin-ssl_capath is required when client certificate verification is enabled"; + return false; + } + return true; +} + +static bool admin_tls_set_protocol_version( + SSL_CTX *ctx, const char *tls_version, std::string& msg +) { + const int min_tls_version = + !strcasecmp(tls_version, "TLSv1.3") ? TLS1_3_VERSION : TLS1_2_VERSION; + if (SSL_CTX_set_min_proto_version(ctx, min_tls_version) != 1) { + msg = admin_tls_openssl_error("Unable to set minimum Admin TLS version"); + return false; + } + return true; +} + +static bool admin_tls_load_identity( + SSL_CTX *ctx, const std::string& key, const std::string& cert, std::string& msg +) { + if (SSL_CTX_use_certificate_chain_file(ctx, cert.c_str()) != 1) { + msg = admin_tls_openssl_error("Unable to load Admin TLS certificate"); + return false; + } + if (SSL_CTX_use_PrivateKey_file(ctx, key.c_str(), SSL_FILETYPE_PEM) != 1) { + msg = admin_tls_openssl_error("Unable to load Admin TLS private key"); + return false; + } + if (SSL_CTX_check_private_key(ctx) != 1) { + msg = admin_tls_openssl_error("Admin TLS private key does not match the certificate"); + return false; + } + return true; +} + +static bool admin_tls_configure_client_verification( + SSL_CTX *ctx, + const std::string& ca, + const std::string& capath, + const std::string& crl, + const std::string& crlpath, + int verify_client, + std::string& msg +) { + if (!ca.empty() || !capath.empty()) { + if (SSL_CTX_load_verify_locations( + ctx, ca.empty() ? NULL : ca.c_str(), capath.empty() ? NULL : capath.c_str() + ) != 1) { + msg = admin_tls_openssl_error("Unable to load Admin TLS CA"); + return false; + } + } + + switch (verify_client) { + case 0: + // DISABLED intentionally preserves the default behavior: Admin clients + // are not required to present a certificate. + SSL_CTX_set_verify(ctx, SSL_VERIFY_NONE, NULL); // NOSONAR + break; + case 1: + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, NULL); + break; + case 2: + SSL_CTX_set_verify( + ctx, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT | SSL_VERIFY_CLIENT_ONCE, NULL + ); + break; + default: + msg = "Invalid admin-ssl_verify_client value"; + return false; + } + + return admin_tls_load_crls(ctx, crl, crlpath, msg); +} + +static bool admin_tls_configure_ciphers( + SSL_CTX *ctx, const char *cipher, const char *curves, std::string& msg +) { + if (cipher[0] != '\0' && SSL_CTX_set_cipher_list(ctx, cipher) != 1) { + msg = admin_tls_openssl_error("Unable to set Admin TLS cipher list"); + return false; + } + if (curves[0] != '\0' && SSL_CTX_set1_curves_list(ctx, curves) != 1) { + msg = admin_tls_openssl_error("Unable to set Admin TLS curves"); + return false; + } + if (SSL_CTX_set_dh_auto(ctx, 1) != 1) { + msg = admin_tls_openssl_error("Unable to initialize Admin TLS DH parameters"); + return false; + } + return true; +} + +/** + * Build and atomically activate the dedicated TLS context used by the Admin + * MySQL and PostgreSQL interfaces. + * + * The configured context is completely validated before it becomes visible + * to new connections. SSL objects created before a reload retain their own + * reference to the previous SSL_CTX. + */ +int ProxySQL_Admin::reload_admin_tls(std::string& msg) { + wrlock(); + const int result = reload_admin_tls_unlocked(msg); + wrunlock(); + return result; +} + +int ProxySQL_Admin::reload_admin_tls_unlocked(std::string& msg) { + msg.clear(); + + if (!variables.admin_ssl_enabled) { + GloVars.set_admin_SSL_ctx(NULL, false); + msg = "Admin TLS disabled"; + return 0; + } + + const std::string key = admin_tls_resolve_path(variables.admin_ssl_key); + const std::string cert = admin_tls_resolve_path(variables.admin_ssl_cert); + const std::string ca = admin_tls_resolve_path(variables.admin_ssl_ca); + const std::string capath = admin_tls_resolve_path(variables.admin_ssl_capath); + const std::string crl = admin_tls_resolve_path(variables.admin_ssl_crl); + const std::string crlpath = admin_tls_resolve_path(variables.admin_ssl_crlpath); + + if (!admin_tls_validate_paths( + key, cert, ca, capath, variables.admin_ssl_verify_client, msg + )) { + return 1; + } + + ERR_clear_error(); + SSL_CTX *ctx = SSL_CTX_new(TLS_server_method()); + if (ctx == NULL) { + msg = admin_tls_openssl_error("Unable to create Admin TLS context"); + return 1; + } + + const bool configured = + admin_tls_set_protocol_version(ctx, variables.admin_tls_version, msg) + && admin_tls_load_identity(ctx, key, cert, msg) + && admin_tls_configure_client_verification( + ctx, ca, capath, crl, crlpath, variables.admin_ssl_verify_client, msg + ) + && admin_tls_configure_ciphers( + ctx, variables.admin_ssl_cipher, variables.admin_ssl_curves, msg + ); + if (!configured) { + SSL_CTX_free(ctx); + proxy_error("Unable to load Admin TLS context: %s\n", msg.c_str()); + return 1; + } + + SSL_CTX_set_options(ctx, SSL_OP_NO_TICKET); + SSL_CTX_set_session_cache_mode(ctx, SSL_SESS_CACHE_OFF); + + GloVars.set_admin_SSL_ctx(ctx, true); + msg = "Admin TLS context loaded"; + return 0; +} diff --git a/src/proxysql_global.cpp b/src/proxysql_global.cpp index 4fae899dca..3180c2ee58 100644 --- a/src/proxysql_global.cpp +++ b/src/proxysql_global.cpp @@ -16,6 +16,43 @@ SSL * ProxySQL_GlobalVariables::get_SSL_new() { return SSL_new(GloVars.global.ssl_ctx); } +SSL * ProxySQL_GlobalVariables::get_admin_SSL_new() { + { + std::lock_guard lock(global.admin_ssl_mutex); + if (global.admin_ssl_enabled && global.admin_ssl_ctx) { + return SSL_new(global.admin_ssl_ctx); + } + } + + // Admin TLS is disabled. Preserve the historical behavior and use the + // shared frontend TLS context. + return get_SSL_new(); +} + +bool ProxySQL_GlobalVariables::is_admin_SSL_enabled() { + std::lock_guard lock(global.admin_ssl_mutex); + return global.admin_ssl_enabled; +} + +void ProxySQL_GlobalVariables::set_admin_SSL_ctx(SSL_CTX *ctx, bool enabled) { + assert(!enabled || ctx); + + SSL_CTX *old_ctx = NULL; + { + std::lock_guard lock(global.admin_ssl_mutex); + old_ctx = global.admin_ssl_ctx; + global.admin_ssl_ctx = enabled ? ctx : NULL; + global.admin_ssl_enabled = enabled; + } + + // SSL_new() retains its SSL_CTX. It is therefore safe to release the + // global reference after the swap while established connections continue + // using the old context. + if (old_ctx && old_ctx != ctx) { + SSL_CTX_free(old_ctx); + } +} + void ProxySQL_GlobalVariables::get_SSL_pem_mem(char **key, char **cert) { // take the mutex std::lock_guard lock(global.ssl_mutex); diff --git a/test/tap/groups/groups.json b/test/tap/groups/groups.json index e9505c121c..1761861399 100644 --- a/test/tap/groups/groups.json +++ b/test/tap/groups/groups.json @@ -5,6 +5,7 @@ "admin_show_create_table-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ], "admin_show_fields_from-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ], "admin_show_table_status-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ], + "admin_tls-t" : [ "legacy-g9","mysql-auto_increment_delay_multiplex=0-g4","mysql-multiplexing=false-g4","mysql-query_digests=0-g4","mysql-query_digests_keep_comment=1-g4","mysql84-g9","mysql90-g4","mysql95-g4" ], "admin_various_commands-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ], "admin_various_commands2-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ], "admin_various_commands3-t" : [ "legacy-g1","mariadb10-galera-g1","mysql-auto_increment_delay_multiplex=0-g1","mysql-multiplexing=false-g1","mysql-query_digests=0-g1","mysql-query_digests_keep_comment=1-g1","mysql84-g1","mysql84-gr-g1","mysql90-g1","mysql90-gr-g1","mysql93-g1","mysql93-gr-g1","mysql95-g1","mysql95-gr-g1" ], diff --git a/test/tap/test_helpers/test_globals.cpp b/test/tap/test_helpers/test_globals.cpp index b7b92cb7f8..b03241a954 100644 --- a/test/tap/test_helpers/test_globals.cpp +++ b/test/tap/test_helpers/test_globals.cpp @@ -156,6 +156,8 @@ char *binary_sha1 = nullptr; #include "SQLite3_Server.h" int ProxySQL_create_or_load_TLS(bool, std::string &) { return 0; } +int ProxySQL_Admin::reload_admin_tls(std::string &) { return 0; } +int ProxySQL_Admin::reload_admin_tls_unlocked(std::string &) { return 0; } char *SQLite3_Server::get_variable(char *) { return nullptr; } bool SQLite3_Server::has_variable(const char *) { return false; } @@ -196,6 +198,32 @@ SSL *ProxySQL_GlobalVariables::get_SSL_new() { return SSL_new(global.ssl_ctx); } +SSL *ProxySQL_GlobalVariables::get_admin_SSL_new() { + { + std::lock_guard lock(global.admin_ssl_mutex); + if (global.admin_ssl_enabled && global.admin_ssl_ctx != nullptr) { + return SSL_new(global.admin_ssl_ctx); + } + } + return get_SSL_new(); +} + +bool ProxySQL_GlobalVariables::is_admin_SSL_enabled() { + std::lock_guard lock(global.admin_ssl_mutex); + return global.admin_ssl_enabled; +} + +void ProxySQL_GlobalVariables::set_admin_SSL_ctx(SSL_CTX *ctx, bool enabled) { + SSL_CTX *old_ctx = nullptr; + { + std::lock_guard lock(global.admin_ssl_mutex); + old_ctx = global.admin_ssl_ctx; + global.admin_ssl_ctx = enabled ? ctx : nullptr; + global.admin_ssl_enabled = enabled; + } + if (old_ctx && old_ctx != ctx) SSL_CTX_free(old_ctx); +} + void ProxySQL_GlobalVariables::get_SSL_pem_mem(char **key, char **cert) { std::lock_guard lock(global.ssl_mutex); if (global.ssl_key_pem_mem != nullptr) @@ -265,6 +293,8 @@ int test_globals_init() { // SSL pointers — nullptr means no SSL GloVars.global.ssl_ctx = nullptr; GloVars.global.tmp_ssl_ctx = nullptr; + GloVars.global.admin_ssl_ctx = nullptr; + GloVars.global.admin_ssl_enabled = false; GloVars.global.ssl_key_pem_mem = nullptr; GloVars.global.ssl_cert_pem_mem = nullptr; diff --git a/test/tap/tests/admin_tls-t.cpp b/test/tap/tests/admin_tls-t.cpp new file mode 100644 index 0000000000..a42dfd3ca5 --- /dev/null +++ b/test/tap/tests/admin_tls-t.cpp @@ -0,0 +1,267 @@ +/** + * End-to-end coverage for the dedicated Admin TLS context. + * + * The test intentionally starts with plaintext connections to verify backward + * compatibility, enables TLS for both Admin protocols, exercises atomic reload + * failure, and finally enables required client-certificate verification. + */ + +#include +#include +#include +#include +#include + +#include + +#include "libpq-fe.h" +#include "mysql.h" + +#include "command_line.h" +#include "tap.h" + +using MysqlPtr = std::unique_ptr; +using PgPtr = std::unique_ptr; + +static MysqlPtr mysql_connect( + const CommandLine& cl, bool tls, const std::string& key = "", const std::string& cert = "", + const std::string& ca = "" +) { + MYSQL *raw = mysql_init(NULL); + if (raw == NULL) { + return MysqlPtr(NULL, &mysql_close); + } + if (tls) { + mysql_ssl_set( + raw, key.empty() ? NULL : key.c_str(), cert.empty() ? NULL : cert.c_str(), + ca.empty() ? NULL : ca.c_str(), NULL, NULL + ); + } + if (mysql_real_connect( + raw, cl.host, cl.admin_username, cl.admin_password, NULL, cl.admin_port, NULL, + tls ? CLIENT_SSL : 0 + ) == NULL) { + mysql_close(raw); + return MysqlPtr(NULL, &mysql_close); + } + return MysqlPtr(raw, &mysql_close); +} + +static PgPtr pg_connect( + const CommandLine& cl, bool tls, const std::string& key = "", const std::string& cert = "", + const std::string& ca = "" +) { + const std::string port = std::to_string(cl.pgsql_admin_port); + const char *keywords[] = { + "host", "port", "user", "password", "sslmode", "sslkey", "sslcert", "sslrootcert", NULL + }; + const char *values[] = { + cl.pgsql_admin_host, port.c_str(), cl.admin_username, cl.admin_password, + tls ? "require" : "disable", + key.empty() ? NULL : key.c_str(), cert.empty() ? NULL : cert.c_str(), + ca.empty() ? NULL : ca.c_str(), NULL + }; + return PgPtr(PQconnectdbParams(keywords, values, 0), &PQfinish); +} + +static bool query_ok(MYSQL *mysql, const std::string& query) { + if (mysql_query(mysql, query.c_str()) != 0) { + diag("Query failed: %s; error: %s", query.c_str(), mysql_error(mysql)); + return false; + } + MYSQL_RES *result = mysql_store_result(mysql); + if (result != NULL) { + mysql_free_result(result); + } + return true; +} + +static std::string scalar(MYSQL *mysql, const std::string& query) { + if (mysql_query(mysql, query.c_str()) != 0) { + diag("Query failed: %s; error: %s", query.c_str(), mysql_error(mysql)); + return ""; + } + MYSQL_RES *result = mysql_store_result(mysql); + if (result == NULL) { + return ""; + } + MYSQL_ROW row = mysql_fetch_row(result); + const std::string value = row != NULL && row[0] != NULL ? row[0] : ""; + mysql_free_result(result); + return value; +} + +static std::string sql_quote(MYSQL *mysql, const std::string& value) { + std::string escaped(value.size() * 2 + 1, '\0'); + const unsigned long size = mysql_real_escape_string( + mysql, escaped.data(), value.c_str(), value.size() + ); + escaped.resize(size); + return "'" + escaped + "'"; +} + +static bool set_variable(MYSQL *mysql, const char *name, const std::string& value) { + return query_ok(mysql, std::string("SET ") + name + "=" + sql_quote(mysql, value)); +} + +static std::string make_libpq_key_copy(const std::string& source) { + const size_t separator = source.find_last_of('/'); + const std::string directory = + separator == std::string::npos ? "." : source.substr(0, separator); + const std::string path_template = directory + "/proxysql-admin-tls-key-XXXXXX"; + std::vector path(path_template.begin(), path_template.end()); + path.push_back('\0'); + + const int fd = mkstemp(path.data()); + if (fd == -1) { + return ""; + } + close(fd); + + std::ifstream input(source, std::ios::binary); + std::ofstream output(path.data(), std::ios::binary | std::ios::trunc); + output << input.rdbuf(); + if (!input || !output) { + unlink(path.data()); + return ""; + } + return path.data(); +} + +int main() { + plan(23); + + CommandLine cl; + if (cl.getEnv()) { + BAIL_OUT("Failed to get the required environment variables"); + return exit_status(); + } + + auto control = mysql_connect(cl, false); + if (!control) { + BAIL_OUT("Unable to connect to the MySQL Admin interface"); + return exit_status(); + } + + ok( + scalar(control.get(), + "SELECT variable_value FROM runtime_global_variables " + "WHERE variable_name='admin-ssl_enabled'") == "false", + "dedicated Admin TLS is disabled by default" + ); + ok(mysql_connect(cl, false) != nullptr, "default MySQL Admin behavior accepts plaintext"); + auto default_pg = pg_connect(cl, false); + ok( + default_pg && PQstatus(default_pg.get()) == CONNECTION_OK, + "default PostgreSQL Admin behavior accepts plaintext" + ); + + const std::string key = scalar( + control.get(), + "SELECT Variable_Value FROM stats.stats_proxysql_global WHERE Variable_Name='TLS_Key_File'" + ); + const std::string cert = scalar( + control.get(), + "SELECT Variable_Value FROM stats.stats_proxysql_global " + "WHERE Variable_Name='TLS_Server_Cert_File'" + ); + const std::string ca = scalar( + control.get(), + "SELECT Variable_Value FROM stats.stats_proxysql_global " + "WHERE Variable_Name='TLS_CA_Cert_File'" + ); + ok(!key.empty() && !cert.empty() && !ca.empty(), "default TLS certificate paths are available"); + + bool configured = + set_variable(control.get(), "admin-ssl_key", key) + && set_variable(control.get(), "admin-ssl_cert", cert) + && set_variable(control.get(), "admin-ssl_ca", ca) + && set_variable(control.get(), "admin-ssl_enabled", "true") + && query_ok(control.get(), "LOAD ADMIN VARIABLES TO RUNTIME"); + ok(configured, "dedicated Admin TLS context loads successfully"); + + const bool invalid_set = + set_variable(control.get(), "admin-ssl_cert", "/no/such/admin-cert.pem"); + const bool invalid_rejected = + invalid_set && mysql_query(control.get(), "LOAD ADMIN VARIABLES TO RUNTIME") != 0; + ok(invalid_rejected, "invalid Admin TLS reload is rejected"); + ok( + scalar(control.get(), + "SELECT variable_value FROM runtime_global_variables " + "WHERE variable_name='admin-ssl_cert'") == cert, + "failed reload preserves the previous runtime configuration" + ); + const bool restored = + set_variable(control.get(), "admin-ssl_cert", cert) + && query_ok(control.get(), "LOAD ADMIN VARIABLES TO RUNTIME"); + ok(restored, "valid Admin TLS configuration can be loaded after a failed reload"); + + ok(mysql_connect(cl, false) == nullptr, "MySQL Admin rejects new plaintext connections"); + auto mysql_tls = mysql_connect(cl, true); + ok( + mysql_tls && mysql_get_ssl_cipher(mysql_tls.get()) != NULL, + "MySQL Admin negotiates the dedicated TLS context" + ); + auto pg_plain = pg_connect(cl, false); + ok( + pg_plain && PQstatus(pg_plain.get()) != CONNECTION_OK, + "PostgreSQL Admin rejects new plaintext connections" + ); + auto pg_tls = pg_connect(cl, true); + ok( + pg_tls && PQstatus(pg_tls.get()) == CONNECTION_OK && PQsslInUse(pg_tls.get()) == 1, + "PostgreSQL Admin negotiates the dedicated TLS context" + ); + ok(query_ok(control.get(), "PROXYSQL RELOAD ADMIN TLS"), "explicit Admin TLS reload succeeds"); + + const bool optional = + set_variable(control.get(), "admin-ssl_verify_client", "OPTIONAL") + && query_ok(control.get(), "LOAD ADMIN VARIABLES TO RUNTIME"); + ok(optional, "optional client-certificate verification loads"); + ok(mysql_connect(cl, true) != nullptr, "optional mTLS accepts a MySQL client without a certificate"); + + const bool required = + set_variable(control.get(), "admin-ssl_verify_client", "REQUIRED") + && query_ok(control.get(), "LOAD ADMIN VARIABLES TO RUNTIME"); + ok(required, "required client-certificate verification loads"); + ok(mysql_connect(cl, true) == nullptr, "required mTLS rejects a MySQL client without a certificate"); + ok( + mysql_connect(cl, true, key, cert, ca) != nullptr, + "required mTLS accepts a MySQL client with a trusted certificate" + ); + auto pg_without_cert = pg_connect(cl, true); + ok( + pg_without_cert && PQstatus(pg_without_cert.get()) != CONNECTION_OK, + "required mTLS rejects a PostgreSQL client without a certificate" + ); + // libpq rejects private key files with permissions wider than 0600. ProxySQL's + // generated test key can be wider, so keep its permissions untouched and use + // a private test-only copy. + const std::string libpq_key = make_libpq_key_copy(key); + auto pg_with_cert = pg_connect(cl, true, libpq_key, cert, ca); + if (!libpq_key.empty()) { + unlink(libpq_key.c_str()); + } + ok( + pg_with_cert && PQstatus(pg_with_cert.get()) == CONNECTION_OK + && PQsslInUse(pg_with_cert.get()) == 1, + "required mTLS accepts a PostgreSQL client with a trusted certificate" + ); + + const bool disabled = + set_variable(control.get(), "admin-ssl_verify_client", "DISABLED") + && set_variable(control.get(), "admin-ssl_enabled", "false") + && set_variable(control.get(), "admin-ssl_key", "") + && set_variable(control.get(), "admin-ssl_cert", "") + && set_variable(control.get(), "admin-ssl_ca", "") + && query_ok(control.get(), "LOAD ADMIN VARIABLES TO RUNTIME"); + ok(disabled, "dedicated Admin TLS can be disabled at runtime"); + ok(mysql_connect(cl, false) != nullptr, "MySQL Admin plaintext behavior is restored after disable"); + auto final_pg = pg_connect(cl, false); + ok( + final_pg && PQstatus(final_pg.get()) == CONNECTION_OK, + "PostgreSQL Admin plaintext behavior is restored after disable" + ); + + return exit_status(); +}