From 69a4bd20920f8bb70e3aa23421fa4f1d5047cc74 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Sat, 18 Jun 2016 21:34:22 +0200 Subject: [PATCH] Support Consul credential generation Fixes gh-5 --- .gitignore | 4 +- .travis.yml | 2 + .../asciidoc/spring-cloud-vault-config.adoc | 40 +++++-- .../cloud/vault/SecureBackendAccessors.java | 36 ++++++ .../cloud/vault/VaultProperties.java | 28 +++++ .../vault/ConsulSecretIntegrationTests.java | 113 ++++++++++++++++++ .../src/test/resources/consul.json | 8 ++ src/test/bash/install_consul.sh | 45 +++++++ src/test/bash/install_vault.sh | 4 +- src/test/bash/local_run_consul.sh | 18 +++ 10 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/ConsulSecretIntegrationTests.java create mode 100644 spring-cloud-vault-config/src/test/resources/consul.json create mode 100755 src/test/bash/install_consul.sh create mode 100755 src/test/bash/local_run_consul.sh diff --git a/.gitignore b/.gitignore index 2d9ca923..3c8daef1 100644 --- a/.gitignore +++ b/.gitignore @@ -15,7 +15,7 @@ _site/ *.iws .factorypath download/ -vault/download -vault/vault +/vault/ +/consul/ work build/ diff --git a/.travis.yml b/.travis.yml index eb7b2185..2960304d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,7 +15,9 @@ install: - apache-cassandra-2.2.6/bin/cassandra - src/test/bash/create_certificates.sh - src/test/bash/install_vault.sh + - src/test/bash/install_consul.sh - src/test/bash/local_run_vault.sh & + - src/test/bash/local_run_consul.sh & before_script: - mysql -e "CREATE USER 'spring' IDENTIFIED by 'vault';" diff --git a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc index b363630f..3e793cbb 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc @@ -113,15 +113,15 @@ spring.cloud.vault: ---- [source,yaml] -MyUserIdMechanism.java +.MyUserIdMechanism.java ---- public class MyUserIdMechanism implements AppIdUserIdMechanism { - @Override - public String createUserId() { - String userId = ... - return userId; - } + @Override + public String createUserId() { + String userId = ... + return userId; + } } ---- @@ -151,7 +151,7 @@ JDBC secrets need to be configured separately. Spring Cloud Vault allows to obtain credentials for Apache Cassandra. The integration can be enabled by setting `spring.cloud.vault.cassandra.enabled=true` -(default "false"). Username and password are stored in `spring.data.cassandra.username` +(default `false`). Username and password are stored in `spring.data.cassandra.username` and `spring.data.cassandra.password` so using Spring Boot will pick up the generated credentials without further configuration. You can configure the property names by setting `spring.cloud.vault.cassandra.username-property` and @@ -173,7 +173,7 @@ See also: https://www.vaultproject.io/docs/secrets/cassandra/index.html[Vault Do Spring Cloud Vault allows to obtain credentials for MySQL. The integration can be enabled by setting `spring.cloud.vault.mysql.enabled=true` -(default "false"). Username and password are stored in `spring.datasource.username` +(default `false`). Username and password are stored in `spring.datasource.username` and `spring.datasource.password` so using Spring Boot will pick up the generated credentials without further configuration. You can configure the property names by setting `spring.cloud.vault.mysql.username-property` and @@ -195,7 +195,7 @@ See also: https://www.vaultproject.io/docs/secrets/mysql/index.html[Vault Docume Spring Cloud Vault allows to obtain credentials for PostgreSQL. The integration can be enabled by setting `spring.cloud.vault.postgresql.enabled=true` -(default "false"). Username and password are stored in `spring.datasource.username` +(default `false`). Username and password are stored in `spring.datasource.username` and `spring.datasource.password` so using Spring Boot will pick up the generated credentials without further configuration. You can configure the property names by setting `spring.cloud.vault.postgresql.username-property` and @@ -212,6 +212,28 @@ spring.cloud.vault: See also: https://www.vaultproject.io/docs/secrets/postgresql/index.html[Vault Documentation: Setting up PostgreSQL with Vault] +[[vault-client-consul]] +== Consul + +Spring Cloud Vault allows to obtain credentials for Hashicorp Consil. +The integration can be enabled by setting `spring.cloud.vault.consul.enabled=true` +(default `false`). The obtained token is stored in `spring.cloud.consul.token` +so using Spring Cloud Consul can pick up the generated +credentials without further configuration. You can configure the property name +by setting `spring.cloud.vault.consul.token-property`. + +[source,yaml] +---- +spring.cloud.vault: + enabled: true + ... + consul: + enabled: true +---- + +See also: https://www.vaultproject.io/docs/secrets/consul/index.html[Vault Documentation: Setting up Consul with Vault] + + [[vault-client-fail-fast]] == Vault Client Fail Fast diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/SecureBackendAccessors.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/SecureBackendAccessors.java index d29a83f2..2c81337a 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/SecureBackendAccessors.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/SecureBackendAccessors.java @@ -110,4 +110,40 @@ public class SecureBackendAccessors { } }; } + + /** + * Creates a {@link SecureBackendAccessor} for a secure backend using + * {@link org.springframework.cloud.vault.VaultProperties.Consul}. + * This accessor transforms Vault's token property names to names provided + * with {@link VaultProperties.Consul#getTokenProperty()}. + * + * @param properties must not be {@literal null}. + * @return the {@link SecureBackendAccessor} + */ + public static SecureBackendAccessor consul( + final VaultProperties.Consul properties) { + + Assert.notNull(properties, "Consul properties 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 Map transformProperties(Map input) { + + Map result = new HashMap(); + result.put(properties.getTokenProperty(), input.get("token")); + + return result; + } + }; + } } diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java index 860fa94d..0553be82 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/VaultProperties.java @@ -100,6 +100,8 @@ public class VaultProperties { private Cassandra cassandra = new Cassandra(); + private Consul consul = new Consul(); + /** * Application name for AppId authentication. */ @@ -251,6 +253,32 @@ public class VaultProperties { private String passwordProperty = "spring.data.cassandra.password"; } + @Data + public static class Consul { + + /** + * Enable consul backend usage. + */ + private boolean enabled = false; + + /** + * Role name for credentials. + */ + private String role; + + /** + * Consul backend path. + */ + @NotEmpty + private String backend = "consul"; + + /** + * Target property for the obtained token. + */ + @NotEmpty + private String tokenProperty = "spring.cloud.consul.token"; + } + /** * Configuration properties interface for database secrets. */ diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/ConsulSecretIntegrationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/ConsulSecretIntegrationTests.java new file mode 100644 index 00000000..e7355b07 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/ConsulSecretIntegrationTests.java @@ -0,0 +1,113 @@ +/* + * 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; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; +import static org.springframework.cloud.vault.SecureBackendAccessors.*; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.codec.binary.Base64; +import org.junit.Before; +import org.junit.Test; +import org.springframework.boot.test.TestRestTemplate; +import org.springframework.cloud.vault.util.CanConnect; +import org.springframework.cloud.vault.util.Settings; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; + +/** + * Integration tests for {@link VaultClient} using the consul secret backend. This test + * requires a running Consul instance, see {@link #CONNECTION_URL}. + * + * @author Mark Paluch + */ +public class ConsulSecretIntegrationTests extends AbstractIntegrationTests { + + private final static String CONSUL_HOST = "localhost"; + private final static int CONSUL_PORT = 8500; + + private final static String CONNECTION_URL = String.format("%s:%d", CONSUL_HOST, + CONSUL_PORT); + + private final static String POLICY = "key \"\" { policy = \"read\" }"; + private final static String CONSUL_ACL_MASTER_TOKEN = "consul-master-token"; + + private final static ParameterizedTypeReference> STRING_MAP = new ParameterizedTypeReference>() { + }; + + private VaultProperties vaultProperties = Settings.createVaultProperties(); + private VaultClient vaultClient = new VaultClient(vaultProperties); + private VaultProperties.Consul consul = vaultProperties.getConsul(); + + private TestRestTemplate restTemplate = new TestRestTemplate(); + + /** + * Initialize the postgresql secret backend. + * + * @throws Exception + */ + @Before + public void setUp() throws Exception { + + assumeTrue(CanConnect.to(new InetSocketAddress(CONSUL_HOST, CONSUL_PORT))); + + consul.setEnabled(true); + consul.setRole("readonly"); + + if (!prepare().hasSecret(consul.getBackend())) { + prepare().mountSecret(consul.getBackend()); + } + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Consul-Token", CONSUL_ACL_MASTER_TOKEN); + HttpEntity requestEntity = new HttpEntity<>( + "{\"Name\": \"sample\", \"Type\": \"management\"}", headers); + ResponseEntity> tokenResponse = restTemplate.exchange( + "http://{address}/v1/acl/create", HttpMethod.PUT, requestEntity, + STRING_MAP, CONNECTION_URL); + + Map consulAccess = new HashMap<>(); + consulAccess.put("address", CONNECTION_URL); + consulAccess.put("token", tokenResponse.getBody().get("ID")); + + prepare().write(String.format("%s/config/access", consul.getBackend()), + consulAccess); + + prepare().write( + String.format("%s/roles/%s", consul.getBackend(), consul.getRole()), + Collections.singletonMap("policy", + Base64.encodeBase64String(POLICY.getBytes()))); + + vaultClient.setRest(TestRestTemplateFactory.create(vaultProperties)); + } + + @Test + public void shouldCreateCredentialsCorrectly() throws Exception { + + Map secretProperties = vaultClient.read(consul(consul), + Settings.token()); + + assertThat(secretProperties).containsKeys("spring.cloud.consul.token"); + } +} diff --git a/spring-cloud-vault-config/src/test/resources/consul.json b/spring-cloud-vault-config/src/test/resources/consul.json new file mode 100644 index 00000000..73936e0d --- /dev/null +++ b/spring-cloud-vault-config/src/test/resources/consul.json @@ -0,0 +1,8 @@ +{ + "log_level": "INFO", + "server": true, + "datacenter": "dc1", + "advertise_addr": "127.0.0.1", + "acl_datacenter": "dc1", + "acl_master_token": "consul-master-token" +} \ No newline at end of file diff --git a/src/test/bash/install_consul.sh b/src/test/bash/install_consul.sh new file mode 100755 index 00000000..20c673e1 --- /dev/null +++ b/src/test/bash/install_consul.sh @@ -0,0 +1,45 @@ +#!/bin/bash + +########################################################################### +# Download and Install Consul # +# This script is prepared for caching of the download directory # +########################################################################### + + +CONSUL_VER="0.6.3" +UNAME=$(uname -s | tr '[:upper:]' '[:lower:]') +CONSUL_ZIP="consul_${CONSUL_VER}_${UNAME}_amd64.zip" +IGNORE_CERTS="${IGNORE_CERTS:-no}" + +# cleanup +mkdir -p consul + +if [[ ! -f "download/${CONSUL_ZIP}" ]] ; then + cd download + # install Vault + if [[ "${IGNORE_CERTS}" == "no" ]] ; then + echo "Downloading Consul with certs verification" + wget "https://releases.hashicorp.com/consul/${CONSUL_VER}/${CONSUL_ZIP}" + else + echo "WARNING... Downloading Consul WITHOUT certs verification" + wget "https://releases.hashicorp.com/consul/${CONSUL_VER}/${CONSUL_ZIP}" --no-check-certificate + fi + + if [[ $? != 0 ]] ; then + echo "Cannot download Consul" + exit 1 + fi + cd .. +fi + +cd consul + +if [[ -f consul ]] ; then + rm consul +fi + +unzip ../download/${CONSUL_ZIP} +chmod a+x consul + +# check +./consul --version \ No newline at end of file diff --git a/src/test/bash/install_vault.sh b/src/test/bash/install_vault.sh index 1db72bfb..a85d8ca7 100755 --- a/src/test/bash/install_vault.sh +++ b/src/test/bash/install_vault.sh @@ -2,7 +2,7 @@ ########################################################################### # Download and Install Vault # -# This script is prepared for caching of the vault/download directory # +# This script is prepared for caching of the download directory # ########################################################################### @@ -26,7 +26,7 @@ if [[ ! -f "download/${VAULT_ZIP}" ]] ; then fi if [[ $? != 0 ]] ; then - echo "Cannot download vault" + echo "Cannot download Vault" exit 1 fi cd .. diff --git a/src/test/bash/local_run_consul.sh b/src/test/bash/local_run_consul.sh new file mode 100755 index 00000000..ee604fe3 --- /dev/null +++ b/src/test/bash/local_run_consul.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +########################################################################### +# Start Consul on localhost:8500 # +########################################################################### + +BASEDIR=`dirname $0`/../../.. + +#!/bin/bash +mkdir -p ${BASEDIR}/consul/config +mkdir -p ${BASEDIR}/consul/data + +./consul/consul agent -server \ + -bootstrap-expect 1 \ + -data-dir ${BASEDIR}/consul/data \ + -config-file=${BASEDIR}/spring-cloud-vault-config/src/test/resources/consul.json + +exit $?