From c962cda57ac6c01c626fa006cf2a094d95cfc9f7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 11 Sep 2025 23:16:12 +0000 Subject: [PATCH 1/6] Add automatic SCRAM-SHA-256 password escaping and comprehensive documentation Co-authored-by: damacus <40786+damacus@users.noreply.github.com> --- README.md | 4 + documentation/postgresql_role.md | 54 +++++ documentation/scram-sha-256.md | 198 ++++++++++++++++++ libraries/sql/role.rb | 18 +- spec/libraries/role_spec.rb | 87 ++++++++ test/cookbooks/test/recipes/access.rb | 7 + .../access/controls/base_access.rb | 11 + 7 files changed, 377 insertions(+), 2 deletions(-) create mode 100644 documentation/scram-sha-256.md create mode 100644 spec/libraries/role_spec.rb diff --git a/README.md b/README.md index cfbd8cf1d..7da2aa7e4 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,10 @@ We follow the currently supported versions listed on :$:` +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 +``` diff --git a/documentation/scram-sha-256.md b/documentation/scram-sha-256.md new file mode 100644 index 000000000..ab26d3798 --- /dev/null +++ b/documentation/scram-sha-256.md @@ -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 +- **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: +``` +SCRAM-SHA-256$:$: +``` + +Example: +``` +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) \ No newline at end of file diff --git a/libraries/sql/role.rb b/libraries/sql/role.rb index 5f100118a..7de4e6b69 100644 --- a/libraries/sql/role.rb +++ b/libraries/sql/role.rb @@ -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 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('$', '\$') + else + password + end + end + def role_sql(new_resource) sql = [] @@ -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 @@ -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 diff --git a/spec/libraries/role_spec.rb b/spec/libraries/role_spec.rb new file mode 100644 index 000000000..fd4b73c66 --- /dev/null +++ b/spec/libraries/role_spec.rb @@ -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 \ No newline at end of file diff --git a/test/cookbooks/test/recipes/access.rb b/test/cookbooks/test/recipes/access.rb index c88ceba07..f52b6be96 100644 --- a/test/cookbooks/test/recipes/access.rb +++ b/test/cookbooks/test/recipes/access.rb @@ -137,3 +137,10 @@ address '127.0.0.1/32' auth_method 'scram-sha-256' end + +# Test SCRAM-SHA-256 password handling with $ characters +postgresql_user 'scram_test_user' do + encrypted_password 'SCRAM-SHA-256$4096:27klCUc487uwvJVGKI5YNA==$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4=' + login true + action [:create] +end diff --git a/test/integration/access/controls/base_access.rb b/test/integration/access/controls/base_access.rb index 224ee27f6..9354f63cd 100644 --- a/test/integration/access/controls/base_access.rb +++ b/test/integration/access/controls/base_access.rb @@ -140,3 +140,14 @@ its('auth_params') { should cmp 'ldapbasedn="dc=example, dc=net" ldapsearchattribute=uid ldapserver=ldap.example.net' } end end + +control 'scram_test_user role should exist' do + impact 1.0 + desc 'The scram_test_user database role should exist to test SCRAM-SHA-256 password handling' + + postgres_access = postgres_session('postgres', '12345', '127.0.0.1') + + describe postgres_access.query('SELECT rolname FROM pg_roles;') do + its('output') { should cmp /scram_test_user/ } + end +end From ae4cbde2fe5db2959d45d5bb60c7e1f2c1c17e7c Mon Sep 17 00:00:00 2001 From: Lance Albertson Date: Fri, 12 Sep 2025 15:35:19 -0700 Subject: [PATCH 2/6] Potential fix for code scanning alert no. 5: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- libraries/sql/role.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/sql/role.rb b/libraries/sql/role.rb index 7de4e6b69..7b43aa914 100644 --- a/libraries/sql/role.rb +++ b/libraries/sql/role.rb @@ -68,7 +68,7 @@ def escape_password_for_sql(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('$', '\$') + password.gsub('\\', '\\\\\\\\').gsub('$', '\\$') else password end From 3eafb714a0d645b02821a06f8c23b59b72e6207c Mon Sep 17 00:00:00 2001 From: Ben Hughes Date: Sun, 14 Sep 2025 18:16:05 +0100 Subject: [PATCH 3/6] Potential fix for code scanning alert no. 6: Incomplete string escaping or encoding Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> --- libraries/sql/role.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/sql/role.rb b/libraries/sql/role.rb index 7b43aa914..12109b6a3 100644 --- a/libraries/sql/role.rb +++ b/libraries/sql/role.rb @@ -68,9 +68,9 @@ def escape_password_for_sql(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('$', '\\$') + password.gsub('\\', '\\\\\\\\').gsub("'", "''").gsub('$', '\\$') else - password + password.gsub("'", "''") end end From e4937c811abd48f77fea3ed59e5ad5f7bda1e656 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 17:26:05 +0000 Subject: [PATCH 4/6] Use nil_or_empty? function for password check in escape_password_for_sql Co-authored-by: bmhughes <23269128+bmhughes@users.noreply.github.com> --- libraries/sql/role.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/sql/role.rb b/libraries/sql/role.rb index 12109b6a3..4db52626e 100644 --- a/libraries/sql/role.rb +++ b/libraries/sql/role.rb @@ -63,7 +63,7 @@ def pg_role_encrypted_password(name) end def escape_password_for_sql(password) - return password if password.nil? || password.empty? + 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. From 42b40e4d6d4697cae3160366a8e7dc53d07a7622 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 17:40:10 +0000 Subject: [PATCH 5/6] Fix backslash escaping in SCRAM-SHA-256 password handling Changed from excessive \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ (8 backslashes) to proper \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ (4 backslashes) for SQL escaping. Each backslash in the original password is now correctly escaped as \\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ (2 backslashes) in the result. Co-authored-by: bmhughes <23269128+bmhughes@users.noreply.github.com> --- libraries/sql/role.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/sql/role.rb b/libraries/sql/role.rb index 4db52626e..402094ed6 100644 --- a/libraries/sql/role.rb +++ b/libraries/sql/role.rb @@ -68,7 +68,7 @@ def escape_password_for_sql(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('$', '\\$') + password.gsub('\\', '\\\\').gsub("'", "''").gsub('$', '\\$') else password.gsub("'", "''") end From d55d38e37919eb57887f3fdd70a306788c80cf6c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 14 Sep 2025 17:44:02 +0000 Subject: [PATCH 6/6] Fix markdownlint issues in SCRAM-SHA-256 documentation Co-authored-by: bmhughes <23269128+bmhughes@users.noreply.github.com> --- documentation/scram-sha-256.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/documentation/scram-sha-256.md b/documentation/scram-sha-256.md index ab26d3798..92bc9b721 100644 --- a/documentation/scram-sha-256.md +++ b/documentation/scram-sha-256.md @@ -5,6 +5,7 @@ SCRAM-SHA-256 (Salted Challenge Response Authentication Mechanism) is a password ## 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 @@ -13,12 +14,14 @@ SCRAM-SHA-256 authentication offers several advantages: ## Password Format SCRAM-SHA-256 passwords have this specific format: -``` + +```text SCRAM-SHA-256$:$: ``` Example: -``` + +```text SCRAM-SHA-256$4096:27klCUc487uwvJVGKI5YNA==$6K2Y+S3YBlpfRNrLROoO2ulWmnrQoRlGI1GqpNRq0T0=:y4esBVjK/hMtxDB5aWN4ynS1SnQcT1TFTqV0J/snls4= ``` @@ -41,6 +44,7 @@ end 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 @@ -50,6 +54,7 @@ end ``` **Now (automatic escaping):** + ```ruby postgresql_role 'user1' do # No manual escaping needed @@ -195,4 +200,4 @@ WHERE rolname = 'your_username'; - [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) \ No newline at end of file +- [PostgreSQL Security Best Practices](https://www.postgresql.org/docs/current/auth-methods.html)