diff --git a/ansible/tasks/setup-postgres.yml b/ansible/tasks/setup-postgres.yml index 3fcc5796a..c44c8fe24 100644 --- a/ansible/tasks/setup-postgres.yml +++ b/ansible/tasks/setup-postgres.yml @@ -175,6 +175,21 @@ group: 'postgres' src: 'files/postgresql_config/conf.d/read_replica.conf' + - name: configure pam + block: + - name: Check if psql_version is psql_15 + ansible.builtin.set_fact: + is_psql_15: "{{ psql_version in ['psql_15'] }}" + + - name: create placeholder pam config + file: + path: '/etc/pam.d/postgresql' + state: touch + owner: postgres + group: postgres + mode: 0664 + when: not is_psql_15 + # Install extensions before init - name: Install Postgres extensions ansible.builtin.import_tasks: diff --git a/ansible/tasks/stage2-setup-postgres.yml b/ansible/tasks/stage2-setup-postgres.yml index e2217353c..e04029b1c 100644 --- a/ansible/tasks/stage2-setup-postgres.yml +++ b/ansible/tasks/stage2-setup-postgres.yml @@ -155,6 +155,27 @@ path: '/var/lib/postgresql/.nix-profile/bin/' register: 'nix_links' + - name: setup gatekeeper + block: + - name: Check if psql_version is psql_15 + ansible.builtin.set_fact: + is_psql_15: "{{ psql_version == 'psql_15' }}" + + - name: Install gatekeeper if not pg15 + when: + - stage2_nix + - not is_psql_15 + block: + - name: Install gatekeeper from nix binary cache + become: yes + shell: | + sudo -u postgres bash -c ". /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh && nix profile install github:supabase/postgres/{{ git_commit_sha }}#gatekeeper" + + - name: Create symbolic link for linux-pam to find pam_jit_pg.so + become: yes + shell: | + sudo ln -s /var/lib/postgresql/.nix-profile/lib/security/pam_jit_pg.so $(find /nix/store -type d -path "/nix/store/*-linux-pam-*/lib/security" -print -quit)/pam_jit_pg.so + - name: Create symlinks for Nix files into /usr/lib/postgresql/bin ansible.builtin.file: group: 'postgres' diff --git a/ansible/vars.yml b/ansible/vars.yml index 53662adb3..2e9cd0d92 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.6.0.022-orioledb" - postgres17: "17.6.1.065" - postgres15: "15.14.1.065" + postgresorioledb-17: "17.6.0.023-orioledb" + postgres17: "17.6.1.066" + postgres15: "15.14.1.066" # Non Postgres Extensions pgbouncer_release: 1.19.0 diff --git a/nix/packages/default.nix b/nix/packages/default.nix index 6c9993bc9..f49a8f170 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -41,6 +41,7 @@ github-matrix = pkgs.callPackage ./github-matrix { nix-eval-jobs = inputs'.nix-eval-jobs.packages.default; }; + gatekeeper = pkgs.callPackage ./gatekeeper.nix { inherit inputs pkgs; }; supabase-groonga = pkgs.callPackage ./groonga { }; http-mock-server = pkgs.callPackage ./http-mock-server.nix { }; local-infra-bootstrap = pkgs.callPackage ./local-infra-bootstrap.nix { }; diff --git a/nix/packages/gatekeeper.nix b/nix/packages/gatekeeper.nix new file mode 100644 index 000000000..f4f8b99d5 --- /dev/null +++ b/nix/packages/gatekeeper.nix @@ -0,0 +1,47 @@ +{ pkgs, inputs, ... }: +let + + go124 = inputs.nixpkgs-go124.legacyPackages.${pkgs.system}.go_1_24; + buildGoModule124 = pkgs.buildGoModule.override { go = go124; }; + + upstream-gatekeeper = buildGoModule124 { + pname = "jit-db-gatekeeper"; + version = "1.0.1"; + src = pkgs.fetchFromGitHub { + owner = "supabase"; + repo = "jit-db-gatekeeper"; + rev = "v1.0.1"; + sha256 = "sha256-4xSqQnuBYPZU6kl2LVnZbCBLCPUMKyZkezq2mPYox6k"; + }; + vendorHash = null; + + buildInputs = [ pkgs.pam ]; + + buildPhase = '' + runHook preBuild + go build -buildmode=c-shared -o pam_jit_pg.so + runHook postBuild + ''; + + installPhase = '' + runHook preInstall + mkdir -p $out/lib/security + cp pam_jit_pg.so $out/lib/security/ + runHook postInstall + ''; + }; +in + +pkgs.stdenv.mkDerivation { + pname = "gatekeeper"; + version = "1.0.1"; + + buildInputs = [ upstream-gatekeeper ]; + + dontUnpack = true; + + installPhase = '' + mkdir -p $out/lib/security/ + cp ${upstream-gatekeeper}/lib/security/pam_jit_pg.so $out/lib/security/pam_jit_pg.so + ''; +} diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index ceed6c763..083f54458 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -633,6 +633,227 @@ def test_libpq5_version(host): print("✓ libpq5 version is >= 14") +def test_jit_pam_module_installed(host): + """Test that the JIT PAM module (pam_jit_pg.so) is properly installed.""" + # Check PostgreSQL version first + result = run_ssh_command( + host["ssh"], "sudo -u postgres psql --version | grep -oE '[0-9]+' | head -1" + ) + pg_major_version = 15 # Default + if result["succeeded"] and result["stdout"].strip(): + try: + pg_major_version = int(result["stdout"].strip()) + except ValueError: + pass + + # Skip test for PostgreSQL 15 as gatekeeper is not installed for PG15 + if pg_major_version == 15: + print("\nSkipping JIT PAM module test for PostgreSQL 15 (not installed)") + return + + # Check if gatekeeper is installed via Nix + result = run_ssh_command( + host["ssh"], + "sudo -u postgres ls -la /var/lib/postgresql/.nix-profile/lib/security/pam_jit_pg.so 2>/dev/null", + ) + if result["succeeded"]: + print(f"\nJIT PAM module found in Nix profile:\n{result['stdout']}") + else: + print("\nJIT PAM module not found in postgres user's Nix profile") + assert False, "JIT PAM module (pam_jit_pg.so) not found in expected location" + + # Check if the symlink exists in the Linux PAM security directory + result = run_ssh_command( + host["ssh"], + "find /nix/store -type f -path '*/lib/security/pam_jit_pg.so' 2>/dev/null | head -5", + ) + if result["succeeded"] and result["stdout"].strip(): + print(f"\nJIT PAM module symlinks found:\n{result['stdout']}") + else: + print("\nNo JIT PAM module symlinks found in /nix/store") + + # Verify the module is a valid shared library + result = run_ssh_command( + host["ssh"], "file /var/lib/postgresql/.nix-profile/lib/security/pam_jit_pg.so" + ) + if result["succeeded"]: + print(f"\nJIT PAM module file type:\n{result['stdout']}") + assert ( + "shared object" in result["stdout"].lower() + or "dynamically linked" in result["stdout"].lower() + ), "JIT PAM module is not a valid shared library" + + print("✓ JIT PAM module is properly installed") + + +def test_pam_postgresql_config(host): + """Test that the PAM configuration for PostgreSQL exists and is properly configured.""" + # Check PostgreSQL version to determine if PAM config should exist + result = run_ssh_command( + host["ssh"], "sudo -u postgres psql --version | grep -oE '[0-9]+' | head -1" + ) + pg_major_version = 15 # Default + if result["succeeded"] and result["stdout"].strip(): + try: + pg_major_version = int(result["stdout"].strip()) + except ValueError: + pass + + print(f"\nPostgreSQL major version: {pg_major_version}") + + # PAM config should exist for non-PostgreSQL 15 versions + if pg_major_version != 15: + # Check if PAM config file exists + result = run_ssh_command(host["ssh"], "ls -la /etc/pam.d/postgresql") + if result["succeeded"]: + print(f"\nPAM config file found:\n{result['stdout']}") + + # Check file permissions + result = run_ssh_command( + host["ssh"], "stat -c '%a %U %G' /etc/pam.d/postgresql" + ) + if result["succeeded"]: + perms = result["stdout"].strip() + print(f"PAM config permissions: {perms}") + # Should be owned by postgres:postgres with 664 permissions + assert ( + "postgres postgres" in perms + ), "PAM config not owned by postgres:postgres" + else: + print("\nPAM config file not found") + assert False, "PAM configuration file /etc/pam.d/postgresql not found" + else: + print("\nSkipping PAM config check for PostgreSQL 15") + # For PostgreSQL 15, the PAM config should NOT exist + result = run_ssh_command(host["ssh"], "test -f /etc/pam.d/postgresql") + if result["succeeded"]: + print("\nWARNING: PAM config exists for PostgreSQL 15 (not expected)") + + print("✓ PAM configuration is properly set up") + + +def test_jit_pam_gatekeeper_profile(host): + """Test that the gatekeeper package is properly installed in the postgres user's Nix profile.""" + # Check PostgreSQL version first + result = run_ssh_command( + host["ssh"], "sudo -u postgres psql --version | grep -oE '[0-9]+' | head -1" + ) + pg_major_version = 15 # Default + if result["succeeded"] and result["stdout"].strip(): + try: + pg_major_version = int(result["stdout"].strip()) + except ValueError: + pass + + # Skip test for PostgreSQL 15 as gatekeeper is not installed for PG15 + if pg_major_version == 15: + print("\nSkipping gatekeeper profile test for PostgreSQL 15 (not installed)") + return + + # Check if gatekeeper is in the postgres user's Nix profile + result = run_ssh_command( + host["ssh"], + "sudo -u postgres nix profile list --json | jq -r '.elements.gatekeeper.storePaths[0]'", + ) + if result["succeeded"] and result["stdout"].strip(): + print(f"\nGatekeeper found in Nix profile:\n{result['stdout']}") + else: + # Try alternative check + result = run_ssh_command( + host["ssh"], + "sudo -u postgres ls -la /var/lib/postgresql/.nix-profile/ | grep -i gate", + ) + if result["succeeded"] and result["stdout"].strip(): + print(f"\nGatekeeper-related files in profile:\n{result['stdout']}") + else: + print("\nGatekeeper not found in postgres user's Nix profile") + # This might be expected if it's installed system-wide instead + + # Check if we can find the gatekeeper derivation + result = run_ssh_command( + host["ssh"], + "find /nix/store -maxdepth 1 -type d -name '*gatekeeper*' 2>/dev/null | head -5", + ) + if result["succeeded"] and result["stdout"].strip(): + print(f"\nGatekeeper derivations found:\n{result['stdout']}") + else: + print("\nNo gatekeeper derivations found in /nix/store") + + print("✓ Gatekeeper package installation check completed") + + +def test_jit_pam_module_dependencies(host): + """Test that the JIT PAM module has all required dependencies.""" + # Check PostgreSQL version first + result = run_ssh_command( + host["ssh"], "sudo -u postgres psql --version | grep -oE '[0-9]+' | head -1" + ) + pg_major_version = 15 # Default + if result["succeeded"] and result["stdout"].strip(): + try: + pg_major_version = int(result["stdout"].strip()) + except ValueError: + pass + + # Skip test for PostgreSQL 15 as gatekeeper is not installed for PG15 + if pg_major_version == 15: + print( + "\nSkipping JIT PAM module dependencies test for PostgreSQL 15 (not installed)" + ) + return + + # Check dependencies of the PAM module + result = run_ssh_command( + host["ssh"], + "ldd /var/lib/postgresql/.nix-profile/lib/security/pam_jit_pg.so 2>/dev/null", + ) + if result["succeeded"]: + print(f"\nJIT PAM module dependencies:\n{result['stdout']}") + + # Check for required libraries + required_libs = ["libpam", "libc"] + for lib in required_libs: + if lib not in result["stdout"].lower(): + print(f"WARNING: Required library {lib} not found in dependencies") + + # Check for any missing dependencies + if "not found" in result["stdout"].lower(): + assert False, "JIT PAM module has missing dependencies" + else: + print("\nCould not check JIT PAM module dependencies") + + print("✓ JIT PAM module dependencies are satisfied") + + +def test_jit_pam_postgresql_integration(host): + """Test that PostgreSQL can be configured to use PAM authentication.""" + # Check if PAM is available as an authentication method in PostgreSQL + result = run_ssh_command( + host["ssh"], + "sudo -u postgres psql -c \"SELECT name, setting FROM pg_settings WHERE name LIKE '%pam%';\" 2>/dev/null", + ) + if result["succeeded"]: + print(f"\nPostgreSQL PAM-related settings:\n{result['stdout']}") + + # Check pg_hba.conf for potential PAM entries (even if not currently active) + result = run_ssh_command( + host["ssh"], + "grep -i pam /etc/postgresql/pg_hba.conf 2>/dev/null || echo 'No PAM entries in pg_hba.conf'", + ) + if result["succeeded"]: + print(f"\nPAM entries in pg_hba.conf:\n{result['stdout']}") + + # Verify PostgreSQL was compiled with PAM support + result = run_ssh_command( + host["ssh"], + "sudo -u postgres pg_config --configure 2>/dev/null | grep -i pam || echo 'PAM compile flag not found'", + ) + if result["succeeded"]: + print(f"\nPostgreSQL PAM compile flags:\n{result['stdout']}") + + print("✓ PostgreSQL PAM integration check completed") + + def test_postgrest_read_only_session_attrs(host): """Test PostgREST with target_session_attrs=read-only and check for session errors.""" # First, check if PostgreSQL is configured for read-only mode