Skip to content

Commit

Permalink
[#21] Enable fetching KeyPair from AWS SecretsManager
Browse files Browse the repository at this point in the history
This also somewhat reworks the configuration and logic
so as to simplify adding more sources for secrets
(e.g., Vault, Azure, etc.).
  • Loading branch information
massenz committed Oct 29, 2022
1 parent a840817 commit 5802aab
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 28 deletions.
41 changes: 25 additions & 16 deletions aws-upload-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,31 @@
#
# Generates a Elliptict Cryptography keypair using openssl

set -e
set -eu

function usage {
echo "Usage: $(basename $0) KEY [DIR]
echo "Usage: $(basename $0) KEY SECRET
KEY the name of the key pair to upload, required;
DIR optionally, a directory where the keypair is stored
KEY the path to the key pair to upload, WITHOUT extension
SECRET the name of the secret to create in AWS Secrets Manager
This script uploads a key pair named 'KEY.pem' and 'KEY.pub' to AWS Secrets Manager,
using the \$AWS_PROFILE env var to obtain the credentials and the region to upload to.
Use \$AWS_ENDPOINT to specify a custom endpoint for the Secrets Manager service, if not using
the default AWS endpoint (eg, when testing against a localstack container, you can use
http://localhost:4566).
The pair can be generated using the keygen.sh script.
Requires the aws binary CLI (https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html)
"
}

KEY=${1:-}
DIR=${2:-}
SECRET=${2:-}
ENDPOINT_URL=""

if [[ -z ${KEY} || ${1:-} == "--help" || ${1:-} == "-h" ]]; then
if [[ -z ${KEY} || -z ${SECRET} || ${1:-} == "-h" ]]; then
usage
exit 1
fi
Expand All @@ -52,27 +56,32 @@ then
exit 1
fi

if [[ -n ${AWS_ENDPOINT:-} ]]; then
ENDPOINT_URL="--endpoint-url ${AWS_ENDPOINT}"
fi

PRIV=${KEY}.pem
PUB=${KEY}.pub
if [[ -n ${DIR} && -d ${DIR} ]]; then
PRIV=${DIR}/${PRIV}
PUB=${DIR}/${PUB}
if [[ ! -f ${PRIV} || ! -f ${PUB} ]]; then
usage
echo "ERROR: Cannot find ${PRIV} and/or ${PUB} keys"
exit 1
fi

out=$(mktemp /tmp/secret-XXXXXXXX.tmp)
cat <<EOF >$out
{
"priv": "$(while read -r line; do if [[ ! ${line} =~ ^----- ]]; \
then echo -n ${line}; fi; done < $PRIV)",
"pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; done < $PUB)",
then echo -n ${line}; fi; done < ${PRIV})",
"pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; done < ${PUB})",
"algorithm": "EC"
}
EOF

aws secretsmanager create-secret --name $KEY \
--description "Keypair $KEY generated by the $(basename $0) script" \
--secret-string file://$out

arn=$(aws ${ENDPOINT_URL} secretsmanager create-secret --name ${SECRET} \
--description "Keypair ${KEY} generated by the $(basename $0) script" \
--secret-string file://${out} | jq -r '.ARN')

rm $out
echo "[SUCCESS] Key Pair uploaded to AWS: ${KEY}"
echo "[SUCCESS] Key Pair ${KEY} uploaded to AWS: ${arn}"
3 changes: 3 additions & 0 deletions jwt-opa/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ dependencies {
// Test Containers: https://www.testcontainers.org/
testImplementation "org.testcontainers:testcontainers:$tcVersion"
testImplementation "org.testcontainers:junit-jupiter:$tcVersion"
testImplementation "org.testcontainers:localstack:$tcVersion"
testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.12.326'

}

jacocoTestCoverageVerification {
Expand Down
12 changes: 10 additions & 2 deletions jwt-opa/src/main/java/com/alertavert/opa/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,18 @@ public class Constants {
/** Marker for a Private Key object */
public static final String PRIVATE_KEY = "PRIVATE KEY";


/**
* Passphrase-based encryption (see
* {@link com.alertavert.opa.configuration.KeyMaterialConfiguration}.
*/
public static final String PASSPHRASE = "SECRET";

/**
* The name of the Env Var which contains the name of the file storing the AWS API Token in a
* running EKS container.
*/
public static final String AWS_TOKEN_FILE = "AWS_WEB_IDENTITY_TOKEN_FILE";

/** OPA API version */
public static final String OPA_VERSION = "v1";

Expand All @@ -73,7 +78,7 @@ public class Constants {
public static final String DEFAULT_HEALTH_ROUTE = "/health";

/**
* The default login endpoing, by default only allowed using HTTP Basic auth, but will not
* The default login endpoint, by default only allowed using HTTP Basic auth, but will not
* require a valid API Token and won't try to authorize access.
*/
public static final String DEFAULT_LOGIN_ROUTE = "/login";
Expand All @@ -99,6 +104,9 @@ public class Constants {
public static final String KEYPAIR_ERROR = "Cannot load secret from AWS: secret-name = {}, "
+ "error = {}";

public static final String CREDENTIALS_PROVIDER_LOG = "Creating a {} Credentials Provider: {}";
public static final String CREDENTIALS_PROVIDER_ERROR = "Cannot create Credentials Provider: {}";

/**
* A completely inactive user, that needs to act as a placeholder when the `username` is not
* found in the Users DB, and would trigger an exception in the Java Security HTTP Basic
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) 2022 AlertAvert.com. All rights reserved.
//

package com.alertavert.opa.security.aws;

import com.alertavert.opa.Constants;
import com.alertavert.opa.ExcludeFromCoverageGenerated;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.util.StringUtils;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider;
import software.amazon.awssdk.auth.credentials.WebIdentityTokenFileCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClientBuilder;

import java.net.URI;

import static com.alertavert.opa.Constants.AWS_TOKEN_FILE;
import static com.alertavert.opa.Constants.CREDENTIALS_PROVIDER_LOG;


/**
* <H2>PatientsApplicationConfiguration</H2>
*
* <p>System and application configuration goes here.
*
* <p>Here we are trying to configure the AWS client, based on
* whether we auto-detect the Server to be running locally, or on EKS.</p>
*
* @author M. Massenzio, 2020-10-06
*/
@Configuration
@Slf4j
@ExcludeFromCoverageGenerated
public class AwsClientConfiguration {

@Value("${aws.region:}")
String region;

@Value("${aws.profile:}")
String profile;

@Value("${aws.endpoint:}")
String endpoint;

@Value("${aws.keypair.secret_name:}")
private String keypairSecretName;

public Region region() {
return Region.of(region);
}

public String awsProfile() {
return profile;
}

/**
* <p>Instantiates a Client for AWS Secrets Manager service, using a `profile` (whose credentials
* are stored in {@literal ~/.aws/credentials}) if the {@literal aws.profile} property is set
* (typically for local runs/tests); otherwise it uses the
* {@link WebIdentityTokenFileCredentialsProvider Token Provider} which derives the API Token
* from a file stored in the container at the location pointed at by the env var
* {@link Constants#AWS_TOKEN_FILE}; the latter is used when running the service in an EKS Container.
*
* <p><strong>For the above to work, the STS service SDK must be in the classpath</strong>
* <pre>
* implementation 'software.amazon.awssdk:sts'
* </pre>
*
* @return a Client to access Secrets Manager and retrieve secrets (passwords)
*/
@Bean @Profile("aws")
public SecretsManagerClient secretsManagerClient() {
log.debug("Instantiating the SecretsManager Client for region = {}", region());

AwsCredentialsProvider provider;
if (StringUtils.hasText(profile)) {
log.info(CREDENTIALS_PROVIDER_LOG, "Profile", profile);
provider = ProfileCredentialsProvider.create(profile);
} else {
log.info(CREDENTIALS_PROVIDER_LOG, "Token File", System.getenv(AWS_TOKEN_FILE));
provider = WebIdentityTokenFileCredentialsProvider.create();
}

SecretsManagerClientBuilder builder = SecretsManagerClient.builder()
.region(region())
.credentialsProvider(provider);

// Typically this is overridden in tests, to point to a local Secrets Manager (e.g. using
// LocalStack, pointing to http://localhost:4566)
if (StringUtils.hasText(endpoint)) {
builder.endpointOverride(URI.create(endpoint));
}
return builder.build();
}

@Bean
String keypairSecretName() {
return keypairSecretName;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,12 @@ public String algorithm() {
.block();
}

PublicKey loadPublicKey(SecretKeys secretKeys) {
private PublicKey loadPublicKey(SecretKeys secretKeys) {
PemObject object = new PemObject(PUBLIC_KEY, Base64.decode(secretKeys.pub));
return PemUtils.getPublicKey(object.getContent(), secretKeys.algorithm);
}

PrivateKey loadPrivateKey(SecretKeys secretKeys) {
private PrivateKey loadPrivateKey(SecretKeys secretKeys) {
PemObject object = new PemObject(PRIVATE_KEY, Base64.decode(secretKeys.priv));
return PemUtils.getPrivateKey(object.getContent(), secretKeys.algorithm);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
* @author M. Massenzio, 2022-01-04
*/
@Slf4j
@ExcludeFromCoverageGenerated
public class AwsSecretsManagerResolver implements SecretsResolver {

private final SecretsManagerClient secretsClient;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright (c) 2022 AlertAvert.com. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* Author: Marco Massenzio (marco@alertavert.com)
*/

package com.alertavert.opa.security.aws;

import com.alertavert.opa.security.crypto.KeypairReader;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;
import software.amazon.awssdk.services.secretsmanager.model.CreateSecretRequest;

import java.security.KeyPair;
import java.util.Base64;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;

/**
* <H2>AwsSecretsKeypairReaderTest</H2>
*
* @author M. Massenzio, 2022-10-28
*/
@SpringBootTest(classes = {
AwsClientConfiguration.class,
})
@ActiveProfiles(profiles = {"test", "aws"})
@ContextConfiguration(initializers = {AwsSecretsManagerResolverTest.Initializer.class})
class AwsSecretsKeypairReaderTest {

@Autowired
SecretsManagerClient secretsManagerClient;

String keypairName = "test-keypair";
String secret = """
{
"priv": "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYEIUVkTtwBilNcNoEzsP3jdslIlOtXQ5pByuzxhLJTChRANCAAS+gitL0EgxyFCvdT6rJ39DbCrLLwLReTA5OXahcIEeCBygfyh35H8T9r9uHszSOCpAk1QQMuhqURzyWEaKjk92",
"pub": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvoIrS9BIMchQr3U+qyd/Q2wqyy8C0XkwOTl2oXCBHggcoH8od+R/E/a/bh7M0jgqQJNUEDLoalEc8lhGio5Pdg==",
"algorithm": "EC"
}
""";
String priv = "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYEIUVkTtwBilNcNoEzsP3jdslIlO"
+ "tXQ5pByuzxhLJTChRANCAAS+gitL0EgxyFCvdT6rJ39DbCrLLwLReTA5OXahcIEeCBygfyh35H8T9r9uHs"
+ "zSOCpAk1QQMuhqURzyWEaKjk92";


@BeforeEach
void setUp() {
secretsManagerClient.createSecret(CreateSecretRequest.builder()
.name(keypairName)
.secretString(secret)
.build());
}

@Test
public void getSecret() {
KeypairReader reader = new AwsSecretsKeypairReader(
new AwsSecretsManagerResolver(secretsManagerClient), keypairName);
KeyPair keyPair = reader.loadKeys();
assertNotNull(keyPair);
assertThat(Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()))
.isEqualTo(priv);
assertThat(reader.algorithm()).isEqualTo("EC");
}
}
Loading

0 comments on commit 5802aab

Please sign in to comment.