Secrets ๐
Production applications need secrets that must never end up in the artifact or in git: cookie secrets and keys, database passwords, SMTP credentials. mangoo I/O reads them from outside the JAR with three placeholders you write directly into config.yaml:
| Placeholder | Source |
|---|---|
env{} |
Operating-system environment variable |
arg{} |
JVM system property (-D...) |
file{} |
File in the secrets directory, as Kubernetes Secrets and Docker secrets provide them |
Every placeholder accepts a fallback in the braces, env{fallback}, which is used only if the source has no value. A placeholder without a value and without a fallback fails the startup and names the key, so a missing secret is noticed at deploy time and not on the first request. Use fallbacks only for values that are not secret; a secret written as fallback sits in config.yaml again.
For a value that may be missing, write env{?}, arg{?} or file{?}. If the source is not set or empty, the key stays unset, as if it were not in config.yaml, and the default of the framework applies. An empty source never replaces a default with an empty string, so an empty SMTP_PROTOCOL keeps smtps and an empty PERSISTENCE_MONGO_URL falls back to host and port:
smtp:
host: env{?}
username: env{?}
password: env{?}
persistence:
mongo:
url: env{?}
To find out whether an optional value was given, read it without a default, for example config.getString(Key.SMTP_HOST), which returns null while getSmtpHost() returns the default localhost.
Mounted secret files are the safest of the three sources: environment variables are visible in /proc/<pid>/environ, docker inspect and the pod spec and are inherited by child processes, a file on a tmpfs mount is not.
Secrets in prod
In prod mode the framework secrets must come from one of the placeholders. A literal value in the config.yaml on the classpath fails the startup:
application.secret,application.admin.secret,application.api.keysession.cookie.secret/.key,authentication.cookie.secret/.key,flash.cookie.secret/.keypersistence.mongo.passwordandpersistence.<prefix>.mongo.password,smtp.passwordconnector.https.password
A fallback in the braces counts as literal, an empty value does not, as it holds no secret. A config.yaml passed with -Dapplication.config lives outside the JAR and may contain literal values. application.admin.password is an Argon2id hash and may stay literal. In dev and test literal values are allowed, a project created with the archetype ships generated secrets for both.
The cookie secrets and keys fall back to application.secret, so the minimal prod setup is a single APPLICATION_SECRET. application.secret is always required, even if every cookie has dedicated values; the startup warns as long as a cookie uses it, as dedicated values per cookie are recommended.
Secret and key length
The cookie secrets (*.cookie.secret) and application.secret are used directly as encryption key for dir with A256CBC_HS512. That algorithm accepts a key of exactly 512 bit, so these values must be exactly 64 bytes long โ not more, not less. Bytes count, not characters: a non-ASCII character such as รค takes two bytes in UTF-8. The cookie keys (*.cookie.key) are used for HS512 signing and must be at least 64 bytes, as must application.api.key. The application refuses to start if a value does not meet these rules.
Generate a value with 64 ASCII characters, for example:
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 64
Environment variables
Use env{} to read a value from the process environment. The environment name is the config key in uppercase with dots replaced by underscores:
session:
cookie:
secret: env{}
This reads SESSION_COOKIE_SECRET.
JVM arguments
Use arg{} to read a JVM system property with the same dotted key:
application:
db:
username: arg{}
java -Dapplication.db.username=myuser -jar myapp.jar
Secret files
Use file{} to read a value from a file named like the key in the secrets directory:
session:
cookie:
secret: file{}
This reads /run/secrets/session.cookie.secret. The directory is /run/secrets, the default of Docker secrets, and can be changed with the environment variable APPLICATION_SECRETS_PATH or the JVM property application.secrets.path; it can not be set in config.yaml, as the files are read while config.yaml is resolved.
- The file name is the key as is; Kubernetes and Docker allow dots in secret names.
- Symbolic links are followed, as Kubernetes mounts secrets through them.
- One trailing line break (
\nor\r\n) is removed, as files written withechoend with one. Everything else is kept byte for byte. - Files larger than 64 KiB are rejected.
- A file that exists but can not be read fails the startup with the key and the reason, e.g. missing permissions.
file{}reads the same directory in every mode. On a development machine there is usually no/run/secrets, so usefile{}in the prod section only, or setAPPLICATION_SECRETS_PATHin the run configuration of the IDE to a local directory.
Kubernetes:
volumes:
- name: secrets
secret:
secretName: myapp
containers:
- name: myapp
volumeMounts:
- name: secrets
mountPath: /run/secrets
readOnly: true
The keys of the Secret myapp are the config keys, e.g. session.cookie.secret.
Docker Compose
With Compose the secrets are files in one directory on the host, mounted read-only as /run/secrets into the container. /run/secrets is the default secrets directory, so APPLICATION_SECRETS_PATH is not needed.
Creating the secret files
Keep the files in a directory next to compose.yaml, e.g. ./secrets, and add it to .gitignore. Name each file like the config key:
| File | Content | Create with |
|---|---|---|
application.secret |
exactly 64 bytes | tr command below |
session.cookie.secret, authentication.cookie.secret, flash.cookie.secret |
exactly 64 bytes | tr command below |
session.cookie.key, authentication.cookie.key, flash.cookie.key |
at least 64 bytes | tr command below |
application.api.key |
at least 64 bytes | tr command below |
application.admin.secret |
TOTP secret, Base32 | tr -dc 'A-Z2-7' or TotpUtils.createSecret() |
application.admin.password |
Argon2id hash | mvn -q mangooio:hash, see Administration |
persistence.mongo.password, smtp.password |
password of the service | set in MongoDB or given by the SMTP provider |
Use a separate random value for every key. The cookie secrets and keys fall back to application.secret, but dedicated values per cookie are recommended. Create the random values in one go:
mkdir -p secrets && cd secrets
umask 077
for key in application.secret \
session.cookie.secret session.cookie.key \
authentication.cookie.secret authentication.cookie.key \
flash.cookie.secret flash.cookie.key; do
LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 64 > "$key"
done
LC_ALL=C tr -dc 'A-Z2-7' < /dev/urandom | head -c 64 > application.admin.secret
printf '%s' 'the-mongo-password' > persistence.mongo.password
umask 077 makes the files readable by their owner only, wc -c < application.secret prints 64. A single trailing line break is removed when the file is read, more than one breaks the length check.
compose.yaml
Mount the whole directory, a new secret is then just a new file:
services:
myapp:
image: myapp:latest
user: "10001:10001"
environment:
JAVA_TOOL_OPTIONS: "-Xms512m -Xmx1024m"
volumes:
- ./secrets:/run/secrets:ro
Without Swarm the secrets: block of Compose is a bind mount per file as well, so the directory mount offers the same protection with one line. Use the secrets: block with Docker Swarm, which stores secrets encrypted and mounts them on tmpfs. Use the long syntax there and set target to the config key, so the file name inside the container does not depend on which characters Swarm allows in secret names:
services:
myapp:
secrets:
- source: application_secret
target: application.secret
secrets:
application_secret:
external: true
Podman does the same with podman secret create and --secret <name>,target=<config key>.
config.yaml
Set every key that comes from a file to file{} in the prod section:
environments:
prod:
application:
secret: file{}
session:
cookie:
secret: file{}
key: file{}
persistence:
mongo:
password: file{}
A key set to file{} without a file fails the startup and names the key. Use file{?} for a key that may be missing.
File permissions
A bind mount keeps owner and permissions of the host, so the container user must be able to read the directory and its files. Give it a fixed numeric id, in the image with useradd --uid 10001 or with user: in compose.yaml, and hand directory and files to that id. Change the permissions first, after the chown your own user can no longer list the directory:
chmod 0400 secrets/*
chmod 0500 secrets
sudo chown -R 10001:10001 secrets
The application refuses to start as root in prod mode, so a permission problem can not be solved by running the container as root.
Without Compose, mount the directory the same way, docker run -v "$PWD/secrets:/run/secrets:ro" myapp, or point APPLICATION_SECRETS_PATH to another directory.
Without Docker
file{} works the same without a container, the files and their names do not change. Outside a container /run is a tmpfs that is emptied on every reboot, so keep the files in a permanent directory and point APPLICATION_SECRETS_PATH to it:
sudo mkdir -p /etc/myapp/secrets
Create the files as described in Creating the secret files, then hand them to the user the application runs as. Change the permissions first, after the chown your own user can no longer list the directory:
sudo chmod 0400 /etc/myapp/secrets/*
sudo chmod 0500 /etc/myapp/secrets
sudo chown -R myapp:myapp /etc/myapp/secrets
systemd:
[Service]
User=myapp
Environment=APPLICATION_SECRETS_PATH=/etc/myapp/secrets
ExecStart=/usr/bin/java -Dapplication.mode=prod -jar /opt/myapp/myapp.jar
With LoadCredential= the files can stay readable by root only. systemd copies them on start into a directory of the service that only the service user can read, %d is that directory:
[Service]
User=myapp
LoadCredential=application.secret:/etc/myapp/secrets/application.secret
LoadCredential=persistence.mongo.password:/etc/myapp/secrets/persistence.mongo.password
Environment=APPLICATION_SECRETS_PATH=%d
ExecStart=/usr/bin/java -Dapplication.mode=prod -jar /opt/myapp/myapp.jar
For supervisord set APPLICATION_SECRETS_PATH in environment=, see Operating.
External configuration
You can also point the whole configuration file elsewhere, which is useful when your deployment mounts config outside the JAR entirely (a Kubernetes ConfigMap or Secret, for instance) instead of baking it into the artifact:
java -Dapplication.config=/etc/myapp/config.yaml -jar myapp.jar
HTTPS certificates
An HTTPS connector reads its key and certificate chain from a PKCS12 file:
connector:
https:
host: 0.0.0.0
port: 8443
keystore: /etc/myapp/https.p12
password: file{}
connector.https.certificate.alias selects the entry and may stay empty if the file holds exactly one key entry. The connector accepts TLS 1.3 only; clients that need TLS 1.2 have to connect through a proxy that terminates TLS. Behind a load balancer TLS usually ends at the proxy and the application runs plain HTTP, see Operating. For local HTTPS a tool like mkcert creates a trusted certificate.
See Configuration for all keys and Migrations for the move from the vault of 10.x.