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
This commit is contained in:
2025-11-07 14:43:34 +00:00
parent 1c72bf5e64
commit f5f302a11c
5 changed files with 737 additions and 0 deletions

379
AUTHENTICATION_PLAN.md Normal file
View File

@ -0,0 +1,379 @@
# 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:
```bash
./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**:
```go
// 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**:
```go
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**:
```go
// 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**:
```go
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
## Recommended Approach: Phase 1 + Phase 2
**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**:
```bash
# 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