Hire Me
← All Writing AWS

Managing Secrets Securely with AWS Secrets Manager and Spring Boot

How to load secrets from AWS Secrets Manager into Spring Boot at startup, wire IAM least-privilege access, enable automatic rotation, and avoid the common configuration anti-patterns.

The number of production breaches that start with a secret committed to a git repository, embedded in a Docker image, or sitting in an unencrypted environment variable is uncomfortable. In most cases the fix is straightforward — but only if you design secret management into your infrastructure from the start rather than bolting it on later.

AWS Secrets Manager is the right tool for secrets in an AWS-hosted Spring Boot service. It stores secrets encrypted at rest, integrates with IAM for access control, supports automatic rotation, and has a Spring Cloud AWS integration that loads secrets directly into the Spring Environment with no custom code.

The anti-patterns to replace

Before looking at the solution, the patterns worth eliminating:

Hardcoded credentials: datasource.password=supersecret in application.properties committed to version control. Rotated or not, the history is permanent.

Plain environment variables: DB_PASSWORD=supersecret passed to a container. Better than version control, but environment variables are visible to any process in the container and logged by many observability tools. They are not encrypted in transit.

SSM Parameter Store strings: Parameter Store works for configuration. For secrets — database passwords, API keys, private keys — use Secrets Manager: it adds encryption, access auditing, and a rotation API that Parameter Store lacks.

Creating a secret with CDK

Define the secret in infrastructure code so it is version-controlled alongside the stack that uses it:

Secret dbCredentials = Secret.Builder.create(this, "DbCredentials")
    .secretName("/myapp/production/db-credentials")
    .generateSecretString(SecretStringGenerator.builder()
        .secretStringTemplate("{\"username\": \"myapp\"}")
        .generateStringKey("password")
        .passwordLength(32)
        .excludePunctuation(false)
        .build())
    .build();

This generates a JSON secret like {"username":"myapp","password":"generated-32-char-string"}. Secrets Manager handles the generation — your CDK code never contains the actual password.

IAM least-privilege

Grant only the IAM actions your service needs:

dbCredentials.grantRead(taskRole);

Under the hood this adds secretsmanager:GetSecretValue and secretsmanager:DescribeSecret for that specific secret ARN. The task role has no access to any other secrets. Principle of least privilege enforced by policy, not convention.

For a manual policy when the CDK grantRead is not available:

{
  "Effect": "Allow",
  "Action": [
    "secretsmanager:GetSecretValue",
    "secretsmanager:DescribeSecret"
  ],
  "Resource": "arn:aws:secretsmanager:eu-west-2:123456789:secret:/myapp/production/db-credentials-*"
}

The wildcard at the end is deliberate — AWS appends a random suffix to secret ARNs that changes on rotation. Without it, the policy breaks after the first rotation.

Spring Cloud AWS integration

Add the dependency:

<dependency>
    <groupId>io.awspring.cloud</groupId>
    <artifactId>spring-cloud-aws-starter-secrets-manager</artifactId>
    <version>3.1.1</version>
</dependency>

Then configure which secrets to load in application.yml:

spring:
  config:
    import: "aws-secretsmanager:/myapp/production/db-credentials"
  datasource:
    username: ${username}
    password: ${password}

Spring Cloud AWS fetches the secret at startup, parses the JSON, and flattens the keys into the Spring Environment. ${username} and ${password} resolve to the values from Secrets Manager before any @Value injection runs.

For multiple secrets:

spring:
  config:
    import:
      - "aws-secretsmanager:/myapp/production/db-credentials"
      - "aws-secretsmanager:/myapp/production/betfair-credentials"

Each secret’s keys are merged into the Environment. Name them carefully to avoid collisions.

Caching to avoid rate limits

Secrets Manager has a rate limit of 10,000 API calls per second per region. In practice the limit you’ll hit is much lower in normal use — but if you have dozens of pods all calling GetSecretValue on startup simultaneously, or a configuration refresh loop, you can breach it.

Spring Cloud AWS caches the secret value for the lifetime of the application context by default. For long-running services this is usually correct: you load once at startup and the value doesn’t change unless the pod restarts.

For services that need to pick up rotated secrets without restarting, configure a cache TTL:

spring:
  cloud:
    aws:
      secretsmanager:
        reload:
          strategy: refresh
          period: 1m

This re-fetches the secret every minute and refreshes any @RefreshScope beans that depend on it. The cost is one GetSecretValue call per minute per pod — entirely within the rate limit for any reasonable fleet size.

Automatic rotation

Secrets Manager can rotate secrets automatically using a Lambda function. AWS provides managed rotation Lambdas for common databases (RDS, Redshift, DocumentDB). For a custom secret, you write a Lambda that follows the four-step rotation protocol: createSecret, setSecret, testSecret, finishSecret.

Enable rotation in CDK:

dbCredentials.addRotationSchedule("Rotation", RotationScheduleOptions.builder()
    .automaticallyAfter(Duration.days(30))
    .rotationLambda(rotationLambda)
    .build());

With rotation enabled and a TTL-based cache refresh in Spring, your application picks up the new password within one refresh cycle without any deployment. Zero manual credential rotation, zero downtime.

Local development

Your local developer machine does not have an ECS task role. Options:

AWS SSO / credential file: If your developers authenticate via aws sso login, Spring Cloud AWS picks up the credential chain automatically. The local call hits the real Secrets Manager endpoint using the developer’s permissions.

LocalStack: For fully offline development, LocalStack emulates Secrets Manager locally. Configure the endpoint override:

spring:
  cloud:
    aws:
      secretsmanager:
        endpoint: http://localhost:4566
      region:
        static: eu-west-2
      credentials:
        access-key: test
        secret-key: test

Create the secret in LocalStack via the CLI:

aws --endpoint-url http://localhost:4566 secretsmanager create-secret \
  --name /myapp/production/db-credentials \
  --secret-string '{"username":"myapp","password":"localdev"}'

Environment variable override: For simple cases, override the injected property in application-local.yml:

username: myapp
password: localdev

Spring’s property override order means local profiles take precedence over secrets-manager-loaded values, so local developers never need AWS access at all.

What good looks like

A well-configured secret setup has these properties: the secret value exists nowhere in your git history, the running application fetches it over TLS from a service with access logging enabled, the IAM policy that grants access is as narrow as possible, rotation happens automatically, and local development works without touching production infrastructure.

With Spring Cloud AWS and Secrets Manager, all of this is achievable in an afternoon. The harder part is enforcing it consistently across a team — which is why CDK infrastructure-as-code and peer-reviewed IAM policies matter as much as the runtime configuration.

If you’re working on AWS security posture for a Spring Boot application and want to review how secrets are managed, get in touch.

Samuel Jackson

Samuel Jackson

Senior Java Back End Developer & Contractor

Senior Java Back End Developer — Betfair Exchange API specialist, Spring Boot, AWS, and event-driven architecture. 20+ years delivering high-performance systems across betting, finance, energy, retail, and government. Available for Java contracting.