{"record":{"id":"3e5fcaef040b3605","repo":"JuliusBrussee/caveman","slug":"postgres-s-or-s-is-required-in-production","errorCode":null,"errorMessage":"postgres: %s or %s is required in production","messagePattern":"postgres: (.+?) or (.+?) is required in production","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"shared/platform/postgresconfig/postgresconfig.go","lineNumber":50,"sourceCode":"\t\tparsed, err := url.Parse(databaseURL)\n\t\tif err != nil || (parsed.Scheme != \"postgres\" && parsed.Scheme != \"postgresql\") || parsed.Hostname() == \"\" {\n\t\t\treturn nil, errors.New(\"postgres: production DATABASE_URL must be a Postgres URL\")\n\t\t}\n\t\tif parsed.Query().Get(\"sslmode\") != \"verify-full\" {\n\t\t\treturn nil, errors.New(\"postgres: production DATABASE_URL requires sslmode=verify-full\")\n\t\t}\n\t}\n\n\tconfig, err := pgxpool.ParseConfig(databaseURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"postgres: parse DATABASE_URL: %w\", err)\n\t}\n\tcaPEM, err := caPEMFromEnvironment()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif production && caPEM == \"\" {\n\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\")","sourceCodeStart":32,"sourceCodeEnd":68,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/shared/platform/postgresconfig/postgresconfig.go#L32-L68","documentation":"ParsePoolConfig enforces that in production a TLS CA is available via one of two environment variables (caEnvironment or caFileEnvironment, e.g. DB_CA_PEM or DB_CA_FILE). If production mode is set and neither is present, startup is refused so the pool never connects with an unverifiable server certificate. The message interpolates the two env var names the operator must set.","triggerScenarios":"Calling ParsePoolConfig or NewPool with production=true (production build/env flag) while caPEMFromEnvironment() returns empty — i.e., neither the inline CA PEM variable nor the CA file path variable is set to a readable value.","commonSituations":"Deploying the production image without injecting the RDS/Cloud SQL CA bundle; k8s Secret mounted at the wrong path so the CA_FILE var points nowhere; local dev compose files promoted to prod unchanged; variable renamed in a newer release while old manifests persist.","solutions":["Set the CA PEM env var named in the error (e.g. DB_CA_PEM) with the full certificate bundle.","Or set the file-path variable (e.g. DB_CA_FILE) to a mounted CA file readable by the process.","In k8s, mount the CA as a Secret/ConfigMap volume and point the env var at it.","Verify sslmode=verify-full is also set in DATABASE_URL (production requires it alongside the CA).","Confirm the file is readable by the container user (permissions) and contains valid PEM."],"exampleFix":"// before\ndocker run production-image  # no CA configured\n// after\ndocker run -e DB_CA_FILE=/certs/rds-combined-ca-bundle.pem -v $PWD/certs:/certs:ro production-image","handlingStrategy":"validation","validationCode":"func checkProdTLSConfig() error {\n    if os.Getenv(\"DB_ENV\") != \"production\" {\n        return nil\n    }\n    if os.Getenv(\"DB_CA_PEM\") == \"\" && os.Getenv(\"DB_CA_FILE\") == \"\" {\n        return errors.New(\"production requires DB_CA_PEM or DB_CA_FILE\")\n    }\n    if p := os.Getenv(\"DB_CA_FILE\"); p != \"\" {\n        if _, err := os.ReadFile(p); err != nil {\n            return fmt.Errorf(\"DB_CA_FILE unreadable: %w\", err)\n        }\n    }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"pool, err := postgresconfig.NewPool(ctx, dsn)\nif err != nil {\n    if strings.Contains(err.Error(), \"is required in production\") {\n        return fmt.Errorf(\"deployment missing TLS CA: set %s or %s: %w\", \"DB_CA_PEM\", \"DB_CA_FILE\", err)\n    }\n    return err\n}","preventionTips":["Bake the CA bundle into production images or mount it as a Secret at a fixed path.","Add a startup preflight that fails before ParsePoolConfig when the CA env vars are unset.","Keep env var names in one constants file so renames are caught by grep/tests.","Verify CA file permissions allow the running container user to read it.","Pair the CA with sslmode=verify-full in the DSN — production requires both."],"tags":["postgres","tls","configuration","environment","security"],"backgroundTag":"missing-env-var","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"}