Skip to content

Commit

Permalink
Load and reference secrets from Azure Key Vault in application.proper…
Browse files Browse the repository at this point in the history
…ties file (#266)

* add key vault config

* implement key vault secret config and add UT.

* add integration test

* update key vault doc.

* update screenshot of key vault.

* update CI/CD.

* test CI

* fix format issue

* fix keyvault IT failure.

* revert test setting for pr266.

* On branch keyvault-config Copyediting.
modified:   docs/modules/ROOT/pages/quarkus-azure-key-vault.adoc

Signed-off-by: Ed Burns <[email protected]>

* On branch keyvault-config Simple javadoc for getSecretIdentifier.
modified:   extensions/azure-keyvault/runtime/src/main/java/io/quarkiverse/azure/keyvault/secret/runtime/config/KeyVaultSecretConfigUtil.java

Signed-off-by: Ed Burns <[email protected]>

* apply feedback.

* format file

* fix secret builder failure.

* do not public KeyVaultSecretConfigSource class.

* consider different key vault DNS.

* add UT.

* fix extension id in the Installation section of doc.

* add IT test case for not existing secret.

* revert IT changes as the application is not able to start if the property does not exist.

* add DNS corner case

---------

Signed-off-by: Ed Burns <[email protected]>
Co-authored-by: Haixia Cheng <[email protected]>
Co-authored-by: Ed Burns <[email protected]>
  • Loading branch information
3 people authored Aug 8, 2024
1 parent 17b4120 commit c08be36
Show file tree
Hide file tree
Showing 20 changed files with 571 additions and 34 deletions.
5 changes: 5 additions & 0 deletions .github/build-with-maven-native.sh
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,11 @@ az ad signed-in-user show --query id -o tsv \
--object-id @- \
--secret-permissions all

az keyvault secret set \
--vault-name "${KEY_VAULT_NAME}" \
--name secret1 \
--value mysecret

export QUARKUS_AZURE_KEYVAULT_SECRET_ENDPOINT=$(az keyvault show --name "${KEY_VAULT_NAME}" \
--resource-group "${RESOURCE_GROUP_NAME}" \
--query properties.vaultUri\
Expand Down
7 changes: 7 additions & 0 deletions bom/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
<msal4j.version>1.16.1</msal4j.version>
<!-- needed for dependency convergence until Azure SDK is upgraded -->
<woodstox-core.version>7.0.0</woodstox-core.version>
<assertj-core.version>3.26.3</assertj-core.version>
</properties>

<dependencyManagement>
Expand Down Expand Up @@ -137,6 +138,12 @@
<artifactId>quarkus-azure-storage-blob-deployment</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Dependencies for testing -->
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This extension allows to inject a `io.smallrye.config.SmallRyeConfig` object ins

== Installation

If you want to use this extension, you need to add the `io.quarkiverse.azureservices:quarkus-azure-services` extension first to your build file.
If you want to use this extension, you need to add the `io.quarkiverse.azureservices:quarkus-azure-app-configuration` extension first to your build file.

For instance, with Maven, add the following dependency to your POM file:

Expand Down
86 changes: 74 additions & 12 deletions docs/modules/ROOT/pages/quarkus-azure-key-vault.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ include::./includes/attributes.adoc[]
include::./includes/support.adoc[]

https://azure.microsoft.com/en-us/products/key-vault[Azure Key Vault] is a cloud service for securely storing and accessing secrets. A secret is anything that you want to tightly control access to, such as API keys, passwords, certificates, or cryptographic keys.
This extension allows you to create and retrieve secret from Azure Key Vault by injecting the following object inside your Quarkus application.
This extension allows you to:

* `com.azure.security.keyvault.secrets.SecretClient`
* `com.azure.security.keyvault.secrets.SecretAsyncClient`
* Create and retrieve secrets from Azure Key Vault by injecting the one or both of the following objects in a Quarkus application.
- `com.azure.security.keyvault.secrets.SecretClient`
- `com.azure.security.keyvault.secrets.SecretAsyncClient`
* Use secrets from the key vault directly inside your `application.properties`.
The extension produces SecretClient and SecretAsyncClient using https://learn.microsoft.com/java/api/overview/azure/identity-readme#defaultazurecredential[DefaultAzureCredential].
Developers who want more control or whose scenario isn't served by the default settings should build client using other credential types.
The extension produces `SecretClient` and `SecretAsyncClient` using https://learn.microsoft.com/java/api/overview/azure/identity-readme#defaultazurecredential[DefaultAzureCredential].
Developers who want more control or whose scenario isn't served by the default settings should build their clients using other credential types.

== Installation

If you want to use this extension, you need to add the `io.quarkiverse.azureservices:quarkus-azure-services` extension first to your build file.
Add the `io.quarkiverse.azureservices:quarkus-azure-keyvault` extension first to your build file.

For instance, with Maven, add the following dependency to your POM file:

Expand All @@ -30,7 +33,7 @@ For instance, with Maven, add the following dependency to your POM file:

== How to Use It

Once you have added the extension to your project, follow the next steps, so you can inject `com.azure.security.keyvault.secrets.SecretClient` or `com.azure.security.keyvault.secrets.SecretAsyncClient` object in your application to manage secret.
Once you have added the extension to your project, follow the next steps, so you can inject `com.azure.security.keyvault.secrets.SecretClient` or `com.azure.security.keyvault.secrets.SecretAsyncClient` in your application to manage secrets.

=== Setup your Azure Environment

Expand Down Expand Up @@ -60,8 +63,8 @@ Key Vault provides secure storage of generic secrets, such as passwords and data
All secrets in your key vault are stored encrypted. The Azure Key Vault service encrypts your secrets when you add them, and decrypts them automatically when you read them.

Access Control for secrets managed in Key Vault, is provided at the level of the Key Vault.
The following command uses your Microsoft Entra ID to authorize the operation to manage secret.
Even if you are the key vault owner, you need explicit permissions to perform operations against secret.
The following command uses your Microsoft Entra ID to authorize the operation to manage secrets.
Even if you are the key vault owner, you need explicit permissions to perform operations against secrets.

Assign all secret permissions(backup, delete, get, list, purge, recover, restore, set) to yourself:

Expand All @@ -74,7 +77,17 @@ az ad signed-in-user show --query id -o tsv \
--secret-permissions all
----

If you log into the http://portal.azure.com/[Azure portal], you can see the key vault you created. Select **Objects** -> **Secrets**, you will find the Secrets page.
Create a secret **secret1** with value `mysecret`.

[source,shell]
```
az keyvault secret set \
--vault-name kvquarkusazurekv0423 \
--name secret1 \
--value mysecret
```

If you sign into the http://portal.azure.com/[Azure portal], you can see the key vault you created. Select **Objects** -> **Secrets**, you will find the Secrets page.

image::quarkus-azure-keyvault-secret-portal.png[alt=Azure Portal showing Key Vault Secrets]

Expand All @@ -91,9 +104,43 @@ export QUARKUS_AZURE_KEYVAULT_SECRET_ENDPOINT=$(az keyvault show --name kvquarku
--output tsv)
----

Notice that you get the endpoint and set it to environment variable `QUARKUS_AZURE_KEYVAULT_SECRET_ENDPOINT`, instead of setting it to property `quarkus.azure.keyvault.secret.endpoint` in the `application.properties` file.
Notice that you get the endpoint and set it to environment variable `QUARKUS_AZURE_KEYVAULT_SECRET_ENDPOINT`, instead of setting it to property `quarkus.azure.keyvault.secret.endpoint` in the `application.properties` file.

Due to Quarkus implementing the MicroProfile Config specification, both approaches work. Using the environment variable is recommended, more flexible and more secure as there's no risk of committing the endpoint to source control.

=== Read Secrets as Properties

You can load and reference secrets from Azure Key Vault in your application.properties file by using the following syntax. A hierarchical approach is supported, similar to that found in Google Cloud Secret Manager.

Although technically both approaches work, using environment variable is recommended, more flexible and more secure as there's no risk of committing the endpoint to source control.
The syntax supports loading secrets from multiple key vault. Default key vault is configed with `quarkus.azure.keyvault.secret.endpoint`.
[source,property]
----
# 1. Load secret from default key vault - specify the secret name and use the latest version
${kv//<secret-name>}
# 2. Load secret from default key vault - specify the secret name and version
${kv//<secret-name>/versions/<version-id>}
# 3. Load secret from a specified key vault - specify key vault name, secret name, and use latest version
${kv//<keyvault-name>/<secret-name>}
# 4. Load secret from a specified key vault - specify key vault name, secret name, and use latest version
${kv//<keyvault-name>/secrets/<secret-name>}
# 5. Load secret from a specified key vault - specify key vault name, secret name, and version
${kv//<keyvault-name>/secrets/<secret-name>/<version-id>}
# 6. Load secret from a specified key vault - specify key vault name, secret name, and version
${kv//<keyvault-name>/secret/<secret-name>/versions/<version-id>}
----

You can use this syntax to load secrets directly from `application.properties`:

[source,java]
----
@ConfigProperty(name = "kv//secret1")
String secret;
----

=== Inject the Azure Key Vault Secret Client

Expand All @@ -120,9 +167,14 @@ public class KeyVaultSecretApplication implements QuarkusApplication {
@Inject
SecretClient secretClient;
@ConfigProperty(name = "kv//secret1")
String secret;
@Override
public int run(String... args) throws Exception {
System.out.println("Print secret1: " + secret);
Console con = System.console();
String secretName = "mySecret";
Expand Down Expand Up @@ -160,6 +212,16 @@ image::quarkus-azure-keyvault-deleted-secret-portal.png[alt=Azure Portal showing

You can also inject `com.azure.security.keyvault.secrets.SecretAsyncClient` object to your application. For more usage, see https://learn.microsoft.com/java/api/com.azure.security.keyvault.secrets.secretasyncclient?view=azure-java-stable[com.azure.security.keyvault.secrets.secretasyncclient].

After the testing, for security consideration, you can rotate the policy with command:

[source,shell]
----
az ad signed-in-user show --query id -o tsv \
| az keyvault delete-policy \
--name kvquarkusazurekv0423 \
--object-id @-
----

== Extension Configuration Reference

include::includes/quarkus-azure-keyvault-secret.adoc[leveloffset=+1, opts=optional]
2 changes: 1 addition & 1 deletion docs/modules/ROOT/pages/quarkus-azure-storage-blob.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ This is a step by step guide on how to use the Quarkus Azure Blob Storage extens

== Installation

If you want to use this extension, you need to add the `io.quarkiverse.azureservices:quarkus-azure-services` extension first to your build file.
If you want to use this extension, you need to add the `io.quarkiverse.azureservices:quarkus-azure-storage-blob` extension first to your build file.

For instance, with Maven, add the following dependency to your POM file:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,11 @@
import com.azure.json.JsonSerializable;

import io.quarkiverse.azure.keyvault.secret.runtime.KeyVaultSecretClientProducer;
import io.quarkiverse.azure.keyvault.secret.runtime.config.KeyVaultSecretConfigBuilder;
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.ExtensionSslNativeSupportBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.IndexDependencyBuildItem;
import io.quarkus.deployment.builditem.*;
import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
Expand All @@ -37,6 +35,11 @@ AdditionalBeanBuildItem producer() {
return new AdditionalBeanBuildItem(KeyVaultSecretClientProducer.class);
}

@BuildStep
void azureKeyVaultSecretConfigFactory(BuildProducer<RunTimeConfigBuilderBuildItem> runTimeConfigBuilder) {
runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(KeyVaultSecretConfigBuilder.class.getName()));
}

@BuildStep
ExtensionSslNativeSupportBuildItem activateSslNativeSupport() {
return new ExtensionSslNativeSupportBuildItem(FEATURE);
Expand Down Expand Up @@ -105,5 +108,4 @@ void proxyDefinitions(
proxyDefinitions.produce(new NativeImageProxyDefinitionBuildItem(
List.of("com.azure.security.keyvault.secrets.implementation.SecretClientImpl$SecretClientService")));
}

}
12 changes: 12 additions & 0 deletions extensions/azure-keyvault/runtime/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@
<groupId>io.quarkiverse.azureservices</groupId>
<artifactId>quarkus-azure-core-util</artifactId>
</dependency>

<!-- Test Dependencies -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import com.azure.security.keyvault.secrets.SecretClientBuilder;

import io.quarkiverse.azure.core.util.AzureQuarkusIdentifier;
import io.quarkiverse.azure.keyvault.secret.runtime.config.KeyVaultSecretConfig;

public class KeyVaultSecretClientProducer {

Expand All @@ -18,29 +19,29 @@ public class KeyVaultSecretClientProducer {

@Produces
public SecretClient createSecretClient() {
if (!secretConfiguration.enabled) {
if (!secretConfiguration.enabled()) {
return null;
}

assert secretConfiguration.endpoint.isPresent() : "The endpoint of Azure Key Vault Secret must be set";
assert secretConfiguration.endpoint().isPresent() : "The endpoint of Azure Key Vault Secret must be set";
return new SecretClientBuilder()
.clientOptions(new ClientOptions().setApplicationId(AzureQuarkusIdentifier.AZURE_QUARKUS_KEY_VAULT_SYNC_CLIENT))
.vaultUrl(secretConfiguration.endpoint.get())
.vaultUrl(secretConfiguration.endpoint().get())
.credential(new DefaultAzureCredentialBuilder().build())
.buildClient();
}

@Produces
public SecretAsyncClient createSecretAsyncClient() {
if (!secretConfiguration.enabled) {
if (!secretConfiguration.enabled()) {
return null;
}

assert secretConfiguration.endpoint.isPresent() : "The endpoint of Azure Key Vault Secret must be set";
assert secretConfiguration.endpoint().isPresent() : "The endpoint of Azure Key Vault Secret must be set";
return new SecretClientBuilder()
.clientOptions(
new ClientOptions().setApplicationId(AzureQuarkusIdentifier.AZURE_QUARKUS_KEY_VAULT_ASYNC_CLIENT))
.vaultUrl(secretConfiguration.endpoint.get())
.vaultUrl(secretConfiguration.endpoint().get())
.credential(new DefaultAzureCredentialBuilder().build())
.buildAsyncClient();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
package io.quarkiverse.azure.keyvault.secret.runtime;
package io.quarkiverse.azure.keyvault.secret.runtime.config;

import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

@ConfigRoot(name = "azure.keyvault.secret", phase = ConfigPhase.RUN_TIME)
public class KeyVaultSecretConfig {

@ConfigMapping(prefix = "quarkus.azure.keyvault.secret")
@ConfigRoot(phase = ConfigPhase.RUN_TIME)
public interface KeyVaultSecretConfig {
/**
* The flag to enable the key vault secret. If set to false, the key vault secret will be disabled
*/
@ConfigItem(defaultValue = "true")
public boolean enabled;
@WithDefault("true")
boolean enabled();

/**
* The endpoint of Azure Key Vault Secret. Required if quarkus.azure.keyvault.secret.enabled is set to true
*/
@ConfigItem
public Optional<String> endpoint;
Optional<String> endpoint();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.quarkiverse.azure.keyvault.secret.runtime.config;

import io.quarkus.runtime.configuration.ConfigBuilder;
import io.smallrye.config.SmallRyeConfigBuilder;

public class KeyVaultSecretConfigBuilder implements ConfigBuilder {
@Override
public SmallRyeConfigBuilder configBuilder(SmallRyeConfigBuilder builder) {
return builder.withSources(new KeyVaultSecretConfigSourceFactory());
}
}
Loading

0 comments on commit c08be36

Please sign in to comment.