{"record":{"id":"6c94fabaa9ccec9b","repo":"JuliusBrussee/caveman","slug":"postgres-production-connection-includes-a-plainte","errorCode":null,"errorMessage":"postgres: production connection includes a plaintext fallback","messagePattern":"postgres: production connection includes a plaintext fallback","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"shared/platform/postgresconfig/postgresconfig.go","lineNumber":68,"sourceCode":"\t\treturn nil, fmt.Errorf(\"postgres: %s or %s is required in production\", caEnvironment, caFileEnvironment)\n\t}\n\tif caPEM == \"\" {\n\t\treturn config, nil\n\t}\n\troots := x509.NewCertPool()\n\tif !roots.AppendCertsFromPEM([]byte(caPEM)) {\n\t\treturn nil, fmt.Errorf(\"postgres: %s contains no valid certificate\", caEnvironment)\n\t}\n\tif config.ConnConfig.TLSConfig == nil {\n\t\treturn nil, errors.New(\"postgres: CA certificate configured while TLS is disabled\")\n\t}\n\tconfig.ConnConfig.TLSConfig.RootCAs = roots\n\tconfig.ConnConfig.TLSConfig.InsecureSkipVerify = false\n\tconfig.ConnConfig.TLSConfig.ServerName = config.ConnConfig.Host\n\tfor _, fallback := range config.ConnConfig.Fallbacks {\n\t\tif fallback.TLSConfig == nil {\n\t\t\tif production {\n\t\t\t\treturn nil, errors.New(\"postgres: production connection includes a plaintext fallback\")\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tfallback.TLSConfig.RootCAs = roots\n\t\tfallback.TLSConfig.InsecureSkipVerify = false\n\t\tfallback.TLSConfig.ServerName = fallback.Host\n\t}\n\treturn config, nil\n}\n\nfunc caPEMFromEnvironment() (string, error) {\n\tfilePath := strings.TrimSpace(os.Getenv(caFileEnvironment))\n\tdirect := strings.TrimSpace(os.Getenv(caEnvironment))\n\tif filePath != \"\" && direct != \"\" {\n\t\treturn \"\", fmt.Errorf(\"postgres: set only %s or %s, not both\", caFileEnvironment, caEnvironment)\n\t}\n\tif filePath == \"\" {\n\t\treturn direct, nil","sourceCodeStart":50,"sourceCodeEnd":86,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/postgresconfig/postgresconfig.go#L50-L86","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nDATABASE_URL=\"postgres://app:secret@db1:5432,db2:5432/prod?sslmode=verify-full\" // db2 fallback resolves to plaintext\n// after\nDATABASE_URL=\"postgres://app:secret@db1:5432/prod?sslmode=verify-full\" // single verified endpoint, no plaintext fallback","handlingStrategy":"validation","validationCode":"if production && strings.Contains(databaseURL, \",\") {\n    // multi-host URLs can generate plaintext fallbacks; verify each host is TLS-capable\n    return fmt.Errorf(\"multi-host DATABASE_URL may include plaintext fallbacks\")\n}","typeGuard":null,"tryCatchPattern":"cfg, err := postgresconfig.ParsePoolConfig(databaseURL)\nif err != nil && strings.Contains(err.Error(), \"plaintext fallback\") {\n    log.Fatal(\"production pool would allow plaintext fallback; fix TLS on every host\")\n}","preventionTips":["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"],"tags":["postgres","tls","production","fallback"],"backgroundTag":"conflicting-config-options","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}