This is an automated email from the ASF dual-hosted git repository.
snazy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/polaris.git
The following commit(s) were added to refs/heads/main by this push:
new a0436f0b3 Keep generated RSA-key-pair for JWT token broker on heap
(#1661)
a0436f0b3 is described below
commit a0436f0b349cb99cb87ddc72360f36594bc2da2c
Author: Robert Stupp <[email protected]>
AuthorDate: Wed May 28 20:50:08 2025 +0200
Keep generated RSA-key-pair for JWT token broker on heap (#1661)
Polaris allows using RSA key-paris for the JWT token broker. The
recommended way is to [generate the RSA key
pair](https://github.com/apache/polaris/blob/d8b862b13914d526ee147dc0e359bfc9c1e319ad/site/content/in-dev/unreleased/configuring-polaris-for-production.md?plain=1#L61-L66)
and configure the location of the key files.
However, if only `polaris.authentication.token-broker.type=rsa-key-pair`
but not the `public/private-key-pair` options are configured, Polaris generates
those and stores them in `/tmp` using random file names (using
`Files.createTempFile()`) - this happens for each (matching) realm. Each
Polaris startup generates new key-pairs for each of those realms. It's
practically not possible to associate the files to a realm. There is already a
[production readiness check](https://github.com/ap [...]
Due to the issue that the files cannot be associated, those seem to be
somewhat useless and bring no advantage over keeping these "ephemeral RSA key
pairs" on heap. This PR changes the code to not write the key-pair to the file
system and keeps these "ephemeral key pairs" on heap. Since the same code path
is used for key-paris _provided_ by the user (via the `public/private-key-pair`
config options), that code path now only reads those files once and not every
time the private/public [...]
---
.../service/quarkus/auth/JWTRSAKeyPairTest.java | 14 ++--
.../apache/polaris/service/auth/JWTRSAKeyPair.java | 6 +-
.../polaris/service/auth/JWTRSAKeyPairFactory.java | 33 ++++------
.../polaris/service/auth/LocalRSAKeyProvider.java | 61 +++++++++++-------
.../org/apache/polaris/service/auth/PemUtils.java | 18 ++++--
.../service/auth/LocalRSAKeyProviderTest.java | 75 ++++++++++++++++++++++
site/content/in-dev/unreleased/configuration.md | 6 +-
7 files changed, 151 insertions(+), 62 deletions(-)
diff --git
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/auth/JWTRSAKeyPairTest.java
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/auth/JWTRSAKeyPairTest.java
index cae4ab0fe..986282c81 100644
---
a/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/auth/JWTRSAKeyPairTest.java
+++
b/quarkus/service/src/test/java/org/apache/polaris/service/quarkus/auth/JWTRSAKeyPairTest.java
@@ -26,7 +26,6 @@ import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
-import java.nio.file.Path;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.apache.polaris.core.PolarisCallContext;
@@ -39,6 +38,7 @@ import
org.apache.polaris.core.persistence.PolarisMetaStoreManager;
import org.apache.polaris.core.persistence.dao.entity.EntityResult;
import org.apache.polaris.core.persistence.dao.entity.PrincipalSecretsResult;
import org.apache.polaris.service.auth.JWTRSAKeyPair;
+import org.apache.polaris.service.auth.KeyProvider;
import org.apache.polaris.service.auth.LocalRSAKeyProvider;
import org.apache.polaris.service.auth.PemUtils;
import org.apache.polaris.service.auth.TokenBroker;
@@ -46,7 +46,6 @@ import org.apache.polaris.service.auth.TokenRequestValidator;
import org.apache.polaris.service.auth.TokenResponse;
import org.apache.polaris.service.types.TokenType;
import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.io.TempDir;
import org.mockito.Mockito;
@QuarkusTest
@@ -55,10 +54,8 @@ public class JWTRSAKeyPairTest {
@Inject protected PolarisConfigurationStore configurationStore;
@Test
- public void testSuccessfulTokenGeneration(@TempDir Path tempDir) throws
Exception {
- Path privateFileLocation = tempDir.resolve("test-private.pem");
- Path publicFileLocation = tempDir.resolve("test-public.pem");
- PemUtils.generateKeyPair(privateFileLocation, publicFileLocation);
+ public void testSuccessfulTokenGeneration() throws Exception {
+ var keyPair = PemUtils.generateKeyPair();
final String clientId = "test-client-id";
final String scope = "PRINCIPAL_ROLE:TEST";
@@ -82,8 +79,8 @@ public class JWTRSAKeyPairTest {
Mockito.when(
metastoreManager.loadEntity(polarisCallContext, 0L, 1L,
PolarisEntityType.PRINCIPAL))
.thenReturn(new EntityResult(principal));
- TokenBroker tokenBroker =
- new JWTRSAKeyPair(metastoreManager, 420, publicFileLocation,
privateFileLocation);
+ KeyProvider provider = new LocalRSAKeyProvider(keyPair);
+ TokenBroker tokenBroker = new JWTRSAKeyPair(metastoreManager, 420,
provider);
TokenResponse token =
tokenBroker.generateFromClientSecrets(
clientId,
@@ -95,7 +92,6 @@ public class JWTRSAKeyPairTest {
assertThat(token).isNotNull();
assertThat(token.getExpiresIn()).isEqualTo(420);
- LocalRSAKeyProvider provider = new LocalRSAKeyProvider(publicFileLocation,
privateFileLocation);
assertThat(provider.getPrivateKey()).isNotNull();
assertThat(provider.getPublicKey()).isNotNull();
JWTVerifier verifier =
diff --git
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPair.java
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPair.java
index 18f270238..1c637d3a4 100644
---
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPair.java
+++
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPair.java
@@ -19,7 +19,6 @@
package org.apache.polaris.service.auth;
import com.auth0.jwt.algorithms.Algorithm;
-import java.nio.file.Path;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
@@ -32,10 +31,9 @@ public class JWTRSAKeyPair extends JWTBroker {
public JWTRSAKeyPair(
PolarisMetaStoreManager metaStoreManager,
int maxTokenGenerationInSeconds,
- Path publicKeyFile,
- Path privateKeyFile) {
+ KeyProvider keyProvider) {
super(metaStoreManager, maxTokenGenerationInSeconds);
- keyProvider = new LocalRSAKeyProvider(publicKeyFile, privateKeyFile);
+ this.keyProvider = keyProvider;
}
@Override
diff --git
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
index ee74caf46..4fe1fde0d 100644
---
a/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
+++
b/service/common/src/main/java/org/apache/polaris/service/auth/JWTRSAKeyPairFactory.java
@@ -21,9 +21,6 @@ package org.apache.polaris.service.auth;
import io.smallrye.common.annotation.Identifier;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.concurrent.ConcurrentHashMap;
@@ -59,28 +56,26 @@ public class JWTRSAKeyPairFactory implements
TokenBrokerFactory {
private JWTRSAKeyPair createTokenBroker(RealmContext realmContext) {
AuthenticationRealmConfiguration config =
authenticationConfiguration.forRealm(realmContext);
Duration maxTokenGeneration = config.tokenBroker().maxTokenGeneration();
- RSAKeyPairConfiguration keyPairConfiguration =
- config.tokenBroker().rsaKeyPair().orElseGet(this::generateKeyPair);
+ KeyProvider keyProvider =
+ config
+ .tokenBroker()
+ .rsaKeyPair()
+ .map(this::fileSystemKeyPair)
+ .orElseGet(this::generateEphemeralKeyPair);
PolarisMetaStoreManager metaStoreManager =
metaStoreManagerFactory.getOrCreateMetaStoreManager(realmContext);
- return new JWTRSAKeyPair(
- metaStoreManager,
- (int) maxTokenGeneration.toSeconds(),
- keyPairConfiguration.publicKeyFile(),
- keyPairConfiguration.privateKeyFile());
+ return new JWTRSAKeyPair(metaStoreManager, (int)
maxTokenGeneration.toSeconds(), keyProvider);
}
- private RSAKeyPairConfiguration generateKeyPair() {
+ private KeyProvider fileSystemKeyPair(RSAKeyPairConfiguration config) {
+ return LocalRSAKeyProvider.fromFiles(config.publicKeyFile(),
config.privateKeyFile());
+ }
+
+ private KeyProvider generateEphemeralKeyPair() {
try {
- Path privateFileLocation = Files.createTempFile("polaris-private",
".pem");
- Path publicFileLocation = Files.createTempFile("polaris-public", ".pem");
- PemUtils.generateKeyPair(privateFileLocation, publicFileLocation);
- return new GeneratedKeyPair(privateFileLocation, publicFileLocation);
- } catch (IOException | NoSuchAlgorithmException e) {
+ return new LocalRSAKeyProvider(PemUtils.generateKeyPair());
+ } catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
-
- private record GeneratedKeyPair(Path privateKeyFile, Path publicKeyFile)
- implements RSAKeyPairConfiguration {}
}
diff --git
a/service/common/src/main/java/org/apache/polaris/service/auth/LocalRSAKeyProvider.java
b/service/common/src/main/java/org/apache/polaris/service/auth/LocalRSAKeyProvider.java
index a42736844..d9fba39ac 100644
---
a/service/common/src/main/java/org/apache/polaris/service/auth/LocalRSAKeyProvider.java
+++
b/service/common/src/main/java/org/apache/polaris/service/auth/LocalRSAKeyProvider.java
@@ -18,27 +18,55 @@
*/
package org.apache.polaris.service.auth;
+import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.nio.file.Path;
+import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-/**
- * Class that can load public / private keys stored on localhost. Meant to be
a simple
- * implementation for now where a PEM file is loaded off disk.
- */
+/** Holds a public / private key pair in memory. */
public class LocalRSAKeyProvider implements KeyProvider {
private static final Logger LOGGER =
LoggerFactory.getLogger(LocalRSAKeyProvider.class);
- private final Path publicKeyFileLocation;
- private final Path privateKeyFileLocation;
+ private final PublicKey publicKey;
+ private final PrivateKey privateKey;
+
+ public LocalRSAKeyProvider(@Nonnull KeyPair keyPair) {
+ this(keyPair.getPublic(), keyPair.getPrivate());
+ }
+
+ public LocalRSAKeyProvider(@Nonnull PublicKey publicKey, @Nonnull PrivateKey
privateKey) {
+ this.publicKey = publicKey;
+ this.privateKey = privateKey;
+ }
- public LocalRSAKeyProvider(Path publicKeyFileLocation, Path
privateKeyFileLocation) {
- this.publicKeyFileLocation = publicKeyFileLocation;
- this.privateKeyFileLocation = privateKeyFileLocation;
+ public static LocalRSAKeyProvider fromFiles(
+ @Nonnull Path publicKeyFile, @Nonnull Path privateKeyFile) {
+ return new LocalRSAKeyProvider(
+ readPublicKeyFile(publicKeyFile), readPrivateKeyFile(privateKeyFile));
+ }
+
+ private static PrivateKey readPrivateKeyFile(Path privateKeyFileLocation) {
+ try {
+ return PemUtils.readPrivateKeyFromFile(privateKeyFileLocation, "RSA");
+ } catch (IOException e) {
+ LOGGER.error("Unable to read private key from file {}",
privateKeyFileLocation, e);
+ throw new RuntimeException(
+ "Unable to read private key from file " + privateKeyFileLocation, e);
+ }
+ }
+
+ private static PublicKey readPublicKeyFile(Path publicKeyFileLocation) {
+ try {
+ return PemUtils.readPublicKeyFromFile(publicKeyFileLocation, "RSA");
+ } catch (IOException e) {
+ LOGGER.error("Unable to read public key from file {}",
publicKeyFileLocation, e);
+ throw new RuntimeException("Unable to read public key from file " +
publicKeyFileLocation, e);
+ }
}
/**
@@ -48,12 +76,7 @@ public class LocalRSAKeyProvider implements KeyProvider {
*/
@Override
public PublicKey getPublicKey() {
- try {
- return PemUtils.readPublicKeyFromFile(publicKeyFileLocation, "RSA");
- } catch (IOException e) {
- LOGGER.error("Unable to read public key from file {}",
publicKeyFileLocation, e);
- throw new RuntimeException("Unable to read public key from file " +
publicKeyFileLocation, e);
- }
+ return publicKey;
}
/**
@@ -63,12 +86,6 @@ public class LocalRSAKeyProvider implements KeyProvider {
*/
@Override
public PrivateKey getPrivateKey() {
- try {
- return PemUtils.readPrivateKeyFromFile(privateKeyFileLocation, "RSA");
- } catch (IOException e) {
- LOGGER.error("Unable to read private key from file {}",
privateKeyFileLocation, e);
- throw new RuntimeException(
- "Unable to read private key from file " + privateKeyFileLocation, e);
- }
+ return privateKey;
}
}
diff --git
a/service/common/src/main/java/org/apache/polaris/service/auth/PemUtils.java
b/service/common/src/main/java/org/apache/polaris/service/auth/PemUtils.java
index 375599013..c93942401 100644
--- a/service/common/src/main/java/org/apache/polaris/service/auth/PemUtils.java
+++ b/service/common/src/main/java/org/apache/polaris/service/auth/PemUtils.java
@@ -120,15 +120,23 @@ public class PemUtils {
return PemUtils.getPrivateKey(bytes, algorithm);
}
- public static void generateKeyPair(Path privateFileLocation, Path
publicFileLocation)
- throws NoSuchAlgorithmException, IOException {
+ public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
- KeyPair kp = kpg.generateKeyPair();
+ return kpg.generateKeyPair();
+ }
+
+ public static void generateKeyPairFiles(Path privateFileLocation, Path
publicFileLocation)
+ throws NoSuchAlgorithmException, IOException {
+ writeKeyPairFiles(generateKeyPair(), privateFileLocation,
publicFileLocation);
+ }
+
+ public static void writeKeyPairFiles(
+ KeyPair keyPair, Path privateFileLocation, Path publicFileLocation)
throws IOException {
try (BufferedWriter writer = Files.newBufferedWriter(privateFileLocation,
UTF_8)) {
writer.write("-----BEGIN PRIVATE KEY-----");
writer.newLine();
-
writer.write(Base64.getMimeEncoder().encodeToString(kp.getPrivate().getEncoded()));
+
writer.write(Base64.getMimeEncoder().encodeToString(keyPair.getPrivate().getEncoded()));
writer.newLine();
writer.write("-----END PRIVATE KEY-----");
writer.newLine();
@@ -136,7 +144,7 @@ public class PemUtils {
try (BufferedWriter writer = Files.newBufferedWriter(publicFileLocation,
UTF_8)) {
writer.write("-----BEGIN PUBLIC KEY-----");
writer.newLine();
-
writer.write(Base64.getMimeEncoder().encodeToString(kp.getPublic().getEncoded()));
+
writer.write(Base64.getMimeEncoder().encodeToString(keyPair.getPublic().getEncoded()));
writer.newLine();
writer.write("-----END PUBLIC KEY-----");
writer.newLine();
diff --git
a/service/common/src/test/java/org/apache/polaris/service/auth/LocalRSAKeyProviderTest.java
b/service/common/src/test/java/org/apache/polaris/service/auth/LocalRSAKeyProviderTest.java
new file mode 100644
index 000000000..e671d3d90
--- /dev/null
+++
b/service/common/src/test/java/org/apache/polaris/service/auth/LocalRSAKeyProviderTest.java
@@ -0,0 +1,75 @@
+/*
+ * 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.polaris.service.auth;
+
+import static org.assertj.core.api.InstanceOfAssertFactories.BYTE_ARRAY;
+
+import java.nio.file.Path;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import org.assertj.core.api.SoftAssertions;
+import org.assertj.core.api.junit.jupiter.InjectSoftAssertions;
+import org.assertj.core.api.junit.jupiter.SoftAssertionsExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.io.TempDir;
+
+@ExtendWith(SoftAssertionsExtension.class)
+public class LocalRSAKeyProviderTest {
+ @InjectSoftAssertions SoftAssertions soft;
+
+ @Test
+ public void fromFiles(@TempDir Path tempDir) throws Exception {
+ var publicKeyFile = tempDir.resolve("public.key");
+ var privateKeyFile = tempDir.resolve("private.key");
+ PemUtils.generateKeyPairFiles(privateKeyFile, publicKeyFile);
+
+ var generatedPublicKey = PemUtils.readPublicKeyFromFile(publicKeyFile,
"RSA");
+ var generatedPrivateKey = PemUtils.readPrivateKeyFromFile(privateKeyFile,
"RSA");
+
+ var keyProvider = LocalRSAKeyProvider.fromFiles(publicKeyFile,
privateKeyFile);
+ soft.assertThat(keyProvider)
+ .extracting(KeyProvider::getPublicKey)
+ .extracting(PublicKey::getEncoded, BYTE_ARRAY)
+ .containsExactly(generatedPublicKey.getEncoded());
+ soft.assertThat(keyProvider)
+ .extracting(KeyProvider::getPrivateKey)
+ .extracting(PrivateKey::getEncoded, BYTE_ARRAY)
+ .containsExactly(generatedPrivateKey.getEncoded());
+ }
+
+ @Test
+ public void onHeap() throws Exception {
+ var keyPair = PemUtils.generateKeyPair();
+
+ var generatedPublicKey = keyPair.getPublic();
+ var generatedPrivateKey = keyPair.getPrivate();
+
+ var keyProvider = new LocalRSAKeyProvider(keyPair);
+ soft.assertThat(keyProvider)
+ .extracting(KeyProvider::getPublicKey)
+ .extracting(PublicKey::getEncoded, BYTE_ARRAY)
+ .containsExactly(generatedPublicKey.getEncoded());
+ soft.assertThat(keyProvider)
+ .extracting(KeyProvider::getPrivateKey)
+ .extracting(PrivateKey::getEncoded, BYTE_ARRAY)
+ .containsExactly(generatedPrivateKey.getEncoded());
+ }
+}
diff --git a/site/content/in-dev/unreleased/configuration.md
b/site/content/in-dev/unreleased/configuration.md
index 9da84c7d5..95d77230f 100644
--- a/site/content/in-dev/unreleased/configuration.md
+++ b/site/content/in-dev/unreleased/configuration.md
@@ -93,10 +93,10 @@ read-only mode, as Polaris only reads the configuration
file once, at startup.
|
`polaris.features.realm-overrides."my-realm"."SKIP_CREDENTIAL_SUBSCOPING_INDIRECTION"`
| `true` | "Override" realm features, here the skip credential
subscoping indirection flag.
|
| `polaris.authentication.authenticator.type`
| `default` | Define the Polaris authenticator type.
|
| `polaris.authentication.token-service.type`
| `default` | Define the Polaris token service type.
|
-| `polaris.authentication.token-broker.type`
| `rsa-key-pair` | Define the Polaris token broker type.
|
+| `polaris.authentication.token-broker.type`
| `rsa-key-pair` | Define the Polaris token broker type. Also
configure the location of the key files. For RSA: if the locations of the key
files are not configured, an ephemeral key-pair will be created on each Polaris
server instance startup, which breaks existing tokens after server restarts and
is also incompatible with running multiple Polaris server instances. |
| `polaris.authentication.token-broker.max-token-generation`
| `PT1H` | Define the max token generation policy on
the token broker.
|
-| `polaris.authentication.token-broker.rsa-key-pair.public-key-file`
| `/tmp/public.key` | Define the location of the public key file.
|
-| `polaris.authentication.token-broker.rsa-key-pair.private-key-file`
| `/tmp/private.key` | Define the location of the private key
file.
|
+| `polaris.authentication.token-broker.rsa-key-pair.private-key-file`
| | Define the location of the RSA-256 private
key file, if present the `public-key` file must be specified, too.
|
+| `polaris.authentication.token-broker.rsa-key-pair.public-key-file`
| | Define the location of the RSA-256 public
key file, if present the `private-key` file must be specified, too.
|
| `polaris.authentication.token-broker.symmetric-key.secret`
| `secret` | Define the secret of the symmetric key.
|
| `polaris.authentication.token-broker.symmetric-key.file`
| `/tmp/symmetric.key` | Define the location of the symmetric key
file.
|
| `polaris.storage.aws.access-key`
| `accessKey` | Define the AWS S3 access key. If unset, the
default credential provider chain will be used.
|