diff --git a/pom.xml b/pom.xml index e63a5009..046e029d 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,7 @@ spring-cloud-vault-config-databases spring-cloud-vault-config-consul spring-cloud-vault-config-rabbitmq + spring-cloud-vault-config-aws spring-cloud-vault-starter-config docs diff --git a/spring-cloud-vault-config-aws/pom.xml b/spring-cloud-vault-config-aws/pom.xml new file mode 100644 index 00000000..af250213 --- /dev/null +++ b/spring-cloud-vault-config-aws/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + + org.springframework.cloud + spring-cloud-vault-parent + 1.0.0.BUILD-SNAPSHOT + .. + + + org.springframework.cloud + spring-cloud-vault-config-aws + jar + + Spring Cloud Vault Config AWS support + Spring Cloud Vault Config AWS support + + + + + org.springframework.cloud + spring-cloud-aws + 1.1.0.RELEASE + pom + import + + + + + + + org.springframework.cloud + spring-cloud-vault-core + + + + org.springframework.cloud + spring-cloud-vault-core + test-jar + + + + org.springframework.cloud + spring-cloud-vault-config + + + + com.squareup.okhttp + okhttp + test + + + + diff --git a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java new file mode 100644 index 00000000..d8395b2f --- /dev/null +++ b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultAwsProperties.java @@ -0,0 +1,60 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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. + */ +package org.springframework.cloud.vault.config.aws; + +import org.hibernate.validator.constraints.NotEmpty; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.cloud.vault.VaultSecretBackend; + +import lombok.Data; + +/** + * Configuration properties for Vault using the AWS integration. + * + * @author Mark Paluch + */ +@ConfigurationProperties("spring.cloud.vault.aws") +@Data +public class VaultAwsProperties implements VaultSecretBackend { + + /** + * Enable aws backend usage. + */ + private boolean enabled = false; + + /** + * Role name for credentials. + */ + private String role; + + /** + * aws backend path. + */ + @NotEmpty + private String backend = "aws"; + + /** + * Target property for the obtained access key. + */ + @NotEmpty + private String accessKeyProperty = "cloud.aws.credentials.accessKey"; + + /** + * Target property for the obtained secret key. + */ + @NotEmpty + private String secretKeyProperty = "cloud.aws.credentials.secretKey"; +} \ No newline at end of file diff --git a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java new file mode 100644 index 00000000..12eb29a4 --- /dev/null +++ b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsBootstrapConfiguration.java @@ -0,0 +1,104 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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. + */ +package org.springframework.cloud.vault.config.aws; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cloud.vault.SecureBackendAccessor; +import org.springframework.cloud.vault.VaultSecretBackend; +import org.springframework.cloud.vault.config.SecureBackendAccessorFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; + +/** + * @author Mark Paluch + */ +@Configuration +@EnableConfigurationProperties +public class VaultConfigAwsBootstrapConfiguration { + + @Bean + public SecureBackendAccessorFactory secureBackendAccessorFactory() { + return new AwsSecureBackendAccessorFactory(); + } + + @Bean + public VaultAwsProperties awsProperties() { + return new VaultAwsProperties(); + } + + static class AwsSecureBackendAccessorFactory + implements SecureBackendAccessorFactory { + + @Override + public SecureBackendAccessor createSecureBackendAccessor( + VaultAwsProperties properties) { + return forAws(properties); + } + + @Override + public boolean supports(VaultSecretBackend secretBackend) { + return secretBackend instanceof VaultAwsProperties; + } + + /** + * Creates a {@link SecureBackendAccessor} for a secure backend using + * {@link VaultAwsProperties}. This accessor transforms Vault's + * username/password property names to names provided with + * {@link VaultAwsProperties#getAccessKeyProperty()} and + * {@link VaultAwsProperties#getSecretKeyProperty()}. + * + * @param properties must not be {@literal null}. + * @return the {@link SecureBackendAccessor} + */ + public static SecureBackendAccessor forAws( + final VaultAwsProperties properties) { + Assert.notNull(properties, "VaultAwsProperties must not be null"); + + return new SecureBackendAccessor() { + + @Override + public Map variables() { + + Map variables = new HashMap<>(); + variables.put("backend", properties.getBackend()); + variables.put("key", String.format("creds/%s", properties.getRole())); + return variables; + } + + @Override + public String getName() { + return String.format("%s with Role %s", properties.getBackend(), + properties.getRole()); + } + + @Override + public Map transformProperties( + Map input) { + + Map result = new HashMap(); + result.put(properties.getAccessKeyProperty(), input.get("access_key")); + result.put(properties.getSecretKeyProperty(), input.get("secret_key")); + + return result; + } + }; + } + } +} diff --git a/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/package-info.java b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/package-info.java new file mode 100644 index 00000000..fac1e29b --- /dev/null +++ b/spring-cloud-vault-config-aws/src/main/java/org/springframework/cloud/vault/config/aws/package-info.java @@ -0,0 +1,5 @@ +/** + * AWS integration with Vault. + * @author Mark Paluch + */ +package org.springframework.cloud.vault.config.aws; \ No newline at end of file diff --git a/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories b/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000..63d2d414 --- /dev/null +++ b/spring-cloud-vault-config-aws/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Bootstrap Configuration +org.springframework.cloud.bootstrap.BootstrapConfiguration=\ +org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration \ No newline at end of file diff --git a/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/AwsSecretIntegrationTests.java b/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/AwsSecretIntegrationTests.java new file mode 100644 index 00000000..f0e8a8e6 --- /dev/null +++ b/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/AwsSecretIntegrationTests.java @@ -0,0 +1,95 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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. + */ +package org.springframework.cloud.vault.config.aws; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; +import static org.springframework.cloud.vault.config.aws.VaultConfigAwsBootstrapConfiguration.AwsSecureBackendAccessorFactory.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.cloud.vault.AbstractIntegrationTests; +import org.springframework.cloud.vault.TestRestTemplateFactory; +import org.springframework.cloud.vault.VaultClient; +import org.springframework.cloud.vault.VaultProperties; +import org.springframework.cloud.vault.util.Settings; +import org.springframework.util.StringUtils; + +/** + * Integration tests for {@link VaultClient} using the aws secret backend. This test + * requires AWS credentials and a region, see {@link #AWS_ACCESS_KEY} and + * {@link #AWS_SECRET_KEY} to be provided externally. + * + * @author Mark Paluch + */ +public class AwsSecretIntegrationTests extends AbstractIntegrationTests { + + private final static String AWS_REGION = "eu-west-1"; + private final static String AWS_ACCESS_KEY = System.getProperty("aws.access.key"); + private final static String AWS_SECRET_KEY = System.getProperty("aws.secret.key"); + + private final static String ARN = "arn:aws:iam::aws:policy/ReadOnlyAccess"; + + private VaultProperties vaultProperties = Settings.createVaultProperties(); + private VaultClient vaultClient = new VaultClient(vaultProperties); + private VaultAwsProperties aws = new VaultAwsProperties(); + + /** + * Initialize the aws secret backend. + * + * @throws Exception + */ + @Before + public void setUp() throws Exception { + + assumeTrue(StringUtils.hasText(AWS_ACCESS_KEY) + && StringUtils.hasText(AWS_SECRET_KEY)); + + aws.setEnabled(true); + aws.setRole("readonly"); + + if (!prepare().hasSecret(aws.getBackend())) { + prepare().mountSecret(aws.getBackend()); + } + + Map connection = new HashMap<>(); + connection.put("region", AWS_REGION); + connection.put("access_key", AWS_ACCESS_KEY); + connection.put("secret_key", AWS_SECRET_KEY); + + prepare().write(String.format("%s/config/root", aws.getBackend()), connection); + + prepare().write(String.format("%s/roles/%s", aws.getBackend(), aws.getRole()), + Collections.singletonMap("arn", ARN)); + + vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties)); + } + + @Test + public void shouldCreateCredentialsCorrectly() throws Exception { + + Map secretProperties = vaultClient.read(forAws(aws), + Settings.token()); + + assertThat(secretProperties).containsKeys("cloud.aws.credentials.accessKey", + "cloud.aws.credentials.secretKey"); + } + +} diff --git a/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsTests.java b/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsTests.java new file mode 100644 index 00000000..b1e425c4 --- /dev/null +++ b/spring-cloud-vault-config-aws/src/test/java/org/springframework/cloud/vault/config/aws/VaultConfigAwsTests.java @@ -0,0 +1,111 @@ +/* + * Copyright 2016 the original author or authors. + * + * 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. + */ +package org.springframework.cloud.vault.config.aws; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.IntegrationTest; +import org.springframework.boot.test.SpringApplicationConfiguration; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.util.StringUtils; + +/** + * Integration tests using the aws secret backend. In case this test should fail because + * of SSL make sure you run the test within the + * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is + * referenced with {@code ../work/keystore.jks}. + *

+ * This test requires AWS credentials and a region, see {@link #AWS_ACCESS_KEY}, + * {@link #AWS_SECRET_KEY} and the {@link IntegrationTest} properties to be provided + * externally. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringApplicationConfiguration(classes = VaultConfigAwsTests.TestApplication.class) +@IntegrationTest({ "spring.cloud.vault.aws.enabled=true", + "spring.cloud.vault.aws.role=readonly", "cloud.aws.region.auto=false", + "cloud.aws.region.static=eu-west-1" }) +public class VaultConfigAwsTests { + + private final static String AWS_REGION = "eu-west-1"; + private final static String AWS_ACCESS_KEY = System.getProperty("aws.access.key"); + private final static String AWS_SECRET_KEY = System.getProperty("aws.secret.key"); + + private final static String ARN = "arn:aws:iam::aws:policy/ReadOnlyAccess"; + + /** + * Initialize the aws secret backend. + * + * @throws Exception + */ + @BeforeClass + public static void beforeClass() throws Exception { + + assumeTrue(StringUtils.hasText(AWS_ACCESS_KEY) + && StringUtils.hasText(AWS_SECRET_KEY)); + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + if (!vaultRule.prepare().hasSecret("aws")) { + vaultRule.prepare().mountSecret("aws"); + } + + Map connection = new HashMap<>(); + connection.put("region", AWS_REGION); + connection.put("access_key", AWS_ACCESS_KEY); + connection.put("secret_key", AWS_SECRET_KEY); + + vaultRule.prepare().write("aws/config/root", connection); + + vaultRule.prepare().write("aws/roles/readonly", + Collections.singletonMap("arn", ARN)); + } + + @Value("${cloud.aws.credentials.accessKey}") + String accessKey; + + @Value("${cloud.aws.credentials.secretKey}") + String secretKey; + + @Test + public void shouldInitializeAwsProperties() throws Exception { + + assertThat(accessKey).isNotEmpty(); + assertThat(secretKey).isNotEmpty(); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/spring-cloud-vault-config-aws/src/test/resources/bootstrap.yml b/spring-cloud-vault-config-aws/src/test/resources/bootstrap.yml new file mode 100644 index 00000000..c598d962 --- /dev/null +++ b/spring-cloud-vault-config-aws/src/test/resources/bootstrap.yml @@ -0,0 +1,5 @@ +spring: + application.name: testVaultApp + cloud.vault.token: 00000000-0000-0000-0000-000000000000 + cloud.vault.ssl.trust-store: file:../work/keystore.jks + cloud.vault.ssl.trust-store-password: changeit