Stop Hardcoding Secrets: Secure Spring Boot Configurations with @SecretValue
This article explains why storing passwords and API keys in plain‑text configuration files is risky, outlines the three core principles of configuration security, and provides a step‑by‑step guide to using the @SecretValue annotation from the spring‑secret‑starter library to inject secrets safely from services such as AWS Secrets Manager.
Configuration Security
In the micro‑service and cloud‑native era, protecting sensitive configuration items such as database passwords, API keys, and certificates is a core application‑security challenge. Traditional Spring Boot projects often store these values directly in application.properties or application.yml, which leads to three main problems:
Source‑code leakage : committing configuration files to a Git repository exposes plaintext secrets to anyone with repository access.
Insider threat : developers and operators can see production credentials.
Compliance violations : standards like SOC2, ISO27001, and GDPR forbid storing clear‑text keys in code repositories.
The industry‑accepted solution follows the 12‑Factor App configuration principle, which recommends externalizing configuration. Common approaches include environment variables, dedicated KMS services (e.g., AWS Secrets Manager, HashiCorp Vault), and encrypted configuration files (e.g., Jasypt).
@SecretValue Annotation
The @SecretValue annotation, provided by the spring-secret-starter library (author: Lucas Fernandes, version 1.3.0), abstracts secret‑retrieval logic from business code and offers a declarative, type‑safe way to inject secrets.
Core features are illustrated in the following diagram:
@SecretValue vs Other Solutions
Building a Project from Scratch
Required stack: JDK 17+, Spring Boot 3.x, Maven 3.6+, optionally an AWS account for testing Secrets Manager.
Adding the Dependency
<dependency>
<groupId>io.github.open-source-lfernandes</groupId>
<artifactId>spring-secret-starter</artifactId>
<version>1.3.0</version>
</dependency>Configuring a Secret Provider (AWS example)
spring:
application:
name: secret-value-demo
# Secret loading configuration
secret:
enabled: true
provider:
type: aws
region: us-east-1
credentials:
profile: default
cache:
enabled: true
ttl: 300
max-size: 50Creating Secrets in AWS Secrets Manager
# Create a database secret
aws secretsmanager create-secret \
--name prod/myapp/database \
--secret-string '{"username":"db_admin","password":"SecurePass123!","host":"mydb.cluster-xxx.us-east-1.rds.amazonaws.com"}'
# Create an API‑key secret
aws secretsmanager create-secret \
--name prod/myapp/api-key \
--secret-string "sk-abc123def456ghi789"Using @SecretValue in Code
Scenario 1 – Injecting a simple string
package com.example.demo.service;
import io.github.open_source_lfernandes.secret.SecretValue;
import org.springframework.stereotype.Service;
/** API service demonstrating simple secret injection */
@Service
public class ApiService {
@SecretValue(key = "${secret.api.key-name}")
private String apiKey;
@SecretValue(key = "prod/myapp/api-key")
private String directApiKey;
public void callExternalApi() {
System.out.println("Calling external service with API Key: " + maskSecret(apiKey));
}
private String maskSecret(String secret) {
if (secret == null || secret.length() <= 8) {
return "****";
}
return secret.substring(0, 4) + "****" + secret.substring(secret.length() - 4);
}
}Scenario 2 – Injecting a complex JSON object
package com.example.demo.config;
import io.github.open_source_lfernandes.secret.SecretValue;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
@Configuration
public class DatabaseConfig {
public record DatabaseCredentials(String username, String password, String host) {}
@SecretValue(key = "prod/myapp/database", type = DatabaseCredentials.class)
private DatabaseCredentials credentials;
@Bean
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://" + credentials.host() + "/myapp");
config.setUsername(credentials.username());
config.setPassword(credentials.password());
config.setMaximumPoolSize(10);
return new HikariDataSource(config);
}
}Scenario 3 – Using secrets in a non‑Spring bean
package com.example.demo.util;
import io.github.open_source_lfernandes.secret.SecretInjector;
import io.github.open_source_lfernandes.secret.SecretValue;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
@Component
public class SecretHolder {
@SecretValue(key = "prod/myapp/encryption-key")
private String encryptionKey;
@SecretValue(key = "prod/myapp/signature-secret")
private String signatureSecret;
private static String STATIC_ENCRYPTION_KEY;
private static String STATIC_SIGNATURE_SECRET;
@PostConstruct
public void init() {
STATIC_ENCRYPTION_KEY = this.encryptionKey;
STATIC_SIGNATURE_SECRET = this.signatureSecret;
}
public static String getEncryptionKey() { return STATIC_ENCRYPTION_KEY; }
public static String getSignatureSecret() { return STATIC_SIGNATURE_SECRET; }
}Deep Dive into the Internals
The overall architecture is shown below:
Core Processing Flow
Startup scanning : During Spring container initialization, a BeanPostProcessor scans all beans for fields annotated with @SecretValue.
Expression resolution : If the key attribute contains a ${...} placeholder, Spring's Environment resolves it to the actual secret name from configuration files.
Secret retrieval : Based on provider.type, the corresponding SecretProvider implementation (AWS, Vault, Env, or Custom) fetches the secret value.
Type conversion :
If a type is specified and the target is not String, the library attempts to deserialize the JSON string into the given class.
If no type is set or the target is String, the raw string is injected.
Value injection : The retrieved (and possibly converted) value is set on the field via reflection.
Cache handling : Successfully fetched secrets are cached; subsequent requests for the same key return the cached value, reducing load on the external secret store.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Senior Xiao Ying
Dedicated to sharing Java backend technical experience and original tutorials, offering career transition advice and resume editing. Recognized as a rising star in CSDN's Java backend community and ranked Top 3 in the 2022 New Star Program for Java backend.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
