Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
```
198 changes: 198 additions & 0 deletions documentation/scram-sha-256.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
# 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

Check failure on line 8 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Lists should be surrounded by blank lines

documentation/scram-sha-256.md:8 MD032/blanks-around-lists Lists should be surrounded by blank lines [Context: "- **Stronger security**: Uses ..."] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md032.md
- **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:
```

Check failure on line 16 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Fenced code blocks should have a language specified

documentation/scram-sha-256.md:16 MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md040.md

Check failure on line 16 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Fenced code blocks should be surrounded by blank lines

documentation/scram-sha-256.md:16 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Specific the code block language as ruby to statisfy markdownlink MD040

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specified code block language as text to satisfy markdownlint MD040. Commit: 623bf6e

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

Example:
```

Check failure on line 21 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Fenced code blocks should have a language specified

documentation/scram-sha-256.md:21 MD040/fenced-code-language Fenced code blocks should have a language specified [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md040.md

Check failure on line 21 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Fenced code blocks should be surrounded by blank lines

documentation/scram-sha-256.md:21 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Specific the code block language as ruby to statisfy markdownlink MD040

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Specified code block language as text to satisfy markdownlint MD040. Commit: 623bf6e

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

Check failure on line 44 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Fenced code blocks should be surrounded by blank lines

documentation/scram-sha-256.md:44 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```ruby"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
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

Check failure on line 53 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Fenced code blocks should be surrounded by blank lines

documentation/scram-sha-256.md:53 MD031/blanks-around-fences Fenced code blocks should be surrounded by blank lines [Context: "```ruby"] https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md031.md
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)

Check failure on line 198 in documentation/scram-sha-256.md

View workflow job for this annotation

GitHub Actions / lint-unit / runner / markdownlint

Files should end with a single newline character

documentation/scram-sha-256.md:198:97 MD047/single-trailing-newline Files should end with a single newline character https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md047.md
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 @@
authid&.to_a&.pop&.fetch('rolpassword')
end

def escape_password_for_sql(password)
return password if password.nil? || password.empty?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Use nil_or_empty? function for this check

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use nil_or_empty? function for the password check as suggested. The change replaces password.nil? || password.empty? with the more concise nil_or_empty?(password). Commit: 9e7b3d4


# 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('$', '\\$')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Update this to escape backslash characters please

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the backslash escaping from excessive \\\\\\\\ (8 backslashes) to proper \\\\ (4 backslashes) for correct SQL escaping. Commit: 6b2f5c7

else
password.gsub("'", "''")
end
end

def role_sql(new_resource)
sql = []

Expand All @@ -80,7 +92,8 @@
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 @@
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
87 changes: 87 additions & 0 deletions spec/libraries/role_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
require 'spec_helper'
require_relative '../../libraries/sql/role'

# Mock the dependencies for testing
module PostgreSQL
module Cookbook
module Utils
def nil_or_empty?(value)
value.nil? || value.empty?
end
end

module SqlHelpers
module Connection
end
end
end
end

class Utils
end

describe 'PostgreSQL::Cookbook::SqlHelpers::Role' do
let(:test_class) do
Class.new do
include PostgreSQL::Cookbook::SqlHelpers::Role
include PostgreSQL::Cookbook::Utils
end
end

let(:instance) { test_class.new }

describe '#escape_password_for_sql' do
context 'with SCRAM-SHA-256 passwords' do
let(:scram_password) { 'SCRAM-SHA-256$4096:27klCUc487uwvJVGKI5YNA==$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4=' }

it 'escapes dollar signs in SCRAM-SHA-256 passwords' do
result = instance.send(:escape_password_for_sql, scram_password)
expect(result).to eq('SCRAM-SHA-256\$4096:27klCUc487uwvJVGKI5YNA==\$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4=')
end

it 'handles SCRAM-SHA-256 passwords with multiple dollar signs' do
password = 'SCRAM-SHA-256$1024:salt$key1$key2'
result = instance.send(:escape_password_for_sql, password)
expect(result).to eq('SCRAM-SHA-256\$1024:salt\$key1\$key2')
end
end

context 'with non-SCRAM passwords' do
it 'does not modify MD5 passwords' do
md5_password = 'md5c5e1324c052bd9e8471c44a3d2bda0c8'
result = instance.send(:escape_password_for_sql, md5_password)
expect(result).to eq(md5_password)
end

it 'does not modify plain text passwords' do
plain_password = 'my$plain$password'
result = instance.send(:escape_password_for_sql, plain_password)
expect(result).to eq(plain_password)
end

it 'does not modify other hash types' do
other_hash = 'sha256$somehash$value'
result = instance.send(:escape_password_for_sql, other_hash)
expect(result).to eq(other_hash)
end
end

context 'with edge cases' do
it 'handles nil passwords' do
result = instance.send(:escape_password_for_sql, nil)
expect(result).to be_nil
end

it 'handles empty passwords' do
result = instance.send(:escape_password_for_sql, '')
expect(result).to eq('')
end

it 'handles passwords that start with SCRAM-SHA-256 but have no dollar signs' do
password = 'SCRAM-SHA-256-invalid'
result = instance.send(:escape_password_for_sql, password)
expect(result).to eq(password)
end
end
end
end
Loading
Loading