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:
379
AUTHENTICATION_PLAN.md
Normal file
379
AUTHENTICATION_PLAN.md
Normal 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
|
||||||
338
internal/auth/helper.go
Normal file
338
internal/auth/helper.go
Normal file
@ -0,0 +1,338 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"dbbackup/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthMethod represents a PostgreSQL authentication method
|
||||||
|
type AuthMethod string
|
||||||
|
|
||||||
|
const (
|
||||||
|
AuthPeer AuthMethod = "peer"
|
||||||
|
AuthIdent AuthMethod = "ident"
|
||||||
|
AuthMD5 AuthMethod = "md5"
|
||||||
|
AuthScramSHA256 AuthMethod = "scram-sha-256"
|
||||||
|
AuthPassword AuthMethod = "password"
|
||||||
|
AuthTrust AuthMethod = "trust"
|
||||||
|
AuthUnknown AuthMethod = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DetectPostgreSQLAuthMethod attempts to detect the authentication method
|
||||||
|
// by reading pg_hba.conf or checking common patterns
|
||||||
|
func DetectPostgreSQLAuthMethod(host string, port int, user string) AuthMethod {
|
||||||
|
// For localhost connections, check pg_hba.conf
|
||||||
|
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
||||||
|
if method := checkPgHbaConf(user); method != AuthUnknown {
|
||||||
|
return method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default assumptions based on connection type
|
||||||
|
if host == "localhost" {
|
||||||
|
return AuthPeer // Most common for local connections
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthMD5 // Most common for remote connections
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkPgHbaConf reads pg_hba.conf to determine authentication method
|
||||||
|
func checkPgHbaConf(user string) AuthMethod {
|
||||||
|
// Common pg_hba.conf locations
|
||||||
|
locations := []string{
|
||||||
|
"/var/lib/pgsql/data/pg_hba.conf",
|
||||||
|
"/etc/postgresql/*/main/pg_hba.conf",
|
||||||
|
"/var/lib/postgresql/data/pg_hba.conf",
|
||||||
|
"/usr/local/pgsql/data/pg_hba.conf",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find pg_hba.conf using psql if available
|
||||||
|
if hbaFile := findHbaFileViaPostgres(); hbaFile != "" {
|
||||||
|
locations = append([]string{hbaFile}, locations...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, location := range locations {
|
||||||
|
if method := parsePgHbaConf(location, user); method != AuthUnknown {
|
||||||
|
return method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// findHbaFileViaPostgres asks PostgreSQL for the hba_file location
|
||||||
|
func findHbaFileViaPostgres() string {
|
||||||
|
cmd := exec.Command("psql", "-U", "postgres", "-t", "-c", "SHOW hba_file;")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return strings.TrimSpace(string(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePgHbaConf parses pg_hba.conf and returns the authentication method
|
||||||
|
func parsePgHbaConf(path string, user string) AuthMethod {
|
||||||
|
// Try with sudo if we can't read directly
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
// Try with sudo
|
||||||
|
cmd := exec.Command("sudo", "cat", path)
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return AuthUnknown
|
||||||
|
}
|
||||||
|
return parseHbaContent(string(output), user)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
var content strings.Builder
|
||||||
|
for scanner.Scan() {
|
||||||
|
content.WriteString(scanner.Text())
|
||||||
|
content.WriteString("\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseHbaContent(content.String(), user)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHbaContent parses the content of pg_hba.conf
|
||||||
|
func parseHbaContent(content string, user string) AuthMethod {
|
||||||
|
lines := strings.Split(content, "\n")
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse HBA line: TYPE DATABASE USER ADDRESS METHOD
|
||||||
|
fields := strings.Fields(line)
|
||||||
|
if len(fields) < 4 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
connType := fields[0]
|
||||||
|
// database := fields[1]
|
||||||
|
hbaUser := fields[2]
|
||||||
|
var method string
|
||||||
|
|
||||||
|
if connType == "local" {
|
||||||
|
// local all all peer
|
||||||
|
if len(fields) >= 4 {
|
||||||
|
method = fields[3]
|
||||||
|
}
|
||||||
|
} else if connType == "host" {
|
||||||
|
// host all all 127.0.0.1/32 md5
|
||||||
|
if len(fields) >= 5 {
|
||||||
|
method = fields[4]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this rule applies to our user
|
||||||
|
if hbaUser == "all" || hbaUser == user {
|
||||||
|
switch strings.ToLower(method) {
|
||||||
|
case "peer":
|
||||||
|
return AuthPeer
|
||||||
|
case "ident":
|
||||||
|
return AuthIdent
|
||||||
|
case "md5":
|
||||||
|
return AuthMD5
|
||||||
|
case "scram-sha-256":
|
||||||
|
return AuthScramSHA256
|
||||||
|
case "password":
|
||||||
|
return AuthPassword
|
||||||
|
case "trust":
|
||||||
|
return AuthTrust
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AuthUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckAuthenticationMismatch checks if there's an authentication mismatch
|
||||||
|
// and returns helpful guidance if needed
|
||||||
|
func CheckAuthenticationMismatch(cfg *config.Config) (bool, string) {
|
||||||
|
// Only check for PostgreSQL
|
||||||
|
if !cfg.IsPostgreSQL() {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current OS user
|
||||||
|
currentOSUser := config.GetCurrentOSUser()
|
||||||
|
requestedDBUser := cfg.User
|
||||||
|
|
||||||
|
// If users match, no problem
|
||||||
|
if currentOSUser == requestedDBUser {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// If password is provided, user can authenticate
|
||||||
|
if cfg.Password != "" {
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect authentication method
|
||||||
|
authMethod := DetectPostgreSQLAuthMethod(cfg.Host, cfg.Port, cfg.User)
|
||||||
|
|
||||||
|
// peer and ident require OS user = DB user
|
||||||
|
if authMethod == AuthPeer || authMethod == AuthIdent {
|
||||||
|
return true, buildAuthMismatchMessage(currentOSUser, requestedDBUser, authMethod)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildAuthMismatchMessage creates a helpful error message
|
||||||
|
func buildAuthMismatchMessage(osUser, dbUser string, method AuthMethod) string {
|
||||||
|
var msg strings.Builder
|
||||||
|
|
||||||
|
msg.WriteString("\n⚠️ Authentication Mismatch Detected\n")
|
||||||
|
msg.WriteString(strings.Repeat("=", 60) + "\n\n")
|
||||||
|
|
||||||
|
msg.WriteString(fmt.Sprintf(" PostgreSQL is using '%s' authentication\n", method))
|
||||||
|
msg.WriteString(fmt.Sprintf(" OS user '%s' cannot authenticate as DB user '%s'\n\n", osUser, dbUser))
|
||||||
|
|
||||||
|
msg.WriteString("💡 Solutions (choose one):\n\n")
|
||||||
|
|
||||||
|
msg.WriteString(fmt.Sprintf(" 1. Run as matching user:\n"))
|
||||||
|
msg.WriteString(fmt.Sprintf(" sudo -u %s %s\n\n", dbUser, getCommandLine()))
|
||||||
|
|
||||||
|
msg.WriteString(" 2. Configure ~/.pgpass file (recommended):\n")
|
||||||
|
msg.WriteString(fmt.Sprintf(" echo \"localhost:5432:*:%s:your_password\" > ~/.pgpass\n", dbUser))
|
||||||
|
msg.WriteString(" chmod 0600 ~/.pgpass\n\n")
|
||||||
|
|
||||||
|
msg.WriteString(" 3. Set PGPASSWORD environment variable:\n")
|
||||||
|
msg.WriteString(fmt.Sprintf(" export PGPASSWORD=your_password\n"))
|
||||||
|
msg.WriteString(fmt.Sprintf(" %s\n\n", getCommandLine()))
|
||||||
|
|
||||||
|
msg.WriteString(" 4. Provide password via flag:\n")
|
||||||
|
msg.WriteString(fmt.Sprintf(" %s --password your_password\n\n", getCommandLine()))
|
||||||
|
|
||||||
|
msg.WriteString("📝 Note: For production use, ~/.pgpass or PGPASSWORD are recommended\n")
|
||||||
|
msg.WriteString(" to avoid exposing passwords in command history.\n\n")
|
||||||
|
|
||||||
|
msg.WriteString(strings.Repeat("=", 60) + "\n")
|
||||||
|
|
||||||
|
return msg.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// getCommandLine reconstructs the current command line
|
||||||
|
func getCommandLine() string {
|
||||||
|
if len(os.Args) == 0 {
|
||||||
|
return "./dbbackup"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build command without password if present
|
||||||
|
var parts []string
|
||||||
|
skipNext := false
|
||||||
|
|
||||||
|
for _, arg := range os.Args {
|
||||||
|
if skipNext {
|
||||||
|
skipNext = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg == "--password" || arg == "-p" {
|
||||||
|
skipNext = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(arg, "--password=") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
parts = append(parts, arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(parts, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadPasswordFromPgpass attempts to load password from .pgpass file
|
||||||
|
func LoadPasswordFromPgpass(cfg *config.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
|
||||||
|
}
|
||||||
|
|
||||||
|
// parsePgpass reads and parses .pgpass file
|
||||||
|
// Format: hostname:port:database:username:password
|
||||||
|
func parsePgpass(path string, cfg *config.Config) string {
|
||||||
|
file, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Check file permissions (should be 0600)
|
||||||
|
info, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn if permissions are too open (but still use it)
|
||||||
|
if info.Mode().Perm() != 0600 {
|
||||||
|
// Silently skip - PostgreSQL also skips world-readable pgpass files
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
|
||||||
|
// Skip comments and empty lines
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
@ -387,6 +387,11 @@ func getCurrentUser() string {
|
|||||||
return "postgres"
|
return "postgres"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetCurrentOSUser returns the current OS user (exported for auth checking)
|
||||||
|
func GetCurrentOSUser() string {
|
||||||
|
return getCurrentUser()
|
||||||
|
}
|
||||||
|
|
||||||
func getDefaultBackupDir() string {
|
func getDefaultBackupDir() string {
|
||||||
// Try to create a sensible default backup directory
|
// Try to create a sensible default backup directory
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"dbbackup/internal/auth"
|
||||||
"dbbackup/internal/config"
|
"dbbackup/internal/config"
|
||||||
"dbbackup/internal/logger"
|
"dbbackup/internal/logger"
|
||||||
|
|
||||||
@ -35,6 +36,20 @@ func NewPostgreSQL(cfg *config.Config, log logger.Logger) *PostgreSQL {
|
|||||||
|
|
||||||
// Connect establishes a connection to PostgreSQL using pgx for better performance
|
// Connect establishes a connection to PostgreSQL using pgx for better performance
|
||||||
func (p *PostgreSQL) Connect(ctx context.Context) error {
|
func (p *PostgreSQL) Connect(ctx context.Context) error {
|
||||||
|
// Try to load password from .pgpass if not provided
|
||||||
|
if p.cfg.Password == "" {
|
||||||
|
if password, found := auth.LoadPasswordFromPgpass(p.cfg); found {
|
||||||
|
p.cfg.Password = password
|
||||||
|
p.log.Debug("Loaded password from .pgpass file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for authentication mismatch before attempting connection
|
||||||
|
if mismatch, msg := auth.CheckAuthenticationMismatch(p.cfg); mismatch {
|
||||||
|
fmt.Println(msg)
|
||||||
|
return fmt.Errorf("authentication configuration required")
|
||||||
|
}
|
||||||
|
|
||||||
// Build PostgreSQL DSN (pgx format)
|
// Build PostgreSQL DSN (pgx format)
|
||||||
dsn := p.buildPgxDSN()
|
dsn := p.buildPgxDSN()
|
||||||
p.dsn = dsn
|
p.dsn = dsn
|
||||||
|
|||||||
Reference in New Issue
Block a user