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.