JuliusBrussee/caveman · error
postgres: production connection includes a plaintext…
Error message
postgres: production connection includes a plaintext fallback
What it means
After pinning the CA onto the primary TLSConfig, ParsePoolConfig walks pgx's connection fallbacks; in production, any fallback whose TLSConfig is nil (i.e., a plaintext retry target) aborts pool creation. This prevents pgx from silently falling back to an unencrypted connection when the TLS handshake to the primary host fails.
Solutions
- Use sslmode=verify-full so every generated fallback inherits TLS settings (the code then pins the CA on each fallback)
- Remove secondary hosts from DATABASE_URL and use a single TLS-terminating endpoint or a connection pooler like pgbouncer with TLS
- Explicitly restructure the URL so all listed hosts are TLS-capable
- If truly non-production, relax the environment flag rather than the TLS posture
Example fix
// before DATABASE_URL="postgres://app:secret@db1:5432,db2:5432/prod?sslmode=verify-full" // db2 fallback resolves to plaintext // after DATABASE_URL="postgres://app:secret@db1:5432/prod?sslmode=verify-full" // single verified endpoint, no plaintext fallback
Defensive patterns
Strategy: validation
Validate before calling
if production && strings.Contains(databaseURL, ",") {
// multi-host URLs can generate plaintext fallbacks; verify each host is TLS-capable
return fmt.Errorf("multi-host DATABASE_URL may include plaintext fallbacks")
} Try / catch
cfg, err := postgresconfig.ParsePoolConfig(databaseURL)
if err != nil && strings.Contains(err.Error(), "plaintext fallback") {
log.Fatal("production pool would allow plaintext fallback; fix TLS on every host")
} Prevention
- Prefer a single TLS endpoint or a TLS-enabled pooler over multi-host URLs in production
- Always pair secondary hosts with the same sslmode settings
- After config changes, assert cfg.ConnConfig.Fallbacks all have non-nil TLSConfig in a startup check
- Keep pgx updated and re-verify fallback generation behavior after upgrades
When it happens
Trigger: Production call to NewPool/ParsePoolConfig where pgx generated fallback entries (e.g. multi-host URLs, or target_session_attrs producing fallbacks) and at least one fallback has no TLSConfig; URLs like postgres://host1,host2/db where the second host config lacks sslmode.
Common situations: Multi-host production connection strings where only the first host has TLS settings; pgx versions that add a plaintext fallback for 'prefer'-style sslmodes; load-balancer DNS names resolving to endpoints with mixed TLS support.
Understand the failure class
Background: Conflicting config options: "cannot be used together" — configuration validation errors across open-source libraries — this error's family across 162 libraries.
Related errors
- postgres: production DATABASE_URL requires…
- postgres: CA certificate configured while TLS is disabled
- postgres: production DATABASE_URL must be a Postgres URL
- postgres: read
- postgres: contains no valid certificate
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/6c94fabaa9ccec9b.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/postgresconfig/postgresconfig.go:68
return nil, fmt.Errorf("postgres: %s or %s is required in production", caEnvironment, caFileEnvironment)
}
if caPEM == "" {
return config, nil
}
roots := x509.NewCertPool()
if !roots.AppendCertsFromPEM([]byte(caPEM)) {
return nil, fmt.Errorf("postgres: %s contains no valid certificate", caEnvironment)
}
if config.ConnConfig.TLSConfig == nil {
return nil, errors.New("postgres: CA certificate configured while TLS is disabled")
}
config.ConnConfig.TLSConfig.RootCAs = roots
config.ConnConfig.TLSConfig.InsecureSkipVerify = false
config.ConnConfig.TLSConfig.ServerName = config.ConnConfig.Host
for _, fallback := range config.ConnConfig.Fallbacks {
if fallback.TLSConfig == nil {
if production {
return nil, errors.New("postgres: production connection includes a plaintext fallback")
}
continue
}
fallback.TLSConfig.RootCAs = roots
fallback.TLSConfig.InsecureSkipVerify = false
fallback.TLSConfig.ServerName = fallback.Host
}
return config, nil
}
func caPEMFromEnvironment() (string, error) {
filePath := strings.TrimSpace(os.Getenv(caFileEnvironment))
direct := strings.TrimSpace(os.Getenv(caEnvironment))
if filePath != "" && direct != "" {
return "", fmt.Errorf("postgres: set only %s or %s, not both", caFileEnvironment, caEnvironment)
}
if filePath == "" {
return direct, nilView on GitHub (pinned to 3ee70a1026)