Secrets & Environment Variables
Secrets & Environment Variables
Every production application has secrets: database passwords, API keys, JWT signing keys, third-party service tokens. How you handle those secrets is one of the highest-impact security decisions you make as a developer. This lesson covers the full spectrum — from the most common mistakes to the patterns used by professional teams deploying Spring Boot 3 applications.
The Core Problem: Secrets in Source Code
The most common mistake is hardcoding credentials directly in application.properties and committing that file to version control. Once a secret reaches a Git repository, it is effectively public — even in private repos, because history persists and ex-employees, compromised accounts, or accidental exposure can all leak it.
Here is the pattern you should never use in anything beyond a local demo:
Environment Variables: The Twelve-Factor Foundation
The Twelve-Factor App methodology defines the canonical solution: store config that varies between environments (dev, staging, prod) in environment variables. Spring Boot reads OS environment variables automatically and maps them to property keys using a relaxed binding rule: SPRING_DATASOURCE_PASSWORD maps to spring.datasource.password, and STRIPE_API_KEY maps to stripe.api-key.
MY_APP_API_KEY, my.app.api-key, and myApp.apiKey all bind to the same property. Prefer SCREAMING_SNAKE_CASE for environment variables — it is the Unix convention and unambiguous in shell scripts.
On a Linux or macOS server you export variables before launching the JVM:
In application.properties you reference them with ${} placeholders:
The colon-separated value (${DB_HOST:localhost}) is the default if the variable is absent. Use defaults for non-sensitive values like hostnames in development, but never provide a default for a secret — a missing secret should cause a startup failure so you notice it immediately rather than silently using a wrong value.
The .env File and Spring Profiles
For local development, typing export commands before every run is tedious. The conventional solution is a .env file at the project root:
Add .env to .gitignore immediately. Spring Boot does not read .env natively, but several approaches handle it:
- IDE run configurations — IntelliJ IDEA and VS Code let you specify an env file in the run config. This is the cleanest local approach.
- dotenv-spring-boot — a thin library (
me.paulschwarz:spring-dotenv) that loads.envinto Spring'sEnvironmentautomatically. - Shell sourcing —
set -a; source .env; set +a; ./mvnw spring-boot:runexports every line before invoking Maven.
.env.example file in the repository. It lists every required variable with a placeholder value and a comment explaining what each one is. New team members copy it to .env and fill it in. This documents requirements without leaking secrets.
Reading Secrets Programmatically with @Value and @ConfigurationProperties
Once a variable is in the environment (or in application.properties via a placeholder), Spring resolves it automatically. You can inject it with @Value:
Or bind a group of related secrets to a @ConfigurationProperties class (covered in depth in lesson 2, but worth a reminder here for secrets):
log.debug("API key: {}", apiKey) in a service called on every request will spray your secret into every log aggregator your organisation uses.
Secrets in Containerised Environments (Docker & Kubernetes)
When running in Docker, pass environment variables at container startup:
In Docker Compose for development, use an env_file directive:
In Kubernetes, the idiomatic mechanism is a Secret object mounted as environment variables or a volume. Never bake secrets into a Docker image or a ConfigMap (which is not encrypted at rest).
External Secrets Managers
For teams that need audit trails, automatic rotation, and fine-grained access control, a dedicated secrets manager is the right tool. The main options integrate with Spring Boot through Spring Cloud:
- HashiCorp Vault — on-premises or cloud;
spring-cloud-vaultreads secrets at startup and injects them as properties. - AWS Secrets Manager / Parameter Store —
spring-cloud-awsresolvesaws.secretsmanager.secret-nameproperties automatically. - Azure Key Vault —
azure-spring-cloud-starter-keyvault-secretsmaps vault entries to Spring properties.
With Spring Cloud Vault, your bootstrap.yml (or application.yml with the right import) is as simple as:
Spring Boot then resolves ${stripe.api-key} by fetching the myapp secret path from Vault at startup — the secret never touches the filesystem or a properties file.
Summary: The Secret-Handling Checklist
- Add
.envand any*-secrets.propertiesfiles to.gitignorebefore your first commit. - Use
${ENV_VAR}placeholders in committedapplication.properties— no hardcoded values. - Provide
.env.examplewith comments but no real values. - Never provide a default value for a secret placeholder (fail loudly when it is missing).
- Never log secret values — not even in
DEBUG. - In production, inject via OS environment variables, Docker secrets, Kubernetes Secrets, or a secrets manager.
- Rotate secrets that were ever accidentally exposed — treat them as permanently compromised.
Following these rules costs almost no extra effort and prevents the most common category of production security incidents. The next lesson builds on this foundation to cover the full YAML configuration syntax that makes complex Spring Boot config readable and maintainable.