diff --git a/ansible/files/permission_check.py b/ansible/files/permission_check.py index 22e881f11..8b1dc69cb 100644 --- a/ansible/files/permission_check.py +++ b/ansible/files/permission_check.py @@ -244,6 +244,96 @@ def check_postgresql_mount(): print("postgresql.service mounts /etc as read-only.") +def check_copy_program_blocked(): + """Check that COPY ... PROGRAM is blocked by AppArmor.""" + import psycopg2 + from psycopg2 import sql + + errors = [] + + try: + # Connect to PostgreSQL as the postgres superuser + conn = psycopg2.connect( + host="/var/run/postgresql", database="postgres", user="postgres" + ) + cursor = conn.cursor() + + # Test 1: COPY TO PROGRAM should be blocked + try: + cursor.execute( + "COPY (SELECT 'test') TO PROGRAM 'cat > /tmp/apparmor_copy_test.txt'" + ) + errors.append( + "ERROR: COPY TO PROGRAM was not blocked by AppArmor!\n" + " This is a security vulnerability - postgres should not be able to execute arbitrary programs." + ) + except psycopg2.Error as e: + # Expected to fail - check if it's a permission denied error + error_msg = str(e).lower() + if ( + "permission denied" in error_msg + or "cannot execute" in error_msg + or "could not execute" in error_msg + ): + print("✓ COPY TO PROGRAM correctly blocked by AppArmor") + else: + errors.append( + f"ERROR: COPY TO PROGRAM failed but with unexpected error:\n" + f" {str(e)}\n" + f" Expected permission denied error from AppArmor" + ) + + # Test 2: COPY FROM PROGRAM should be blocked + try: + cursor.execute("CREATE TEMP TABLE apparmor_test (data text)") + cursor.execute("COPY apparmor_test FROM PROGRAM 'echo test'") + errors.append( + "ERROR: COPY FROM PROGRAM was not blocked by AppArmor!\n" + " This is a security vulnerability - postgres should not be able to execute arbitrary programs." + ) + except psycopg2.Error as e: + error_msg = str(e).lower() + if ( + "permission denied" in error_msg + or "cannot execute" in error_msg + or "could not execute" in error_msg + ): + print("✓ COPY FROM PROGRAM correctly blocked by AppArmor") + else: + errors.append( + f"ERROR: COPY FROM PROGRAM failed but with unexpected error:\n" + f" {str(e)}\n" + f" Expected permission denied error from AppArmor" + ) + + # Verify no test files were created + import os + + if os.path.exists("/tmp/apparmor_copy_test.txt"): + errors.append( + "ERROR: Test file /tmp/apparmor_copy_test.txt was created!\n" + " AppArmor did not successfully block COPY TO PROGRAM" + ) + os.remove("/tmp/apparmor_copy_test.txt") + + cursor.close() + conn.close() + + except Exception as e: + errors.append(f"ERROR: Failed to test COPY PROGRAM blocking: {str(e)}") + + if errors: + print("\n" + "=" * 80) + print("COPY PROGRAM BLOCKING ERRORS DETECTED:") + print("=" * 80) + for error in errors: + print(error) + print("=" * 80) + sys.exit(1) + + print("\nCOPY ... PROGRAM is correctly blocked by AppArmor.") + + def check_directory_permissions(): """Check that security-critical directories have the correct permissions.""" errors = [] @@ -385,6 +475,9 @@ def main(): # Check directory permissions for security-critical paths check_directory_permissions() + # Check that COPY ... PROGRAM is blocked by AppArmor + check_copy_program_blocked() + if __name__ == "__main__": main() diff --git a/ansible/vars.yml b/ansible/vars.yml index fd40cbdd2..839d521d1 100644 --- a/ansible/vars.yml +++ b/ansible/vars.yml @@ -10,9 +10,9 @@ postgres_major: # Full version strings for each major version postgres_release: - postgresorioledb-17: "17.5.1.069-orioledb" - postgres17: "17.6.1.048" - postgres15: "15.14.1.048" + postgresorioledb-17: "17.5.1.069-orioledb-copy-1" + postgres17: "17.6.1.048-copy-1" + postgres15: "15.14.1.048-copy-1" # Non Postgres Extensions pgbouncer_release: 1.19.0 diff --git a/ebssurrogate/files/apparmor_profiles/usr.lib.postgresql.bin.postgres b/ebssurrogate/files/apparmor_profiles/usr.lib.postgresql.bin.postgres index 8e2efc3e6..ae69d4644 100644 --- a/ebssurrogate/files/apparmor_profiles/usr.lib.postgresql.bin.postgres +++ b/ebssurrogate/files/apparmor_profiles/usr.lib.postgresql.bin.postgres @@ -14,6 +14,17 @@ capability dac_read_search, deny @{HOME}/** rwx, +# Explicitly deny shell execution to block COPY ... PROGRAM +# This prevents arbitrary command execution via postgres +deny /bin/sh x, +deny /bin/bash x, +deny /bin/dash x, +deny /usr/bin/sh x, +deny /usr/bin/bash x, +deny /usr/bin/dash x, +deny /usr/bin/cat x, +deny /usr/bin/mknod x, + /data/pgdata/** r, /dev/shm rw, /etc/java-11-openjdk/logging.properties r, @@ -26,11 +37,10 @@ deny @{HOME}/** rwx, /etc/timezone r, /etc/wal-g/config.json r, /run/systemd/notify rw, -/usr/bin/cat rix, -/usr/bin/dash rix, -/usr/bin/mknod rix, +# admin-mgr: unrestricted for WAL operations (archive_command/restore_command) /usr/bin/admin-mgr Ux, /usr/lib/postgresql/bin/* mrix, +# wal-g: allowed for backup operations /usr/local/bin/wal-g rix, /usr/local/lib/groonga/plugins/tokenizers/mecab.so mr, /usr/local/lib/libSFCGAL.so.* mr,