-
-
Notifications
You must be signed in to change notification settings - Fork 559
Add automatic SCRAM-SHA-256 password escaping and comprehensive documentation #797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
a7a620f
b7551a6
e86ab3b
3d40e50
f588465
f743322
623bf6e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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
|
||
- **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
|
||
|
||
SCRAM-SHA-256$<iteration_count>:<salt>$<StoredKey>:<ServerKey> | ||
``` | ||
|
||
Example: | ||
``` | ||
Check failure on line 21 in documentation/scram-sha-256.md
|
||
|
||
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
|
||
bmhughes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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
|
||
bmhughes marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -62,6 +62,18 @@ | |
authid&.to_a&.pop&.fetch('rolpassword') | ||
end | ||
|
||
def escape_password_for_sql(password) | ||
return password if password.nil? || password.empty? | ||
|
||
|
||
# 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 = [] | ||
|
||
|
@@ -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 | ||
|
@@ -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 | ||
|
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 |
Uh oh!
There was an error while loading. Please reload this page.