Lab 11: Cloud Database Security โ Neon Branching & Connection Hardening¶
Course: SCIA-340 ยท Secure Databases ยท Frostburg State University Week: 11 | Difficulty: โญโญ Intermediate | Time: 60 min
Overview¶
Neon's branching feature mirrors Git branching for databases. Each branch gets its own dedicated compute endpoint with a fully isolated connection string โ meaning your lab-11 branch is completely separate from main at the network level, not just at the schema level. In this lab you will:
- Inspect your current Neon branch via SQL metadata
- Document branch configuration in a registry table
- Verify and reason about Neon's always-on SSL/TLS enforcement
- Audit active sessions and connection limits
- Map the cloud shared-responsibility model into a SQL table
- Audit all login roles for over-privilege using
pg_roles
Neon Connection
Connect to your lab-11 branch endpoint. Your connection string looks like:
Open a psql session or use the Neon SQL Editor before starting.
Work on branch lab-11
All work in this lab must be done on the lab-11 branch, not main. Create the branch in the Neon console (Branches โ New Branch) before running any SQL. Dr. Chen will connect to this branch to verify your work.
Part 1 โ Neon Branch Architecture¶
Step 1.1 โ Understand Branches via SQL Metadata¶
Every Neon branch has its own compute endpoint. When you connect, PostgreSQL metadata reflects the current branch's compute node. Run the following to capture your branch's identity:
-- Each Neon branch has its own endpoint (visible as app_name)
SELECT
current_database() AS database,
current_user AS connected_as,
inet_server_addr() AS server_ip,
inet_server_port() AS port,
pg_postmaster_start_time() AS server_started;
What to observe: The
server_ipandserver_startedvalues will differ between branches even in the same Neon project, confirming compute-level isolation.
๐ธ Screenshot checkpoint โ Step 1.1: Capture the full result showing database, connected_as, server_ip, port, and server_started.
Step 1.2 โ Track Branch Configuration¶
Create a registry table that documents each branch's security posture and purpose:
CREATE TABLE IF NOT EXISTS public.branch_registry (
branch_name TEXT PRIMARY KEY,
purpose TEXT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
ssl_enforced BOOLEAN DEFAULT TRUE,
max_conns INT DEFAULT 10,
environment TEXT NOT NULL
CHECK (environment IN ('prod','staging','dev','test'))
);
INSERT INTO branch_registry VALUES
('main', 'Production database', NOW(), TRUE, 100, 'prod'),
('lab-11', 'Lab 11 branch', NOW(), TRUE, 10, 'dev'),
('staging', 'Pre-production testing', NOW(), TRUE, 50, 'staging')
ON CONFLICT (branch_name) DO NOTHING;
SELECT * FROM branch_registry ORDER BY environment;
Note:
ssl_enforced = TRUEfor every row reflects Neon's architecture โ TLS is non-negotiable on all branch endpoints.
๐ธ Screenshot checkpoint โ Step 1.2: Capture the result showing all three branch rows ordered by environment.
Part 2 โ SSL and Connection Security¶
Step 2.1 โ Verify Neon SSL (Always On)¶
Neon mandates TLS on every connection. Use pg_stat_ssl to confirm the cipher and protocol version for your current session:
-- Neon always uses SSL; verify TLS version and cipher
SELECT
ssl,
version,
cipher,
bits,
client_dn
FROM pg_stat_ssl
WHERE pid = pg_backend_pid();
Expected:
ssl = true,versionshowing TLS 1.3 or 1.2, a strong cipher suite (e.g.,TLS_AES_256_GCM_SHA384), andbits >= 128.
๐ธ Screenshot checkpoint โ Step 2.1: Capture the SSL verification row showing ssl, version, cipher, and bits.
Step 2.2 โ Connection Limit Enforcement¶
Check how many connections are currently open against the database's configured maximum:
-- Check current connection count vs limit
SELECT
datname,
numbackends AS current_connections,
(SELECT setting::INT FROM pg_settings
WHERE name = 'max_connections') AS max_connections
FROM pg_stat_database
WHERE datname = current_database();
Why this matters: Neon's serverless compute imposes connection limits per branch. Exhausting connections is a denial-of-service vector; understanding this limit is part of secure capacity planning.
Step 2.3 โ Active Sessions Audit¶
Audit all active sessions on this branch (excluding your own) to detect unexpected connections:
SELECT
pid,
usename,
application_name,
client_addr,
state,
wait_event_type,
query_start,
LEFT(query, 80) AS current_query
FROM pg_stat_activity
WHERE datname = current_database()
AND pid != pg_backend_pid()
ORDER BY query_start DESC NULLS LAST;
In a real environment: Any unexpected
client_addrorusenamehere is a red flag that warrants immediate investigation.
๐ธ Screenshot checkpoint โ Step 2.3: Capture the active sessions result (even if it returns 0 rows, that is expected for a lab branch).
Part 3 โ Neon-Specific Security Features¶
Step 3.1 โ Shared Responsibility Model Documentation¶
Cloud databases operate under a shared responsibility model. Create a controls table documenting who is responsible for each security layer:
CREATE TABLE IF NOT EXISTS public.cloud_security_controls (
control_id TEXT PRIMARY KEY,
category TEXT NOT NULL,
responsible TEXT NOT NULL
CHECK (responsible IN ('Neon','Customer','Shared')),
description TEXT NOT NULL,
implemented BOOLEAN DEFAULT FALSE
);
INSERT INTO cloud_security_controls VALUES
('NEON-01', 'Physical Security', 'Neon', 'Data center physical access control', TRUE),
('NEON-02', 'Infrastructure', 'Neon', 'Hypervisor and OS patching', TRUE),
('NEON-03', 'Network', 'Neon', 'DDoS protection and network isolation', TRUE),
('NEON-04', 'Encryption in transit', 'Neon', 'TLS enforcement on all connections', TRUE),
('CUST-01', 'Authentication', 'Customer', 'Role passwords and connection limits', FALSE),
('CUST-02', 'Access Control', 'Customer', 'GRANT/REVOKE, RLS, schema privileges', FALSE),
('CUST-03', 'Encryption at rest', 'Customer', 'Column-level encryption with pgcrypto', FALSE),
('CUST-04', 'Auditing', 'Customer', 'Audit triggers and log monitoring', FALSE),
('CUST-05', 'Branch isolation', 'Customer', 'Use separate branches per environment', FALSE),
('SHR-01', 'Monitoring', 'Shared', 'Neon provides metrics; customer monitors app queries', FALSE)
ON CONFLICT (control_id) DO NOTHING;
SELECT
responsible,
COUNT(*) AS control_count,
SUM(CASE WHEN implemented THEN 1 ELSE 0 END) AS implemented
FROM cloud_security_controls
GROUP BY responsible
ORDER BY responsible;
๐ธ Screenshot checkpoint โ Step 3.1: Capture the grouped result showing Neon vs. Customer vs. Shared control counts and implementation status.
Step 3.2 โ Mark Customer Controls as Implemented¶
Based on the controls you implemented in Labs 4โ10, update the registry to reflect completed work:
UPDATE public.cloud_security_controls
SET implemented = TRUE
WHERE responsible = 'Customer'
AND control_id IN ('CUST-01','CUST-02','CUST-03','CUST-04');
-- Verify implementation status
SELECT
control_id,
category,
responsible,
implemented
FROM public.cloud_security_controls
ORDER BY responsible, control_id;
๐ธ Screenshot checkpoint โ Step 3.2: Capture the full controls list showing updated implemented values for CUST-01 through CUST-04.
Part 4 โ Principle of Least Privilege for Neon Roles¶
Step 4.1 โ Audit All Login Roles for Over-Privilege¶
Query pg_roles to surface any login roles with dangerous privilege flags. Every flag marked DANGER or RISK requires justification:
SELECT
rolname,
CASE WHEN rolsuper
THEN 'DANGER: superuser' ELSE 'ok' END AS superuser_check,
CASE WHEN rolcreaterole
THEN 'RISK: can create roles' ELSE 'ok' END AS createrole_check,
CASE WHEN rolcreatedb
THEN 'RISK: can create dbs' ELSE 'ok' END AS createdb_check,
rolconnlimit,
rolvaliduntil
FROM pg_roles
WHERE rolcanlogin = TRUE
AND rolname NOT LIKE 'pg_%'
ORDER BY rolname;
Best practice: Application login roles should show
okfor all three checks, have a positiverolconnlimit, and have arolvaliduntildate set. In Neon, the project owner role will appear here โ note its flags.
๐ธ Screenshot checkpoint โ Step 4.1: Capture the full login role security audit table.
Assessment¶
Verification Script¶
Run this script and submit the output. Dr. Chen will also run it on your lab-11 branch.
-- VERIFY LAB 11
SELECT
-- Branch registry exists with entries
(SELECT COUNT(*) FROM public.branch_registry)::INT
AS branch_entries,
-- Shared responsibility table filled
(SELECT COUNT(*) FROM public.cloud_security_controls)::INT
AS control_count,
-- Customer controls marked implemented
(SELECT COUNT(*) FROM public.cloud_security_controls
WHERE responsible = 'Customer' AND implemented = TRUE)::INT
AS customer_controls_implemented,
-- SSL is active (Neon always on)
(SELECT ssl FROM pg_stat_ssl WHERE pid = pg_backend_pid())
AS ssl_active;
Expected results:
| branch_entries | control_count | customer_controls_implemented | ssl_active |
|---|---|---|---|
| โฅ 3 | 10 | โฅ 4 | true |
Additional Requirement (20 pts)¶
Implement a connection policy checker for your database. Create the following function:
CREATE OR REPLACE FUNCTION public.check_connection_policy()
RETURNS TABLE(
policy_name TEXT,
status TEXT,
detail TEXT
) AS $$
BEGIN
-- Check 1: password encryption method is scram-sha-256
RETURN QUERY
SELECT
'Password Encryption'::TEXT,
CASE WHEN setting = 'scram-sha-256' THEN 'PASS'
WHEN setting = 'md5' THEN 'FAIL'
ELSE 'WARN'
END::TEXT,
('Current setting: ' || setting)::TEXT
FROM pg_settings WHERE name = 'password_encryption';
-- Check 2: current connection uses SSL
RETURN QUERY
SELECT
'SSL Enforcement'::TEXT,
CASE WHEN ssl THEN 'PASS' ELSE 'FAIL' END::TEXT,
CASE WHEN ssl
THEN ('TLS active โ cipher: ' || cipher)
ELSE 'Connection is NOT encrypted'
END::TEXT
FROM pg_stat_ssl WHERE pid = pg_backend_pid();
-- Check 3: no login role (except postgres) is superuser
RETURN QUERY
SELECT
'No Unexpected Superusers'::TEXT,
CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END::TEXT,
CASE WHEN COUNT(*) = 0
THEN 'No unexpected superuser login roles found'
ELSE COUNT(*)::TEXT || ' superuser login role(s) found: '
|| STRING_AGG(rolname, ', ')
END::TEXT
FROM pg_roles
WHERE rolcanlogin = TRUE
AND rolsuper = TRUE
AND rolname NOT IN ('postgres')
AND rolname NOT LIKE 'pg_%';
-- Check 4: no login role has unlimited connections (rolconnlimit = -1)
RETURN QUERY
SELECT
'Connection Limits Set'::TEXT,
CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'WARN' END::TEXT,
CASE WHEN COUNT(*) = 0
THEN 'All login roles have explicit connection limits'
ELSE COUNT(*)::TEXT || ' role(s) with unlimited connections: '
|| STRING_AGG(rolname, ', ')
END::TEXT
FROM pg_roles
WHERE rolcanlogin = TRUE
AND rolconnlimit = -1
AND rolname NOT LIKE 'pg_%';
END;
$$ LANGUAGE plpgsql;
-- Run the policy check
SELECT * FROM public.check_connection_policy();
Submit a screenshot of SELECT * FROM public.check_connection_policy() showing all four checks and their PASS/WARN/FAIL status with detail strings.
Reflection Questions¶
Reflection (answer in your lab submission)
-
Branch vs. Schema isolation: Neon's branching means each lab has its own isolated compute endpoint. How does this differ from just having separate schemas in one database? What security boundaries does a branch provide that a schema does not?
-
Shared responsibility and credential leakage: The shared responsibility model shows that Neon handles physical security, network, and infrastructure โ but the customer is responsible for authentication, access control, and encryption. A data breach occurs because a customer left their Neon project credentials in a public GitHub repository. Who is responsible: Neon or the customer? Justify your answer with reference to the
cloud_security_controlstable you built. -
Scale-to-zero and session state: Neon is serverless โ compute scales to zero when inactive and spins back up on the next connection. What security implication does "scale to zero" have for audit logs and in-memory state (such as prepared statements or session variables set with
SET LOCAL)? How does this affect security designs that rely on persistent session state?
Grading Rubric
| Component | Points |
|---|---|
| Lab steps completed (Parts 1โ4, all screenshots) | 50 |
| Verification script โ all expected values correct | 30 |
Additional requirement (check_connection_policy) | 20 |
| Total | 100 |