This is an automated email from the ASF dual-hosted git repository.

oscerd pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git


The following commit(s) were added to refs/heads/main by this push:
     new 957b28aa7fad CAMEL-24447: camel-pqc - restrict private key files to 
their owner (#25826)
957b28aa7fad is described below

commit 957b28aa7fad123bfafeea6bfb33a2b1d0a5a188
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Aug 28 06:34:03 2026 +0200

    CAMEL-24447: camel-pqc - restrict private key files to their owner (#25826)
    
    FileBasedKeyLifecycleManager stores private keys unencrypted, as Base64 
PKCS#8 inside a
    JSON file, and created both the key directory and those files with whatever 
the process
    umask allowed. Under the common 022 that is rw-r--r-- and rwxr-xr-x, so 
every account on
    the host could read them.
    
    Create the key directory as rwx------ and each <keyId>.private.json as 
rw-------, where
    the file system supports POSIX permissions, falling back to the equivalent 
owner-only
    java.io.File flags elsewhere. The file is restricted before its content is 
written, so
    the key is never briefly readable while being written.
    
    A private key file left behind by an earlier version is tightened the next 
time that key
    is stored: storeKey opens it with TRUNCATE_EXISTING rather than recreating 
it, so it
    would otherwise keep the permissions it was created with.
    
    Public keys and metadata are left alone - they are not secret, and 
restricting them would
    break readers for no benefit.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Andrea Cosentino <[email protected]>
---
 .../lifecycle/FileBasedKeyLifecycleManager.java    | 49 +++++++++++-
 ...ileBasedKeyLifecycleManagerPermissionsTest.java | 91 ++++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc    | 14 ++++
 3 files changed, 151 insertions(+), 3 deletions(-)

diff --git 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
index 19750a941857..33901921c08f 100644
--- 
a/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
+++ 
b/components/camel-pqc/src/main/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManager.java
@@ -17,6 +17,7 @@
 package org.apache.camel.component.pqc.lifecycle;
 
 import java.io.BufferedInputStream;
+import java.io.File;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.nio.charset.StandardCharsets;
@@ -24,6 +25,8 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
 import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermissions;
 import java.security.KeyFactory;
 import java.security.KeyPair;
 import java.security.PrivateKey;
@@ -68,6 +71,7 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
         this.objectMapper = new ObjectMapper();
         this.objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
         Files.createDirectories(keyDirectory);
+        restrictToOwner(keyDirectory);
         LOG.info("Initialized FileBasedKeyLifecycleManager with directory: 
{}", keyDirectory);
         loadExistingKeys();
     }
@@ -150,9 +154,7 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
         byte[] privateKeyBytes = keyPair.getPrivate().getEncoded();
         String privateKeyBase64 = 
Base64.getEncoder().encodeToString(privateKeyBytes);
         KeyFileData privateData = new KeyFileData(privateKeyBase64, "PKCS8", 
metadata.getAlgorithm());
-        Files.writeString(privateKeyFile, 
objectMapper.writeValueAsString(privateData),
-                StandardCharsets.UTF_8,
-                StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
+        writePrivateKeyFile(privateKeyFile, 
objectMapper.writeValueAsString(privateData));
 
         // Store public key in X.509 format
         Path publicKeyFile = getPublicKeyFile(keyId);
@@ -398,6 +400,47 @@ public class FileBasedKeyLifecycleManager implements 
KeyLifecycleManager {
         }
     }
 
+    /**
+     * Writes the private key, restricting the file to its owner first so the 
content is never briefly readable by
+     * everyone. A file left behind by an earlier version is tightened here 
too, since it is opened for truncation
+     * rather than recreated and would otherwise keep whatever permissions the 
umask gave it.
+     */
+    private void writePrivateKeyFile(Path file, String content) throws 
IOException {
+        if (!Files.exists(file)) {
+            Files.createFile(file);
+        }
+        restrictToOwner(file);
+        Files.writeString(file, content, StandardCharsets.UTF_8,
+                StandardOpenOption.CREATE, 
StandardOpenOption.TRUNCATE_EXISTING);
+    }
+
+    /**
+     * Restricts a path to its owner. Private keys are stored unencrypted, so 
the common 022 umask - which leaves them
+     * world readable - is not an acceptable default for them.
+     */
+    private static void restrictToOwner(Path path) {
+        boolean directory = Files.isDirectory(path);
+        try {
+            if (Files.getFileAttributeView(path, PosixFileAttributeView.class) 
!= null) {
+                Files.setPosixFilePermissions(path,
+                        PosixFilePermissions.fromString(directory ? 
"rwx------" : "rw-------"));
+                return;
+            }
+            // Not a POSIX file system: fall back to the java.io.File flags, 
which do the same job less precisely
+            File file = path.toFile();
+            file.setReadable(false, false);
+            file.setWritable(false, false);
+            file.setReadable(true, true);
+            file.setWritable(true, true);
+            if (directory) {
+                file.setExecutable(false, false);
+                file.setExecutable(true, true);
+            }
+        } catch (IOException | UnsupportedOperationException e) {
+            LOG.warn("Cannot restrict permissions on {}; it may be readable by 
other users on this host", path, e);
+        }
+    }
+
     private Path getPrivateKeyFile(String keyId) {
         return keyDirectory.resolve(keyId + ".private.json");
     }
diff --git 
a/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManagerPermissionsTest.java
 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManagerPermissionsTest.java
new file mode 100644
index 000000000000..7749a02c3e17
--- /dev/null
+++ 
b/components/camel-pqc/src/test/java/org/apache/camel/component/pqc/lifecycle/FileBasedKeyLifecycleManagerPermissionsTest.java
@@ -0,0 +1,91 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.pqc.lifecycle;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.attribute.PosixFileAttributeView;
+import java.nio.file.attribute.PosixFilePermission;
+import java.nio.file.attribute.PosixFilePermissions;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.condition.DisabledOnOs;
+import org.junit.jupiter.api.condition.OS;
+import org.junit.jupiter.api.io.TempDir;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Private keys are stored unencrypted, so leaving them at whatever the umask 
gives - commonly world readable under 022
+ * - is not an acceptable default.
+ */
+@DisabledOnOs(OS.WINDOWS)
+class FileBasedKeyLifecycleManagerPermissionsTest {
+
+    @Test
+    void thePrivateKeyAndItsDirectoryAreReadableOnlyByTheOwner(@TempDir Path 
tempDir) throws Exception {
+        Path keyDir = tempDir.resolve("keys");
+        FileBasedKeyLifecycleManager manager = new 
FileBasedKeyLifecycleManager(keyDir.toString());
+
+        manager.storeKey("k1", keyPair(), metadata());
+
+        Path privateKeyFile = keyDir.resolve("k1.private.json");
+        assertTrue(Files.exists(privateKeyFile), "expected the private key at 
" + privateKeyFile);
+
+        assertEquals(PosixFilePermissions.fromString("rw-------"), 
Files.getPosixFilePermissions(privateKeyFile));
+        assertEquals(PosixFilePermissions.fromString("rwx------"), 
Files.getPosixFilePermissions(keyDir));
+    }
+
+    /**
+     * A key written by an earlier version keeps its permissions across a 
rewrite, because the file is truncated rather
+     * than recreated.
+     */
+    @Test
+    void aPreExistingWorldReadableKeyFileIsTightened(@TempDir Path tempDir) 
throws Exception {
+        Path keyDir = tempDir.resolve("keys");
+        FileBasedKeyLifecycleManager manager = new 
FileBasedKeyLifecycleManager(keyDir.toString());
+
+        Path privateKeyFile = keyDir.resolve("k2.private.json");
+        Files.writeString(privateKeyFile, "{}");
+        Files.setPosixFilePermissions(privateKeyFile, 
PosixFilePermissions.fromString("rw-r--r--"));
+
+        manager.storeKey("k2", keyPair(), metadata());
+
+        Set<PosixFilePermission> actual = 
Files.getPosixFilePermissions(privateKeyFile);
+        assertEquals(PosixFilePermissions.fromString("rw-------"), actual);
+    }
+
+    @Test
+    void posixIsSupportedHere(@TempDir Path tempDir) {
+        assertTrue(Files.getFileAttributeView(tempDir, 
PosixFileAttributeView.class) != null,
+                "this test is meaningless without POSIX permissions");
+    }
+
+    private static KeyPair keyPair() throws Exception {
+        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+        generator.initialize(2048);
+        return generator.generateKeyPair();
+    }
+
+    private static KeyMetadata metadata() {
+        return new KeyMetadata("k", "RSA");
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index b3c9bc1e2826..201280ca686b 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -477,3 +477,17 @@ camel.routeController.enabled = true
 camel.routeController.backOffDelay = 2000
 camel.routeController.backOffMaxDelay = 60000
 ----
+=== camel-pqc
+
+`FileBasedKeyLifecycleManager` stores private keys unencrypted, as Base64 
PKCS#8 inside a JSON file, and
+used to create both the key directory and those files with whatever the 
process umask allowed — commonly
+`rw-r--r--` and `rwxr-xr-x` under the usual `022`, leaving private keys 
readable by every account on the
+host.
+
+The key directory is now created as `rwx------` and each 
`<keyId>.private.json` as `rw-------`, on file
+systems that support POSIX permissions; elsewhere the equivalent owner-only 
flags are applied. A private
+key file left behind by an earlier version is tightened the next time that key 
is stored, because the file
+is truncated rather than recreated and would otherwise keep its original 
permissions.
+
+Deployments where another account legitimately reads these files — a sidecar 
or a backup agent running as
+a different user — need to run as the owner, or use a group-aware key store 
instead.

Reply via email to