Securely Store Application Secrets in AWS Secrets Manager

This guide describes how to create a Java application that accesses secrets in Amazon Web Services (AWS) Secrets Manager.

Instead of storing a database URL, username, and password in plain text or environment variables, a secret manager provides a convenient way to store API keys, passwords, certificates, and other sensitive data while improving security.

Prerequisites #

Follow the steps below to create the application from scratch. However, you can also download the completed example in Java.

A note regarding your development environment

Consider using Visual Studio Code that provides native support for developing applications with the Graal Cloud Native Tools extension.

Note: If you use IntelliJ IDEA, enable annotation processing.

Windows platform: The GCN guides are compatible with Gradle only. Maven support is coming soon.

1. Create the Application #

Create an application using the GCN Launcher.

  1. Open the GCN Launcher in advanced mode.

  2. Create a new project using the following selections.
    • Project Type: Application (Default)
    • Project Name: aws-secrets-demo
    • Base Package: com.example (Default)
    • Clouds: AWS
    • Language: Java (Default)
    • Build Tool: Gradle (Groovy) or Maven
    • Test Framework: JUnit (Default)
    • Java Version: 17 (Default)
    • Micronaut Version: (Default)
    • Cloud Services: Secret Management
    • Features: GraalVM Native Image (Default)
    • Sample Code: Yes (Default)
  3. Click Generate Project, then click Download Zip. The GCN Launcher creates an application with the default package com.example in a directory named aws-secrets-demo. The application ZIP file will be downloaded in your default downloads directory. Unzip it, open in your code editor, and proceed to the next steps.

Alternatively, use the GCN CLI as follows:

gcn create-app com.example.aws-secrets-demo \
    --clouds=aws \
    --services=secretmanagement \
    --features=graalvm \
    --build=gradle \
    --jdk=17 \
    --lang=java
gcn create-app com.example.aws-secrets-demo \
    --clouds=aws \
    --services=secretmanagement \
    --features=graalvm \
    --build=maven \
    --jdk=17 \
    --lang=java

For more information, see Using the GCN CLI.

GCN adds the OAuth and AWS Secrets Manager dependencies to your build file, as follows

aws/build.gradle

implementation("io.micronaut.security:micronaut-security-oauth2")
implementation("io.micronaut.aws:micronaut-aws-secretsmanager")
implementation("io.micronaut.aws:micronaut-aws-sdk-v2")

aws/pom.xml

<dependency>
    <groupId>io.micronaut.security</groupId>
    <artifactId>micronaut-security-oauth2</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-aws-secretsmanager</artifactId>
    <scope>compile</scope>
</dependency>
<dependency>
    <groupId>io.micronaut.aws</groupId>
    <artifactId>micronaut-aws-sdk-v2</artifactId>
    <version>3.5.1</version>
    <scope>compile</scope>
</dependency>

1.1. Configure the Application #

To enable distributed configuration, GCN has created a configuration file named aws/src/main/resources/bootstrap.properties. Edit its contents so that it matches the following:

aws.client.system-manager.parameterstore.enabled=true
aws.secretsmanager.secrets[0].prefix=micronaut.security.oauth2.clients.demo-oauth
# <1>
micronaut.application.name=gcn-secrets-demo
# <2>
micronaut.config-client.enabled=true
# <3>
aws.secretsmanager.secrets[0].secret-name=demo-oauth
  1. Set the name of the application to gcn-secrets-demo. This name will be used for retrieving secrets.
  2. Reading and resolving configuration from distributed sources is enabled.
  3. Define a secret named demo-oauth and specify its prefix. Micronaut will look for a secret with this name in AWS Secrets Manager and load it to the configuration with the prefix.

    Note: Read the Micronaut AWS Secrets Manager integration documentation to find out more about configuring secrets.

When the secret is loaded, the application will create an OAuth Client Configuration named demo-oauth as described in the Micronaut OAuth2 Security documentation. The secret should therefore include a supported OAuth2 credentials configuration. In this example we will create a secret that will include two fields: client-secret and client-id.

To use client-secret and client-id in your application, edit aws/src/main/resources/application.properties to match the following:

micronaut.security.authentication=cookie
micronaut.security.oauth2.clients.demo-oauth.client-id=${OAUTH_CLIENT_ID\:XXX}
micronaut.security.oauth2.clients.demo-oauth.client-secret=${OAUTH_CLIENT_SECRET\:YYY}

1.2. Controller #

The GCN Launcher created an example controller class (named ClientIdController) that exposes the values read from AWS Secrets Manager. The contents of aws/src/main/java/com/example/ClientIdController.java are as follows:

package com.example;

import io.micronaut.http.annotation.Controller;
import io.micronaut.http.annotation.Get;
import io.micronaut.security.annotation.Secured;
import io.micronaut.security.oauth2.configuration.OauthClientConfiguration;
import jakarta.inject.Named;

import static io.micronaut.http.MediaType.TEXT_PLAIN;
import static io.micronaut.security.rules.SecurityRule.IS_ANONYMOUS;

@Controller
class ClientIdController {

    private final OauthClientConfiguration oauthClientConfiguration;

    ClientIdController(@Named("demo-oauth") OauthClientConfiguration oauthClientConfiguration) { // <1>
        this.oauthClientConfiguration = oauthClientConfiguration;
    }

    @Secured(IS_ANONYMOUS)
    @Get(uri = "/oauth-client-id", produces = TEXT_PLAIN) // <2>
    public String index() {
        return oauthClientConfiguration.getClientId();
    }
}
  1. Constructor injection is used to inject a Micronaut OAuth2 client configuration named demo-oauth. It will be configured by setting the micronaut.security.oauth2.clients.demo-oauth property values loaded from AWS Secrets Manager.
  2. A method to respond to the GET request to the /oauth-client-id route. The response will contain the client id from the OAuth2 configuration. You will use it to test that the configuration is read correctly from a secret.

Note: The authentication client configuration will automatically be used if a Micronaut client is defined, but this guide only focuses on reading the secret and not using it to authenticate to an external server.

1.2. Logs #

To produce a more verbose output when the application starts up, edit the file named aws/src/main/resources/logback.xml. Insert the following <logger> element in the <configuration> element.

<logger name="io.micronaut.aws.distributedconfiguration" level="TRACE"/>

2. Create the Secret #

Create a secret to be used for OAuth2 in the AWS Secret Manager. The secret includes two fields: client-id and client-secret, which correspond to fields of a Micronaut OAuth2 client configuration. Then use the secret in the application and retrieve it with the controller created in the previous section.

2.1. Create a Secret in the AWS Secret Manager #

  1. Sign in to the AWS console. In the header, click Services, then in the Security, Identity & Compliance section click Secrets Manager to open it.
  2. In the AWS Secrets Manager, click Store a new secret. The secret-creation wizard with will open with four steps.
  3. In the Secret type section choose Other type of secret.
  4. In the Key/value pairs section choose Plain text and enter the following secrets (or enter the secrets manually as key-values):
    {
        "client-id": "xXx",
        "client-secret": "zZz"
    }
    
  5. Click Next at the bottom of the page to go to the next step.
  6. Enter /config/gcn-secrets-demo/demo-oauth as the secret name. Skip all the other settings. Click Next.

    Note: The secret name should be of the /config/[APPLICATION-NAME][-APPLICATION-ENV]/[SECRET-NAME] format where application environment is optional. The name must begin with a slash.

  7. Skip the optional Configure Rotation step by clicking Next again.
  8. Review the details and click Store. You should now see your secret in the secrets list.

2.2. Authenticate to AWS #

To test using the secret from AWS Secrets Manager on your local machine, you may use an AWS Access key configured in the AWS CLI. If you do not have the AWS CLI, follow the AWS documentation to install or update the latest version of the AWS CLI.

If you do not have an access key, complete the steps in the Programmatic Access section of the AWS Secure Credential Types guide to create a long-term access key for your account. Run aws configure and provide access key ID and secret access key to authenticate as your user.

Note: The AWS SDK will attempt to find the credentials automatically. Micronaut AWS Documentation describes how to supply specific credentials to use instead.

3. Run the Application #

To run the application, use the following command, which starts the application on port 8080.

./gradlew :aws:run
./mvnw install -pl lib -am
./mvnw mn:run -pl aws

You should see traces such as:

12:03:48.435 [main] INFO  i.m.context.env.DefaultEnvironment - Established active environments: [ec2]
12:03:48.442 [main] INFO  i.m.context.env.DefaultEnvironment - Established active environments: [ec2]
12:03:48.581 [main] INFO  i.m.context.DefaultBeanContext - Reading bootstrap environment configuration
12:03:50.441 [main] TRACE i.m.a.d.AwsDistributedConfigurationClient - application name: gcn-secrets-demo
12:03:51.673 [main] TRACE i.m.a.d.AwsDistributedConfigurationClient - evaluating 2 keys
12:03:51.673 [main] TRACE i.m.a.d.AwsDistributedConfigurationClient - adding property micronaut.security.oauth2.clients.demo-oauth.client-id from prefix /config/gcn-secrets-demo/
12:03:51.673 [main] TRACE i.m.a.d.AwsDistributedConfigurationClient - adding property micronaut.security.oauth2.clients.demo-oauth.client-secret from prefix /config/gcn-secrets-demo/
12:03:51.673 [main] DEBUG i.m.a.d.AwsDistributedConfigurationClient - Property source awssecretsmanager with #2 items
12:03:51.673 [main] TRACE i.m.a.d.AwsDistributedConfigurationClient - property micronaut.security.oauth2.clients.demo-oauth.client-id resolved
12:03:51.673 [main] TRACE i.m.a.d.AwsDistributedConfigurationClient - property micronaut.security.oauth2.clients.demo-oauth.client-secret resolved
12:03:51.927 [main] INFO  i.m.d.c.c.DistributedPropertySourceLocator - Resolved 1 configuration sources from client: compositeConfigurationClient(AWS Secrets Manager)
12:03:53.022 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 6037ms. Server Running: http://localhost:8080

Test that the secret was loaded correctly by opening http://localhost:8080/oauth-client-id in your browser or using curl:

curl http://localhost:8080/oauth-client-id

You should see the following response:

xXx

4. Generate a Native Executable Using GraalVM #

GCN supports compiling a Java application ahead-of-time into a native executable using GraalVM Native Image. You can use the Gradle plugin for GraalVM Native Image building/Maven plugin for GraalVM Native Image building. Packaged as a native executable, it significantly reduces application startup time and memory footprint.

To generate a native executable, run the following command:

./gradlew :aws:nativeCompile

The native executable is created in the aws/build/native/nativeCompile/ directory and can be run with the following command:

aws/build/native/nativeCompile/aws
./mvnw install -pl lib -am
./mvnw package -pl aws -Dpackaging=native-image

The native executable is created in the aws/target/ directory and can be run with the following command:

aws/target/aws

The application starts on port 8080. Test it as described in the previous section. The application should behave identically as if you run it from a JAR file, but with reduced startup time and smaller memory footprint.

Summary #

This guide demonstrated how to create a Java application that accesses secrets in Amazon Web Services (AWS) Secrets Manager. You also saw how to package this application into a native executable and deploy it from a virtual machine.