Injecting credentials into a Docker container running a Spring Boot application

java spring boot docker container


November 5, 2018

The profile concept in Spring Boot makes is extremely easy to provide configuration specific to a runtime environment. A typical setup might include configuration for the local development environment, a testing environment, staging and production. All you need to do is to provide a properties files per environment on the classpath e.g. application-dev.properties and application-prod.properties. At runtime, you can tell Spring Boot which profile to pick via command line option or environment variable.

Now, different environments require different credentials. Let’s say your application uses an embedded H2 database for development purposes and PostgreSQL for all other environments. It feels natural to just add username/password or access tokens to the profile property files. When building the binary file, the executable JAR or WAR file of a Spring Boot application, those files would simply be included into the deliverable.

Depending on how you are planning to share the binary file and the applied deployment strategy this approach may raise security concerns. Should anyone with access to the file allowed to crack open the archive and access the plain-text credentials? What if you build a Docker image for your application and share it on a Docker registry? You might have guessed it. It’s likely a better idea to externalize the credentials and inject them at runtime to elminate a potential security breach.

In this blog post, I am going to show you how to use Docker secrets to create credentials in a Docker Swarm. We’ll run a Spring Boot application on that Swarm distributed to multiple replicas and only inject the credentials when creating a service for that application.

Docker secrets is an enterprise feature. Unfortunately, you won’t be able to use Docker secrets without distributing your application in a Swarm. In a future post, I am going to discuss other options for storing credentials like HashiCorp’s Vault.

Reading a Docker secrets file

Switching between different environments for a Spring Boot application is easy, even if it is run within a Docker container. Simply pass the environment variable SPRING_PROFILES_ACTIVE when starting the container and point it to the desired profile name. In addition, we’ll want to read credentials from a Docker secrets file if it does exist. By default, Docker secrets are available in the directory /run/secrets when run on a node in a Docker Swarm.

A good fit for reading a Docker secret credentials file is an implementation of an EnvironmentPostProcessor. Let’s say we wanted to read a database password and use it for our application’s data source. The class shown in listing 1 achieves exactly that.

DockerSecretsDatabasePasswordProcessor.java

package com.bmuschko.todo.webservice.env;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;
import org.springframework.core.io.FileSystemResource;
import org.springframework.core.io.Resource;
import org.springframework.util.StreamUtils;

import java.io.IOException;
import java.nio.charset.Charset;
import java.util.Properties;

public class DockerSecretsDatabasePasswordProcessor implements EnvironmentPostProcessor {
    private final Logger logger = LoggerFactory.getLogger(DockerSecretsDatabasePasswordProcessor.class);

    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        Resource resource = new FileSystemResource("/run/secrets/db-password");

        if (resource.exists()) {
            try {
                if (logger.isInfoEnabled()) {
                    logger.info("Using database password from injected Docker secret file");
                }

                String dbPassword = StreamUtils.copyToString(resource.getInputStream(), Charset.defaultCharset());
                Properties props = new Properties();
                props.put("spring.datasource.password", dbPassword);
                environment.getPropertySources().addLast(new PropertiesPropertySource("dbProps", props));
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        }
    }
}

Listing 1. Reading and applying database credentials from a Docker secrets file

Implementing the EnvironmentPostProcessor is not enough. You’ll have to register the class in a file named spring.factories. For Maven and Gradle projects this file usually sits in the directory src/main/resources and is bundled with the application when creating the executable binary.

META-INF/spring.factories

org.springframework.boot.env.EnvironmentPostProcessor=com.bmuschko.todo.webservice.env.DockerSecretsDatabasePasswordProcessor

The application is ready to go. Next, we’ll have a look at the container infrastructure.

Creating a Docker secret

The prerequisite for using Docker secrets is a Docker Swarm. A Docker Swarm has to have at least one manager node. Additionally, you can have any number of worker nodes. Refer to the Docker documentation if you want to learn more about fault tolerance and load balancing.

You can create a Docker secret with a single command on the Swarm manager. The following command creates the secret db-password containing the password prodpwd.

$ printf "prodpwd" | docker secret create db-password -

After creating the secret you should be able to list it.

$ docker secret ls
ID                          NAME                DRIVER              CREATED             UPDATED
nskgie2gozso364hwl92ne3og   db-password                             12 days ago         12 days ago

We created a secret. Let’s inject it into the container running our application.

Using a Docker secret

In Docker, you distribute an application across multiple nodes with the help of a service. When creating a service you can provide options for setting environment variables and Docker secrets.

Let’s say we wanted to run a Spring Boot application in a production environment. First, we activate the profile with --env SPRING_PROFILES_ACTIVE=prod. Second, we pass in the Docker secret with a name via --secret db-password. Below you can find the full command for creating a Docker service with five replicas.

$ docker service create --name todo-web-service --publish 8080:8080 --replicas 5 --secret db-password --env SPRING_PROFILES_ACTIVE=prod bmuschko/todo-web-service:latest

All set. Your Spring Boot application should be able to read the Docker secret and use it to connect to the database.

Summary

Credentials shouldn’t be baked into a Spring Boot application binary whether you run it in a container or just as a plain, executable JAR file. Docker secrets can help with keeping credentials secure. In a Docker Swarm environment, it’s easy to inject the secret into a running container. Reading the secret from a Spring Boot application only requires the implementation of a post processor.



comments powered by Disqus