From 75bec1802e04708a9ebc719fc19f5924be08819d Mon Sep 17 00:00:00 2001 From: Art Date: Wed, 29 Sep 2021 15:51:44 +0300 Subject: [PATCH] 38.4 Using Secrets Manager to store access keys (#38) --- README.md | 20 ++++ support-portal-backend/pom.xml | 2 - .../backend/config/S3PropertiesListener.java | 108 ++++++++++++++++++ .../main/resources/META-INF/spring.factories | 2 + 4 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/S3PropertiesListener.java create mode 100644 support-portal-backend/src/main/resources/META-INF/spring.factories diff --git a/README.md b/README.md index cb6160c..4c8460c 100644 --- a/README.md +++ b/README.md @@ -510,3 +510,23 @@ systemctl restart docker 5. Create configuration 6. Create ProfileImageService implementation +##### 38.4 Using Secrets Manager to store access keys + +1. Use this tutorial + - [Using AWS Secrets Manager to manage secrets in Spring Boot Applications](https://raymondhlee.wordpress.com/2019/10/11/using-aws-secrets-manager-to-manage-secrets-in-spring-boot-applications/) +2. Create secrets for API calls + - Secrets Manager console + - Create new secret + - Other type of secrets + - AMAZON_S3_ACCESS_KEY: {provide value} + - AMAZON_S3_SECRET_KEY: {provide value} + - Select the encryption key + - DefaultEncryptionKey + - Secret Name: `/image-s3/portal-api` + - Disable automatic rotation + - Next -> view Sample code + - Store +3. Create ApplicationListener +4. Add the new application listener to the `spring.factories` file in the folder `src/main/resources/META-INF` +5. Test locally -> works + \ No newline at end of file diff --git a/support-portal-backend/pom.xml b/support-portal-backend/pom.xml index 1d4ecac..18f2332 100644 --- a/support-portal-backend/pom.xml +++ b/support-portal-backend/pom.xml @@ -193,8 +193,6 @@ aws-rds,image-s3 - AKI...{THIS IS NOT SECURE TO STORE ACCESS_KEY in GitHub Repo}...BJ - LUS...{THIS IS NOT SECURE TO STORE SECRET_KEY in GitHub Repo}...AW always diff --git a/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/S3PropertiesListener.java b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/S3PropertiesListener.java new file mode 100644 index 0000000..a914637 --- /dev/null +++ b/support-portal-backend/src/main/java/net/shyshkin/study/fullstack/supportportal/backend/config/S3PropertiesListener.java @@ -0,0 +1,108 @@ +package net.shyshkin.study.fullstack.supportportal.backend.config; + +import com.amazonaws.services.secretsmanager.AWSSecretsManager; +import com.amazonaws.services.secretsmanager.AWSSecretsManagerClientBuilder; +import com.amazonaws.services.secretsmanager.model.*; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.context.ApplicationListener; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertiesPropertySource; + +import java.io.IOException; +import java.util.Base64; +import java.util.Properties; + +@Slf4j +public class S3PropertiesListener implements ApplicationListener { + + private ObjectMapper mapper = new ObjectMapper(); + + @Override + public void onApplicationEvent(ApplicationPreparedEvent event) { + System.out.println("onApplicationEvent"); + String secretJson = getSecret(); + log.info("Retrieved secretJson from Secret Manager: {}", secretJson); + System.out.println("Retrieved secretJson from Secret Manager: " + secretJson); + String accessKey = getString(secretJson, "AMAZON_S3_ACCESS_KEY"); + String secretKey = getString(secretJson, "AMAZON_S3_SECRET_KEY"); + + ConfigurableEnvironment environment = event.getApplicationContext().getEnvironment(); + Properties props = new Properties(); + props.put("app.amazon-s3.access-key", accessKey); + props.put("app.amazon-s3.secret-key", secretKey); + environment.getPropertySources().addFirst(new PropertiesPropertySource("aws.secret.manager", props)); + + } + +// Use this code snippet in your app. +// If you need more information about configurations or implementing the sample code, visit the AWS docs: +// https://docs.aws.amazon.com/sdk-for-java/v1/developer-guide/java-dg-samples.html#prerequisites + + private String getSecret() { + + String secretName = "/image-s3/portal-api"; + String region = "eu-north-1"; + + // Create a Secrets Manager client + AWSSecretsManager client = AWSSecretsManagerClientBuilder.standard() + .withRegion(region) + .build(); + + // In this sample we only handle the specific exceptions for the 'GetSecretValue' API. + // See https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html + // We rethrow the exception by default. + + String secret = null, decodedBinarySecret = null; + GetSecretValueRequest getSecretValueRequest = new GetSecretValueRequest() + .withSecretId(secretName); + GetSecretValueResult getSecretValueResult = null; + + try { + getSecretValueResult = client.getSecretValue(getSecretValueRequest); + } catch (DecryptionFailureException e) { + // Secrets Manager can't decrypt the protected secret text using the provided KMS key. + // Deal with the exception here, and/or rethrow at your discretion. + throw e; + } catch (InternalServiceErrorException e) { + // An error occurred on the server side. + // Deal with the exception here, and/or rethrow at your discretion. + throw e; + } catch (InvalidParameterException e) { + // You provided an invalid value for a parameter. + // Deal with the exception here, and/or rethrow at your discretion. + throw e; + } catch (InvalidRequestException e) { + // You provided a parameter value that is not valid for the current state of the resource. + // Deal with the exception here, and/or rethrow at your discretion. + throw e; + } catch (ResourceNotFoundException e) { + // We can't find the resource that you asked for. + // Deal with the exception here, and/or rethrow at your discretion. + throw e; + } + + // Decrypts secret using the associated KMS CMK. + // Depending on whether the secret is a string or binary, one of these fields will be populated. + if (getSecretValueResult.getSecretString() != null) { + secret = getSecretValueResult.getSecretString(); + } else { + decodedBinarySecret = new String(Base64.getDecoder().decode(getSecretValueResult.getSecretBinary()).array()); + } + + // Your code goes here. + return secret != null ? secret : decodedBinarySecret; + } + + private String getString(String json, String path) { + try { + JsonNode root = mapper.readTree(json); + return root.path(path).asText(); + } catch (IOException e) { + log.error("Can't get {} from json {}", path, json, e); + return null; + } + } +} diff --git a/support-portal-backend/src/main/resources/META-INF/spring.factories b/support-portal-backend/src/main/resources/META-INF/spring.factories new file mode 100644 index 0000000..ee80885 --- /dev/null +++ b/support-portal-backend/src/main/resources/META-INF/spring.factories @@ -0,0 +1,2 @@ +# file src/main/resources/META-INF/spring.factories +org.springframework.context.ApplicationListener=net.shyshkin.study.fullstack.supportportal.backend.config.S3PropertiesListener \ No newline at end of file