systemd Credentials in Spring Boot
systemd, Linux’s most widely used system and service manager, introduced system and service credentials with version 247. They allow for securely passing passwords and keys to applications, thereby solving a longstanding problem. Let’s find out how we can access them in Spring Boot.
Introducing systemd Credentials
In their simplest form, systemd credentials are straightforward:
[Service]
ExecStart=/path/to/my-program
LoadCredential=my-secret:/etc/my-program/credentials/some-password
In this unit file, we tell systemd to start my-program
and to expose an unencrypted credential to it. The credential is stored in the file /etc/my-program/credentials/some-password
.
Now, how can my-program
access it? Not by opening /etc/my-program/credentials/some-password
. Instead, systemd will prepare a special directory1. There, it will place the credential in a file named my-secret
. systemd passes my-program
the path to the special directory using the environment variable CREDENTIALS_DIRECTORY
. If my-program
were a shell script, it could read the secret by opening "$CREDENTIALS_DIRECTORY/my-secret"
.
There are many more options. It is not only possible to encrypt secrets, but even to bind them to the Trusted Platform Module of a computer. For details, please consult systemd’s documentation.
Now that we know how they work, the only thing left to do is to figure out how we can get Spring Boot to read systemd credentials from files in CREDENTIALS_DIRECTORY
.
Externalized Configuration in Spring Boot
In Spring Boot, configuration is declared as properties like spring.datasource.url
. Those properties are not only used to configure Spring Boot itself. You can define your own properties to make your application configurable through the very same system. Spring Boot can obtain configuration values from a multitude of sources, for example, .properties, the environment, JNDI or Kubernetes ConfigMaps.
In addition to properties, Spring Boot has dedicated SSL bundles to load keys and certificates from files.
Populating SSL Bundles From systemd Credentials
I start with the simplest case: populating SSL bundles from systemd credentials.
Without systemd credentials, the configuration to define an SSL bundle called mybundle
to be used by the embedded web server looks as follows:
spring.ssl.bundle.pem.mybundle.keystore.certificate=file:/path/to/domain.crt
spring.ssl.bundle.pem.mybundle.keystore.private-key=file:/path/to/domain.key
To load the certificate and private key from a systemd credential, specify them in the unit file:
[Service]
ExecStart=/path/to/java -jar /path/to/demo-0.0.1-SNAPSHOT.jar
LoadCredential=domain.key:/path/to/domain.key
LoadCredential=domain.crt:/path/to/domain.crt
ExecStart=
defines the executable JAR to launch. The next two lines starting with LoadCredential=
specify the private key (/path/to/domain.key
) and certificate (/path/to/domain.crt
) that will be made available to the application as domain.key
and domain.crt
respectively. The only step remaining is to tell Spring Boot to read both files from CREDENTIALS_DIRECTORY
:
spring.ssl.bundle.pem.mybundle.keystore.certificate=file:${CREDENTIALS_DIRECTORY}/domain.crt
spring.ssl.bundle.pem.mybundle.keystore.private-key=file:${CREDENTIALS_DIRECTORY}/domain.key
Using systemd Credentials in Any Configuration Property
Now, most Spring Boot applications will probably want to access a database. And that database will most likely require a password, which can be configured with spring.datasource.password
. Unfortunately, we cannot leverage our newly learnt knowledge and use the file:
prefix to load the database password from a file because file:
is only understood by SSL bundles. Instead, we have to repurpose a configuration tree.
A configuration tree turns files into Spring Boot properties by converting their file name into the property’s name and the file’s content into the property’s value:
[Service]
ExecStart=/path/to/java \
-jar /path/to/demo-0.0.1-SNAPSHOT.jar \
--spring.config.import=configtree:${CREDENTIALS_DIRECTORY}
LoadCredential=spring.datasource.password:/path/to/database-password
--spring.config.import
is the magical argument that makes it all work. If you prefer, you can put spring.config.import=configtree:${CREDENTIALS_DIRECTORY}
into your application.properties
, instead.
If you find it too cumbersome to specify every secret individually, you can treat your entire configuration as a systemd credential and load it from CREDENTIALS_DIRECTORY
:
[Service]
ExecStart=/path/to/java \
-jar /path/to/demo-0.0.1-SNAPSHOT.jar \
--spring.config.import=${CREDENTIALS_DIRECTORY}/application.properties
LoadCredential=application.properties:/path/to/application.properties
Credentials Do Not Belong in Environment Variables
While environment variables are handy for configuring an application, they have many drawbacks. Subprocesses inherit them, they are awkward to use with binary data, have size limitations, and can be accessed easily with various tools. ps auxe
lists them and systemctl show <service>
allows any user on a system to print them if they are defined in a unit file.
-
To make it only accessible to
my-program
enable mount namespacing withPrivateMounts=
. ↩︎