Part 4: The Enigma of Complexity – Taming Passwords with pgtle (Part 2)
Or, “SQL-Powered Password Fortresses: Building Beyond ‘Password123’!”
Welcome back, brave coders and database defenders! In Part 1: The Guardians of the Gate – Unmasking PCI DSS Password Rules, we deciphered the ancient scrolls of PCI DSS v4.0.1 password rules. Then, in Part 2: The Environment Setup – Preparing for pgtle Glory, we meticulously prepared our PostgreSQL environment and successfully completed our pgtle installation. Finally, Part 3: The Enigma of Complexity – Taming Passwords with pgtle (Part 1) laid out our grand strategy for tackling password complexity.
If you’re here to conquer the entire saga, a true hero’s journey, then make sure to check out our Series Overview Page for all the other exciting chapters and quick-reference guides. This series is your ultimate resource for achieving PostgreSQL PCI DSS compliance.
Now, with our pgtle extension standing ready in our PostgreSQL fortress and our battle plan for password complexity understood, it’s time for the main event: deploying the code! This installment of The Epic Quest for Secure Passwords is all about getting your hands dirty with PL/pgSQL and transforming those theoretical PCI DSS password complexity requirements into active, in-database enforcement. Get ready to witness your PostgreSQL database actively rejecting weak passwords with the sternness of a grumpy wizard, enhancing your overall PostgreSQL password security!
Before We Code: The Essential pgtle Setup Ritual (Again!)
Before you unleash the code, ensure your PostgreSQL environment is properly configured for pgtle‘s powerful password validation capabilities. Think of these as the pre-flight checks before launching your security rocket! This pgtle setup is crucial for robust database security.
Once these pgtle setup steps are complete, you’re ready to deploy your custom pgtle extension and its powerful PostgreSQL password policy enforcement!
The Grand Unveiling: Our pci_password_check_rules Extension Code – Version 0.1
Our journey begins with pci_password_check_rules_0.1.sql. This script represents our initial foray into pgtle-powered password validation. It’s a “proof of concept” that focuses solely on enforcing the core PCI DSS 8.3.6 complexity rules for standard users. It does not yet include the sophisticated profiles logic for role separation. Think of it as building a basic, but functional, laser blaster before you upgrade to a lightsaber. It ensures your passwords meet the minimum length and character type requirements.
The full SQL code for this version is available in a separate document: pci_password_check_rules_0.1.sql – Initial PCI DSS Password Complexity Code.
SELECT
PGTLE.INSTALL_EXTENSION (
'pci_password_check_rules', -- Name of your custom pg_tle extension
'0.1', -- Incremented version for non-human account handling
'Enforces PCI DSS 4.0.1 password complexity', -- Description of the extension
$_pgtle_$
CREATE SCHEMA IF NOT EXISTS password_check;
REVOKE ALL ON SCHEMA password_check FROM PUBLIC;
GRANT USAGE ON SCHEMA password_check TO PUBLIC;
CREATE OR REPLACE FUNCTION password_check.passcheck_hook(
username TEXT,
password TEXT,
password_type pgtle.password_types,
valid_until TIMESTAMPTZ,
valid_null BOOLEAN
) RETURNS VOID AS $_FUNCTION_$ -- ADDED THIS BLOCK LABEL
DECLARE
invalid_pw_reason TEXT := '';
current_min_length INTEGER := 12;
current_require_special_char BOOLEAN := TRUE;
current_require_uppercase BOOLEAN := TRUE;
current_require_lowercase BOOLEAN := TRUE;
current_require_digit BOOLEAN := TRUE;
BEGIN
--- 1. Apply Password Complexity Checks (PCI DSS 8.3.6) ---
IF length(password) < current_min_length THEN
invalid_pw_reason := invalid_pw_reason || 'Password must be at least ' || current_min_length || ' characters long. ';
END IF;
IF current_require_uppercase AND password !~ '[A-Z]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one uppercase letter. ';
END IF;
IF current_require_lowercase AND password !~ '[a-z]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one lowercase letter. ';
END IF;
IF current_require_digit AND password !~ '[0-9]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one number. ';
END IF;
IF current_require_special_char AND password !~ '[^a-zA-Z0-9\s]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one special character. ';
END IF;
--- 2. Final Check and Raise Exception / Update History ---
IF invalid_pw_reason != '' THEN
RAISE EXCEPTION 'Password validation failed for user %: %', username, invalid_pw_reason;
END IF;
END;
$_FUNCTION_$ LANGUAGE plpgsql;
REVOKE ALL ON FUNCTION password_check.passcheck_hook(TEXT, TEXT, pgtle.password_types, TIMESTAMPTZ, BOOLEAN) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION password_check.passcheck_hook(TEXT, TEXT, pgtle.password_types, TIMESTAMPTZ, BOOLEAN) TO PUBLIC;
SELECT pgtle.register_feature_if_not_exists('password_check.passcheck_hook', 'passcheck');
$_pgtle_$
);
Logic Explained: pci_password_check_rules_0.1.sql
This script uses pgtle.install_extension to package our custom logic into a pg_tle extension. Here’s a breakdown of its key components and their purpose:
Installation: Unleashing Version 0.1!
Ready to make your PostgreSQL database a bastion of password strength? Follow these steps to install our shiny new pci_password_check_rules extension (Version 0.1):
Version 0.2: The Evolution – Role-Based Policies
Now, let’s level up! PCI DSS v4.0.1 (especially 8.6.3) distinguishes between human and non-human accounts, often requiring even stronger policies for the latter. This version 0.2 update introduces role-based password policies, allowing you to define different rules for different user groups. It’s like giving your Jedi Masters a different, more powerful lightsaber than your standard troopers.
This update path handles:
The full SQL code for this update path is available in a separate document: pci_password_check_rules_0.2_up_0.1-0.2.sql – Role-Based Password Policies Update Code.
SELECT
PGTLE.INSTALL_UPDATE_PATH (
'pci_password_check_rules', -- Name of your custom pg_tle extension
'0.1',
'0.2', -- Incremented version to add role separation for non-human account handling
$_pgtle_$
--Check if the ROLES exists. It's a requirement that the roles exists in order to proceed.
--pci_admin_users,pci_app_users,pci_standard_users
do
$$
declare
v_arr_rolnames text ARRAY;
v_rolname text;
begin
v_arr_rolnames := '{"pci_admin_users","pci_app_users","pci_standard_users"}';
FOREACH v_rolname IN ARRAY v_arr_rolnames
LOOP
if not exists (SELECT 1 FROM pg_catalog.pg_roles r WHERE r.rolname = v_rolname) then
RAISE NOTICE 'Role % does not exist, creating it.', v_rolname;
EXECUTE format($e$
CREATE ROLE %1$s
$e$, v_rolname);
end if;
END LOOP;
end
$$
;
--Create the table to hold the profiles.
--The table must check if the group roles already exists (create a trigger).
--Or maybe the table must not allow any update to its values, and make it fixed.
CREATE TABLE IF NOT EXISTS password_check.profiles (
role TEXT PRIMARY KEY,
min_length INTEGER DEFAULT 15 NOT NULL ,
require_special_char BOOLEAN DEFAULT true NOT NULL,
require_uppercase BOOLEAN DEFAULT true NOT NULL,
require_lowercase BOOLEAN DEFAULT true NOT NULL,
require_digit BOOLEAN DEFAULT true NOT NULL
);
INSERT INTO password_check.profiles (role,min_length) values ('pci_admin_users',15) ON CONFLICT (role) DO NOTHING;
INSERT INTO password_check.profiles (role,min_length) values ('pci_app_users',15) ON CONFLICT (role) DO NOTHING;
INSERT INTO password_check.profiles (role,min_length) values ('pci_standard_users',12) ON CONFLICT (role) DO NOTHING;
INSERT INTO password_check.profiles (role,min_length) values ('pci_new_users',12) ON CONFLICT (role) DO NOTHING;
-- Helper function to check if a given username is a member of a specified PostgreSQL role (group).
CREATE OR REPLACE FUNCTION password_check.is_member_of_role(
target_username TEXT,
group_role_name TEXT
) RETURNS BOOLEAN AS $$
DECLARE
is_member BOOLEAN := FALSE;
BEGIN
SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_roles AS r_member
JOIN pg_catalog.pg_auth_members AS am ON r_member.oid = am.member
JOIN pg_catalog.pg_roles AS r_role ON am.roleid = r_role.oid
WHERE r_member.rolname = target_username
AND r_role.rolname = group_role_name
) INTO is_member;
RETURN is_member;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER; --Executed with the privileges of the user that owns the function.
-- The main passcheck hook function that enforces password policies.
CREATE OR REPLACE FUNCTION password_check.passcheck_hook(
username TEXT,
password TEXT,
password_type pgtle.password_types,
valid_until TIMESTAMPTZ,
valid_null BOOLEAN
) RETURNS VOID AS $_FUNCTION_$ -- ADDED THIS BLOCK LABEL
DECLARE
invalid_pw_reason TEXT := '';
current_min_length INTEGER := 12;
current_require_special_char BOOLEAN := TRUE;
current_require_uppercase BOOLEAN := TRUE;
current_require_lowercase BOOLEAN := TRUE;
current_require_digit BOOLEAN := TRUE;
-- Add on 0.2: Flag to check if the user already exists in pg_roles
-- This helps differentiate between CREATE ROLE and ALTER ROLE.
user_exists BOOLEAN;
BEGIN
-- Check if the user already exists in pg_roles.
-- This helps differentiate between CREATE ROLE and ALTER ROLE.
SELECT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = passcheck_hook.username)
INTO user_exists;
--- 1. Determine Role-Based Policies ---
--- For CREATE ROLE, we allow a default policy. For ALTER ROLE, we enforce role membership.
IF user_exists AND password_check.is_member_of_role(username, 'pci_admin_users') THEN
SELECT
min_length,
require_special_char,
require_uppercase,
require_lowercase,
require_digit
INTO
current_min_length,
current_require_special_char,
current_require_uppercase,
current_require_lowercase,
current_require_digit
FROM
password_check.profiles
WHERE
role='pci_admin_users';
ELSIF user_exists AND password_check.is_member_of_role(username, 'pci_app_users') THEN -- NOW FOR NON-HUMAN APP ACCOUNTS
SELECT
min_length,
require_special_char,
require_uppercase,
require_lowercase,
require_digit
INTO
current_min_length,
current_require_special_char,
current_require_uppercase,
current_require_lowercase,
current_require_digit
FROM
password_check.profiles
WHERE
role='pci_app_users';
ELSIF user_exists AND password_check.is_member_of_role(username, 'pci_standard_users') THEN
SELECT
min_length,
require_special_char,
require_uppercase,
require_lowercase,
require_digit
INTO
current_min_length,
current_require_special_char,
current_require_uppercase,
current_require_lowercase,
current_require_digit
FROM
password_check.profiles
WHERE
role='pci_app_users';
--If the user does not exists (It's a CREATE ROLE), allow it to be created and set the default password rules.
--Later, the user will not be allowed to CHANGE THE PASSWORD if not set to any of the PCI roles.
ELSIF NOT user_exists THEN
SELECT
min_length,
require_special_char,
require_uppercase,
require_lowercase,
require_digit
INTO
current_min_length,
current_require_special_char,
current_require_uppercase,
current_require_lowercase,
current_require_digit
FROM
password_check.profiles
WHERE
role='pci_new_users';
RAISE NOTICE 'Policy: Default for NEW user (CREATE ROLE)';
RAISE NOTICE 'Assign a PCI ROLE to the user IMMEDIATELY';
ELSE
-- If the user exists but does not belong to any defined PCI role, prevent password change.
RAISE EXCEPTION 'Password change not allowed for user %: User must be assigned to one of the defined roles (pci_admin_users, pci_app_users, pci_standard_users).', username;
END IF;
--- 2. Apply Password Complexity Checks (PCI DSS 8.3.6) ---
IF length(password) < current_min_length THEN
invalid_pw_reason := invalid_pw_reason || 'Password must be at least ' || current_min_length || ' characters long. ';
END IF;
IF current_require_uppercase AND password !~ '[A-Z]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one uppercase letter. ';
END IF;
IF current_require_lowercase AND password !~ '[a-z]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one lowercase letter. ';
END IF;
IF current_require_digit AND password !~ '[0-9]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one number. ';
END IF;
IF current_require_special_char AND password !~ '[^a-zA-Z0-9\s]' THEN
invalid_pw_reason := invalid_pw_reason || 'Password must contain at least one special character. ';
END IF;
--- 3. Final Check and Raise Exception / Update History ---
IF invalid_pw_reason != '' THEN
RAISE EXCEPTION 'Password validation failed for user %: %', username, invalid_pw_reason;
END IF;
END;
$_FUNCTION_$ LANGUAGE plpgsql;
REVOKE ALL ON FUNCTION password_check.passcheck_hook(TEXT, TEXT, pgtle.password_types, TIMESTAMPTZ, BOOLEAN) FROM PUBLIC;
GRANT EXECUTE ON FUNCTION password_check.passcheck_hook(TEXT, TEXT, pgtle.password_types, TIMESTAMPTZ, BOOLEAN) TO PUBLIC;
SELECT pgtle.register_feature_if_not_exists('password_check.passcheck_hook', 'passcheck');
$_pgtle_$
);
Logic Explained: pci_password_check_rules_0.2_up_0.1-0.2.sql
This update script builds upon Version 0.1 by introducing a more granular, role-based approach to password policies.
To update your extension to Version 0.2:
Verification: Did It Work, Captain?
After installing your pgtle extension, you can verify its presence and version information using these handy commands. It’s like checking the diagnostics on your starship’s new warp core!
Check available extensions using:
SELECT * FROM pgtle.available_extensions();
Check all available versions for your extension using:
SELECT * FROM pgtle.available_extension_versions();
You should see pci_password_check_rules listed with 0.2 as its default_version. If you encounter any errors during installation, double-check your pgtle setup ritual (especially the pgtle.passcheck_db_name setting and the pg_reload_conf()!).
Check the update paths defined for your specific extension:
SELECT * FROM pgtle.extension_update_paths('pci_password_check_rules');
Testing Your New Fortress!
Now for the fun part: testing! Try creating users and changing passwords to see your new password validation rules in action. This will demonstrate your enhanced PostgreSQL password security and adherence to PCI DSS compliance.
Example 1: Creating a pci_standard_users member with a weak password (should fail):
Try creating a user with a weak password, like:
CREATE ROLE test_standard_user WITH PASSWORD 'short';
Expected error: Password validation failed for user test_standard_user: Password must be at least 12 characters long.
Or with insufficient complexity:
CREATE ROLE another_standard_user WITH PASSWORD 'nopassword';
Expected error: Password validation failed for user another_standard_user: Password must contain at least one uppercase letter. Password must contain at least one number. Password must contain at least one special character.
Example 2: Creating a pci_admin_users member with a strong password (should succeed):
CREATE ROLE test_admin_user WITH PASSWORD 'P@ssw0rdF0rAdm1n!'; -- Should succeed (17 chars, complex)
GRANT pci_admin_users TO test_admin_user; -- Assign to the role
This should succeed (17 chars, complex), demonstrating effective PostgreSQL password policy for administrators.
Example 3: Creating a pci_app_users member with a strong password (should succeed, demonstrating application accounts security):
CREATE ROLE test_app_service WITH PASSWORD 'AppS3rv!ce_P@ssw0rd_2025!'; -- Should succeed (26 chars, complex)
GRANT pci_app_users TO test_app_service; -- Assign to the role
This should succeed (26 chars, complex), showcasing robust service accounts security.
Example 4: Trying to change password for a user not assigned to a PCI role (should fail after initial creation):
Create an unassigned user:
CREATE ROLE unassigned_user WITH PASSWORD 'InitialP@ssw0rd1!';
This succeeds initially (uses pci_new_users profile).
Now try to change its password without assigning it to a PCI role:
ALTER ROLE unassigned_user WITH PASSWORD 'NewP@ssw0rd2!';
Expected error: Password change not allowed for user unassigned_user: User must be assigned to one of the defined PCI roles (pci_admin_users, pci_app_users, pci_standard_users).
Assign it to a role, then try again (should succeed if new password is valid for that role):
GRANT pci_standard_users TO unassigned_user;
ALTER ROLE unassigned_user WITH PASSWORD 'NewP@ssw0rd2!';
This should now succeed if it meets the standard user policy, confirming your dynamic PostgreSQL password policy.
This robust pgtle extension ensures that your PostgreSQL password security is now directly integrated and enforced at the database level, making your system significantly more compliant with PCI DSS v4.0.1. This is a prime example of PostgreSQL security best practices in action!
The Adventure Continues…
Congratulations, brave DBA! You’ve just deployed a powerful pgtle extension that enforces PCI DSS password complexity, even differentiating rules based on user roles. This is a huge leap in securing your PostgreSQL fortress and a major win for PostgreSQL password security!
In the next parts of our saga, we’ll continue to build upon this foundation, tackling other crucial PCI DSS password requirements like reusability, change frequency, account lockout, and more. Stay tuned, because the quest for ultimate PostgreSQL security best practices is far from over!