From 5a68469c395aeb0be08a9a8eda87f957356534b3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 12 Oct 2016 17:55:46 +0200 Subject: [PATCH] Support AppRole authentication. We now support AppRole authentication. This authentication method uses a provided RoleId and optionally SecretId to authenticate against Vault. Fixes gh-39. --- README.adoc | 2 +- docs/src/main/asciidoc/README.adoc | 4 +- .../asciidoc/spring-cloud-vault-config.adoc | 41 ++++++ .../config/VaultBootstrapConfiguration.java | 70 +++++----- .../cloud/vault/config/VaultProperties.java | 23 +++- .../vault/config/VaultConfigAppRoleTests.java | 120 ++++++++++++++++++ 6 files changed, 224 insertions(+), 36 deletions(-) create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigAppRoleTests.java diff --git a/README.adoc b/README.adoc index 6a7a5d32..8c2852cf 100644 --- a/README.adoc +++ b/README.adoc @@ -12,7 +12,7 @@ Specifically for Spring applications: * Retrieve secrets from Vault and initialize Spring `Environment` with remote property sources * Obtain secrets secured with SSL * Generate credentials for MySQL, PostgreSQL, Apache Cassandra, MongoDB, Consul, AWS, and RabbitMQ. -* https://www.vaultproject.io/docs/auth/token.html[Token], https://www.vaultproject.io/docs/auth/app-id.html[AppId] authentication, +* https://www.vaultproject.io/docs/auth/token.html[Token], https://www.vaultproject.io/docs/auth/app-id.html[AppId], https://www.vaultproject.io/docs/auth/approle.html[AppRole], and https://www.vaultproject.io/docs/auth/aws-ec2.html[AWS-EC2] authentication * Bootstrap application context: a parent context for the main application that can be trained to do anything diff --git a/docs/src/main/asciidoc/README.adoc b/docs/src/main/asciidoc/README.adoc index 842761bb..19180abd 100644 --- a/docs/src/main/asciidoc/README.adoc +++ b/docs/src/main/asciidoc/README.adoc @@ -10,7 +10,7 @@ Specifically for Spring applications: * Retrieve secrets from Vault and initialize Spring `Environment` with remote property sources * Obtain secrets secured with SSL * Generate credentials for MySQL, PostgreSQL, Apache Cassandra, MongoDB, Consul, AWS, and RabbitMQ. -* https://www.vaultproject.io/docs/auth/token.html[Token], https://www.vaultproject.io/docs/auth/app-id.html[AppId] authentication, +* https://www.vaultproject.io/docs/auth/token.html[Token], https://www.vaultproject.io/docs/auth/app-id.html[AppId], https://www.vaultproject.io/docs/auth/approle.html[AppRole], and https://www.vaultproject.io/docs/auth/aws-ec2.html[AWS-EC2] authentication * Bootstrap application context: a parent context for the main application that can be trained to do anything @@ -44,4 +44,4 @@ include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/maste == Contributing -include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] \ No newline at end of file +include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[] diff --git a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc index 55905ab9..0d5606e8 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc @@ -41,6 +41,7 @@ authentication method See also: https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens] +[[vault.authentication.appid]] === AppId authentication Vault supports https://www.vaultproject.io/docs/auth/app-id.html[AppId] @@ -144,6 +145,46 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism { See also: https://www.vaultproject.io/docs/auth/app-id.html[Vault Documentation: Using the App ID auth backend] +== AppRole authentication + +https://www.vaultproject.io/docs/auth/app-id.html[AppRole] is intended for machine +authentication, like the deprecated (since Vault 0.6.1) <>. +AppRole authentication consists of two hard to guess (secret) tokens: RoleId and SecretId. + +Spring Vault supports AppRole authentication by providing either RoleId only +or together with a provided SecretId (push or pull mode). + +RoleId and optionally SecretId must be provided by configuration, +Spring Vault will not look up these or create a custom SecretId. + +[source,yaml] +.bootstrap.yml with AppRole authentication properties +---- +spring.cloud.vault: + authentication: APPROLE + app-role: + role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52 +---- + +* `role-id` sets the RoleId. + +[source,yaml] +.bootstrap.yml with all AppRole authentication properties +---- +spring.cloud.vault: + authentication: APPROLE + app-role: + role-id: bde2076b-cccb-3cf0-d57e-bca7b1e83a52 + secret-id: 1696536f-1976-73b1-b241-0b4213908d39 + app-auth-path: approle +---- + +* `role-id` sets the RoleId. +* `secret-id` sets the SecretId. SecretId can be omitted if AppRole is configured without requiring SecretId (See `bind_secret_id`) +* `approle-path` sets the path of the approle authentication mount to use + +See also: https://www.vaultproject.io/docs/auth/approle.html[Vault Documentation: Using the AppRole auth backend] + === AWS-EC2 authentication The https://www.vaultproject.io/docs/auth/aws-ec2.html[aws-ec2] diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java index 30aa75de..3b68ae92 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java @@ -37,22 +37,7 @@ import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; -import org.springframework.vault.authentication.AppIdAuthentication; -import org.springframework.vault.authentication.AppIdAuthenticationOptions; -import org.springframework.vault.authentication.AppIdUserIdMechanism; -import org.springframework.vault.authentication.AwsEc2Authentication; -import org.springframework.vault.authentication.AwsEc2AuthenticationOptions; -import org.springframework.vault.authentication.ClientAuthentication; -import org.springframework.vault.authentication.ClientCertificateAuthentication; -import org.springframework.vault.authentication.CubbyholeAuthentication; -import org.springframework.vault.authentication.CubbyholeAuthenticationOptions; -import org.springframework.vault.authentication.IpAddressUserId; -import org.springframework.vault.authentication.LifecycleAwareSessionManager; -import org.springframework.vault.authentication.MacAddressUserId; -import org.springframework.vault.authentication.SessionManager; -import org.springframework.vault.authentication.SimpleSessionManager; -import org.springframework.vault.authentication.StaticUserId; -import org.springframework.vault.authentication.TokenAuthentication; +import org.springframework.vault.authentication.*; import org.springframework.vault.client.VaultClient; import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.config.AbstractVaultConfiguration.ClientFactoryWrapper; @@ -92,15 +77,16 @@ public class VaultBootstrapConfiguration { this.applicationContext = applicationContext; this.vaultProperties = vaultProperties; - this.vaultSecretBackendDescriptors = applicationContext - .getBeansOfType(VaultSecretBackendDescriptor.class).values(); - this.factories = (Collection) applicationContext - .getBeansOfType(SecretBackendMetadataFactory.class).values(); + this.vaultSecretBackendDescriptors = applicationContext.getBeansOfType( + VaultSecretBackendDescriptor.class).values(); + this.factories = (Collection) applicationContext.getBeansOfType( + SecretBackendMetadataFactory.class).values(); } @Bean public VaultPropertySourceLocator vaultPropertySourceLocator( - VaultOperations operations, VaultProperties vaultProperties, + VaultOperations operations, + VaultProperties vaultProperties, VaultGenericBackendProperties vaultGenericBackendProperties, ObjectProvider> taskSchedulerProvider) { @@ -152,8 +138,8 @@ public class VaultBootstrapConfiguration { sslConfiguration = SslConfiguration.NONE; } - return new ClientFactoryWrapper( - ClientHttpRequestFactoryFactory.create(clientOptions, sslConfiguration)); + return new ClientFactoryWrapper(ClientHttpRequestFactoryFactory.create( + clientOptions, sslConfiguration)); } /** @@ -169,9 +155,8 @@ public class VaultBootstrapConfiguration { vaultEndpoint.setPort(vaultProperties.getPort()); vaultEndpoint.setScheme(vaultProperties.getScheme()); - return new VaultClient( - clientHttpRequestFactoryWrapper().getClientHttpRequestFactory(), - vaultEndpoint); + return new VaultClient(clientHttpRequestFactoryWrapper() + .getClientHttpRequestFactory(), vaultEndpoint); } /** @@ -225,7 +210,8 @@ public class VaultBootstrapConfiguration { */ @Bean @ConditionalOnMissingBean - public SessionManager sessionManager(ClientAuthentication clientAuthentication, + public SessionManager sessionManager( + ClientAuthentication clientAuthentication, ObjectProvider> asyncTaskExecutorProvider) { if (vaultProperties.getConfig().getLifecycle().isEnabled()) { @@ -253,6 +239,9 @@ public class VaultBootstrapConfiguration { case APPID: return appIdAuthentication(vaultProperties, vaultClient); + case APPROLE: + return appRoleAuthentication(vaultProperties, vaultClient); + case CERT: return new ClientCertificateAuthentication(vaultClient); @@ -264,9 +253,9 @@ public class VaultBootstrapConfiguration { } - throw new UnsupportedOperationException( - String.format("Client authentication %s not supported", - vaultProperties.getAuthentication())); + throw new UnsupportedOperationException(String.format( + "Client authentication %s not supported", + vaultProperties.getAuthentication())); } private ClientAuthentication appIdAuthentication(VaultProperties vaultProperties, @@ -302,8 +291,8 @@ public class VaultBootstrapConfiguration { if (StringUtils.hasText(appId.getNetworkInterface())) { try { - return new MacAddressUserId( - Integer.parseInt(appId.getNetworkInterface())); + return new MacAddressUserId(Integer.parseInt(appId + .getNetworkInterface())); } catch (NumberFormatException e) { return new MacAddressUserId(appId.getNetworkInterface()); @@ -317,6 +306,23 @@ public class VaultBootstrapConfiguration { } } + private ClientAuthentication appRoleAuthentication(VaultProperties vaultProperties, + VaultClient vaultClient) { + + VaultProperties.AppRoleProperties appRole = vaultProperties.getAppRole(); + Assert.hasText(appRole.getRoleId(), + "RoleId (spring.cloud.vault.app-role.role-id) must not be empty"); + + AppRoleAuthenticationOptions.AppRoleAuthenticationOptionsBuilder builder = AppRoleAuthenticationOptions + .builder().path(appRole.getAppRolePath()).roleId(appRole.getRoleId()); + + if (StringUtils.hasText(appRole.getSecretId())) { + builder = builder.secretId(appRole.getSecretId()); + } + + return new AppRoleAuthentication(builder.build(), vaultClient); + } + private ClientAuthentication awsEc2Authentication(VaultProperties vaultProperties, VaultClient vaultClient) { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index 33fa76e8..e1c083c7 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java @@ -76,6 +76,8 @@ public class VaultProperties { private AppIdProperties appId = new AppIdProperties(); + private AppRoleProperties appRole = new AppRoleProperties(); + private AwsEc2Properties awsEc2 = new AwsEc2Properties(); private Ssl ssl = new Ssl(); @@ -125,6 +127,25 @@ public class VaultProperties { private String userId = MAC_ADDRESS; } + @Data + public static class AppRoleProperties { + + /** + * Mount path of the AppId authentication backend. + */ + private String appRolePath = "approle"; + + /** + * The RoleId. + */ + private String roleId = null; + + /** + * The SecretId. + */ + private String secretId = null; + } + @Data public static class AwsEc2Properties { @@ -209,6 +230,6 @@ public class VaultProperties { } public enum AuthenticationMethod { - TOKEN, APPID, AWS_EC2, CERT, CUBBYHOLE; + TOKEN, APPID, APPROLE, AWS_EC2, CERT, CUBBYHOLE; } } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigAppRoleTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigAppRoleTests.java new file mode 100644 index 00000000..eed31954 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigAppRoleTests.java @@ -0,0 +1,120 @@ +/* + * 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; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.vault.util.Settings; +import org.springframework.cloud.vault.util.VaultRule; +import org.springframework.cloud.vault.util.Version; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.vault.authentication.IpAddressUserId; +import org.springframework.vault.core.VaultOperations; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.assumeTrue; + +import org.junit.BeforeClass; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Integration test using config infrastructure with AppRole authentication. 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}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = VaultConfigAppRoleTests.TestApplication.class, properties = { + "spring.cloud.vault.authentication=approle", + "spring.application.name=VaultConfigAppRoleTests" }) +public class VaultConfigAppRoleTests { + + @BeforeClass + public static void beforeClass() throws Exception { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + assumeTrue(vaultRule.prepare().getVersion() + .isGreaterThanOrEqualTo(Version.parse("0.6.1"))); + + VaultProperties vaultProperties = Settings.createVaultProperties(); + + if (!vaultRule.prepare().hasAuth(vaultProperties.getAppRole().getAppRolePath())) { + vaultRule.prepare().mountAuth(vaultProperties.getAppRole().getAppRolePath()); + } + + VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations(); + + String rules = "{ \"name\": \"testpolicy\",\n" // + + " \"path\": {\n" // + + " \"*\": { \"policy\": \"read\" }\n" // + + " }\n" // + + "}"; + + vaultOperations.write("sys/policy/testpolicy", + Collections.singletonMap("rules", rules)); + + String appId = VaultConfigAppRoleTests.class.getSimpleName(); + + vaultOperations.write("secret/" + VaultConfigAppRoleTests.class.getSimpleName(), + Collections.singletonMap("vault.value", "foo")); + + Map withSecretId = new HashMap(); + withSecretId.put("policies", "testpolicy"); // policy + withSecretId.put("bound_cidr_list", "0.0.0.0/0"); + withSecretId.put("bind_secret_id", "true"); + + vaultOperations.write("auth/approle/role/with-secret-id", withSecretId); + + String roleId = (String) vaultOperations + .read("auth/approle/role/with-secret-id/role-id").getData() + .get("role_id"); + String secretId = (String) vaultOperations + .write(String.format("auth/approle/role/with-secret-id/secret-id", + "with-secret-id"), null).getData().get("secret_id"); + + System.setProperty("spring.cloud.vault.app-role.role-id", roleId); + System.setProperty("spring.cloud.vault.app-role.secret-id", secretId); + + } + + @Value("${vault.value}") + String configValue; + + @Test + public void contextLoads() { + + assertThat(configValue).isEqualTo("foo"); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +}