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}