|
9 | 9 | "fmt" |
10 | 10 | "log/slog" |
11 | 11 | "net/url" |
| 12 | + "os" |
| 13 | + "path/filepath" |
12 | 14 | "slices" |
13 | 15 | "strings" |
14 | 16 | "time" |
@@ -232,32 +234,119 @@ func (c *ClickHouseConnector) ValidateCheck(ctx context.Context) error { |
232 | 234 | return nil |
233 | 235 | } |
234 | 236 |
|
235 | | -func Connect(ctx context.Context, env map[string]string, config *protos.ClickhouseConfig) (clickhouse.Conn, error) { |
236 | | - var tlsSetting *tls.Config |
237 | | - if !config.DisableTls { |
238 | | - tlsSetting = &tls.Config{MinVersion: tls.VersionTLS13} |
239 | | - if config.Certificate != nil || config.PrivateKey != nil { |
240 | | - if config.Certificate == nil || config.PrivateKey == nil { |
241 | | - return nil, fmt.Errorf("both certificate and private key must be provided if using certificate-based authentication") |
242 | | - } |
243 | | - cert, err := tls.X509KeyPair([]byte(*config.Certificate), []byte(*config.PrivateKey)) |
244 | | - if err != nil { |
245 | | - return nil, fmt.Errorf("failed to parse provided certificate: %w", err) |
246 | | - } |
247 | | - tlsSetting.Certificates = []tls.Certificate{cert} |
| 237 | +// configureDirectoryTLS configures the tls.Config by loading certificate files |
| 238 | +// from a directory. It expects tls.crt and tls.key files, and optionally ca.crt. |
| 239 | +// This is typically used when a Kubernetes Secret is mounted as a volume. |
| 240 | +// |
| 241 | +// Client certificates are loaded via GetClientCertificate so that every TLS |
| 242 | +// handshake re-reads the files from disk, automatically picking up rotated |
| 243 | +// certificates (e.g. renewed by cert-manager) without requiring a reconnect. |
| 244 | +func configureDirectoryTLS(tlsConfig *tls.Config, dir string) error { |
| 245 | + certPath := filepath.Join(dir, "tls.crt") |
| 246 | + keyPath := filepath.Join(dir, "tls.key") |
| 247 | + caPath := filepath.Join(dir, "ca.crt") |
| 248 | + |
| 249 | + // Verify that the required files are readable at configuration time |
| 250 | + if _, err := os.ReadFile(certPath); err != nil { |
| 251 | + return fmt.Errorf("failed to read TLS certificate from %q: %w", certPath, err) |
| 252 | + } |
| 253 | + if _, err := os.ReadFile(keyPath); err != nil { |
| 254 | + return fmt.Errorf("failed to read TLS private key from %q: %w", keyPath, err) |
| 255 | + } |
| 256 | + |
| 257 | + // Load CA certificate upfront — RootCAs must be set before the TLS |
| 258 | + // handshake so the server certificate can be verified. |
| 259 | + // ca.crt is optional: when absent, the system CA pool is used instead. |
| 260 | + caCertPEM, err := os.ReadFile(caPath) |
| 261 | + if errors.Is(err, os.ErrNotExist) { |
| 262 | + // ca.crt is optional — use system CA pool |
| 263 | + } else if err != nil { |
| 264 | + return fmt.Errorf("failed to read CA certificate from %q: %w", caPath, err) |
| 265 | + } else if len(caCertPEM) > 0 { |
| 266 | + caPool := x509.NewCertPool() |
| 267 | + if !caPool.AppendCertsFromPEM(caCertPEM) { |
| 268 | + return fmt.Errorf("failed to parse CA certificate from %q", caPath) |
248 | 269 | } |
249 | | - if config.RootCa != nil { |
250 | | - caPool := x509.NewCertPool() |
251 | | - if !caPool.AppendCertsFromPEM([]byte(*config.RootCa)) { |
252 | | - return nil, fmt.Errorf("failed to parse provided root CA") |
253 | | - } |
254 | | - tlsSetting.RootCAs = caPool |
| 270 | + tlsConfig.RootCAs = caPool |
| 271 | + } |
| 272 | + |
| 273 | + // Use GetClientCertificate so the cert/key are re-read on every TLS |
| 274 | + // handshake, ensuring rotated certificates are picked up immediately. |
| 275 | + tlsConfig.GetClientCertificate = func(*tls.CertificateRequestInfo) (*tls.Certificate, error) { |
| 276 | + certPEM, err := os.ReadFile(certPath) |
| 277 | + if err != nil { |
| 278 | + return nil, fmt.Errorf("failed to read TLS certificate from %q: %w", certPath, err) |
| 279 | + } |
| 280 | + keyPEM, err := os.ReadFile(keyPath) |
| 281 | + if err != nil { |
| 282 | + return nil, fmt.Errorf("failed to read TLS private key from %q: %w", keyPath, err) |
| 283 | + } |
| 284 | + cert, err := tls.X509KeyPair(certPEM, keyPEM) |
| 285 | + if err != nil { |
| 286 | + return nil, fmt.Errorf("failed to parse TLS certificate from directory %q: %w", dir, err) |
| 287 | + } |
| 288 | + return &cert, nil |
| 289 | + } |
| 290 | + |
| 291 | + return nil |
| 292 | +} |
| 293 | + |
| 294 | +// configureInlineTLS configures the tls.Config using inline certificate/key |
| 295 | +// PEM values from the ClickHouse peer config. |
| 296 | +func configureInlineTLS(tlsConfig *tls.Config, config *protos.ClickhouseConfig) error { |
| 297 | + if config.Certificate != nil || config.PrivateKey != nil { |
| 298 | + if config.Certificate == nil || config.PrivateKey == nil { |
| 299 | + return errors.New("both certificate and private key must be provided if using certificate-based authentication") |
| 300 | + } |
| 301 | + cert, err := tls.X509KeyPair([]byte(*config.Certificate), []byte(*config.PrivateKey)) |
| 302 | + if err != nil { |
| 303 | + return fmt.Errorf("failed to parse provided certificate: %w", err) |
| 304 | + } |
| 305 | + tlsConfig.Certificates = []tls.Certificate{cert} |
| 306 | + } |
| 307 | + if config.RootCa != nil { |
| 308 | + caPool := x509.NewCertPool() |
| 309 | + if !caPool.AppendCertsFromPEM([]byte(*config.RootCa)) { |
| 310 | + return errors.New("failed to parse provided root CA") |
| 311 | + } |
| 312 | + tlsConfig.RootCAs = caPool |
| 313 | + } |
| 314 | + return nil |
| 315 | +} |
| 316 | + |
| 317 | +// buildTLSConfig builds the TLS configuration for a ClickHouse connection. |
| 318 | +// When a TLS certificate directory is configured, it loads certificates from the directory. |
| 319 | +// Otherwise, inline certificates are used. |
| 320 | +func buildTLSConfig(config *protos.ClickhouseConfig) (*tls.Config, error) { |
| 321 | + if config.DisableTls { |
| 322 | + return nil, nil |
| 323 | + } |
| 324 | + |
| 325 | + tlsConfig := &tls.Config{ |
| 326 | + MinVersion: tls.VersionTLS13, |
| 327 | + ServerName: config.TlsHost, |
| 328 | + } |
| 329 | + certDir := config.GetTlsCertificateDirectory() |
| 330 | + |
| 331 | + if certDir != "" { |
| 332 | + if err := configureDirectoryTLS(tlsConfig, certDir); err != nil { |
| 333 | + return nil, err |
255 | 334 | } |
256 | | - if config.TlsHost != "" { |
257 | | - tlsSetting.ServerName = config.TlsHost |
| 335 | + } else { |
| 336 | + if err := configureInlineTLS(tlsConfig, config); err != nil { |
| 337 | + return nil, err |
258 | 338 | } |
259 | 339 | } |
260 | 340 |
|
| 341 | + return tlsConfig, nil |
| 342 | +} |
| 343 | + |
| 344 | +func Connect(ctx context.Context, env map[string]string, config *protos.ClickhouseConfig) (clickhouse.Conn, error) { |
| 345 | + tlsSetting, err := buildTLSConfig(config) |
| 346 | + if err != nil { |
| 347 | + return nil, err |
| 348 | + } |
| 349 | + |
261 | 350 | settings := clickhouse.Settings{ |
262 | 351 | // See: https://clickhouse.com/docs/en/cloud/reference/shared-merge-tree#consistency |
263 | 352 | "select_sequential_consistency": uint64(1), |
|
0 commit comments