From 2a962a9c809a245ae689a610189c90080818c8cd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 04:58:02 +0900 Subject: [PATCH 1/9] test(state): require bounded runtime principal mapping --- tests/postgres_runtime_principal_mapping.rs | 279 ++++++++++++++++++++ 1 file changed, 279 insertions(+) create mode 100644 tests/postgres_runtime_principal_mapping.rs diff --git a/tests/postgres_runtime_principal_mapping.rs b/tests/postgres_runtime_principal_mapping.rs new file mode 100644 index 00000000..546d307f --- /dev/null +++ b/tests/postgres_runtime_principal_mapping.rs @@ -0,0 +1,279 @@ +use std::process::{Command, Output, Stdio}; +use std::thread; +use std::time::{Duration, Instant}; + +const POSTGRES_IMAGE: &str = "postgres:18.4-bookworm"; +const MIGRATION_ENTRYPOINT_IN_CONTAINER: &str = + "/wardnet/deploy/postgresql/reputation_state_migrate.sql"; +const ROLE_INSTALLER_IN_CONTAINER: &str = + "/wardnet/deploy/postgresql/reputation_state_roles.sql"; +const PRINCIPAL_MAPPER_IN_CONTAINER: &str = + "/wardnet/deploy/postgresql/reputation_state_runtime_principal.sql"; +const FINAL_STARTUP_MARKER: &str = "PostgreSQL init process complete; ready for start up."; + +struct PostgresContainer { + name: String, +} + +impl Drop for PostgresContainer { + fn drop(&mut self) { + let _ = Command::new("docker") + .args(["rm", "-f", &self.name]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +fn run_docker(args: &[&str]) -> Output { + Command::new("docker") + .args(args) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .expect("docker command must finish") +} + +fn assert_success(output: Output, context: &str) -> String { + if !output.status.success() { + panic!( + "{context} failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + } + String::from_utf8(output.stdout).expect("command output must be UTF-8") +} + +fn psql(container: &PostgresContainer, sql: &str) -> Output { + run_docker(&[ + "exec", + &container.name, + "psql", + "-X", + "-q", + "-A", + "-t", + "-v", + "ON_ERROR_STOP=1", + "-U", + "postgres", + "-d", + "postgres", + "-c", + sql, + ]) +} + +fn psql_file(container: &PostgresContainer, path: &str) -> Output { + run_docker(&[ + "exec", + &container.name, + "psql", + "-X", + "-q", + "-A", + "-t", + "-v", + "ON_ERROR_STOP=1", + "-U", + "postgres", + "-d", + "postgres", + "-f", + path, + ]) +} + +fn map_runtime_principal(container: &PostgresContainer, principal: &str) -> Output { + let variable = format!("wardnet_runtime_principal={principal}"); + run_docker(&[ + "exec", + &container.name, + "psql", + "-X", + "-q", + "-A", + "-t", + "-v", + "ON_ERROR_STOP=1", + "-v", + &variable, + "-U", + "postgres", + "-d", + "postgres", + "-f", + PRINCIPAL_MAPPER_IN_CONTAINER, + ]) +} + +fn start_postgres() -> Option { + let available = Command::new("docker") + .arg("version") + .arg("--format") + .arg("{{.Server.Version}}") + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()); + if !available { + if std::env::var_os("CI").is_some() { + panic!("Docker is required for PostgreSQL integration tests in CI"); + } + eprintln!("skipping PostgreSQL integration test because Docker is unavailable"); + return None; + } + + let name = format!("wardnet-postgres-runtime-principal-{}", std::process::id()); + assert_success( + run_docker(&[ + "run", + "--rm", + "-d", + "--name", + &name, + "-e", + "POSTGRES_HOST_AUTH_METHOD=trust", + POSTGRES_IMAGE, + ]), + "start PostgreSQL 18.4 container", + ); + let container = PostgresContainer { name }; + let deadline = Instant::now() + Duration::from_secs(60); + loop { + let ready = Command::new("docker") + .args([ + "exec", + &container.name, + "pg_isready", + "-U", + "postgres", + "-d", + "postgres", + ]) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status() + .is_ok_and(|status| status.success()); + let logs = run_docker(&["logs", &container.name]); + let started = logs.status.success() + && (String::from_utf8_lossy(&logs.stdout).contains(FINAL_STARTUP_MARKER) + || String::from_utf8_lossy(&logs.stderr).contains(FINAL_STARTUP_MARKER)); + if ready && started { + return Some(container); + } + assert!( + Instant::now() < deadline, + "PostgreSQL 18.4 final server did not become ready within 60 seconds" + ); + thread::sleep(Duration::from_millis(500)); + } +} + +fn stage_deployment_tree(container: &PostgresContainer) { + assert_success( + run_docker(&["exec", &container.name, "mkdir", "-p", "/wardnet"]), + "create deployment fixture root", + ); + let destination = format!("{}:/wardnet/", container.name); + assert_success( + run_docker(&["cp", "migrations", &destination]), + "stage canonical migrations", + ); + assert_success( + run_docker(&["cp", "deploy", &destination]), + "stage deployment artifacts", + ); +} + +#[test] +fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_safe() { + let Some(container) = start_postgres() else { + return; + }; + stage_deployment_tree(&container); + assert_success( + psql_file(&container, MIGRATION_ENTRYPOINT_IN_CONTAINER), + "migrate empty database to current supported schema", + ); + assert_success( + psql_file(&container, ROLE_INSTALLER_IN_CONTAINER), + "install Wardnet capability roles", + ); + + assert_success( + psql( + &container, + "CREATE ROLE wardnet_app LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION;", + ), + "create externally managed synthetic application login", + ); + let before = assert_success( + psql( + &container, + "SELECT concat_ws(':', pg_has_role('wardnet_app', 'wardnet_runtime', 'member'), pg_has_role('wardnet_app', 'wardnet_state_owner', 'member'), has_table_privilege('wardnet_app', 'public.reputation_source_generation', 'SELECT'), has_table_privilege('wardnet_app', 'public.reputation_source_generation', 'INSERT'), has_function_privilege('wardnet_app', 'public.wardnet_publish_reputation_source_generation(text,text,text,text,bigint,bigint,text,text,text,text)', 'EXECUTE'), has_function_privilege('wardnet_app', 'public.wardnet_admit_reputation_source_generation(text,text,text,bigint,bigint,text)', 'EXECUTE'));", + ), + "inspect unmapped application login", + ); + assert_eq!(before.trim(), "false:false:false:false:false:false"); + + assert_success( + map_runtime_principal(&container, "wardnet_app"), + "map externally managed application login to bounded runtime capability", + ); + assert_success( + map_runtime_principal(&container, "wardnet_app"), + "replay application principal mapping idempotently", + ); + let after = assert_success( + psql( + &container, + "SELECT concat_ws(':', pg_has_role('wardnet_app', 'wardnet_runtime', 'member'), pg_has_role('wardnet_app', 'wardnet_state_owner', 'member'), has_table_privilege('wardnet_app', 'public.reputation_source_generation', 'SELECT'), has_table_privilege('wardnet_app', 'public.reputation_source_generation', 'INSERT'), has_function_privilege('wardnet_app', 'public.wardnet_publish_reputation_source_generation(text,text,text,text,bigint,bigint,text,text,text,text)', 'EXECUTE'), has_function_privilege('wardnet_app', 'public.wardnet_admit_reputation_source_generation(text,text,text,bigint,bigint,text)', 'EXECUTE'), (SELECT count(*) FROM pg_catalog.pg_auth_members membership JOIN pg_catalog.pg_roles granted_role ON granted_role.oid = membership.roleid JOIN pg_catalog.pg_roles member_role ON member_role.oid = membership.member WHERE granted_role.rolname = 'wardnet_runtime' AND member_role.rolname = 'wardnet_app'));", + ), + "inspect mapped application login", + ); + assert_eq!(after.trim(), "true:false:true:false:true:false:1"); + + assert_success( + psql( + &container, + "CREATE ROLE wardnet_privileged LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT BYPASSRLS NOREPLICATION;", + ), + "create privileged synthetic login", + ); + let privileged = map_runtime_principal(&container, "wardnet_privileged"); + assert!( + !privileged.status.success(), + "BYPASSRLS principal must fail closed rather than inherit Wardnet runtime authority" + ); + let privileged_membership = assert_success( + psql( + &container, + "SELECT pg_has_role('wardnet_privileged', 'wardnet_runtime', 'member');", + ), + "inspect rejected privileged principal", + ); + assert_eq!(privileged_membership.trim(), "false"); + + let hostile = "wardnet_hostile; CREATE ROLE wardnet_injected"; + assert_success( + psql( + &container, + "CREATE ROLE \"wardnet_hostile; CREATE ROLE wardnet_injected\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION;", + ), + "create synthetic hostile-name login", + ); + assert_success( + map_runtime_principal(&container, hostile), + "map hostile-name principal without SQL injection", + ); + let hostile_result = assert_success( + psql( + &container, + "SELECT concat_ws(':', pg_has_role('wardnet_hostile; CREATE ROLE wardnet_injected', 'wardnet_runtime', 'member'), to_regrole('wardnet_injected') IS NULL);", + ), + "inspect hostile-name mapping", + ); + assert_eq!(hostile_result.trim(), "true:true"); +} From c7e958b2232c25a8f993e4ba41d1cebe0ffa0af4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:00:20 +0900 Subject: [PATCH 2/9] test(state): align PostgreSQL boolean wire form --- tests/postgres_runtime_principal_mapping.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/postgres_runtime_principal_mapping.rs b/tests/postgres_runtime_principal_mapping.rs index 546d307f..bd4283dd 100644 --- a/tests/postgres_runtime_principal_mapping.rs +++ b/tests/postgres_runtime_principal_mapping.rs @@ -216,7 +216,7 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ ), "inspect unmapped application login", ); - assert_eq!(before.trim(), "false:false:false:false:false:false"); + assert_eq!(before.trim(), "f:f:f:f:f:f"); assert_success( map_runtime_principal(&container, "wardnet_app"), @@ -233,7 +233,7 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ ), "inspect mapped application login", ); - assert_eq!(after.trim(), "true:false:true:false:true:false:1"); + assert_eq!(after.trim(), "t:f:t:f:t:f:1"); assert_success( psql( @@ -254,7 +254,7 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ ), "inspect rejected privileged principal", ); - assert_eq!(privileged_membership.trim(), "false"); + assert_eq!(privileged_membership.trim(), "f"); let hostile = "wardnet_hostile; CREATE ROLE wardnet_injected"; assert_success( @@ -275,5 +275,5 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ ), "inspect hostile-name mapping", ); - assert_eq!(hostile_result.trim(), "true:true"); + assert_eq!(hostile_result.trim(), "t:t"); } From c678273addaa98594fc9282a213dbc589874739b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:01:48 +0900 Subject: [PATCH 3/9] fix(state): bind external runtime principal safely --- .../reputation_state_runtime_principal.sql | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 deploy/postgresql/reputation_state_runtime_principal.sql diff --git a/deploy/postgresql/reputation_state_runtime_principal.sql b/deploy/postgresql/reputation_state_runtime_principal.sql new file mode 100644 index 00000000..41e97c7c --- /dev/null +++ b/deploy/postgresql/reputation_state_runtime_principal.sql @@ -0,0 +1,138 @@ +-- Map one externally managed PostgreSQL LOGIN identity to Wardnet's existing +-- least-privilege runtime capability role. +-- +-- This artifact deliberately does not CREATE ROLE, set passwords, own schema +-- objects, or enable the application PostgreSQL adapter. Deployment/IAM owns +-- login lifecycle and credentials; Wardnet owns only the bounded membership +-- edge required by its reputation-state repository boundary. +-- +-- Usage: +-- psql -v ON_ERROR_STOP=1 \ +-- -v wardnet_runtime_principal='wardnet_app' \ +-- -f deploy/postgresql/reputation_state_runtime_principal.sql +\set ON_ERROR_STOP on + +\if :{?wardnet_runtime_principal} +\else +DO $wardnet_missing_runtime_principal$ +BEGIN + RAISE EXCEPTION 'Wardnet runtime principal mapping requires wardnet_runtime_principal.'; +END +$wardnet_missing_runtime_principal$; +\endif + +BEGIN; + +-- The capability roles are installed by reputation_state_roles.sql. Refuse to +-- grant through drifted or nested capability roles: an unexpected membership +-- can import authority that this mapper cannot safely characterize. +SELECT + count(*) = 2 + AND bool_and(NOT rolcanlogin) + AND bool_and(NOT rolsuper) + AND bool_and(NOT rolcreatedb) + AND bool_and(NOT rolcreaterole) + AND bool_and(NOT rolinherit) + AND bool_and(NOT rolreplication) + AND bool_and(NOT rolbypassrls) + AND NOT EXISTS ( + SELECT 1 + FROM pg_catalog.pg_auth_members membership + JOIN pg_catalog.pg_roles member_role + ON member_role.oid = membership.member + WHERE member_role.rolname IN ('wardnet_runtime', 'wardnet_state_owner') + ) AS capability_roles_safe +FROM pg_catalog.pg_roles +WHERE rolname IN ('wardnet_runtime', 'wardnet_state_owner') +\gset + +\if :capability_roles_safe +\else +DO $wardnet_unsafe_capability_roles$ +BEGIN + RAISE EXCEPTION 'Wardnet runtime principal mapping refused: capability-role attributes or memberships are unsafe.'; +END +$wardnet_unsafe_capability_roles$; +\endif + +-- The supplied login must already exist and remain an ordinary inheriting +-- application identity. Special PostgreSQL role attributes are not inherited, +-- but SET-capable membership in an elevated role could still acquire them, so +-- reject any direct or indirect privileged-role membership as well as any +-- access to Wardnet's state-owner role. +SELECT + count(*) = 1 + AND bool_and(rolcanlogin) + AND bool_and(NOT rolsuper) + AND bool_and(NOT rolcreatedb) + AND bool_and(NOT rolcreaterole) + AND bool_and(rolinherit) + AND bool_and(NOT rolreplication) + AND bool_and(NOT rolbypassrls) + AND NOT pg_catalog.pg_has_role( + :'wardnet_runtime_principal', + 'wardnet_state_owner', + 'MEMBER' + ) + AND NOT EXISTS ( + SELECT 1 + FROM pg_catalog.pg_roles elevated_role + WHERE ( + elevated_role.rolsuper + OR elevated_role.rolcreatedb + OR elevated_role.rolcreaterole + OR elevated_role.rolreplication + OR elevated_role.rolbypassrls + ) + AND pg_catalog.pg_has_role( + :'wardnet_runtime_principal', + elevated_role.oid, + 'MEMBER' + ) + ) AS runtime_principal_safe +FROM pg_catalog.pg_roles +WHERE rolname = :'wardnet_runtime_principal' +\gset + +\if :runtime_principal_safe +\else +DO $wardnet_unsafe_runtime_principal$ +BEGIN + RAISE EXCEPTION 'Wardnet runtime principal mapping refused: principal is absent, non-login, non-inheriting, privileged, or state-owner capable.'; +END +$wardnet_unsafe_runtime_principal$; +\endif + +-- format(%I) is the sole conversion from the supplied identity string to a SQL +-- identifier. INHERIT exposes only wardnet_runtime object privileges; SET FALSE +-- prevents the login from changing current_user to the capability role, and +-- ADMIN FALSE prevents delegation of the capability to another principal. +SELECT pg_catalog.format( + 'GRANT wardnet_runtime TO %I WITH ADMIN FALSE, INHERIT TRUE, SET FALSE', + :'wardnet_runtime_principal' +) +\gexec + +SELECT + count(*) = 1 + AND bool_and(NOT membership.admin_option) + AND bool_and(membership.inherit_option) + AND bool_and(NOT membership.set_option) AS runtime_mapping_complete +FROM pg_catalog.pg_auth_members membership +JOIN pg_catalog.pg_roles granted_role + ON granted_role.oid = membership.roleid +JOIN pg_catalog.pg_roles member_role + ON member_role.oid = membership.member +WHERE granted_role.rolname = 'wardnet_runtime' + AND member_role.rolname = :'wardnet_runtime_principal' +\gset + +\if :runtime_mapping_complete + COMMIT; +\else +DO $wardnet_runtime_mapping_failed$ +BEGIN + RAISE EXCEPTION 'Wardnet runtime principal mapping refused: bounded membership postcondition is incomplete.'; +END +$wardnet_runtime_mapping_failed$; +\endif From d66e4e8652b15f9cb82cd760cc6aeccbd5f75a6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:03:17 +0900 Subject: [PATCH 4/9] fix(state): format runtime principal regression --- tests/postgres_runtime_principal_mapping.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/postgres_runtime_principal_mapping.rs b/tests/postgres_runtime_principal_mapping.rs index bd4283dd..3808ed5d 100644 --- a/tests/postgres_runtime_principal_mapping.rs +++ b/tests/postgres_runtime_principal_mapping.rs @@ -5,8 +5,7 @@ use std::time::{Duration, Instant}; const POSTGRES_IMAGE: &str = "postgres:18.4-bookworm"; const MIGRATION_ENTRYPOINT_IN_CONTAINER: &str = "/wardnet/deploy/postgresql/reputation_state_migrate.sql"; -const ROLE_INSTALLER_IN_CONTAINER: &str = - "/wardnet/deploy/postgresql/reputation_state_roles.sql"; +const ROLE_INSTALLER_IN_CONTAINER: &str = "/wardnet/deploy/postgresql/reputation_state_roles.sql"; const PRINCIPAL_MAPPER_IN_CONTAINER: &str = "/wardnet/deploy/postgresql/reputation_state_runtime_principal.sql"; const FINAL_STARTUP_MARKER: &str = "PostgreSQL init process complete; ready for start up."; From eec8b567c03f9376122509348d26712c4da8a2ba Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:15:43 +0900 Subject: [PATCH 5/9] test(state): reject shadow runtime membership paths --- tests/postgres_runtime_principal_mapping.rs | 53 ++++++++++++++++----- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/tests/postgres_runtime_principal_mapping.rs b/tests/postgres_runtime_principal_mapping.rs index 3808ed5d..42805943 100644 --- a/tests/postgres_runtime_principal_mapping.rs +++ b/tests/postgres_runtime_principal_mapping.rs @@ -255,24 +255,53 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ ); assert_eq!(privileged_membership.trim(), "f"); - let hostile = "wardnet_hostile; CREATE ROLE wardnet_injected"; assert_success( psql( &container, - "CREATE ROLE \"wardnet_hostile; CREATE ROLE wardnet_injected\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION;", + "CREATE ROLE wardnet_runtime_delegate NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION; GRANT wardnet_runtime TO wardnet_runtime_delegate WITH ADMIN TRUE, INHERIT TRUE, SET TRUE; CREATE ROLE wardnet_shadowed LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION; GRANT wardnet_runtime_delegate TO wardnet_shadowed WITH ADMIN FALSE, INHERIT TRUE, SET TRUE;", ), - "create synthetic hostile-name login", + "create a shadow runtime-membership path outside the bounded mapper", ); - assert_success( - map_runtime_principal(&container, hostile), - "map hostile-name principal without SQL injection", - ); - let hostile_result = assert_success( + let shadowed_before = assert_success( psql( &container, - "SELECT concat_ws(':', pg_has_role('wardnet_hostile; CREATE ROLE wardnet_injected', 'wardnet_runtime', 'member'), to_regrole('wardnet_injected') IS NULL);", + "SELECT concat_ws(':', pg_has_role('wardnet_shadowed', 'wardnet_runtime', 'member'), (SELECT count(*) FROM pg_catalog.pg_auth_members membership JOIN pg_catalog.pg_roles granted_role ON granted_role.oid = membership.roleid JOIN pg_catalog.pg_roles member_role ON member_role.oid = membership.member WHERE granted_role.rolname = 'wardnet_runtime' AND member_role.rolname = 'wardnet_shadowed'));", ), - "inspect hostile-name mapping", + "inspect shadowed runtime principal before mapping", ); - assert_eq!(hostile_result.trim(), "t:t"); -} + assert_eq!(shadowed_before.trim(), "t:0"); ++ let shadowed = map_runtime_principal(&container, "wardnet_shadowed"); ++ assert!( ++ !shadowed.status.success(), ++ "principal with an inherited runtime path must fail closed instead of gaining a second direct mapping" ++ ); ++ let shadowed_after = assert_success( ++ psql( ++ &container, ++ "SELECT count(*) FROM pg_catalog.pg_auth_members membership JOIN pg_catalog.pg_roles granted_role ON granted_role.oid = membership.roleid JOIN pg_catalog.pg_roles member_role ON member_role.oid = membership.member WHERE granted_role.rolname = 'wardnet_runtime' AND member_role.rolname = 'wardnet_shadowed';", ++ ), ++ "inspect rejected shadow runtime principal", ++ ); ++ assert_eq!(shadowed_after.trim(), "0"); ++ ++ let hostile = "wardnet_hostile; CREATE ROLE wardnet_injected"; ++ assert_success( ++ psql( ++ &container, ++ "CREATE ROLE \"wardnet_hostile; CREATE ROLE wardnet_injected\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION;", ++ ), ++ "create synthetic hostile-name login", ++ ); ++ assert_success( ++ map_runtime_principal(&container, hostile), ++ "map hostile-name principal without SQL injection", ++ ); ++ let hostile_result = assert_success( ++ psql( ++ &container, ++ "SELECT concat_ws(':', pg_has_role('wardnet_hostile; CREATE ROLE wardnet_injected', 'wardnet_runtime', 'member'), to_regrole('wardnet_injected') IS NULL);", ++ ), ++ "inspect hostile-name mapping", ++ ); ++ assert_eq!(hostile_result.trim(), "t:t"); ++} From 32485204874cf80dc06de897066b961c9d31ee7c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:16:29 +0900 Subject: [PATCH 6/9] test(state): format shadow runtime path regression --- tests/postgres_runtime_principal_mapping.rs | 70 ++++++++++----------- 1 file changed, 35 insertions(+), 35 deletions(-) diff --git a/tests/postgres_runtime_principal_mapping.rs b/tests/postgres_runtime_principal_mapping.rs index 42805943..a8ed6ed0 100644 --- a/tests/postgres_runtime_principal_mapping.rs +++ b/tests/postgres_runtime_principal_mapping.rs @@ -270,38 +270,38 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ "inspect shadowed runtime principal before mapping", ); assert_eq!(shadowed_before.trim(), "t:0"); -+ let shadowed = map_runtime_principal(&container, "wardnet_shadowed"); -+ assert!( -+ !shadowed.status.success(), -+ "principal with an inherited runtime path must fail closed instead of gaining a second direct mapping" -+ ); -+ let shadowed_after = assert_success( -+ psql( -+ &container, -+ "SELECT count(*) FROM pg_catalog.pg_auth_members membership JOIN pg_catalog.pg_roles granted_role ON granted_role.oid = membership.roleid JOIN pg_catalog.pg_roles member_role ON member_role.oid = membership.member WHERE granted_role.rolname = 'wardnet_runtime' AND member_role.rolname = 'wardnet_shadowed';", -+ ), -+ "inspect rejected shadow runtime principal", -+ ); -+ assert_eq!(shadowed_after.trim(), "0"); -+ -+ let hostile = "wardnet_hostile; CREATE ROLE wardnet_injected"; -+ assert_success( -+ psql( -+ &container, -+ "CREATE ROLE \"wardnet_hostile; CREATE ROLE wardnet_injected\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION;", -+ ), -+ "create synthetic hostile-name login", -+ ); -+ assert_success( -+ map_runtime_principal(&container, hostile), -+ "map hostile-name principal without SQL injection", -+ ); -+ let hostile_result = assert_success( -+ psql( -+ &container, -+ "SELECT concat_ws(':', pg_has_role('wardnet_hostile; CREATE ROLE wardnet_injected', 'wardnet_runtime', 'member'), to_regrole('wardnet_injected') IS NULL);", -+ ), -+ "inspect hostile-name mapping", -+ ); -+ assert_eq!(hostile_result.trim(), "t:t"); -+} + let shadowed = map_runtime_principal(&container, "wardnet_shadowed"); + assert!( + !shadowed.status.success(), + "principal with an inherited runtime path must fail closed instead of gaining a second direct mapping" + ); + let shadowed_after = assert_success( + psql( + &container, + "SELECT count(*) FROM pg_catalog.pg_auth_members membership JOIN pg_catalog.pg_roles granted_role ON granted_role.oid = membership.roleid JOIN pg_catalog.pg_roles member_role ON member_role.oid = membership.member WHERE granted_role.rolname = 'wardnet_runtime' AND member_role.rolname = 'wardnet_shadowed';", + ), + "inspect rejected shadow runtime principal", + ); + assert_eq!(shadowed_after.trim(), "0"); + + let hostile = "wardnet_hostile; CREATE ROLE wardnet_injected"; + assert_success( + psql( + &container, + "CREATE ROLE \"wardnet_hostile; CREATE ROLE wardnet_injected\" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION;", + ), + "create synthetic hostile-name login", + ); + assert_success( + map_runtime_principal(&container, hostile), + "map hostile-name principal without SQL injection", + ); + let hostile_result = assert_success( + psql( + &container, + "SELECT concat_ws(':', pg_has_role('wardnet_hostile; CREATE ROLE wardnet_injected', 'wardnet_runtime', 'member'), to_regrole('wardnet_injected') IS NULL);", + ), + "inspect hostile-name mapping", + ); + assert_eq!(hostile_result.trim(), "t:t"); +} From 6c3158d6644f13efd21bd57e8218880127dedf96 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:20:12 +0900 Subject: [PATCH 7/9] fix(state): reject shadow runtime membership paths --- .../reputation_state_runtime_principal.sql | 46 ++++++++++++++++++- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/deploy/postgresql/reputation_state_runtime_principal.sql b/deploy/postgresql/reputation_state_runtime_principal.sql index 41e97c7c..8109adac 100644 --- a/deploy/postgresql/reputation_state_runtime_principal.sql +++ b/deploy/postgresql/reputation_state_runtime_principal.sql @@ -59,7 +59,10 @@ $wardnet_unsafe_capability_roles$; -- application identity. Special PostgreSQL role attributes are not inherited, -- but SET-capable membership in an elevated role could still acquire them, so -- reject any direct or indirect privileged-role membership as well as any --- access to Wardnet's state-owner role. +-- access to Wardnet's state-owner role. Runtime membership is valid only when +-- absent (first mapping) or already present as the mapper's exact bounded direct +-- edge (idempotent replay); any alternate role path to wardnet_runtime is +-- outside this artifact's authority and therefore fails closed. SELECT count(*) = 1 AND bool_and(rolcanlogin) @@ -89,6 +92,45 @@ SELECT elevated_role.oid, 'MEMBER' ) + ) + AND ( + SELECT + count(*) = 0 + OR ( + count(*) = 1 + AND bool_and(NOT membership.admin_option) + AND bool_and(membership.inherit_option) + AND bool_and(NOT membership.set_option) + ) + FROM pg_catalog.pg_auth_members membership + JOIN pg_catalog.pg_roles granted_role + ON granted_role.oid = membership.roleid + JOIN pg_catalog.pg_roles member_role + ON member_role.oid = membership.member + WHERE granted_role.rolname = 'wardnet_runtime' + AND member_role.rolname = :'wardnet_runtime_principal' + ) + AND NOT EXISTS ( + WITH RECURSIVE alternate_memberships(roleid) AS ( + SELECT membership.roleid + FROM pg_catalog.pg_auth_members membership + JOIN pg_catalog.pg_roles member_role + ON member_role.oid = membership.member + JOIN pg_catalog.pg_roles granted_role + ON granted_role.oid = membership.roleid + WHERE member_role.rolname = :'wardnet_runtime_principal' + AND granted_role.rolname <> 'wardnet_runtime' + UNION + SELECT membership.roleid + FROM pg_catalog.pg_auth_members membership + JOIN alternate_memberships inherited + ON inherited.roleid = membership.member + ) + SELECT 1 + FROM alternate_memberships inherited + JOIN pg_catalog.pg_roles inherited_role + ON inherited_role.oid = inherited.roleid + WHERE inherited_role.rolname = 'wardnet_runtime' ) AS runtime_principal_safe FROM pg_catalog.pg_roles WHERE rolname = :'wardnet_runtime_principal' @@ -98,7 +140,7 @@ WHERE rolname = :'wardnet_runtime_principal' \else DO $wardnet_unsafe_runtime_principal$ BEGIN - RAISE EXCEPTION 'Wardnet runtime principal mapping refused: principal is absent, non-login, non-inheriting, privileged, or state-owner capable.'; + RAISE EXCEPTION 'Wardnet runtime principal mapping refused: principal is absent, non-login, non-inheriting, privileged, state-owner capable, or already has an unbounded runtime membership path.'; END $wardnet_unsafe_runtime_principal$; \endif From 6f43c8627bad7b3bfa381e34745038b9034875f7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:30:23 +0900 Subject: [PATCH 8/9] test(state): reject out-of-band Wardnet state authority --- tests/postgres_runtime_principal_mapping.rs | 29 +++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/postgres_runtime_principal_mapping.rs b/tests/postgres_runtime_principal_mapping.rs index a8ed6ed0..e38c0e36 100644 --- a/tests/postgres_runtime_principal_mapping.rs +++ b/tests/postgres_runtime_principal_mapping.rs @@ -255,6 +255,35 @@ fn runtime_principal_mapping_is_external_identity_least_privilege_and_injection_ ); assert_eq!(privileged_membership.trim(), "f"); + assert_success( + psql( + &container, + "CREATE ROLE wardnet_state_delegate NOLOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION; GRANT INSERT ON TABLE public.reputation_source_generation TO wardnet_state_delegate; GRANT EXECUTE ON FUNCTION public.wardnet_admit_reputation_source_generation(text,text,text,bigint,bigint,text) TO wardnet_state_delegate; CREATE ROLE wardnet_overprivileged LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE INHERIT NOBYPASSRLS NOREPLICATION; GRANT wardnet_state_delegate TO wardnet_overprivileged WITH ADMIN FALSE, INHERIT TRUE, SET TRUE;", + ), + "create an inherited Wardnet state-capability path outside the bounded mapper", + ); + let overprivileged_before = assert_success( + psql( + &container, + "SELECT concat_ws(':', has_table_privilege('wardnet_overprivileged', 'public.reputation_source_generation', 'INSERT'), has_function_privilege('wardnet_overprivileged', 'public.wardnet_admit_reputation_source_generation(text,text,text,bigint,bigint,text)', 'EXECUTE'), pg_has_role('wardnet_overprivileged', 'wardnet_runtime', 'member'));", + ), + "inspect out-of-band Wardnet state authority before mapping", + ); + assert_eq!(overprivileged_before.trim(), "t:t:f"); + let overprivileged = map_runtime_principal(&container, "wardnet_overprivileged"); + assert!( + !overprivileged.status.success(), + "principal with out-of-band Wardnet mutation or inner-admission authority must fail closed" + ); + let overprivileged_after = assert_success( + psql( + &container, + "SELECT pg_has_role('wardnet_overprivileged', 'wardnet_runtime', 'member');", + ), + "inspect rejected overprivileged principal", + ); + assert_eq!(overprivileged_after.trim(), "f"); + assert_success( psql( &container, From e2ff0fe4055a598a5c450e7942fed2051ec21238 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 9 Sep 2026 05:33:41 +0900 Subject: [PATCH 9/9] fix(state): reject preexisting Wardnet mutation authority --- .../reputation_state_runtime_principal.sql | 48 +++++++++++++++++-- 1 file changed, 43 insertions(+), 5 deletions(-) diff --git a/deploy/postgresql/reputation_state_runtime_principal.sql b/deploy/postgresql/reputation_state_runtime_principal.sql index 8109adac..d88a1274 100644 --- a/deploy/postgresql/reputation_state_runtime_principal.sql +++ b/deploy/postgresql/reputation_state_runtime_principal.sql @@ -59,10 +59,13 @@ $wardnet_unsafe_capability_roles$; -- application identity. Special PostgreSQL role attributes are not inherited, -- but SET-capable membership in an elevated role could still acquire them, so -- reject any direct or indirect privileged-role membership as well as any --- access to Wardnet's state-owner role. Runtime membership is valid only when --- absent (first mapping) or already present as the mapper's exact bounded direct --- edge (idempotent replay); any alternate role path to wardnet_runtime is --- outside this artifact's authority and therefore fails closed. +-- access to Wardnet's state-owner role. The principal must also arrive without +-- out-of-band mutation or inner-admission privileges on Wardnet state objects; +-- otherwise adding wardnet_runtime would preserve a wider effective authority +-- than this mapper is allowed to establish. Runtime membership is valid only +-- when absent (first mapping) or already present as the mapper's exact bounded +-- direct edge (idempotent replay); any alternate role path to wardnet_runtime +-- is outside this artifact's authority and therefore fails closed. SELECT count(*) = 1 AND bool_and(rolcanlogin) @@ -93,6 +96,41 @@ SELECT 'MEMBER' ) ) + AND NOT pg_catalog.has_table_privilege( + :'wardnet_runtime_principal', + 'public.reputation_source_generation', + 'INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER' + ) + AND NOT pg_catalog.has_any_column_privilege( + :'wardnet_runtime_principal', + 'public.reputation_source_generation', + 'INSERT,UPDATE,REFERENCES' + ) + AND NOT pg_catalog.has_table_privilege( + :'wardnet_runtime_principal', + 'public.reputation_source_publication', + 'INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER' + ) + AND NOT pg_catalog.has_any_column_privilege( + :'wardnet_runtime_principal', + 'public.reputation_source_publication', + 'INSERT,UPDATE,REFERENCES' + ) + AND NOT pg_catalog.has_table_privilege( + :'wardnet_runtime_principal', + 'public.reputation_source_publication_head', + 'INSERT,UPDATE,DELETE,TRUNCATE,REFERENCES,TRIGGER' + ) + AND NOT pg_catalog.has_any_column_privilege( + :'wardnet_runtime_principal', + 'public.reputation_source_publication_head', + 'INSERT,UPDATE,REFERENCES' + ) + AND NOT pg_catalog.has_function_privilege( + :'wardnet_runtime_principal', + 'public.wardnet_admit_reputation_source_generation(text,text,text,bigint,bigint,text)', + 'EXECUTE' + ) AND ( SELECT count(*) = 0 @@ -140,7 +178,7 @@ WHERE rolname = :'wardnet_runtime_principal' \else DO $wardnet_unsafe_runtime_principal$ BEGIN - RAISE EXCEPTION 'Wardnet runtime principal mapping refused: principal is absent, non-login, non-inheriting, privileged, state-owner capable, or already has an unbounded runtime membership path.'; + RAISE EXCEPTION 'Wardnet runtime principal mapping refused: principal is absent, non-login, non-inheriting, privileged, state-owner capable, already has Wardnet mutation/inner-admission authority, or already has an unbounded runtime membership path.'; END $wardnet_unsafe_runtime_principal$; \endif