Managing the secrets of your applications is an integral part of taking an application to production. After all, they contain the keys to your kingdom.
Many Java frameworks provide out-of-the-box integration with Vault; in most cases, you do not even need to write any boilerplate code. In this article, I will demonstrate how Micronaut integrates with Vault, what are some of the challenges with this method and how we can overcome these challenges.
Micronaut-Vault Integration:
Micronaut supports integration with vault through this dependency:
implementation("io.micronaut.discovery:micronaut-discovery-client")
Now all you have to do is create a boostrap-${env}.yml file and things should work on their own.
Note: You can use these properties in your application-${env}.yml.
This method should work fine for most of the cases. However, there are a few problems with this approach.
- You can integrate with Vault, only through a token. This is not convenient when you want to use other auth-methods with vault, such as K8s authentication (Through a service-account). Related Github issue
- The documentation does not seem to be completely accurate and is very limited. For example, secrets are read from /secret/data/[application-name] but it automatically converts the application name from camelCase to kebab-case. Github repo that shows this behaviour.
So, is there a workaround for these problems?
Enter Agent Sidecar Injector
Note: Sidecar is a feature that is available as part of the vault installation. If you do not have it on, you need to enable it for this feature to work.
A better way to integrate with Vault would be to use Vault’s sidecar injector.
When you annotate your deployment or your pod with specific annotations , as shown in this guide, Vault sidecar will start an init-container and write secrets to your pod’s file-system.
If you want a detailed guide on how this works, you can explore this link. However, this guide should be able to give you all you need.
How do I execute this?
Vault injector allows templatizing to modify the format of the secret when it is written to the filesystem. Refer here.
Now we just have to put these two together!
We inject the secrets to be in the same format as the configuration files and specify the secrets as additional configuration files using the MICRONAUT_CONFIG_FILES env var.
Let us consider an example where database credentials are fetched from Vault.
- The “vault.hashicorp.com/agent-inject” annotation must be set to true for Vault injector to attempt injection.
- The “vault.hashicorp.com/role” annotation is used to specify the Vault role that the pod must be assigned. This determines which secrets are allowed to be accessed by the pod.
- The “vault.hashicorp.com/agent-inject-secret-filename” annotation is used to specify the filename and the path of the secret on Vault that is to be injected. In our example, filename would be db-creds.yaml.
- The “vault.hashicorp.com/agent-inject-template-filename” annotation is used to modify the format of the file using templates to fit our specific needs. In our example, filename would be db-creds.yaml.
- Finally, the MICRONAUT_CONFIG_FILES env var is set to the path of the file that contains our secret.
Terraform script to create the Vault role and attach required policies to it:
In our example, Micronaut SQL Libraries will use these values when establishing the connection. Note: The example given is specific to Micronaut integration with SQL but can be modified to fit your needs easily.
The values can also be accessed manually as you would access any property using the @Property or @Value annotations.
Final thoughts
While this approach works best for our specific use case, depending on how your infrastructure is set up, you might be fine using micronaut’s solution to integrate with vault using the discovery client and a token for authentication.
However, this checks all the boxes for us as it is secure and works well with the k8s environment where storing a vault token poses issues.