Securing Application Secrets with HashiCorp Vault and Spring Boot
The Problem: Managing Secrets in Modern Applications
Imagine this scenario: Your team has built a sophisticated Spring Boot application that connects to multiple databases, third-party APIs, and cloud services. Each connection requires credentials, API keys, or certificates. Where do you store these secrets?
Perhaps you've tried:
- Hardcoding credentials in your application code (dangerous)
- Storing them in properties files that get committed to version control (risky)
- Using environment variables that need to be managed across multiple environments (cumbersome)
- Implementing a homegrown secrets management solution (time-consuming and potentially insecure)
As your application grows, managing these secrets becomes increasingly complex. You need a solution that:
- Securely stores sensitive information
- Provides fine-grained access control
- Supports secret rotation
- Integrates seamlessly with your Spring Boot applications
- Scales across multiple environments and services
This is where HashiCorp Vault comes in.
What is HashiCorp Vault?
HashiCorp Vault is a specialized tool designed to secure, store, and tightly control access to tokens, passwords, certificates, API keys, and other secrets in modern computing environments. Unlike traditional configuration management tools, Vault is built from the ground up with security as its primary focus.
Key features include:
- Secret Storage: Securely store any sensitive data
- Dynamic Secrets: Generate credentials on-demand for various services
- Data Encryption: Encrypt and decrypt data without storing the encryption key
- Leasing and Renewal: Time-based access to secrets with automatic revocation
- Revocation: Instantly revoke access when needed
Setting Up Vault with Spring Boot
Let's walk through the process of integrating HashiCorp Vault with a Spring Boot application. We'll cover:
- Setting up a local Vault server for development
- Configuring Spring Boot to connect to Vault
- Storing and retrieving secrets
- Implementing dynamic database credentials
- Best practices for production deployment
Step 1: Setting Up a Local Vault Server
First, let's set up a local Vault server for development purposes. You can download Vault from the official website or use Docker:
plaintext# Pull the Vault image docker pull vault # Start a Vault server in development mode docker run --name vault -p 8200:8200 vault server -dev -dev-root-token-id="00000000-0000-0000-0000-000000000000"
This starts Vault in development mode with a known root token. In production, you would never use development mode or a predictable token.
The output will include important information:
plaintextRoot Token: 00000000-0000-0000-0000-000000000000 Unseal Key: (not needed in dev mode)
You can now access the Vault UI at http://localhost:8200/ui and log in using the root token.
Step 2: Adding Vault Dependencies to Spring Boot
Add the Spring Cloud Vault dependencies to your pom.xml:
plaintext<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-vault-config</artifactId> </dependency>
Or if you're using Gradle:
plaintextimplementation 'org.springframework.cloud:spring-cloud-starter-vault-config'
Step 3: Configuring Spring Boot to Connect to Vault
Create or update your bootstrap.properties (or bootstrap.yml) file:
plaintextspring.cloud.vault.token=00000000-0000-0000-0000-000000000000 spring.cloud.vault.scheme=http spring.cloud.vault.host=localhost spring.cloud.vault.port=8200 # Enable the key-value secret engine version 2 spring.cloud.vault.kv.enabled=true spring.cloud.vault.kv.backend=secret spring.cloud.vault.kv.default-context=application # Application name for storing application-specific secrets spring.application.name=my-application
Version Compatibility
This guide has been tested with the following versions:
- Spring Boot: 2.6.x - 2.7.x
- Spring Cloud: 2021.0.x (also known as Jubilee)
- Spring Cloud Vault: 3.1.x
- HashiCorp Vault: 1.9.x - 1.12.x
For different versions, consult the specific documentation as APIs and configurations may change.
Bootstrap Configuration Note
For Spring Boot 2.4.0 and later, you'll need to add this dependency to enable bootstrap:
plaintext<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> </dependency>
Step 4: Storing Secrets in Vault
Now, let's store some secrets in Vault. You can use the UI, CLI, or API. Here's how to do it using the CLI:
bash# Set environment variables for Vault CLI export VAULT_ADDR='http://localhost:8200' export VAULT_TOKEN='00000000-0000-0000-0000-000000000000' # Store secrets for your application vault kv put secret/my-application/config \ db.username=admin \ db.password=secret \ api.key=abcdef123456
Step 5: Accessing Secrets in Your Spring Boot Application
You can access these secrets in several ways:
Option 1: Using @ConfigurationProperties
Create a configuration class:
javaimport org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Configuration; @Configuration @ConfigurationProperties("db") public class DatabaseProperties { private String username; private String password; // Getters and setters public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
Then inject and use it:
java@Service public class DatabaseService { private final DatabaseProperties dbProps; public DatabaseService(DatabaseProperties dbProps) { this.dbProps = dbProps; } public void connectToDatabase() { // Use dbProps.getUsername() and dbProps.getPassword() System.out.println("Connecting with: " + dbProps.getUsername()); } }
Option 2: Using @Value Annotations
You can directly inject secrets using @Value:
java@Service public class ApiService { @Value("${api.key:}") private String apiKey; public void callApi() { // Check if the API key was retrieved successfully if (apiKey == null || apiKey.isEmpty()) { throw new IllegalStateException("API key not available. Vault may be unreachable or the secret path is incorrect."); } try { // Use the apiKey to make API calls System.out.println("Using API key: " + apiKey); // Actual API call would go here } catch (Exception e) { // Handle API call failures throw new RuntimeException("API call failed", e); } } }
Option 3: Using Environment
You can also access secrets through the Spring Environment:
java@Service public class ConfigService { private final Environment env; public ConfigService(Environment env) { this.env = env; } public String getSecret(String key) { return env.getProperty(key); } }
Advanced Vault Features with Spring Boot
Now that we have the basics working, let's explore some advanced features.
Dynamic Database Credentials
One of Vault's most powerful features is the ability to generate dynamic, short-lived credentials. Let's configure this for a PostgreSQL database:
1. Enable the Database Secrets Engine in Vault
bash# Enable the database secrets engine vault secrets enable database # Configure the PostgreSQL connection vault write database/config/my-postgresql-database \ plugin_name=postgresql-database-plugin \ allowed_roles="my-role" \ connection_url="postgresql://{{username}}:{{password}}@localhost:5432/mydb" \ username="vault_admin" \ password="vault_admin_password" # Create a role that creates credentials with limited permissions vault write database/roles/my-role \ db_name=my-postgresql-database \ creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \ GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \ default_ttl="1h" \ max_ttl="24h"
2. Configure Spring Boot to Use Dynamic Credentials
Update your bootstrap.properties:
properties# Enable the database secret engine spring.cloud.vault.database.enabled=true spring.cloud.vault.database.role=my-role # Database connection properties spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
Spring Cloud Vault will automatically:
- Request credentials from Vault
- Configure your DataSource with these credentials
- Renew the credentials before they expire
- Request new credentials if renewal fails
Vault Transit for Encryption as a Service
Vault's Transit secret engine provides encryption as a service. Let's configure it:
1. Enable the Transit Engine
bash# Enable the transit secret engine vault secrets enable transit # Create an encryption key vault write -f transit/keys/my-encryption-key
2. Create a Service to Use Transit
javaimport org.springframework.stereotype.Service; import org.springframework.vault.core.VaultOperations; import org.springframework.vault.support.Plaintext; import org.springframework.vault.support.Ciphertext; @Service public class EncryptionService { private final VaultOperations vaultOperations; private final String keyName = "my-encryption-key"; public EncryptionService(VaultOperations vaultOperations) { this.vaultOperations = vaultOperations; } public String encrypt(String data) { Plaintext plaintext = Plaintext.of(data); Ciphertext ciphertext = vaultOperations.opsForTransit().encrypt(keyName, plaintext); return ciphertext.getCiphertext(); } public String decrypt(String ciphertext) { Ciphertext cipher = Ciphertext.of(ciphertext); Plaintext plaintext = vaultOperations.opsForTransit().decrypt(keyName, cipher); return plaintext.asString(); } }
AppRole Authentication
For production environments, you should use AppRole authentication instead of static tokens:
1. Enable AppRole Authentication
bash# Enable AppRole auth method vault auth enable approle # Create a policy for your application vault policy write my-app-policy -<<EOF path "secret/data/my-application/*" { capabilities = ["read"] } path "database/creds/my-role" { capabilities = ["read"] } EOF # Create an AppRole with the policy vault write auth/approle/role/my-app \ token_policies="my-app-policy" \ token_ttl=1h \ token_max_ttl=24h # Get the RoleID vault read auth/approle/role/my-app/role-id # Output: role_id=<your-role-id> # Generate a SecretID vault write -f auth/approle/role/my-app/secret-id # Output: secret_id=<your-secret-id>
2. Securely Managing AppRole Credentials
The role-id and secret-id are themselves secrets that need to be securely managed. Here are some approaches:
- Environment Variables: For development or simple deployments
- CI/CD Secrets: Store in your CI/CD platform's secure variables (GitHub Secrets, GitLab CI Variables, etc.)
- Kubernetes Secrets: For Kubernetes deployments, store as Kubernetes secrets
- Instance Metadata: For cloud deployments, use instance metadata or user data
- Bootstrapping Service: Use a separate service with higher privileges to fetch initial credentials
Avoid hardcoding these values in your application code or configuration files that might be committed to version control.
3. Configure Spring Boot to Use AppRole
Update your bootstrap.properties:
propertiesspring.cloud.vault.authentication=approle spring.cloud.vault.app-role.role-id=<your-role-id> spring.cloud.vault.app-role.secret-id=<your-secret-id> spring.cloud.vault.app-role.role=my-app spring.cloud.vault.app-role.app-role-path=approle # Remove the token property # spring.cloud.vault.token=00000000-0000-0000-0000-000000000000
Complete Example Application
Let's put everything together in a complete Spring Boot application:
javaimport org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @SpringBootApplication @EnableConfigurationProperties(DatabaseProperties.class) public class VaultDemoApplication { public static void main(String[] args) { SpringApplication.run(VaultDemoApplication.class, args); } } @RestController class SecretController { private final DatabaseProperties dbProps; private final EncryptionService encryptionService; public SecretController(DatabaseProperties dbProps, EncryptionService encryptionService) { this.dbProps = dbProps; this.encryptionService = encryptionService; } @GetMapping("/secrets") public String getSecrets() { return "Database Username: " + dbProps.getUsername(); } @GetMapping("/encrypt") public String encrypt(String data) { return encryptionService.encrypt(data); } @GetMapping("/decrypt") public String decrypt(String ciphertext) { return encryptionService.decrypt(ciphertext); } }
Best Practices for Production
When deploying to production, follow these best practices:
- Never Use Development Mode: Always run Vault in production mode with proper unsealing procedures
- Understanding Unsealing: When Vault starts, it's in a sealed state where the encryption key needed to decrypt data is not available. Unsealing is the process of reconstructing this key, typically requiring a threshold of unseal keys (e.g., 3 of 5) provided by trusted operators. This security measure ensures that no single person can access all secrets.
- Auto-unseal: For production, consider using cloud KMS services (AWS KMS, GCP KMS, Azure Key Vault) to automate the unsealing process securely.
- Use AppRole or Kubernetes Authentication: Avoid using static tokens
- Implement Proper Policies: Follow the principle of least privilege
- Set Up High Availability: Configure Vault for high availability to avoid downtime
- Implement Proper Backup Procedures: Regularly back up your Vault data
- Rotate Root Tokens: Regularly rotate the root token and store it securely
- Monitor Vault: Set up monitoring and alerting for Vault
- Audit Logging: Enable audit logging to track access to secrets
Transitioning from Development to Production
Moving from a development setup to production requires careful planning. Here's a step-by-step approach:
1. Environment-Specific Configurations
Use Spring profiles to manage different environments:
bootstrap-dev.properties:
propertiesspring.cloud.vault.scheme=http spring.cloud.vault.host=localhost spring.cloud.vault.port=8200 spring.cloud.vault.token=00000000-0000-0000-0000-000000000000
bootstrap-staging.properties:
propertiesspring.cloud.vault.scheme=https spring.cloud.vault.host=staging-vault.example.com spring.cloud.vault.authentication=approle spring.cloud.vault.app-role.role-id=${VAULT_ROLE_ID} spring.cloud.vault.app-role.secret-id=${VAULT_SECRET_ID}
bootstrap-prod.properties:
propertiesspring.cloud.vault.scheme=https spring.cloud.vault.host=prod-vault.example.com spring.cloud.vault.authentication=approle spring.cloud.vault.app-role.role-id=${VAULT_ROLE_ID} spring.cloud.vault.app-role.secret-id=${VAULT_SECRET_ID} # Add connection timeouts for production spring.cloud.vault.connection-timeout=5000 spring.cloud.vault.read-timeout=15000
2. Migration Strategy
- Start with Development Mode: Use token authentication during initial development
- Move to AppRole in Lower Environments: Implement AppRole in dev/test environments first
- Secret Migration: Move secrets from development to production Vault instances
- Validate Policies: Ensure policies grant appropriate access in each environment
- Test Failure Scenarios: Verify application behavior when Vault is unavailable
3. Running with the Appropriate Profile
bash# Development java -jar -Dspring.profiles.active=dev your-app.jar # Production with environment variables VAULT_ROLE_ID=your-role-id VAULT_SECRET_ID=your-secret-id java -jar -Dspring.profiles.active=prod your-app.jar
4. Handling Vault Outages
Implement fallback mechanisms for Vault outages:
- Configure reasonable timeouts
- Use Spring Retry for transient failures
- Consider local caching of secrets with appropriate TTL
- Implement circuit breakers for Vault operations
Troubleshooting Common Issues
Connection Issues
If your application can't connect to Vault:
- Verify Vault is running: curl http://localhost:8200/v1/sys/health
- Check your token has appropriate permissions
- Ensure network connectivity between your app and Vault
Secret Not Found
If your application can't find a secret:
- Verify the secret exists in the correct path
- Check your policy allows access to that path
- Ensure you're using the correct context and backend
Authentication Failures
If authentication fails:
- Verify your credentials (token, role-id, secret-id)
- Check if the token has expired
- Ensure the authentication method is enabled
Conclusion
HashiCorp Vault provides a robust solution for managing secrets in Spring Boot applications. By centralizing secret management, you gain:
- Enhanced security through encryption, access controls, and secret rotation
- Simplified operations with a single source of truth for secrets
- Improved compliance through audit logging and access tracking
- Reduced risk of credential exposure in your codebase or CI/CD pipeline
The integration between Spring Boot and Vault is seamless, allowing you to focus on building your application while Vault handles the security aspects of secret management.
As your application grows, Vault can scale with you, supporting everything from simple key-value secrets to dynamic database credentials and encryption as a service.
Further Reading
Related Posts
Continue exploring similar topics
Spring Boot 3.x: What Actually Changed (and What Matters)
A practical look at Spring Boot 3.x features that change how you build services - virtual threads, reactive patterns, security gotchas, and performance lessons from production.
Composite Primary Keys in Spring Data JPA: Complete Guide to @IdClass and @EmbeddedId
Master composite primary keys in Spring Data JPA using @IdClass and @EmbeddedId annotations. Learn when to use each approach with practical examples and production best practices.
Dockerizing a spring boot application
Dockerizing an application means making our application run in a docker container..