QoreDB LogoQoreDB
Back to blog
SecurityTechnical journal

SSL/TLS in QoreDB: one shared configuration, one behavior per engine

Encrypting a database connection looks like a single problem, but every engine exposes it differently. PostgreSQL talks about sslmode with five levels, MySQL has its own enum, MongoDB expects a tls parameter in the URI, Redis switches URL schemes, and SQL Server sets an…

Raphaël – Creator of QoreDBRaphaël – Creator of QoreDB
4 min read
SSL/TLS in QoreDB: one shared configuration, one behavior per engine

Encrypting a database connection looks like a single problem, but every engine exposes it differently. PostgreSQL talks about sslmode with five levels, MySQL has its own enum, MongoDB expects a tls parameter in the URI, Redis switches URL schemes, and SQL Server sets an encryption level at the protocol layer. A database client has to choose: either expose a different form per engine, or force a shared abstraction that betrays each one's specifics.

QoreDB takes a middle path: a small set of three fields shared by every engine, whose interpretation is delegated to each engine's native driver. The configuration is uniform; the behavior is not. Here is how that mapping works, engine by engine, and where its limits are.

Three fields for every engine

The ConnectionConfig struct, defined in qore-core/src/types.rs, carries only three TLS-related fields: a boolean ssl, an optional string ssl_mode, and an optional path ssl_ca_cert to a CA certificate in PEM format.

qore-core/src/types.rs
pub struct ConnectionConfig {
    pub driver: String,
    pub host: String,
    pub port: u16,
    pub username: String,
    #[serde(skip_serializing)]
    pub password: String,
    pub database: Option<String>,
    pub ssl: bool,
    #[serde(default)]
    pub ssl_mode: Option<String>,
    // ...
    /// Path to a custom CA certificate (PEM) used to verify the server's TLS
    /// certificate. Currently honoured by the search drivers; `None` keeps the
    /// system trust store.
    #[serde(default)]
    pub ssl_ca_cert: Option<String>,
}

The principle is straightforward: ssl is the minimal setting available everywhere, while ssl_mode lets the user pin an exact level when they fill it in. The doc comment on ssl_ca_cert already hints at QoreDB's stance: it does not pretend the field applies to every engine, but honestly documents that only the search drivers honor it today.

PostgreSQL and its family: sslmode in the URL

For engines in the PostgreSQL family, everything goes through the build_pg_connection_string function in pg_compat.rs. The rule is explicit in the code: if the user provided an explicit ssl_mode, it wins; otherwise the ssl boolean is translated to require or disable. The result is appended to a postgres:// URL as a query parameter.

qore-drivers/src/drivers/pg_compat.rs
pub fn build_pg_connection_string(config: &ConnectionConfig, default_db: &str) -> String {
    let db = config.database.as_deref().unwrap_or(default_db);

    // Explicit ssl_mode wins; otherwise derive a sslmode from the boolean.
    let ssl_mode = config
        .ssl_mode
        .as_deref()
        .unwrap_or(if config.ssl { "require" } else { "disable" });

    // ... percent-encoding of user/pass ...
    format!(
        "postgres://{}:{}@{}:{}/{}?sslmode={}",
        encoded_user, encoded_pass, config.host, config.port, db, ssl_mode
    )
}

This same function is the foundation for every PostgreSQL-compatible engine: the postgres.rs driver, but also CockroachDB, Neon, Supabase and TimescaleDB, which all call it with their respective default database. Neon, for example, requires TLS and is configured with an ssl_mode of require: the connection string does contain sslmode=require, which its tests verify explicitly.

The string only carries sslmode: no sslrootcert parameter is added. In practice, sslmode=require encrypts the connection without validating the certificate chain, and the verify-ca or verify-full modes rely on the system trust store rather than on a user-supplied CA.

MySQL: a typed enum rather than a string

MySQL and MariaDB do not build a URL: they use sqlx's typed API via MySqlConnectOptions. The resolve_ssl_mode function maps the ssl_mode string to the matching variant of the MySqlSslMode enum. It accepts several spellings for the same intent, and falls back to the ssl boolean when no mode is set.

qore-drivers/src/drivers/mysql.rs
/// Resolve SSL mode from config: explicit ssl_mode string takes precedence over boolean.
fn resolve_ssl_mode(config: &ConnectionConfig) -> MySqlSslMode {
    match config.ssl_mode.as_deref() {
        Some("disabled" | "disable") => MySqlSslMode::Disabled,
        Some("preferred" | "prefer") => MySqlSslMode::Preferred,
        Some("required" | "require") => MySqlSslMode::Required,
        Some("verify-ca" | "verify_ca") => MySqlSslMode::VerifyCa,
        Some("verify-full" | "verify-identity" | "verify_identity") => {
            MySqlSslMode::VerifyIdentity
        }
        _ => {
            if config.ssl { MySqlSslMode::Required } else { MySqlSslMode::Disabled }
        }
    }
}

The point of going through an enum rather than a concatenated string is that an invalid value cannot slip into the connection: the mode is resolved to one of sqlx's five known states before the connection is even attempted. The chosen mode is then applied directly to the options via the .ssl_mode(...) call.

MongoDB and Redis: TLS in the URI scheme

MongoDB and Redis illustrate two different URI conventions for the same intent. MongoDB appends a tls=true or tls=false parameter to the mongodb:// string, whereas Redis has no parameter at all: encryption is selected at the scheme level, rediss:// for a TLS connection, redis:// otherwise.

qore-drivers/src/drivers/{mongodb,redis}.rs
// mongodb.rs
let tls = if config.ssl { "true" } else { "false" };
// ... -> "mongodb://{creds}{host}:{port}/{db}?authSource=admin&tls={tls}"

// redis.rs
let scheme = if config.ssl { "rediss" } else { "redis" };
// ... -> "rediss://default:secret@redis.example.com:6380/2"

In both cases, only the ssl boolean is consulted: these drivers do not try to interpret the five levels of ssl_mode, because the underlying protocol does not define them. This is the exact opposite of a forced abstraction: the field exists in the shared configuration, but each driver reads only what its engine can express.

SQL Server: encryption level and the trust_cert limitation

SQL Server goes through the Tiberius library, which exposes encryption as an EncryptionLevel at the TDS protocol level. The driver maps the ssl boolean to EncryptionLevel::Required or EncryptionLevel::NotSupported.

qore-drivers/src/drivers/sqlserver.rs
tib_config.encryption(if config.ssl {
    EncryptionLevel::Required
} else {
    EncryptionLevel::NotSupported
});
tib_config.trust_cert();

The trust_cert() call is worth flagging plainly: it tells Tiberius to trust the server certificate without validating the chain. The connection is therefore encrypted, but the server's identity is not verified. This is a common pragmatic trade-off with SQL Server, where instances often use self-signed certificates, but it is also a limitation better named than hidden.

Elasticsearch and OpenSearch: the only driver that reads ssl_ca_cert

The search engines are queried over HTTP through a reqwest client, which gives their driver finer control over TLS. This is the only place where ssl_ca_cert is actually honored: when the connection is HTTPS and a certificate path is provided, the PEM is read and added as a trust root via add_root_certificate. The special ssl_mode = "insecure" mode disables certificate verification, reserved for development.

qore-drivers/src/drivers/search_compat.rs
// `ssl_mode = "insecure"` disables certificate verification (dev only;
// the UI surfaces a warning). Anything else keeps strict verification.
if is_https && matches!(config.ssl_mode.as_deref(), Some("insecure")) {
    builder = builder.danger_accept_invalid_certs(true);
}

// Custom CA certificate (PEM) for clusters signed by an internal CA.
if is_https {
    if let Some(path) = config.ssl_ca_cert.as_deref()... {
        let pem = std::fs::read(path)?;
        let cert = reqwest::Certificate::from_pem(&pem)?;
        builder = builder.add_root_certificate(cert);
    }
}
The insecure mode calls danger_accept_invalid_certs(true), which accepts any certificate. It is deliberately named insecure and the UI surfaces a warning: it is a door for self-signed development clusters, not a setting to leave on in production.

A shared configuration is not a uniform abstraction

The through-line is the same as elsewhere in QoreDB: don't invent a layer that pretends to unify what isn't uniform. The three fields ssl, ssl_mode and ssl_ca_cert give a consistent configuration surface across the UI, but each driver consumes only what its engine can express: a typed enum for MySQL, a URL parameter for PostgreSQL, a scheme for Redis, a protocol level for SQL Server, a customizable trust store for the search engines.

The corollary is that the limits differ from one engine to the next, and QoreDB chooses to document them rather than hide them behind an "SSL" checkbox that would suggest a uniform guarantee. Knowing that sslmode=require does not verify the chain, or that SQL Server trusts the certificate by default, is part of an honest client's job: it is better to have encryption whose exact scope you know than a promise of security you couldn't keep across every engine.

Newsletter

Stay updated on new releases

Subscribe to get product releases, new drivers notifications, and technical tutorials.

🎁 Bonus: Get our free SQL Performance Cheat Sheet — 9 pages, PostgreSQL / MySQL / SQLite (PDF)!
Share
SSL/TLS in QoreDB: one shared configuration, one behavior per engine - Blog - QoreDB