Comprehensive Guide to Row-Level Security and Encryption at Rest in PostgreSQL

  • Reading time:11 mins read
  • Post category:HOW TO

Recently, I gave a presentation on database security, focusing on essential aspects such as users, roles, permissions, Row-Level Security (RLS), and Column-Level Security (CLS). Inspired by that experience, I thought of putting together a comprehensive blog post that deeply explores these crucial elements.

This post will provide a concise overview of how to implement a robust security model that addresses the following key elements:

  • User Roles and Permissions: Establishing distinct roles to control access and operations.
  • Row-Level Security (RLS): Customizing data access based on user roles.
  • Column-Level Security (CLS): Protecting sensitive data (e.g., salary, phone number) through encryption.
  • Auditing and Compliance: Utilizing tools like pgAudit to track actions on employee records and ensure adherence to data governance policies.

This post assumes that readers have a basic understanding of users, roles, and privileges in PostgreSQL. While the demonstration may not adhere strictly to best practices, it aims to illustrate how these components work together to create a secure database environment.

Let’s delve into the key requirements and strategies for achieving these objectives.

We will explore these concepts through the example of Pghyd, illustrating how they work together to enhance data security and privacy in PostgreSQL.

Problem Objective:

Implement a database security model for the Pghyd organization to manage employee data while ensuring data privacy and compliance.

Key Requirements:

User Roles and Permissions:

  1. pghyd_viewer: Limited access to specific columns (e.g., cannot see salary or phone number).
  2. pghyd_core: Full access to all data and operations.
  3. pwi_core: Manage PWI-specific records with full access except for delete.
  4. pwi_regular: Read-only access to PWI records, with no modification rights.

Data Privacy and Security:

  1. Implement Row-Level Security (RLS) to restrict data access based on roles.
  2. Prevent certain roles from deleting records to maintain data integrity.

Column-Level Security:

  1. Protect sensitive information (salary, phone number) using encryption and restrict access to specific roles.

Auditing and Compliance with pgAudit:

Ensure that all actions on employee records comply with data governance policies, with tracking of access for audits.

Implementation:

Step 1: Create the Database

First, we create a new database named pghyd_db to hold our employee data. After creating the database,

CREATE DATABASE pghyd_db;

\c pghyd_db;

we connect to it and define the pghyd_employees table to store employee information, including sensitive details like salary and phone numbers.

CREATE TABLE pghyd_employees ( 
id SERIAL PRIMARY KEY, 
name VARCHAR(50), 
department VARCHAR(50), 
salary NUMERIC, 
organization VARCHAR(50), 
finalized BOOLEAN, 
phone_number BYTEA, 
CONSTRAINT phone_number_encrypt CHECK (phone_number IS NOT NULL) );

Next, we insert sample data into the pghyd_employees table, the data has sensitive information, so we need to install pgcrypto before inserting data.

Using the pgp_sym_encrypt function from the pgcrypto extension to encrypt the phone numbers.

-- Create extension pgcrypto 
CREATE extension IF NOT EXISTS pgcrypto;

-- Insert sample data INSERT INTO pghyd_employees (name, department, salary, organization, finalized, phone_number) VALUES ('Frank', 'Engineering', 85000, 'PWI', FALSE, pgp_sym_encrypt('8888888888', 'mysecretpass')), ('Grace', 'HR', 65000, 'PGHYD', TRUE, pgp_sym_encrypt('9999999999', 'mysecretpass')), ('Heidi', 'Marketing', 72000, 'PWI', FALSE, pgp_sym_encrypt('1231231234', 'mysecretpass')), ('Ivan', 'Marketing', 68000, 'PGHYD', TRUE, pgp_sym_encrypt('2342342345', 'mysecretpass')), ('Judy', 'Finance', 92000, 'PWI', FALSE, pgp_sym_encrypt('3453453456', 'mysecretpass')), ('Mallory', 'Engineering', 78000, 'PWI', TRUE, pgp_sym_encrypt('4564564567', 'mysecretpass')), ('Niaj', 'HR', 62000, 'PGHYD', FALSE, pgp_sym_encrypt('5675675678', 'mysecretpass')), ('Olivia', 'Finance', 82000, 'PGHYD', TRUE, pgp_sym_encrypt('6786786789', 'mysecretpass')), ('Peggy', 'Engineering', 80000, 'PWI', FALSE, pgp_sym_encrypt('7897897890', 'mysecretpass')), ('Trent', 'Marketing', 71000, 'PGHYD', TRUE, pgp_sym_encrypt('8908908901', 'mysecretpass'));

Step 2: Create Roles

Now, we create several roles to define different levels of access within the organization:

-- Create roles

CREATE ROLE pghyd_viewer;
CREATE ROLE pghyd_core;
CREATE ROLE pwi_core;
CREATE ROLE pwi_regular;

Step 3: Grant Permissions to Roles

-- Grant full access to pghyd_core and pwi_core roles
GRANT SELECT, INSERT, UPDATE, DELETE ON pghyd_employees TO pghyd_core;
GRANT SELECT, INSERT, UPDATE, DELETE ON pghyd_employees TO pwi_core;

-- Grant SELECT access to pwi_regular role GRANT SELECT ON pghyd_employees TO pwi_regular;
-- Grant SELECT access on few columns to pghyd_viewer GRANT SELECT (name, department,organization, finalized) ON pghyd_employees TO pghyd_viewer;

Step 4: Enable Row-Level Security

To further enhance security, we enable Row-Level Security (RLS) on the pghyd_employees table:

-- Enable row-level security on the table
ALTER TABLE pghyd_employees ENABLE ROW LEVEL SECURITY;

Step 5: Create Row-Level Security Policies

-- pghyd_core: Full access to all data

CREATE POLICY pghyd_core_access ON pghyd_employees
FOR ALL
TO pghyd_core
USING (true);
-- pwi_core: Full access to PWI data CREATE POLICY pwi_core_access ON pghyd_employees FOR ALL TO pwi_core USING (organization = 'PWI'); -- Assuming organization is used to filter PWI data
-- pwi_regular: Select access to PWI data CREATE POLICY pwi_regular_access ON pghyd_employees FOR SELECT TO pwi_regular USING (organization = 'PWI'); -- Restricting to only PWI data
-- pwi_core: Cannot delete PWI data CREATE POLICY restrict_deletes_pwi_core ON pghyd_employees AS RESTRICTIVE FOR DELETE TO pwi_core USING (FALSE);

Testing:

Set Up pgAudit for auditing (Optional)

-- Create pgaudit extension

dnf install pgaudit_17
shared_preload_libraries='pgaudit'
create extension pgaudit;
alter system set pgaudit.log='all';

1. Testing Access for pghyd_viewer User

SET ROLE pghyd_viewer;
SELECT name, department, organization, finalized, phone_number FROM pghyd_employees;

Expected Outcome: pghyd_viewer cannot access the data because the table is enabled with RLS.

pghyd_db=#
pghyd_db=# SET ROLE pghyd_viewer;
SET
pghyd_db=> SELECT name, department, organization, finalized, phone_number FROM pghyd_employees;
ERROR: permission denied for table pghyd_employees
pghyd_db=>

Apply BYPASSRLS as postgres user  to pghyd_viewer

\c - postgres
ALTER USER pghyd_viewer BYPASSRLS;

Check again

SET ROLE pghyd_viewer;
SELECT name, department, organization, finalized, phone_number FROM pghyd_employees;

Expected Outcome: The query should exclude the salary and phone_number columns, as the pghyd_viewer role has limited access to specific columns.

pghyd_db=# set role pghyd_viewer;
SET
pghyd_db=> SELECT name, department, organization, finalized, phone_number FROM pghyd_employees;
ERROR: permission denied for table pghyd_employees

Check Limited Access:

Expected Outcome: You should be able to view the data.

pghyd_db=> SELECT name, department, organization, finalized FROM pghyd_employees;
  name   | department  | organization | finalized
---------+-------------+--------------+-----------
 Frank   | Engineering | PWI          | f
 Grace   | HR          | PGHYD        | t
 Heidi   | Marketing   | PWI          | f
 Ivan    | Marketing   | PGHYD        | t
 Judy    | Finance     | PWI          | f
 Mallory | Engineering | PWI          | t
 Niaj    | HR          | PGHYD        | f
 Olivia  | Finance     | PGHYD        | t
 Peggy   | Engineering | PWI          | f
 Trent   | Marketing   | PGHYD        | t
(10 rows)

2. Testing Access for pwi_regular User

SET ROLE pwi_regular;
SELECT id, name, department, organization, finalized
FROM pghyd_employees;

Expected Outcome: The user should only see PWI records, ensuring that the row-level security policy restricts access based on the organization. You cannot update/delete the data

pghyd_db=> SET ROLE pwi_regular; SELECT id, name, department, organization, finalized FROM pghyd_employees;
SET
 id |  name   | department  | organization | finalized
----+---------+-------------+--------------+-----------
  3 | Heidi   | Marketing   | PWI          | f
  5 | Judy    | Finance     | PWI          | f
  6 | Mallory | Engineering | PWI          | t
  9 | Peggy   | Engineering | PWI          | f
  1 | Frank   | Engineering | PWI          | f
(5 rows)

pghyd_db=>

3. Testing Access for pwi_core User

SET ROLE pwi_core;

SELECT id, name, department, salary, organization, finalized, pgp_sym_decrypt(phone_number, 'mysecretpass') AS decrypted_phone_number FROM pghyd_employees;
-- you can update the data UPDATE pghyd_employees SET salary = 1000 WHERE id = 1;
-- You cannot delete the data DELETE FROM pghyd_employees WHERE id = 1;

Expected Outcome: You should be able to view PWI data, update it but cannot delete

pghyd_db=>
pghyd_db=>  SET ROLE pwi_core;
SET
pghyd_db=> SELECT id, name, department, salary, organization, finalized, pgp_sym_decrypt(phone_number, 'mysecretpass') AS decrypted_phone_number FROM pghyd_employees;
 id |  name   | department  | salary | organization | finalized | decrypted_phone_number
----+---------+-------------+--------+--------------+-----------+------------------------
  3 | Heidi   | Marketing   |  72000 | PWI          | f         | 1231231234
  5 | Judy    | Finance     |  92000 | PWI          | f         | 3453453456
  6 | Mallory | Engineering |  78000 | PWI          | t         | 4564564567
  9 | Peggy   | Engineering |  80000 | PWI          | f         | 7897897890
  1 | Frank   | Engineering |   1000 | PWI          | f         | 8888888888
(5 rows)

pghyd_db=> UPDATE pghyd_employees SET salary = 1000 WHERE id = 1;
UPDATE 1
pghyd_db=> DELETE FROM pghyd_employees WHERE id = 1;
DELETE 0
pghyd_db=>

4. Final Testing for pghyd_core User

SET ROLE pghyd_core;
SELECT id, name, department, salary, organization, finalized, pgp_sym_decrypt(phone_number, 'mysecretpass') AS decrypted_phone_number FROM pghyd_employees;

-- you can update the data UPDATE pghyd_employees SET salary = 1000 WHERE id = 1;
-- You cannot delete the data DELETE FROM pghyd_employees WHERE id = 1;

Expected Outcome: You can do everything.

pghyd_db=> SET ROLE pghyd_core;
SET
pghyd_db=> SELECT id, name, department, salary, organization, finalized, pgp_sym_decrypt(phone_number, 'mysecretpass') AS decrypted_phone_number FROM pghyd_employees;
 id |  name   | department  | salary | organization | finalized | decrypted_phone_number
----+---------+-------------+--------+--------------+-----------+------------------------
  2 | Grace   | HR          |  65000 | PGHYD        | t         | 9999999999
  3 | Heidi   | Marketing   |  72000 | PWI          | f         | 1231231234
  4 | Ivan    | Marketing   |  68000 | PGHYD        | t         | 2342342345
  5 | Judy    | Finance     |  92000 | PWI          | f         | 3453453456
  6 | Mallory | Engineering |  78000 | PWI          | t         | 4564564567
  7 | Niaj    | HR          |  62000 | PGHYD        | f         | 5675675678
  8 | Olivia  | Finance     |  82000 | PGHYD        | t         | 6786786789
  9 | Peggy   | Engineering |  80000 | PWI          | f         | 7897897890
 10 | Trent   | Marketing   |  71000 | PGHYD        | t         | 8908908901
  1 | Frank   | Engineering |   1000 | PWI          | f         | 8888888888
(10 rows)

pghyd_db=>

Handy PostgreSQL Commands for Security and Privilege Management

List users

\du

Check Role Privileges

SELECT 
    r.rolname, 
    r.rolsuper, 
    r.rolinherit, 
    r.rolcreaterole, 
    r.rolcreatedb, 
    r.rolcanlogin, 
    r.rolreplication
FROM 
    pg_roles r;

Check Table-Level Privileges

SELECT 
    grantee, 
    table_schema, 
    table_name, 
    privilege_type
FROM 
    information_schema.role_table_grants
WHERE 
    table_name = 'pghyd_employees';

Check Column-Level Privileges

SELECT 
    grantee, 
    table_schema, 
    table_name, 
    column_name, 
    privilege_type
FROM 
    information_schema.column_privileges
WHERE 
    table_name = 'pghyd_employees';

Check Row-Level Security (RLS) Policies

SELECT 
    * 
FROM 
    pg_policies;

Check Default Privileges

SELECT 
    defaclrole::regrole, 
    defaclnamespace::regnamespace, 
    defaclobjtype, 
    defaclacl
FROM 
    pg_default_acl
WHERE 
    defaclrole = 'pghyd_core'::regrole;

Check Database-Level Privileges

SELECT 
    grantee, 
    privilege_type 
FROM 
    information_schema.role_usage_grants 
WHERE 
    table_catalog = 'pghyd_db';  -- Replace with your database name

Check Schema-Level Privileges

SELECT 
    grantee, 
    schema_name, 
    privilege_type 
FROM 
    information_schema.role_schema_grants;

Conclusion

In this blog, we explored the implementation of a comprehensive database security model for the Pghyd organization, focusing on user roles, Row-Level Security (RLS), and data privacy.

As we move forward, securing data in transit becomes equally critical. In the next blog post, we will discuss how to implement Secure Sockets Layer (SSL) to protect data during transmission, ensuring that sensitive information remains confidential and secure across networks. Stay tuned!