Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ We follow the currently supported versions listed on <https://www.postgresql.org
- [postgresql_role](documentation/postgresql_role.md)
- [postgresql_service](documentation/postgresql_service.md)

## Additional Documentation

- [SCRAM-SHA-256 Authentication](documentation/scram-sha-256.md)

## Contributors

This project exists thanks to all the people who [contribute.](https://opencollective.com/sous-chefs/contributors.svg?width=890&button=false)
Expand Down
54 changes: 54 additions & 0 deletions documentation/postgresql_role.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,3 +65,57 @@ postgresql_role 'user1' do
valid_until '2018-12-31'
end
```

Create a user with a pre-hashed SCRAM-SHA-256 password:

```ruby
postgresql_role 'secure_user' do
encrypted_password 'SCRAM-SHA-256$4096:27klCUc487uwvJVGKI5YNA==$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4='
login true
createdb true
end
```

## SCRAM-SHA-256 Authentication

SCRAM-SHA-256 is a password authentication method that provides better security than MD5. When using SCRAM-SHA-256 authentication:

1. **Pre-hashed passwords**: If you have a pre-computed SCRAM-SHA-256 password hash, use the `encrypted_password` property.
2. **Password format**: SCRAM-SHA-256 passwords have the format: `SCRAM-SHA-256$<iter>:<salt>$<StoredKey>:<ServerKey>`
3. **Automatic escaping**: The cookbook automatically handles escaping of special characters (`$`) in SCRAM-SHA-256 passwords.

### Password Generation

To generate a SCRAM-SHA-256 password hash, you can use:

```bash
# Using PostgreSQL's built-in function
psql -c "SELECT gen_random_uuid();" # for salt generation
# Then use a SCRAM-SHA-256 library to generate the hash
```

Or use a Ruby library like `scram-sha-256`:

```ruby
require 'scram-sha-256'
password_hash = ScramSha256.hash_password('your_password', 4096)
```

### Configuration Example

```ruby
# Configure access method
postgresql_access 'scram access' do
type 'host'
database 'all'
user 'myuser'
address '127.0.0.1/32'
auth_method 'scram-sha-256'
end

# Create user with SCRAM password
postgresql_role 'myuser' do
encrypted_password 'SCRAM-SHA-256$4096:abc123...$def456...:ghi789...'
login true
end
```
203 changes: 203 additions & 0 deletions documentation/scram-sha-256.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
# SCRAM-SHA-256 Authentication

SCRAM-SHA-256 (Salted Challenge Response Authentication Mechanism) is a password authentication method in PostgreSQL that provides better security than traditional MD5 authentication.

## Overview

SCRAM-SHA-256 authentication offers several advantages:

- **Stronger security**: Uses SHA-256 instead of MD5
- **Salt protection**: Prevents rainbow table attacks
- **Iteration count**: Makes brute force attacks more difficult
- **Mutual authentication**: Both client and server verify each other

## Password Format

SCRAM-SHA-256 passwords have this specific format:

```text
SCRAM-SHA-256$<iteration_count>:<salt>$<StoredKey>:<ServerKey>
```

Example:

```text
SCRAM-SHA-256$4096:27klCUc487uwvJVGKI5YNA==$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4=
```

## Usage with Chef

### Creating Users with SCRAM-SHA-256 Passwords

When you have a pre-computed SCRAM-SHA-256 password hash:

```ruby
postgresql_role 'secure_user' do
encrypted_password 'SCRAM-SHA-256$4096:27klCUc487uwvJVGKI5YNA==$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4='
login true
action :create
end
```

### Automatic Character Escaping

The cookbook automatically handles escaping of special characters (`$`) in SCRAM-SHA-256 passwords. You don't need to manually escape these characters - the cookbook will handle this transparently.

**Before (manual escaping required):**

```ruby
postgresql_role 'user1' do
# Manual escaping was required
password 'SCRAM-SHA-256$4096:salt$key:server'.gsub('$', '\$')
action [:create, :update]
end
```

**Now (automatic escaping):**

```ruby
postgresql_role 'user1' do
# No manual escaping needed
encrypted_password 'SCRAM-SHA-256$4096:salt$key:server'
action [:create, :update]
end
```

## Configuring Authentication

To use SCRAM-SHA-256 authentication, configure the access method:

```ruby
postgresql_access 'scram authentication' do
type 'host'
database 'all'
user 'myuser'
address '127.0.0.1/32'
auth_method 'scram-sha-256'
end
```

## Password Generation

### Using PostgreSQL

Generate a SCRAM-SHA-256 password directly in PostgreSQL:

```sql
-- Set password for existing user (PostgreSQL will hash it)
ALTER ROLE myuser PASSWORD 'plaintext_password';

-- Check the generated hash
SELECT rolpassword FROM pg_authid WHERE rolname = 'myuser';
```

### Using Ruby

Generate a SCRAM-SHA-256 hash using the `scram-sha-256` gem:

```ruby
require 'scram-sha-256'

# Generate hash with default iteration count (4096)
password_hash = ScramSha256.hash_password('my_plain_password')

# Generate hash with custom iteration count
password_hash = ScramSha256.hash_password('my_plain_password', 8192)
```

### Using Python

Generate a SCRAM-SHA-256 hash using Python:

```python
import hashlib
import hmac
import base64
import secrets

def generate_scram_sha256(password, salt=None, iterations=4096):
if salt is None:
salt = secrets.token_bytes(16)

# Implementation details would go here
# This is a simplified example
pass
```

## Common Use Cases

### Control Panel Integration

When integrating with control panels that pre-hash passwords:

```ruby
# Control panel provides pre-hashed password
hashed_password = control_panel.get_user_password_hash(username)

postgresql_role username do
encrypted_password hashed_password
login true
createdb user_permissions.include?('createdb')
action [:create, :update]
end
```

### Migration from MD5

When migrating from MD5 to SCRAM-SHA-256:

```ruby
# First, configure SCRAM-SHA-256 authentication
postgresql_access 'upgrade to scram' do
type 'host'
database 'all'
user 'all'
address '127.0.0.1/32'
auth_method 'scram-sha-256'
end

# Users will need to reset their passwords
# The new passwords will automatically use SCRAM-SHA-256
```

## Troubleshooting

### Common Issues

1. **Password mangling**: If you see passwords with missing `$` characters, ensure you're using this cookbook version that includes automatic escaping.

2. **Authentication failures**: Verify that:
- The `pg_hba.conf` is configured for `scram-sha-256`
- The password hash format is correct
- The user has login privileges

3. **Iteration count**: Higher iteration counts (e.g., 8192 or 16384) provide better security but require more CPU time.

### Debugging

Check the PostgreSQL logs for authentication details:

```bash
tail -f /var/log/postgresql/postgresql-*.log
```

Verify user configuration:

```sql
SELECT rolname, rolcanlogin, rolpassword
FROM pg_authid
WHERE rolname = 'your_username';
```

## Security Recommendations

1. **Use high iteration counts**: 4096 is the minimum; consider 8192 or higher for sensitive applications.
2. **Enforce SCRAM-SHA-256**: Disable MD5 authentication entirely when possible.
3. **Regular password rotation**: Implement password rotation policies.
4. **Monitor authentication**: Log and monitor authentication attempts.

## References

- [PostgreSQL SCRAM-SHA-256 Documentation](https://www.postgresql.org/docs/current/auth-password.html)
- [RFC 7677: SCRAM-SHA-256 and SCRAM-SHA-256-PLUS](https://tools.ietf.org/html/rfc7677)
- [PostgreSQL Security Best Practices](https://www.postgresql.org/docs/current/auth-methods.html)
18 changes: 16 additions & 2 deletions libraries/sql/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ def pg_role_encrypted_password(name)
authid&.to_a&.pop&.fetch('rolpassword')
end

def escape_password_for_sql(password)
return password if nil_or_empty?(password)

# SCRAM-SHA-256 passwords contain $ characters that can be interpreted
# by shell or string processing. Escape them to prevent mangling.
if password.start_with?('SCRAM-SHA-256')
password.gsub('\\', '\\\\').gsub("'", "''").gsub('$', '\\$')
else
password.gsub("'", "''")
end
end

def role_sql(new_resource)
sql = []

Expand All @@ -80,7 +92,8 @@ def role_sql(new_resource)
sql.push("CONNECTION LIMIT #{new_resource.connection_limit}")

if new_resource.encrypted_password
sql.push("ENCRYPTED PASSWORD '#{new_resource.encrypted_password}'")
escaped_password = escape_password_for_sql(new_resource.encrypted_password)
sql.push("ENCRYPTED PASSWORD '#{escaped_password}'")
elsif new_resource.unencrypted_password
sql.push("PASSWORD '#{new_resource.unencrypted_password}'")
else
Expand Down Expand Up @@ -121,7 +134,8 @@ def update_role_password(new_resource)
sql.push("ALTER ROLE \"#{new_resource.rolename}\"")

if new_resource.encrypted_password
sql.push("ENCRYPTED PASSWORD '#{new_resource.encrypted_password}'")
escaped_password = escape_password_for_sql(new_resource.encrypted_password)
sql.push("ENCRYPTED PASSWORD '#{escaped_password}'")
elsif new_resource.unencrypted_password
sql.push("PASSWORD '#{new_resource.unencrypted_password}'")
else
Expand Down
Loading
Loading