Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Full support for AWS keypair/secrets & Refactoring for local secrets/keypair #46

Merged
merged 3 commits into from
Nov 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ Save both keys in a `private` folder (not under source control) and then point t
secrets:
keypair:
private: "private/ec-key-1.pem"
pub: "private/ec-key-pub.pem"
pub: "private/ec-key.pub"
```

You can use either an absolute path, or the relative path to the current directory from where you are launching the Web server.
Expand Down
24 changes: 4 additions & 20 deletions aws-upload-keys.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,7 @@
# Copyright (c) 2021 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)
#

#
# Generates a Elliptict Cryptography keypair using openssl

set -eu

Expand Down Expand Up @@ -72,15 +57,14 @@ 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})",
"algorithm": "EC"
then echo -n ${line}; fi; done < ${PRIV})",
"pub": "$(while read -r line; do [[ ${line} =~ ^----- ]] || echo -n ${line}; \
done < ${PUB})"
}
EOF


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

rm $out
Expand Down
15 changes: 4 additions & 11 deletions jwt-opa/src/main/java/com/alertavert/opa/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
* All constants are grouped here for ease of reference.
*/
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@ExcludeFromCoverageGenerated
public class Constants {
/**
* Basic Authorization header type.
Expand All @@ -41,23 +42,12 @@ public class Constants {
*/
public static final String BEARER_TOKEN = "Bearer";

/**
* The type of encryption accepted by the {@link com.alertavert.opa.jwt.JwtTokenProvider}
*/
public static final String ELLIPTIC_CURVE = "EC";

/** Marker for a Public Key object */
public static final String PUBLIC_KEY = "PUBLIC KEY";

/** 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.
Expand Down Expand Up @@ -129,4 +119,7 @@ public Collection<? extends GrantedAuthority> getAuthorities() {
};
public static final int MAX_TOKEN_LEN_LOG = 6;
public static final ObjectMapper MAPPER = new ObjectMapper();
public static final String PEM_EXT = ".pem";
public static final String PUB_EXT = ".pub";

}
Original file line number Diff line number Diff line change
Expand Up @@ -18,58 +18,105 @@

package com.alertavert.opa.configuration;

import com.alertavert.opa.security.EnvSecretResolver;
import com.alertavert.opa.security.FileSecretResolver;
import com.alertavert.opa.security.NoopKeypairReader;
import com.alertavert.opa.security.NoopSecretResolver;
import com.alertavert.opa.security.SecretsResolver;
import com.alertavert.opa.security.aws.AwsSecretsKeypairReader;
import com.alertavert.opa.security.aws.AwsSecretsManagerResolver;
import com.alertavert.opa.security.crypto.KeyLoadException;
import com.alertavert.opa.security.crypto.KeypairFileReader;
import com.alertavert.opa.security.crypto.KeypairReader;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import software.amazon.awssdk.services.secretsmanager.SecretsManagerClient;

import java.io.IOException;
import javax.annotation.PostConstruct;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;

import static com.alertavert.opa.Constants.ELLIPTIC_CURVE;
import static com.alertavert.opa.Constants.PASSPHRASE;
import static com.alertavert.opa.Constants.PEM_EXT;
import static com.alertavert.opa.Constants.PUB_EXT;

@Slf4j
@Configuration
@EnableConfigurationProperties(TokensProperties.class)
@EnableConfigurationProperties({
KeysProperties.class,
TokensProperties.class
})
@RequiredArgsConstructor
public class KeyMaterialConfiguration {

private final TokensProperties tokensProperties;
private final KeysProperties keyProperties;

public KeyMaterialConfiguration(TokensProperties properties) {
this.tokensProperties = properties;
/**
* AWS SecretsManager client, will only be configured by the
* {@link com.alertavert.opa.security.aws.AwsClientConfiguration AWS Configuration} if the
* {@literal aws} profile is active.
*/
@Autowired(required = false)
SecretsManagerClient secretsManagerClient;

@PostConstruct
private void log() {
log.info("Configuring key material, algorithm = {}, location = {}",
keyProperties.algorithm, keyProperties.location);
}

@Bean
public String issuer() {
return tokensProperties.getIssuer();
SecretsResolver secretsResolver() {
return switch (keyProperties.getLocation()) {
case env -> new EnvSecretResolver();
case file -> new FileSecretResolver();
case keypair -> new NoopSecretResolver();
case awsSecret -> new AwsSecretsManagerResolver(secretsManagerClient);
case vaultPath -> throw new UnsupportedOperationException("Support for Vault not "
+ "implemented yet");
};
}

@Bean
Algorithm hmac(KeypairReader reader) {
switch (reader.algorithm()) {
case PASSPHRASE:
return Algorithm.HMAC256(tokensProperties.getSecret());
case ELLIPTIC_CURVE:
KeyPair keyPair = reader.loadKeys();
return Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(),
(ECPrivateKey) keyPair.getPrivate());
default:
throw new IllegalArgumentException(String.format("Algorithm [%s] not supported",
reader.algorithm()));
}
KeypairReader keypairReader() {
return switch (keyProperties.getLocation()) {
case env, file -> new NoopKeypairReader();
case keypair -> new KeypairFileReader(keyProperties.algorithm.name(),
Path.of(keyProperties.name + PEM_EXT), Path.of(keyProperties.name + PUB_EXT));
case awsSecret -> new AwsSecretsKeypairReader(secretsResolver(), keyProperties.name);
case vaultPath -> throw new UnsupportedOperationException("Support for Vault not "
+ "implemented yet");
};
}


@Bean
JWTVerifier verifier(Algorithm algorithm) throws IOException {
return JWT.require(algorithm)
.withIssuer(issuer())
.build();
Algorithm hmac(SecretsResolver resolver, KeypairReader reader) {
return switch (keyProperties.getAlgorithm()) {
case PASSPHRASE -> {
log.warn("Using insecure passphrase signing secret, name = {}", keyProperties.name);
String passphrase = resolver.getSecret(keyProperties.getName()).block();
if (passphrase == null) {
log.error("Could not resolve secret {}, with SecretsResolver {}",
keyProperties.name, resolver.getClass().getSimpleName());
throw new IllegalArgumentException("Signing secret cannot be resolved");
}
yield Algorithm.HMAC256(passphrase);
}
case EC -> {
KeyPair keyPair = reader.loadKeys().block();
if (keyPair == null) {
throw new KeyLoadException("Cannot load keypair " + keyProperties.name);
}
yield Algorithm.ECDSA256((ECPublicKey) keyPair.getPublic(),
(ECPrivateKey) keyPair.getPrivate());
}
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2021 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.configuration;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* <h2>KeysPropertiers</h2>
*
* @author M. Massenzio, 202-11-19
*/
@Data
@ConfigurationProperties(prefix = "keys")
public class KeysProperties {

/**
* <h3>Location for the signing secret</h3>
*
* <p>Possible values are:
*
* <li>{@literal env}: only available for PASSPHRASE, env var name which contains
* the signing secret</li>
* <li>{@literal file}: only valid if algorithm is PASSPHRASE, the file is simply read</li>
* <li>{@literal keypair}: the filename without extension, to which `.pem` and `.pub`
* will be added</li>
* <li>{@literal aws_secret}: name of AWS SecretsManager secret</li>
* <li>{@literal vault_path}: path in HashiCorp Vault</li>
*
* <p>File paths can be absolute or relative.
*
* <p>For a {@link AlgorithmType#PASSPHRASE passphrase}, the secret is simply read from
* SecretsManager/Vault, the {@link AlgorithmType#EC keypair} keypair is
* stored as a JSON-formatted secret, with two keys: "priv" and "pub" (see
* {@link com.alertavert.opa.security.aws.AwsSecretsKeypairReader.SecretKeys}).
*/
enum KeyLocation {
env, file, keypair, awsSecret, vaultPath
}

public enum AlgorithmType {
/** Plaintext secret */
PASSPHRASE,
/** Elliptic Curve cryptography */
EC
}

AlgorithmType algorithm;
KeyLocation location;
String name;
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
* <h2>KeyPropertiers</h2>
* <h2>TokensPropertiers</h2>
*
* @author M. Massenzio, 2020-12-14
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
import com.alertavert.opa.configuration.TokensProperties;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
Expand Down Expand Up @@ -56,15 +56,15 @@ public class JwtTokenProvider {
@Autowired
Algorithm hmac;

@Autowired
JWTVerifier verifier;

@Autowired
String issuer;

@Autowired
TokensProperties tokensProperties;

public JWTVerifier verifier() {
return JWT.require(hmac)
.withIssuer(tokensProperties.getIssuer())
.build();
}

/**
* <p>Creates a JWT token for the given user, signed with the private key of the issuer.
*
Expand All @@ -88,7 +88,7 @@ public String createToken(String user, List<String> roles, @Nullable Instant exp

log.debug("Issuing JWT for user = {}, roles = {}", user, roles);
JWTCreator.Builder builder = JWT.create()
.withIssuer(issuer)
.withIssuer(tokensProperties.getIssuer())
.withSubject(user)
.withIssuedAt(Date.from(now))
.withArrayClaim(ROLES, roles.toArray(new String[0]));
Expand Down Expand Up @@ -131,7 +131,7 @@ public boolean validateToken(String token) {
}

public DecodedJWT decode(String token) throws JWTVerificationException {
return verifier.verify(token);
return verifier().verify(token);
}

public Authentication getAuthentication(String token) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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;

import reactor.core.publisher.Mono;

/**
* <H2>EnvSecretResolver</H2>
*
* <p>Reads a secret from an environment variable.
*
* <p>In all its triviality, this class is virtually untestable, due to
* the JVM limitations on setting test env vars (see {@literal EnvSecretReaderTest}).
*
* @author M. Massenzio, 2022-11-19
*/
public class EnvSecretResolver implements SecretsResolver {
@Override
public Mono<String> getSecret(String secretName) {
return Mono.justOrEmpty(System.getenv(secretName));
}
}
Loading