postgresql-jdbc.git / summary / log / commit / refs
commit d45f279ada83902ae32ef4d6dcea9bdf2caaf047
Author: Harinath Kanchu <kanchuharinath@gmail.com>
Commit: GitHub <noreply@github.com>
Date: Tue Mar 10 12:51:22 2026 +0000
ssl: align key file permission check with libpq (#3952)
Root-owned key files with GROUP_READ (0640) are now accepted,
matching the behavior of libpq and psql. Non-root-owned files
still require 0600 or stricter.
Splits validatePosixPermissions into isFileOwnedByRoot and
hasInsecurePosixPermissions for clarity. Adds four tests covering
the permission matrix, skipped on non-POSIX systems.
Co-authored-by: harinath <harinath@harinaths-MacBook-Pro.local>
.../org/postgresql/ssl/BaseX509KeyManager.java | 67 +++++++++++++++-----
.../org/postgresql/test/ssl/PEMKeyManagerTest.java | 73 ++++++++++++++++++++++
2 files changed, 125 insertions(+), 15 deletions(-)
diff --git a/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java b/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java
index 3f7de878..10e855af 100644
--- a/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java
+++ b/pgjdbc/src/main/java/org/postgresql/ssl/BaseX509KeyManager.java
@@ -31,6 +31,8 @@ import javax.security.auth.x500.X500Principal;
public abstract class BaseX509KeyManager implements X509KeyManager {
+ private static final int ROOT_UID = 0;
+
protected @Nullable PSQLException error;
/**
@@ -107,9 +109,11 @@ public abstract class BaseX509KeyManager implements X509KeyManager {
}
/**
- * Validates that the private key file has secure permissions (owner-only readable).
- * On POSIX systems, ensures no group or other permissions are set.
- * On Windows systems, checks ACLs to ensure only the owner and trusted system accounts have access.
+ * Validates that the private key file has secure permissions, matching libpq behavior.
+ * On POSIX systems, root-owned files are allowed group-read access (up to 0640), since
+ * it's common for root to own certs and grant read access via group membership. Files
+ * owned by anyone else must be 0600 or stricter.
+ * On Windows, ACLs are checked to ensure only the owner and trusted system accounts have access.
*
* @param keyPath the path to the private key file
* @throws PSQLException if the file has insecure permissions
@@ -131,7 +135,9 @@ public abstract class BaseX509KeyManager implements X509KeyManager {
}
/**
- * Validates POSIX file permissions of key.
+ * Validates POSIX file permissions of key, matching libpq behavior.
+ * Root-owned files (uid 0) allow GROUP_READ (up to 0640).
+ * Non-root-owned files require 0600 or less (no group or other permissions).
*
* @param keyPath the path to the private key file
* @return true if validation succeeded (permissions are secure), false if POSIX is not supported
@@ -140,19 +146,13 @@ public abstract class BaseX509KeyManager implements X509KeyManager {
private static boolean validatePosixPermissions(Path keyPath) throws PSQLException {
try {
Set<PosixFilePermission> permissions = Files.getPosixFilePermissions(keyPath);
+ boolean isOwnedByRoot = isFileOwnedByRoot(keyPath);
- boolean hasGroupPerms = permissions.contains(PosixFilePermission.GROUP_READ)
- || permissions.contains(PosixFilePermission.GROUP_WRITE)
- || permissions.contains(PosixFilePermission.GROUP_EXECUTE);
-
- boolean hasOtherPerms = permissions.contains(PosixFilePermission.OTHERS_READ)
- || permissions.contains(PosixFilePermission.OTHERS_WRITE)
- || permissions.contains(PosixFilePermission.OTHERS_EXECUTE);
-
- if (hasGroupPerms || hasOtherPerms) {
+ if (hasInsecurePosixPermissions(permissions, isOwnedByRoot)) {
throw new PSQLException(
- GT.tr("Private key file \"{0}\" has insecure permissions. "
- + "Permissions for group and other must be revoked. "
+ GT.tr("private key file \"{0}\" has group or world access; "
+ + "file must have permissions u=rw (0600) or less if owned by the current user, "
+ + "or permissions u=rw,g=r (0640) or less if owned by root. "
+ "Current permissions: {1}",
keyPath.toString(),
PosixFilePermissions.toString(permissions)),
@@ -168,6 +168,43 @@ public abstract class BaseX509KeyManager implements X509KeyManager {
}
}
+ /**
+ * Checks whether the file is owned by root (uid 0).
+ * Falls back to false if the unix:uid attribute is not available.
+ */
+ private static boolean isFileOwnedByRoot(Path keyPath) throws IOException {
+ try {
+ Object uid = Files.getAttribute(keyPath, "unix:uid");
+ return Integer.valueOf(ROOT_UID).equals(uid);
+ } catch (UnsupportedOperationException | IllegalArgumentException e) {
+ // unix:uid not available
+ return false;
+ }
+ }
+
+ /**
+ * Determines whether the given POSIX permissions are insecure for a private key file.
+ * Matches libpq behavior: root-owned files allow GROUP_READ (0640),
+ * while non-root-owned files reject all group and other permissions (0600).
+ */
+ private static boolean hasInsecurePosixPermissions(
+ Set<PosixFilePermission> permissions, boolean isOwnedByRoot) {
+ boolean hasOtherPerms = permissions.contains(PosixFilePermission.OTHERS_READ)
+ || permissions.contains(PosixFilePermission.OTHERS_WRITE)
+ || permissions.contains(PosixFilePermission.OTHERS_EXECUTE);
+
+ if (isOwnedByRoot) {
+ boolean hasGroupWriteOrExecute = permissions.contains(PosixFilePermission.GROUP_WRITE)
+ || permissions.contains(PosixFilePermission.GROUP_EXECUTE);
+ return hasGroupWriteOrExecute || hasOtherPerms;
+ }
+
+ boolean hasGroupPerms = permissions.contains(PosixFilePermission.GROUP_READ)
+ || permissions.contains(PosixFilePermission.GROUP_WRITE)
+ || permissions.contains(PosixFilePermission.GROUP_EXECUTE);
+ return hasGroupPerms || hasOtherPerms;
+ }
+
/**
* Validates Windows ACL permissions of the key file.
*
diff --git a/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java b/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java
index 8a6e95fa..0f428d59 100644
--- a/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java
+++ b/pgjdbc/src/test/java/org/postgresql/test/ssl/PEMKeyManagerTest.java
@@ -5,18 +5,24 @@
package org.postgresql.test.ssl;
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.postgresql.PGProperty;
import org.postgresql.core.ServerVersion;
+import org.postgresql.ssl.BaseX509KeyManager;
import org.postgresql.ssl.PEMKeyManager;
import org.postgresql.test.TestUtil;
+import org.postgresql.util.PSQLException;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assumptions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
import java.io.File;
import java.nio.file.Files;
@@ -192,4 +198,71 @@ public class PEMKeyManagerTest {
assertTrue(sslUsed, "SSL should be in use");
}
}
+
+ @Test
+ void testPermissionsOwnerOnly(@TempDir Path tempDir) throws Exception {
+ Assumptions.assumeTrue(
+ tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"),
+ "POSIX file permissions not supported");
+
+ Path keyFile = tempDir.resolve("test.key");
+ Files.createFile(keyFile);
+ Set<PosixFilePermission> perms = EnumSet.of(PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE);
+ Files.setPosixFilePermissions(keyFile, perms);
+
+ assertDoesNotThrow(() -> BaseX509KeyManager.validateKeyFilePermissions(keyFile),
+ "File with 0600 permissions should pass validation");
+ }
+
+ @Test
+ void testPermissionsGroupReadNonRootFails(@TempDir Path tempDir) throws Exception {
+ Assumptions.assumeTrue(
+ tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"),
+ "POSIX file permissions not supported");
+
+ Path keyFile = tempDir.resolve("test.key");
+ Files.createFile(keyFile);
+ Set<PosixFilePermission> perms = EnumSet.of(
+ PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
+ PosixFilePermission.GROUP_READ);
+ Files.setPosixFilePermissions(keyFile, perms);
+
+ // Non-root owned file with GROUP_READ should fail
+ assertThrows(PSQLException.class,
+ () -> BaseX509KeyManager.validateKeyFilePermissions(keyFile),
+ "File with GROUP_READ and non-root owner should fail validation");
+ }
+
+ @Test
+ void testPermissionsOthersReadFails(@TempDir Path tempDir) throws Exception {
+ Assumptions.assumeTrue(
+ tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"),
+ "POSIX file permissions not supported");
+
+ Path keyFile = tempDir.resolve("test.key");
+ Files.createFile(keyFile);
+ Set<PosixFilePermission> perms = EnumSet.of(
+ PosixFilePermission.OWNER_READ, PosixFilePermission.OWNER_WRITE,
+ PosixFilePermission.OTHERS_READ);
+ Files.setPosixFilePermissions(keyFile, perms);
+
+ assertThrows(PSQLException.class,
+ () -> BaseX509KeyManager.validateKeyFilePermissions(keyFile),
+ "File with OTHERS_READ should fail validation");
+ }
+
+ @Test
+ void testPermissionsOwnerReadOnly(@TempDir Path tempDir) throws Exception {
+ Assumptions.assumeTrue(
+ tempDir.getFileSystem().supportedFileAttributeViews().contains("posix"),
+ "POSIX file permissions not supported");
+
+ Path keyFile = tempDir.resolve("test.key");
+ Files.createFile(keyFile);
+ Set<PosixFilePermission> perms = EnumSet.of(PosixFilePermission.OWNER_READ);
+ Files.setPosixFilePermissions(keyFile, perms);
+
+ assertDoesNotThrow(() -> BaseX509KeyManager.validateKeyFilePermissions(keyFile),
+ "File with 0400 permissions should pass validation");
+ }
}
[parent: e8e1ab306a91]