Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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('$', '\\$')
else
password
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