Files
dbbackup/AUTHENTICATION_PLAN.md
Renz f5f302a11c Add authentication mismatch detection and pgpass support
Phase 1: Detection & Guidance
- Detect OS user vs DB user mismatch
- Identify PostgreSQL authentication method (peer/ident/md5)
- Show helpful error messages with 4 solutions:
  1. sudo -u <user> (for peer auth)
  2. ~/.pgpass file (recommended)
  3. PGPASSWORD env variable
  4. --password flag

Phase 2: pgpass Support
- Auto-load passwords from ~/.pgpass file
- Support standard PostgreSQL pgpass format
- Check file permissions (must be 0600)
- Support wildcard matching (host:port:db:user:pass)

Tested on CentOS Stream 10 with PostgreSQL 16
2025-11-07 14:43:34 +00:00

12 KiB
Raw Blame History

Database Authentication Enhancement Plan

Current Situation Analysis

PostgreSQL Authentication Methods (by Distribution)

Current System: CentOS Stream 10

  • Local (Unix socket): peer authentication

    • Requires OS username = PostgreSQL username
    • Example: sudo -u postgres ./dbbackup status --user postgres
    • Fails: ./dbbackup status --user postgres (peer auth failed)
  • TCP (localhost): ident authentication

    • Uses identd protocol to verify OS username
    • Similar to peer but over TCP

Common PostgreSQL Auth Methods Across Distributions

  1. peer - Unix socket only, OS user must match DB user
  2. ident - TCP/IP, uses identd to verify OS user
  3. md5/scram-sha-256 - Password-based (most common for remote)
  4. trust - No authentication (development only)
  5. cert - SSL certificate-based
  6. ldap/pam - Enterprise integration

MySQL/MariaDB Authentication

  • Typically uses password-based authentication by default
  • Can use unix_socket plugin (similar to peer)
  • Less likely to have peer/ident issues

Problem Statement

When user runs:

./dbbackup status --user postgres

The tool attempts to connect as "postgres" user, but:

  1. Root user context: OS user is "root", PostgreSQL expects "postgres"
  2. Peer auth fails: FATAL: Peer authentication failed for user "postgres"
  3. User must know: Need sudo -u postgres or provide password

Solution Strategy: Multi-Level Authentication

Level 1: Smart OS User Detection (Quick Win)

Goal: Detect when OS user ≠ DB user and provide helpful guidance

Implementation:

// Check if OS user matches requested DB user
currentOSUser := getCurrentUser()
requestedDBUser := cfg.User

if currentOSUser != requestedDBUser {
    // Check authentication method
    authMethod := detectPostgreSQLAuthMethod(cfg.Host, cfg.Port)
    
    if authMethod == "peer" || authMethod == "ident" {
        // Peer/ident requires OS user = DB user
        if cfg.Password == "" {
            // No password provided, suggest sudo
            log.Warn("Authentication mismatch detected",
                "os_user", currentOSUser,
                "db_user", requestedDBUser,
                "auth_method", authMethod)
            fmt.Printf("\n⚠  Authentication Note:\n")
            fmt.Printf("   PostgreSQL is using '%s' authentication\n", authMethod)
            fmt.Printf("   OS user '%s' cannot authenticate as DB user '%s'\n", 
                currentOSUser, requestedDBUser)
            fmt.Printf("\n💡 Solutions:\n")
            fmt.Printf("   1. Run as matching user: sudo -u %s %s\n", 
                requestedDBUser, os.Args[0])
            fmt.Printf("   2. Provide password: %s --password <password>\n", 
                os.Args[0])
            fmt.Printf("   3. Set PGPASSWORD environment variable\n")
            fmt.Printf("   4. Configure ~/.pgpass file\n\n")
            
            return fmt.Errorf("authentication method requires matching OS user")
        }
    }
}

Level 2: Auto-Sudo Wrapper (Medium Effort)

Goal: Automatically re-execute with sudo when needed

Implementation:

func autoSudoIfNeeded(cfg *Config) error {
    currentUser := getCurrentUser()
    
    // Check if we need sudo and aren't already using it
    if currentUser != cfg.User && os.Getenv("DBBACKUP_SUDO_RETRY") == "" {
        authMethod := detectAuthMethod(cfg)
        
        if authMethod == "peer" || authMethod == "ident" {
            if cfg.Password == "" {
                fmt.Printf("🔄 Auto-retrying with sudo as user '%s'...\n", cfg.User)
                
                // Re-execute with sudo
                cmd := exec.Command("sudo", "-u", cfg.User, os.Args[0], os.Args[1:]...)
                cmd.Env = append(os.Environ(), "DBBACKUP_SUDO_RETRY=1")
                cmd.Stdin = os.Stdin
                cmd.Stdout = os.Stdout
                cmd.Stderr = os.Stderr
                
                if err := cmd.Run(); err != nil {
                    return fmt.Errorf("sudo retry failed: %w", err)
                }
                
                os.Exit(cmd.ProcessState.ExitCode())
            }
        }
    }
    
    return nil
}

Level 3: pgpass Support (High Value)

Goal: Use ~/.pgpass file for password-less authentication

Implementation:

// Check ~/.pgpass and /var/lib/pgsql/.pgpass
func loadPasswordFromPgpass(cfg *Config) (string, bool) {
    pgpassLocations := []string{
        filepath.Join(os.Getenv("HOME"), ".pgpass"),
        "/var/lib/pgsql/.pgpass",
        filepath.Join("/home", cfg.User, ".pgpass"),
    }
    
    for _, pgpassPath := range pgpassLocations {
        if password := parsePgpass(pgpassPath, cfg); password != "" {
            return password, true
        }
    }
    
    return "", false
}

// Format: hostname:port:database:username:password
func parsePgpass(path string, cfg *Config) string {
    file, err := os.Open(path)
    if err != nil {
        return ""
    }
    defer file.Close()
    
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        line := strings.TrimSpace(scanner.Text())
        if line == "" || strings.HasPrefix(line, "#") {
            continue
        }
        
        parts := strings.Split(line, ":")
        if len(parts) != 5 {
            continue
        }
        
        host, port, db, user, pass := parts[0], parts[1], parts[2], parts[3], parts[4]
        
        // Match hostname (* = wildcard)
        if host != "*" && host != cfg.Host {
            continue
        }
        
        // Match port (* = wildcard)
        if port != "*" && port != strconv.Itoa(cfg.Port) {
            continue
        }
        
        // Match database (* = wildcard)
        if db != "*" && db != cfg.Database {
            continue
        }
        
        // Match user (* = wildcard)
        if user != "*" && user != cfg.User {
            continue
        }
        
        return pass
    }
    
    return ""
}

Level 4: Smart Environment Detection (Advanced)

Goal: Detect distribution and suggest optimal configuration

Implementation:

type OSDistribution struct {
    Name              string
    Family            string // debian, redhat, arch, etc.
    PostgreSQLVersion string
    DefaultAuthMethod string
    SocketLocation    string
    SuggestedUser     string
}

func detectDistribution() *OSDistribution {
    // Read /etc/os-release
    data, err := os.ReadFile("/etc/os-release")
    if err != nil {
        return &OSDistribution{Name: "unknown"}
    }
    
    content := string(data)
    dist := &OSDistribution{}
    
    // Parse os-release
    for _, line := range strings.Split(content, "\n") {
        if strings.HasPrefix(line, "ID=") {
            dist.Name = strings.Trim(strings.TrimPrefix(line, "ID="), "\"")
        }
        if strings.HasPrefix(line, "ID_LIKE=") {
            dist.Family = strings.Trim(strings.TrimPrefix(line, "ID_LIKE="), "\"")
        }
    }
    
    // Distribution-specific defaults
    switch dist.Name {
    case "centos", "rhel", "fedora":
        dist.DefaultAuthMethod = "peer"
        dist.SocketLocation = "/var/run/postgresql"
        dist.SuggestedUser = "postgres"
        
    case "debian", "ubuntu":
        dist.DefaultAuthMethod = "peer"
        dist.SocketLocation = "/var/run/postgresql"
        dist.SuggestedUser = "postgres"
        
    case "arch", "manjaro":
        dist.DefaultAuthMethod = "peer"
        dist.SocketLocation = "/run/postgresql"
        dist.SuggestedUser = "postgres"
        
    case "alpine":
        dist.DefaultAuthMethod = "md5"
        dist.SocketLocation = "/run/postgresql"
        dist.SuggestedUser = "postgres"
    }
    
    return dist
}

Implementation Phases

Phase 1: Detection & Guidance (1-2 hours)

  • Detect OS user vs DB user mismatch
  • Detect PostgreSQL authentication method (peer/ident/md5)
  • Provide helpful error messages with solutions
  • Show example commands for current system

Files to modify:

  • internal/config/config.go - Add OS user detection
  • internal/database/postgresql.go - Add auth method detection
  • cmd/root.go - Add pre-connection validation

Phase 2: pgpass Support (2-3 hours)

  • Read and parse ~/.pgpass file
  • Support wildcard matching
  • Check multiple pgpass locations
  • Fall back to password prompt if needed

Files to modify:

  • internal/config/config.go - Add pgpass loading
  • internal/database/postgresql.go - Integrate pgpass passwords

Phase 3: Auto-Sudo (3-4 hours) - OPTIONAL

  • ⚠️ Automatically detect when sudo is needed
  • ⚠️ Re-execute command with sudo -u
  • ⚠️ Preserve all arguments and flags
  • ⚠️ Handle interactive prompts

Considerations:

  • Security implications of auto-sudo
  • May surprise users (implicit behavior change)
  • Could interfere with scripting/automation

Phase 4: Distribution-Aware Setup (4-5 hours) - OPTIONAL

  • Detect Linux distribution
  • Provide distribution-specific guidance
  • Auto-configure optimal settings
  • Generate setup scripts for first-run

Why this combination?

  1. Phase 1: Immediate value - users understand what's wrong
  2. Phase 2: Standard PostgreSQL solution - no surprises
  3. Skip Phase 3: Auto-sudo can be confusing/dangerous
  4. Skip Phase 4: Users know their own distribution

User Experience Flow:

# User runs without proper auth
$ ./dbbackup status --user postgres
⚠️  Authentication Note:
   PostgreSQL is using 'peer' authentication
   OS user 'root' cannot authenticate as DB user 'postgres'

💡 Solutions:
   1. Run as matching user: sudo -u postgres ./dbbackup
   2. Provide password: ./dbbackup --password <password>
   3. Set PGPASSWORD environment variable
   4. Configure ~/.pgpass file (recommended)

📝 To create ~/.pgpass file:
   echo "localhost:5432:*:postgres:yourpassword" > ~/.pgpass
   chmod 0600 ~/.pgpass

# User fixes authentication
$ sudo -u postgres ./dbbackup status --user postgres
✅ Connected successfully

Testing Matrix

PostgreSQL Authentication Methods

  • peer (Unix socket) - CentOS/RHEL default
  • ident (TCP/IP) - Some distributions
  • md5 (Password) - Common for remote
  • scram-sha-256 (Password) - Modern PostgreSQL
  • trust (No auth) - Development only

Operating Systems

  • CentOS Stream 10 (current system)
  • Ubuntu 22.04/24.04 (most popular)
  • Debian 12 (stable)
  • Fedora 40 (cutting edge)
  • Alpine Linux (containers)

Scenarios

  • Root user connecting as postgres user
  • Postgres user connecting as postgres user
  • Regular user with pgpass file
  • Regular user with PGPASSWORD env
  • Regular user with --password flag
  • TCP vs Unix socket connections
  • Remote database connections

Security Considerations

  1. pgpass file permissions: Must be 0600 (owner read/write only)
  2. Password in command line: Discourage --password flag (visible in ps)
  3. PGPASSWORD env: Better than command line, but still visible
  4. Auto-sudo: Could be security risk if not carefully implemented
  5. Error messages: Don't expose sensitive connection details

Backward Compatibility

No breaking changes - All existing workflows continue to work:

  • sudo -u postgres ./dbbackup
  • PGPASSWORD=secret ./dbbackup
  • ./dbbackup --password secret
  • Current socket detection logic

Conclusion

Recommended Implementation: Phase 1 + Phase 2

  • Effort: 3-5 hours total
  • Value: High - users can authenticate without sudo
  • Risk: Low - using standard PostgreSQL mechanisms
  • Complexity: Medium - well-defined scope

Skip: Phase 3 (Auto-sudo)

  • Can surprise users with implicit behavior
  • Security implications
  • Not standard PostgreSQL practice

Defer: Phase 4 (Distribution detection)

  • Nice-to-have but not essential
  • Users generally know their own system
  • Can be added later if needed