Using Azure KeyVault as property source in Spring Boot

Azure offers the KeyVault as their product to store secrets safely in the environment you deploy your services to.
19.04.2017
Jonatan Reiners
Tags

In my current project we had first contact with the Azure Cloud. Azure offers the KeyVault as their product to store secrets safely in the environment you deploy your services to. While it supports different scenarios, I was focussing my efforts on storing secrets and use it as a source for properties in my Spring Boot application.

When we are using the KeyVault we can store a minimum of secrets to access it in the immediate environment of the application. We can then retrieve all other required secrets from the KeyVault, where we have more control and a central location for our secrets.

First I will start with the implementation. Later I want to mention some considerations if you want to implement it too.

Implementation

Connecting to the KeyVault

Since we are using Kotlin, we have to rely on the Java SDK for azure. I couldn’t find any good documentation that explains the authorization for the KeyVault part of the SDK. The reason is, that the SDK is developed by a very small team and just reached 1.0.0: a lot of documentation is outdated.

The main class to use is the com.microsoft.azure.keyvault.KeyVaultClient. The constructor takes com.microsoft.rest.credentials.ServiceClientCredentials but this is a trap and an example of inheritance gone wrong. In the whole SDK you will find several implementations of that interface but they will not necessarily work with the KeyVaultClient. The implementor of the SDK did a good job of documenting the classes in the KeyVault package therefore a look at the KeyVaultCredentials will reveal the solution. I did nothing else but implementing/copying this straight from the documentation.

class KeyVaultCredentialsImpl : KeyVaultCredentials() {

    override fun doAuthenticate(authorization: String, resource: String, s: String?): String {
        val token = getAccessTokenFromClientCredentials(authorization, resource)
        return token.accessToken
    }

    private fun getAccessTokenFromClientCredentials(authorization: String, resource: String): AuthenticationResult {
        val context: AuthenticationContext
        val result: AuthenticationResult?
        var service: ExecutorService? = null
        val c = KeyVaultConfiguration
        try {
            service = Executors.newFixedThreadPool(1)
            context = AuthenticationContext(authorization, false, service!!)
            val credentials = ClientCredential(c.clientId, c.clientKey)
            val future = context.acquireToken(resource, credentials, null)
            result = future.get()
        } catch (e: Exception) {
            throw RuntimeException(e)
        } finally {
            service!!.shutdown()
        }

        if (result == null) {
            throw RuntimeException("authentication result was null")
        }
        return result
    }
}

This class can be used to create an instance of the KeyVaultClient.

val kvClient = KeyVaultClient(KeyVaultCredentialsImpl())

The KeyVaultConfiguration is pulling the client id and secret from the environment. This is the recommended way by Microsoft and you can use the following guide to generate them.

Granting access to KeyVault

I can recommend the Azure CLI for the job.

az key vault set-policy --name YourKeyVaultName \
          --spn na692-your-service-priciple-client-id \
          -g ResourceGroup \
          --secret-permission get list

Both operations implemented later require the get and list permissions.

Registering a property source in spring

While the process is actually very simple, I had a hard time finding all the bits and pieces of documentation and fit them together.

Step 1

Create an initializer for Spring to load the property source.

class KeyVaultPropertyInitializer : ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {

	   override fun initialize(context: ConfigurableApplicationContext) {
	       val kvClient = KeyVaultClient(KeyVaultCredentialsImpl())
	       val env = context.environment

	       try {
	           KeyVaultConfiguration.valid() // throws exception
	           env.propertySources.addFirst(KeyVaultPropertySource(KeyVaultOperations(kvClient)))
	       } catch (e: IllegalArgumentException) {
	           println(e.message)
	       }
	   }

	   override fun getOrder(): Int {
	       return Ordered.HIGHEST_PRECEDENCE + 10
	   }
}

You implement the initialize method where you create the KeyVaultClient, then create your KeyVaultPropertySource and register it as the first property source.

Being the first property source has the advantage that you can have development or fake values for your secrets in your config files, but if they are available in the KeyVault they will be overwritten. This is beneficial in the production environment, but can become confusing on your local machine. Microsoft allows you to connect to the KeyVault from any machine if you have the right credentials. So better keep this in mind!

I also want to run this initializer early in the process of starting up, since other libraries might expect secrets during their initialization process. The +10 on getOrder() allows to sneak in another initializer to be first, but this should be done consciously.

Step 2

Register the initializer for the Spring startup.

public static void main(String[] args) {
    new SpringApplicationBuilder(KcDemoApplication.class)
            .initializers(new KeyVaultPropertyInitializer())
            .run(args);
}

Just use the builder, it’s straightforward.

Step 3

Creating a property source is very easy. While there is a lot of documentation about different approaches and among the many good ways I found the following being very simple and effective. Implement EnumerablePropertySource.

class KeyVaultPropertySource(private val operations: KeyVaultOperations) : EnumerablePropertySource<KeyVaultOperations>("kv", operations) {

    override fun getPropertyNames(): Array<String> {
        val list = operations.list()
        return list
    }


    override fun getProperty(name: String): Any? {
        val property = operations.getProperty(name)
        return property
    }
}

You just have to implement these two methods. The function follows the name. It is important that getProperty can return anything and has to return null if the property cannot be resolved. The property source will be queried for every property since it is the first source. Nice behavior for any input is important here!

I was confused by the string and type cast of the enumerable source. The string "kv" is important to identify the property source. You cannot add the same source twice.

Step 4

Implementing the actual operations on the KeyVault is the most interesting part. Luckily it’s pretty easy and you have few things to watch out for. Logic will stay simple. But let’s dive in, you will see.

fun list(): Array<String> {

    if (strings == null) {

        strings = try {
            val u = kvc.listSecrets(KeyVaultConfiguration.vaultUri)

            u.stream()
                    .map({ it.id() })
                    .map({ it.removePrefix("${sanitizeUri(KeyVaultConfiguration.vaultUri)}secrets/") })
                    .toArray({Array<String>(it, {i -> ""})})

        } catch (e: Exception) {
            arrayOf()
        }

    }
    return strings as Array<String>
}

To create a list of secrets we are pulling all secrets from our KeyVault.

We will save this list in order to not ask the KeyVault too many times. Spring will query our source a lot since it has the highest priority. A little caching is very beneficial here.

We only need the id of the secret for processing. You can do some filtering before if you like.

The id will be the actual name of the secret plus the full URL of the KeyVault. We will clean it. Be careful (case-insensitive) here since the URL will be derived from the name which is case sensitive and you will find different styles throughout the fields of your requests to Azure.

In case of an error I will just save an empty list. This is a fast and easy solution. It will render all queries unsuccessful with very little effort.

The actual collection of the values is lazy compared to fetching the available keys. This is not the optimal solution but sufficient.

fun sanitizeUri(uri: String?): String {
    if (uri != null && uri.matches(".*/$".toRegex())) {
        return uri
    } else {
        return "${uri}/"
    }
}

If you haven’t seen this little helper before: I want to make sure, that there is always a slash in the end since users might omit it in the configuration.

fun getProperty(path: String): Any? {
    var name: String = path.replace("\\.".toRegex(), "--")

    if (Arrays.asList(*list()).contains(name)) {
        try {

            return kvc.getSecret(KeyVaultConfiguration.vaultUri, name).value()

        } catch (e: KeyVaultErrorException) {
            return null
        }

    } else {
        return null
    }
}

Since KeyVault doesn’t support . in the name of a secret we will replace the . with a -- in the requested secret. With this replacement we are able to store any property in the KeyVault since we can map the typical property format to KeyVault names.

We will also check the cached list first to avoid unnecessary calls to the API.

If successful we return the value and null in any other case.

Thoughts on architecture

We have built a very simple service. The simplicity makes it already kind of robust but also limits its functionality.

You should consider the following aspects if you want to implement your own solution.

  1. The list of values is only fetched in the beginning. While this is good enough for most cases you may want to refresh after a while. Fetching values already supports properties added later because it’s lazy.
  2. This implementation fails silently even without log message. You may want to be more verbose or add an option to fail hard. In production you may want your service to fail if you can’t fetch the secrets for any reason.
  3. You can also store more complex data in the KeyVault. My implementation does not support it, but yours maybe should.

Have fun! I hope you enjoyed reading this, learned a little and I could save you some time finding out how to work with Azure.