diff --git a/.github/workflows/nix-build.yml b/.github/workflows/nix-build.yml index 695dc2abf..1e796456d 100644 --- a/.github/workflows/nix-build.yml +++ b/.github/workflows/nix-build.yml @@ -24,7 +24,7 @@ jobs: fail-fast: false matrix: include: - - runner: blacksmith-32vcpu-ubuntu-2404 + - runner: blacksmith-32vcpu-ubuntu-2404 arch: amd64 - runner: blacksmith-32vcpu-ubuntu-2404-arm arch: arm64 @@ -110,7 +110,7 @@ jobs: df -h - name: Build psql bundle run: > - nix run "github:Mic92/nix-fast-build?rev=b1dae483ab7d4139a6297e02b6de9e5d30e43d48" + nix run "github:Mic92/nix-fast-build?rev=b1dae483ab7d4139a6297e02b6de9e5d30e43d48" -- --skip-cached --no-nom ${{ matrix.runner == 'macos-latest-xlarge' && '--max-jobs 1' || '' }} --flake ".#checks.$(nix eval --raw --impure --expr 'builtins.currentSystem')" env: diff --git a/ansible/tasks/setup-postgres.yml b/ansible/tasks/setup-postgres.yml index 691c24da4..221eaeec7 100644 --- a/ansible/tasks/setup-postgres.yml +++ b/ansible/tasks/setup-postgres.yml @@ -139,6 +139,19 @@ group: postgres when: debpkg_mode or nixpkg_mode +- name: Check if psql_version is psql_15 + 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: (debpkg_mode or nixpkg_mode) and not is_psql_15 + # Add pg_hba.conf - name: import pg_hba.conf template: diff --git a/ansible/tasks/stage2-setup-postgres.yml b/ansible/tasks/stage2-setup-postgres.yml index 6f696d5d3..fe2c4a77a 100644 --- a/ansible/tasks/stage2-setup-postgres.yml +++ b/ansible/tasks/stage2-setup-postgres.yml @@ -94,7 +94,26 @@ 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 }}#{{postgresql_version}}_src" when: stage2_nix - + +- name: Check if psql_version is psql_15 + 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: Set ownership and permissions for /etc/ssl/private become: yes file: diff --git a/nix/packages/default.nix b/nix/packages/default.nix index f297c8359..d3b56c264 100644 --- a/nix/packages/default.nix +++ b/nix/packages/default.nix @@ -1,6 +1,9 @@ { self, inputs, ... }: { - imports = [ ./postgres.nix ]; + imports = [ + ./postgres.nix + # ./gatekeeper.nix + ]; perSystem = { inputs', @@ -34,6 +37,7 @@ cleanup-ami = pkgs.callPackage ./cleanup-ami.nix { }; dbmate-tool = pkgs.callPackage ./dbmate-tool.nix { inherit (self.supabase) defaults; }; docs = pkgs.callPackage ./docs.nix { }; + 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..7cba56bbd --- /dev/null +++ b/nix/packages/gatekeeper.nix @@ -0,0 +1,50 @@ +{ + inputs, + system, + pkgs, + ... +}: +let + go124 = inputs.nixpkgs-go124.legacyPackages.${system}.go_1_24; + # Use completely clean nixpkgs without any overlays for gatekeeper + #cleanPkgs = inputs.nixpkgs.legacyPackages.${system}; + buildGoModule = pkgs.buildGoModule.override { go = go124; }; +in + +buildGoModule { + pname = "gatekeeper"; + version = "0.1.0"; + + src = pkgs.fetchFromGitHub { + owner = "supabase"; + repo = "jit-db-gatekeeper"; + rev = "v1.0.0"; + hash = "sha256-hrYh1dBxk+aN3b/J9mZqk/ZXHmWA/MIqZLVgICT7e90="; + }; + + vendorHash = "sha256-G9x2TARSJMn30R6ZOlsggxEtn5t2ezWz1YtkLXdYiAE="; + + buildInputs = [ + pkgs.pam + ] ++ pkgs.lib.optionals pkgs.stdenv.isDarwin [ pkgs.darwin.apple_sdk.frameworks.Security ]; + + 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 + ''; + + meta = with pkgs.lib; { + description = "PAM module for JWT authentication with PostgreSQL backend"; + homepage = "https://github.com/supabase/jit-db-gatekeeper"; + license = licenses.mit; + platforms = platforms.unix; + }; +} diff --git a/testinfra/test_ami_nix.py b/testinfra/test_ami_nix.py index 42442de18..fc069e529 100644 --- a/testinfra/test_ami_nix.py +++ b/testinfra/test_ami_nix.py @@ -623,6 +623,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