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.

Senior Xiao Ying
Senior Xiao Ying
Senior Xiao Ying
Stop Hardcoding Secrets: Secure Spring Boot Configurations with @SecretValue

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: 50

Creating 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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaSpring BootConfiguration SecurityAWS Secrets Manager@SecretValue
Senior Xiao Ying
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.