JuliusBrussee/caveman · error

postgres: CA certificate configured while TLS is disabled

Error message

postgres: CA certificate configured while TLS is disabled

What it means

ParsePoolConfig parsed a CA certificate (from the CA environment variable) and tried to install it as the TLS root pool, but the pgx connection config has TLSConfig == nil, meaning TLS is disabled for the connection. A CA is meaningless (and misleading) without TLS, so the library rejects the combination instead of ignoring the CA.

Solutions

  1. Set sslmode=verify-full in DATABASE_URL so pgx creates a TLSConfig to attach the CA to
  2. Remove the CA certificate environment variable if the connection is intentionally plaintext (local dev only)
  3. Check that pgxpool.ParseConfig did not strip TLS due to an earlier sslmode parsing issue in the URL

Example fix

// before
DATABASE_URL="postgres://app:secret@db.internal:5432/prod?sslmode=disable" PG_CA=/etc/certs/ca.pem
// after
DATABASE_URL="postgres://app:secret@db.internal:5432/prod?sslmode=verify-full" PG_CA=/etc/certs/ca.pem
Defensive patterns

Strategy: validation

Validate before calling

if caPEMSet && !hasVerifyFull(databaseURL) {
    return fmt.Errorf("CA certificate requires sslmode=verify-full (TLS enabled)")
}

Try / catch

cfg, err := postgresconfig.ParsePoolConfig(databaseURL)
if err != nil && strings.Contains(err.Error(), "CA certificate configured while TLS is disabled") {
    log.Fatal("sslmode=disable is incompatible with a configured CA; enable verify-full or drop the CA")
}

Prevention

When it happens

Trigger: Providing the CA certificate environment variable while the DATABASE_URL uses sslmode=disable or sslmode=allow/prefer in a way that leaves ConnConfig.TLSConfig nil; calling ParsePoolConfig with sslmode=disable plus a CA in non-production code paths that reach the CA handling block.

Common situations: Ops sets the CA env var in a deployment manifest but the DATABASE_URL still says sslmode=disable; local plaintext configs (TestLocalPlaintextRemainsAvailable) accidentally inheriting a CA secret from a shared environment.

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


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/db7774ac3829be87. Report an issue: GitHub.

Appendix: source

Thrown at shared/platform/postgresconfig/postgresconfig.go:60

	if err != nil {
		return nil, fmt.Errorf("postgres: parse DATABASE_URL: %w", err)
	}
	caPEM, err := caPEMFromEnvironment()
	if err != nil {
		return nil, err
	}
	if production && caPEM == "" {
		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
}

View on GitHub (pinned to 3ee70a1026)