Manage Application Secrets in AWS Parameter Store

Posted on Sep 4, 2023 in
Reading time: 5 minutes

Intro

In this tutorial I show you how you can manage application properties (incl. secrets) using the AWS Parmeter Store. If you are already using AWS, it’s a good option for storing your application secrets. Using IAM you can also assign fine-grained access to the parameter store.

Spring Cloud AWS adds first-class support for the parameter store to Spring so we can use it as a property source.

🍿 Watch on YouTube or get the code from GitHub

AWS Setup

In order to use the parameter store you obviously need an AWS account. If you don’t have one yet, you can create one here.

Once you have an account, you need to create an IAM user that has access to the parameter store. You can do this by logging into the AWS console and navigating to the IAM service. There you can create a new user and assign the following policy to it

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "ssm:GetParametersByPath",
"Resource": "*"
}
]
}

You can find the ARN of the parameter store by navigating to the parameter store service and selecting the root node in the tree. The ARN will be displayed in the details section.

Take note of the region you are in, you will need it later.

To access AWS from our application we will need the access credentials of the user we just created. You can find them in the IAM service under the “Security Credentials” tab. You can either use the access key and secret key directly, or create a new access key.

Before we get started, let’s add a parameter to the parameter store. We will use this parameter later in our application. Navigate to the parameter store service and create a new parameter named /config/spring/secret and make it a secret.

Feel free to use any text value, or pick swordfish like I did.

With the AWS setup ready..

Let’s Code

We are using Spring Cloud AWS to do the heavy lifting. So I’m adding the BOM for version 3.0.2 and add the dependency to the parameter store starter as well as a few others that are needed to refresh our config whenever values have changed in the param store.

 ...
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
 
plugins {
id("org.springframework.boot") version "3.1.3"
id("io.spring.dependency-management") version "1.1.3"
kotlin("jvm") version "1.8.22"
kotlin("plugin.spring") version "1.8.22"
}
 
group = "dev.axgr"
version = "0.0.1-SNAPSHOT"
 
java {
sourceCompatibility = JavaVersion.VERSION_17
}
 
repositories {
mavenCentral()
}
 
dependencies {
 ...
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.jetbrains.kotlin:kotlin-reflect")
 
implementation(platform("io.awspring.cloud:spring-cloud-aws-dependencies:3.0.2"))
implementation("io.awspring.cloud:spring-cloud-aws-starter-parameter-store")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.cloud:spring-cloud-starter")
 ...
testImplementation("org.springframework.boot:spring-boot-starter-test")
}
 
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs += "-Xjsr305=strict"
jvmTarget = "17"
}
}
 
tasks.withType<Test> {
useJUnitPlatform()
}
 

Spring Cloud AWS needs the access credentials to perform any operations on AWS. It explores a few defined locations to find the credentials:

  1. Java System Properties - aws.accessKeyId and aws.secretAccessKey
  2. Environment Variables - AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY
  3. Web Identity Token credentials from system properties or environment variables
  4. Credential profiles file at the default location (~/.aws/credentials) shared by all AWS SDKs and the AWS CLI
  5. Credentials delivered through the Amazon EC2 container service if AWS_CONTAINER_CREDENTIALS_RELATIVE_URI environment variable is set and security manager has permission to access the variable,
  6. Instance profile credentials delivered through the Amazon EC2 metadata service

We provide a dedicated secrets.properties file to keep the credentials out of the source code. This file is not checked into version control and looks like this:

# src/main/resources/secrets.properties
 
spring.cloud.aws.credentials.access-key=AWS_ACCESS_KEY
spring.cloud.aws.credentials.secret-key=AWS_SECRET_KEY

These credentials can then optionally be loaded from the primary configuration:

# src/main/resources/application.properties
 
spring.config.import[0]=optional:secrets.properties
spring.config.import[1]=aws-parameterstore:/config/spring/
spring.cloud.aws.region.static=eu-central-1
logging.level.io.awspring.cloud=debug

The second import is for the param store and results in Spring Cloud AWS reading the properties under a given path. This is useful if you want to separate your config per-environment, i.e. having /config/production/ and /config/staging/.

Let’s add two components, one to manage our secret and another one to read it. We also enable scheduling using the @EnableScheduling annotation on our main application class.

package dev.axgr
 
import org.springframework.beans.factory.annotation.Value
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component
 
@Component
class SecretHolder {
 
@Value("\${secret}")
private lateinit var secret: String
 
fun secret() = secret
 
}
 
@Component
class SecretReader(private val holder: SecretHolder) {
 
@Scheduled(fixedDelay = 1000)
fun print() = println(holder.secret())
 
}

If you run the application you should see the text swordfish printed to the console every second. And this value is coming directly from the AWS param store!

But there is more. We also want to refresh values in our application whenever they change in the param store. We can pull this off by adjusting the SecretHolder class a bit.

 
@RefreshScope
@Configuration
class SecretHolder {
 
@Value("\${secret}")
private lateinit var secret: String
 
fun secret() = secret
 
}

We make sure the SecretHolder is a configuration and annotate it with @RefreshScope. This will make sure that the bean is reloaded whenever the value changes.

In order for Spring to realize that a value has changed it needs to poll the param store every now and then. We also need to specify the refresh strategy.

# src/main/resources/application.properties
 
spring.config.import[0]=optional:secrets.properties
spring.config.import[1]=aws-parameterstore:/config/spring/
spring.cloud.aws.region.static=eu-central-1
 
spring.cloud.aws.parameterstore.reload.strategy=refresh
spring.cloud.aws.parameterstore.reload.period=15s
 
logging.level.io.awspring.cloud=debug

Spring Cloud AWS will now poll the param store every 15 seconds and refresh the SecretHolder bean if the value has changed. We could as well have used the second strategy: restart_context which will restart the application context if a value has changed.

Thanks for reading!

The Dead Letter Queue

Love what you're seeing? By subscribing to my newsletter, not only will you be the first to know about fresh tutorials and videos, but you'll also unlock:

Subscribe now and become a part of our growing tech tribe!