Skip to main content

fraiseql_server/
tls.rs

1//! TLS/SSL configuration for **database connections**.
2//!
3//! Server-side TLS termination is **not supported**: FraiseQL serves plaintext HTTP and
4//! expects a reverse proxy (nginx, Caddy, a cloud load balancer, a service mesh) to
5//! terminate TLS in front of it. The server **refuses to boot** if `[tls]` (server-side
6//! TLS) is enabled — see `server/lifecycle.rs`. Previously a rustls `ServerConfig` was
7//! built from `[tls]` and then silently discarded while the server kept serving plaintext
8//! (M-tls-enforce), so the dead `TlsEnforcer` / `create_rustls_config` plumbing was removed.
9//!
10//! This module retains only the **database** connection TLS settings (`postgres_ssl_mode`,
11//! `redis_ssl`, etc.) and the URL-rewriting helpers that apply them, plus
12//! [`TlsSetup::is_tls_enabled`] used by the boot-time refusal check.
13
14use std::{fmt::Write as _, path::Path};
15
16use crate::server_config::{DatabaseTlsConfig, TlsServerConfig};
17
18/// Database connection TLS settings, plus the server-side `[tls]` config retained only so
19/// the boot path can detect (and refuse) an enabled server-TLS configuration.
20pub struct TlsSetup {
21    /// Server TLS configuration (server-side TLS termination is unsupported; this is read
22    /// only by [`is_tls_enabled`](Self::is_tls_enabled) for the boot-time refusal).
23    config: Option<TlsServerConfig>,
24
25    /// Database TLS configuration.
26    db_config: Option<DatabaseTlsConfig>,
27}
28
29impl TlsSetup {
30    /// Create new TLS setup from server configuration.
31    #[must_use]
32    pub const fn new(
33        tls_config: Option<TlsServerConfig>,
34        db_tls_config: Option<DatabaseTlsConfig>,
35    ) -> Self {
36        Self {
37            config:    tls_config,
38            db_config: db_tls_config,
39        }
40    }
41
42    /// Get the database TLS configuration.
43    #[must_use]
44    pub const fn db_config(&self) -> &Option<DatabaseTlsConfig> {
45        &self.db_config
46    }
47
48    /// Whether server-side `[tls]` is enabled in the configuration.
49    ///
50    /// Server-side TLS termination is unsupported, so the boot path uses this to refuse to
51    /// start rather than serve plaintext under an enabled `[tls]` config (M-tls-enforce).
52    #[must_use]
53    pub fn is_tls_enabled(&self) -> bool {
54        self.config.as_ref().is_some_and(|c| c.enabled)
55    }
56
57    /// Get PostgreSQL SSL mode for database connections.
58    #[must_use]
59    pub fn postgres_ssl_mode(&self) -> &str {
60        self.db_config.as_ref().map_or("prefer", |c| c.postgres_ssl_mode.as_str())
61    }
62
63    /// Check if Redis TLS is enabled.
64    #[must_use]
65    pub fn redis_ssl_enabled(&self) -> bool {
66        self.db_config.as_ref().is_some_and(|c| c.redis_ssl)
67    }
68
69    /// Check if `ClickHouse` HTTPS is enabled.
70    #[must_use]
71    pub fn clickhouse_https_enabled(&self) -> bool {
72        self.db_config.as_ref().is_some_and(|c| c.clickhouse_https)
73    }
74
75    /// Check if Elasticsearch HTTPS is enabled.
76    #[must_use]
77    pub fn elasticsearch_https_enabled(&self) -> bool {
78        self.db_config.as_ref().is_some_and(|c| c.elasticsearch_https)
79    }
80
81    /// Check if certificate verification is enabled for databases.
82    #[must_use]
83    pub fn verify_certificates(&self) -> bool {
84        self.db_config.as_ref().is_none_or(|c| c.verify_certificates)
85    }
86
87    /// Get the CA bundle path for verifying database certificates.
88    #[must_use]
89    pub fn ca_bundle_path(&self) -> Option<&Path> {
90        self.db_config
91            .as_ref()
92            .and_then(|c| c.ca_bundle_path.as_ref())
93            .map(|p| p.as_path())
94    }
95
96    /// Get database URL with TLS applied (for PostgreSQL).
97    #[must_use]
98    pub fn apply_postgres_tls(&self, db_url: &str) -> String {
99        let mut url = db_url.to_string();
100
101        // Parse SSL mode into URL parameter
102        let ssl_mode = self.postgres_ssl_mode();
103        if !ssl_mode.is_empty() && ssl_mode != "prefer" {
104            // Add or update sslmode parameter
105            if url.contains('?') {
106                let _ = write!(url, "&sslmode={ssl_mode}");
107            } else {
108                let _ = write!(url, "?sslmode={ssl_mode}");
109            }
110        }
111
112        url
113    }
114
115    /// Get Redis URL with TLS applied.
116    #[must_use]
117    pub fn apply_redis_tls(&self, redis_url: &str) -> String {
118        if self.redis_ssl_enabled() {
119            // Replace redis:// with rediss://
120            redis_url.replace("redis://", "rediss://")
121        } else {
122            redis_url.to_string()
123        }
124    }
125
126    /// Get `ClickHouse` URL with TLS applied.
127    #[must_use]
128    pub fn apply_clickhouse_tls(&self, ch_url: &str) -> String {
129        if self.clickhouse_https_enabled() {
130            // Replace http:// with https://
131            ch_url.replace("http://", "https://")
132        } else {
133            ch_url.to_string()
134        }
135    }
136
137    /// Get Elasticsearch URL with TLS applied.
138    #[must_use]
139    pub fn apply_elasticsearch_tls(&self, es_url: &str) -> String {
140        if self.elasticsearch_https_enabled() {
141            // Replace http:// with https://
142            es_url.replace("http://", "https://")
143        } else {
144            es_url.to_string()
145        }
146    }
147}