This is Part 3 of a 3-part series that explores how to give our enterprise Java + Spring applications identities of their own via Azure AD managed identities. In this article, you will learn how to work with Azure Vault using managed identities for a Spring Boot application deployed on Azure Kubernetes Service.
In the first two articles of this three-part series, we explored using Azure AD managed identities to connect an application hosted on Azure App Service, then Azure Kubernetes Service. In addition to enabling an application to access services such as a database, managed identities can retrieve credentials from a key store.
This third and final article of the series will guide you through working with Azure Vault using managed identities for a Spring Boot application deployed on Azure Kubernetes Service. Azure Key Vault stores credentials for various services, including Azure and external services.
It is best to fetch API access keys from a key-value store instead of managing them in code, keeping the credentials and the services they unlock safe from unauthorized access and reducing the chance of error. Instead of managing secrets ourselves, we can outsource the task to a service specializing in storing them. So, we'll create an application-specific identity and access Azure Key Vault using that identity.
Prerequisites
To follow this tutorial, you should be familiar with Java and set up a few things first:
- Access to Azure Cloud Platform. It’s free to sign up for 12 months of access to popular services and offers a $200 credit.
- Access to the SendGrid email platform. It’s free to sign up for up to 100 emails per day.
- The Rkord application. This demo Spring Boot application enables making user notes. We developed the application using Spring MVC, the Thymeleaf framework, and Cosmos DB. Previously, we hosted it on the Azure App Service, then the Azure Kubernetes Service.
You can find the complete code for this article’s version of the Rkord application on GitHub.
Creating a Spring Boot Application
So far, the Rkord application can take user notes in the Markdown format. It also displays the previous user notes using the oldest-first approach.
The application’s new requirement is to share user notes via email without any specific email format. The mail message contains only the selected user notes in HTML format. Moreover, our application will send the mail using the SendGrid
external service.
To enhance the app with mail capabilities, start by adding the SendGrid
Java SDK:
<dependency>
<groupId>com.sendgrid</groupId>
<artifactId>sendgrid-java</artifactId>
<version>4.8.1</version>
</dependency>
Updating the Notes Controller
Sharing notes over email requires a POST
API with the recipient’s email address parameters. So, add the following method to the NotesController
class. The method first retrieves a user note using the specified ID. Then, it generates a mail message using mail helper classes and invokes the SendGrid
email-sending API.
@PostMapping("/note/{id}/share")
public String shareNote(@PathVariable String id, @RequestParam String emailAddress) {
Notes note=notesRepository.findById(id).block();
String subject = "A Note has been shared";
Email to = new Email(emailAddress);
Content content = new Content("text/html",note.getMessage());
Mail mail = new Mail(from, subject, to, content);
Request request = new Request();
try {
request.setMethod(Method.POST);
request.setEndpoint("mail/send");
request.setBody(mail.build());
sendGrid.api(request);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
return "redirect:/";
}
Note that email messages support various types of content like plain text, HTML, XML, and more. The Rkord application only requires the text/html
type.
Updating Notes HTML
Next, update the Thymeleaf template notes.html (under src/main/resources/template) to render a text field with an email button below every displayed user note. A Bootstrap input group feature creates the user interface (UI) components.
<div class="mb-3" th:each="msg : ${userNotes}">
<div th:utext="${msg.message}" class="alert alert-primary"></div>
<form th:action="@{/note/__${msg.id}__/share}" method="post">
<div class="input-group">
<button class="btn btn-outline-secondary"
type="submit" id="button-addon1">Email</button>
<input type="text" name="emailAddress"
id="emailAddress" class="form-control"
placeholder="Recipient's email address"
aria-label="Recipient's email address"
aria-describedby="button-addon1">
</div>
</form>
</div>
Configuring SendGrid
The NoteContoller
method requires an instance of SendGrid
and the associated mail-from address. There is no SendGrid
starter configuration available in the class path. So, create the following config class, which provides the required beans:
@Configuration
public class SendgridConfig {
@Value("${sendgrid.key}")
private String key;
@Value("${sendgrid.from}")
private String from;
@Bean
public SendGrid sendGridBean() {
SendGrid sg = new SendGrid(key);
return sg;
}
@Bean
public Email emailFrom() {
return new Email(from);
}
}
You must specify your SendGrid
API key using SendGrid.key
and a configured sender email address using sendgrid.from
properties.
sendgrid.key=A_VALID_KEY
sendgrid.from=mail@example.com
The application should build successfully. As discussed in the previous article, you can commit the changes and deploy the application on Azure Kubernetes Service.
Creating an Azure Key Vault Service Instance
The above application works as expected. However, it manages the access keys in code, potentially leading to security issues like unintended charges on your account, unauthorized access, malicious content, potential attacks, and more.
It is better to avoid managing API keys and instead outsource them to a service specializing in storing secrets. You can store API keys in the Azure Key Vault store, which the Rkord application accesses using the managed identity.
Start by creating an instance of the Key Vault service from the Azure portal. Specify a name, resource group, and the intended pricing tier. Review the specified configuration, then click Create to generate an instance.
Next, add the SendGrid
API keys by clicking Generate/Import to the Secrets console. Specify the key Name as SENDGRID-KEY
with Value as the API key.
Setting up Key Vault RBAC
Previously, we enabled the Azure Kubernetes cluster for managed identity exchange. The managed identity service provisions the following credentials:
- Kubernetes service credential, used to access the API server
- Kubelet service credential, used by Nodes to access other services
The application Pod will use the Kubelet service credential to access Key Vault. So, determine the kubelet service principal using this command:
az aks show --resource-group rkord-notes-rg --name rkord-dev-aks
--query "identityProfile.kubeletidentity"
Next, allow Key Vault access to the above service principal. Open the access policy console to create the required access policy. Select Secret Management from the Configure from template dropdown. The value will configure the required permissions. Next, specify the AKS agent pool identity as the principal for the policy and click Add.
Integrating Azure Key Vault Service
Azure Key Vault Service must be intergrated using a language-specific SDK. So, add the SDK to the set of project dependencies:
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-secrets</artifactId>
<version>4.3.5</version>
</dependency>
Since managed identity integrates with the Key Vault service, add the azure-identity
library to the list of project dependencies:
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-identity</artifactId>
<version>1.3.7</version>
</dependency>
You must configure the Key Vault client to connect using the managed identity. Then, load the required key and create an instance of SendGrid
beans.
The application tests do not need this lookup. They will work with a dummy key, since tests do not perform mail sending. So, move the existing SendGridConfig
class to the test folder.
Previously, we have created a configuration to load the managed identity (using ManagedIdentityCredential
) for Cosmos DB integration. There are no necessary configuration changes to use it with Key Vault.
@Profile("prod")
@Configuration
public class CosmosDBConfig extends AbstractCosmosConfiguration {
@Bean
public ManagedIdentityCredential dbCredential() {
ManagedIdentityCredential managedIdentityCredential =
new ManagedIdentityCredentialBuilder()
.build();
return managedIdentityCredential;
}
}
Next, create the following configuration class, which uses managed identity with the Key Vault client. After connecting to the Key Vault service, you can load values based on the named key saved in the instance. The configuration class requires properties for the Key Vault instance location, so add them to the application.properties file in the src/main/resources directory.
@Profile("prod")
@Configuration
public class SendgridConfig {
@Value("${sendgrid.from}")
private String from;
@Value("${vault.loc}")
private String loc;
@Bean
public SendGrid sendGridBean(ManagedIdentityCredential identityCredential) {
SecretClient client = new SecretClientBuilder()
.vaultUrl(loc)
.credential(identityCredential)
.buildClient();
KeyVaultSecret key = client.getSecret("SENDGRID-KEY");
SendGrid sg = new SendGrid(key.getValue());
return sg;
}
@Bean
public Email emailFrom() {
return new Email(from);
}
}
The above class is marked with profile annotations and thus works only when the respective Spring profile is activated. Previously, we enabled the profile for the container by modifying CMD
in the Dockerfile, like this:
CMD ["java", "-jar", "-Dspring.profiles.active=prod","/rkord.jar"]
Push the changes to the GitHub repository to invoke build and deployment. After successful deployment, we determine the application URL from the AKS ingress console.
You can email user notes by providing an email address for the corresponding note.
Summary
You now know how to store secrets in Azure Key Vault and use managed identities to access the key store. The process provides secured credential exchange for connecting to external third-party services and Azure services. Managed identities combined with Azure Key Vault solve the challenge of credential management and security, freeing developers to focus on adding value to the application.
Throughout this series, we have explored how to use managed identities to help applications on the Azure App Service and the Azure Kubernetes Service to access a database. We have also used managed identities to obtain credentials stored securely in a key store. Now you have all the tools you need to ensure your services are secure while still enabling your applications to access the services and data they need.
Apply these same principles to your own Java applications deployed on Azure by setting up managed identities today.
To learn more about Java Azure Identity library, check out Azure authentication with Java and Azure Identity.