In a previous blog post I wrote about how Spring Boot and Kubernetes are widely used together when building modern microservices. This post is a natural sequel to the aforementioned post, so it’s worth reading that first if you haven’t already done so.

Whilst I covered a good example of how Kubernetes ConfigMaps can be utilised in order to externalise application configuration, I raised some questions about security and how to store and use sensitive information in Spring Boot applications.

Secure Configuration is Good Configuration

As discussed in the datasource example in my last post, externalizing application config with ConfigMaps has lots of benefits. On the other hand, whilst ConfigMaps provide flexibility and aid with separation of concerns, they don’t give a lot of security. ConfigMaps are great for non-secure configuration, but when security is paramount, such as for datasource credentials, you have Kubernetes Secrets.

Keeping Secrets

Kubernetes Secrets let you store and manage sensitive data. They are great for storing credentials, tokens, keys etc. A word of caution though, Secrets by themselves won’t necessarily solve all of your security concerns. Secrets are stored as unencrypted, base64-encoded strings - which by themselves aren’t very secure. It’s important to secure your Kubernetes cluster as well as the data within it, such as enabling encryption at rest and enabling RBAC rules. There are other, more mature approaches to using Kubernetes Secrets, such as Kamus or Sealed Secrets, but we’re not going into those here, as we’re focussing on using Kubernetes Secrets as a standalone resource.

For the purposes of this post, I’ll be looking at how Kubernetes Secrets can be used to store sensitive information, such as datasource credentials.

Securing the Datasource

We’d previously stored the datasource configuration in a ConfigMap. This time we’re going to create a Secret in Kubernetes and pass the values to our application via environment variables from our Pod.

Creating the Secret

There are several ways to create Secrets; via yaml files or using kubectl. I find it useful to create Secrets using kubectl initially, outputting the result of the command to a yaml file for later usage. To create the Secret we can run the following command:

kubectl create secret generic datasource-credentials --from-literal=username=root --from-literal=password=password

The above command creates us a Secret named datasource-credentials, from the literal values root for username and password for password. Notice the generic parameter as well, this is the default Secret type in Kubernetes and refers to an Opaque Secret. If we wanted to get the output from creating a Secret without actually creating the resource in the cluster, we could add the following two parameters onto the end of the command:

-o yaml --dry-run

These parameters tell kubectl to output the results of the command in yaml format and not to apply to changes to our Kubernetes environment, this way we can copy the results into a yaml file for later use. The full command would look like this:

kubectl create secret generic datasource-credentials --from-literal=username=root --from-literal=password=password -o yaml --dry-run

Now we can check the Secret has been created successfully:

kubectl get secrets

Which should show us an entry like so:

NAME                              TYPE                                  DATA   AGE
datasource-credentials            Opaque                                2      3s

Using the Secret

Now we have our Secret created within our Kubernetes environment, we can use it within our application and Pod configuration.

The changes to the Spring Boot application are minimal, and involve changing the reference values within the application.yml file. Previously the application configuration for the datasource looked like this:

spring:
  config:
    activate:
      on-profile: dev 
  datasource:
    url: ${example.datasource.url}
    username: ${example.datasource.username}
    password: ${example.datasource.password}

Now we just change it so that the username and password properties can be referenced from environment variables:

spring:
  config:
    activate:
      on-profile: dev 
  datasource:
    url: ${example.datasource.url}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}

The environment variables are set within the Kubernetes Pod manifest, the values for which are pulled from the Secret we created earlier. Our Pod manifest needs some additional entries to create those environment variables:

apiVersion: v1
kind: Pod
metadata:
  labels:
    app: config-demo
  name: config-demo
spec:
  containers:
    - image: config-demo:latest
      name: config-demo
      ports:
        - containerPort: 8080
      imagePullPolicy: IfNotPresent
      env:      
        - name: SPRING_PROFILE
          value: dev
        - name: SPRING_APPLICATION_JSON
          valueFrom:
            configMapKeyRef:
              name: spring-config
              key: dev-config.json
        - name: DB_USERNAME
          valueFrom:
            secretKeyRef:
              name: datasource-credentials
              key: username
        - name: DB_PASSWORD
          valueFrom:
            secretKeyRef:
              name: datasource-credentials
              key: password
  restartPolicy: Never                                  

The key changes from the above are these two env entries:

- name: DB_USERNAME
  valueFrom:
    secretKeyRef:
      name: datasource-credentials
      key: username
- name: DB_PASSWORD
  valueFrom:
    secretKeyRef:
      name: datasource-credentials
      key: password

The above approach is to use Secrets as environment variables within the Pod. We create two environment variables, one for DB_USERNAME and one for DB_PASSWORD, each of these gets assigned the values username and password from the datasource-credentials secret respectively. It’s important to note the naming of each environment variable matches the values defined in the Spring Boot application.yml file, otherwise no value would be passed to the running application.

Demo in Action

Now with the changes in place to use Kubernetes Secrets for our datasource credentials, we can apply the Pod changes and hopefully see the application pick them up.

First off, let’s re-create the Pod using the kubectl apply method:

kubectl apply -f example-pod.yaml

Now we can check the Pod is running by using some other kubectl commands (I always do this to sense check what I’ve created):

kubectl get pods

The output should contain an entry for the newly created Pod:

NAME          READY   STATUS    RESTARTS   AGE
config-demo   1/1     Running   0          3s

So our Pod is running, we should now be able to invoke the service to see if everything has worked as expected. In this scenario, we’ll use Kubernetes port-forwarding again as we don’t have a running Kubernetes Service in front of our Pod. In order to do this, we simply run the following command:

kubectl port-forward pod/config-demo 8080:8080

Now we can open up a browser (or using your favourite REST client) and hit our endpoint localhost:8080/hello

When doing so, we should see (or get a response of) Hello World!

This is great, as we know the application is running, but we want to see if our configuration has been injected properly using the environment variables created from our new Secret. We can now check the logs for the running pod with the following command:

kubectl logs pod/config-demo

All being well, we should see the following output at the end of the logs

2020-12-08 08:51:28.525  INFO 7 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
DB URL: jdbc:mysql://dev-endpoint/example
DB Username: root
DB Password: password

Great stuff! It’s all working as expected, our application has picked up the dev-config.json entry from our ConfigMap, which we created previously and is also picking up the username and password values from our newly created Secret.

Every Good Sequel Needs an Ending

So there we have a simple, but useful example of how Kubernetes Secrets can be used to externalize secure Spring Boot configuration. Whilst by themselves Secrets don’t provide a completely secure solution, there are recommended approaches to ensure your Kubernetes cluster itself is secured as well, which makes leveraging Secrets a more secure approach. Following secure patterns and principles within your Kubernetes cluster, such as Zero Trust Architecture, helps ensure you maintain higher levels of trust and security, which in turn makes using resources like Secrets, inherently more secure.

Kubernetes provides several resources that we can leverage in order to externalize our application configuration securely. The benefits of externalizing application configuration are numerous, and with Kubernetes becoming ever-more popular as an orchestration tool, having tried and tested patterns and principles to utilise those resources is more important than ever.

Example Project Code

I hope you’ve learnt something from this post and found the content and example project useful. If you want to see the project I’ve used to accompany this post, it can be found in the example project repository.

Comments