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.