JuliusBrussee/caveman · error

postgres: read

Error message

postgres: read %s: %w

What it means

os.ReadFile on the validated CA file path failed (e.g. permission denied, I/O error, file removed between stat and read). The error is wrapped with 'postgres: read <ENVVAR>:' so the failing variable is identifiable.

Solutions

  1. Fix file permissions so the process user can read the CA file (chmod/chown or mount with correct mode).
  2. Ensure the secret/volume is mounted before the application starts.
  3. Retest the exact read with `sudo -u <appuser> cat "$POSTGRES_CA_FILE"`.
  4. If the CA is available inline, use the direct PEM env var instead of the file path.

Example fix

// before
// k8s secret volume defaultMode: 0400 owned by root, app runs as uid 1000
// after
// defaultMode: 0440 with fsGroup: 1000 (or chmod o+r the file)
Defensive patterns

Strategy: validation

Validate before calling

func canReadCA(path string) error {
	f, err := os.Open(path)
	if err != nil { return err }
	defer f.Close()
	_, err = f.Read(make([]byte, 1))
	return err
}

Try / catch

if err := canReadCA(os.Getenv("POSTGRES_CA_FILE")); err != nil {
	log.Fatalf("CA file unreadable: %v", err)
}

Prevention

When it happens

Trigger: ParsePoolConfig/NewPool with the CA-file env var set; file exists and is regular but cannot be read by the process (permissions changed, file deleted in a race, filesystem error).

Common situations: Secret mounted as root-only while the app runs as non-root; NFS/overlay filesystem errors; Kubernetes secret volume not yet mounted at read time.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


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

Appendix: source

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

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, nil
	}
	info, err := os.Stat(filePath)
	if err != nil {
		return "", fmt.Errorf("postgres: %s: %w", caFileEnvironment, err)
	}
	if !info.Mode().IsRegular() {
		return "", fmt.Errorf("postgres: %s must point to a regular file", caFileEnvironment)
	}
	contents, err := os.ReadFile(filePath)
	if err != nil {
		return "", fmt.Errorf("postgres: read %s: %w", caFileEnvironment, err)
	}
	return strings.TrimSpace(string(contents)), nil
}

// NewPool constructs a pool from the hardened configuration.
func NewPool(ctx context.Context, databaseURL string) (*pgxpool.Pool, error) {
	config, err := ParsePoolConfig(databaseURL)
	if err != nil {
		return nil, err
	}
	return pgxpool.NewWithConfig(ctx, config)
}

// NewRuntimePool constructs a pool and proves the connected login is a
// least-privilege member of expectedRole. Runtime services must never connect
// as a superuser, BYPASSRLS role, or owner of a tenant table; this remains true
// for local, on-prem, staging, and production deployments alike.
func NewRuntimePool(ctx context.Context, databaseURL, expectedRole string) (*pgxpool.Pool, error) {

View on GitHub (pinned to 3ee70a1026)