diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 4c406479..00000000 --- a/.travis.yml +++ /dev/null @@ -1,68 +0,0 @@ -language: java - -services: - - mysql - - postgresql - - rabbitmq - - mongodb - -addons: - apt: - sources: - - mongodb-3.0-precise - packages: - - mongodb-org-server - - mongodb-org-shell - -jdk: - - oraclejdk8 - -env: - matrix: - - VAULT_VER=0.6.5 - - VAULT_VER=0.7.3 - - VAULT_VER=0.8.3 - - VAULT_VER=0.9.6 - - VAULT_VER=0.10.3 - - VAULT_VER=0.11.5 - - VAULT_VER=1.0.3 - - VAULT_VER=1.1.5 - - VAULT_VER=1.2.2 - -before_install: - - sed -i.bak -e 's|https://nexus.codehaus.org/snapshots/|https://oss.sonatype.org/content/repositories/codehaus-snapshots/|g' ~/.m2/settings.xml - -install: - - mkdir -p download - - test -f download/apache-cassandra-3.11.4-bin.tar.gz || wget https://archive.apache.org/dist/cassandra/3.11.4/apache-cassandra-3.11.4-bin.tar.gz -O download/apache-cassandra-3.11.4-bin.tar.gz - - tar xzf download/apache-cassandra-3.11.4-bin.tar.gz - - cp -f spring-cloud-vault-config-databases/src/test/resources/cassandra.yaml apache-cassandra-3.11.4/conf - - apache-cassandra-3.11.4/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 & - - sudo rabbitmq-plugins enable rabbitmq_management - - sudo service rabbitmq-server restart - -before_script: - - mysql -e "CREATE USER 'springvault' IDENTIFIED by 'springvault';" - - mysql -uroot -e "GRANT ALL PRIVILEGES ON *.* TO 'springvault'@'%' WITH GRANT OPTION;"; - - psql -U postgres -c "CREATE ROLE springvault WITH LOGIN PASSWORD 'springvault' CREATEROLE CREATEUSER;" - - |- - mongo admin --eval "db.createUser({user: 'springvault', pwd:'springvault', roles:['root']});" - - sleep 30 # wait until Cassandra is up - - apache-cassandra-3.11.4/bin/cqlsh localhost -u cassandra -p cassandra -e "CREATE USER 'springvault' WITH PASSWORD 'springvault' SUPERUSER" - -script: mvn clean verify -Pspring,java8 - -after_script: - - apache-cassandra-3.11.4/bin/nodetool stopdaemon - - pkill vault - - pkill consul - -cache: - directories: - - '$HOME/.m2/repository' - - 'download' diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index f5cf6531..310cfd66 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -636,6 +636,50 @@ See also: * https://www.vaultproject.io/docs/auth/kubernetes.html[Vault Documentation: Kubernetes] * https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/[Kubernetes Documentation: Configure Service Accounts for Pods] +[[vault.config.authentication.pcf]] +=== Pivotal CloudFoundry authentication + +The https://www.vaultproject.io/docs/auth/pcf.html[pcf] +auth backend provides a secure introduction mechanism for applications running within Pivotal's CloudFoundry instances allowing automated retrieval of a Vault token. +Unlike most Vault authentication backends, this backend does not require first-deploying, or provisioning security-sensitive credentials (tokens, username/password, client certificates, etc.) as identity provisioning is handled by PCF itself. +Instead, it treats PCF as a Trusted Third Party and uses the managed instance identity. + +.bootstrap.yml with required PCF Authentication properties +==== +[source,yaml] +---- +spring.cloud.vault: + authentication: PCF + pcf: + role: my-dev-role +---- +==== + +.bootstrap.yml with all PCF Authentication properties +==== +[source,yaml] +---- +spring.cloud.vault: + authentication: PCF + pcf: + role: my-dev-role + pcf-path: path + instance-certificate: /etc/cf-instance-credentials/instance.crt + instance-key: /etc/cf-instance-credentials/instance.key +---- +==== + +* `role` sets the name of the role against which the login is being attempted. +* `pcf-path` sets the path of the PCF mount to use. +* `instance-certificate` sets the path to the PCF instance identity certificate. +Defaults to `${CF_INSTANCE_CERT}` env variable. +* `instance-key` sets the path to the PCF instance identity key. +Defaults to `${CF_INSTANCE_KEY}` env variable. + +NOTE: PCF authentication requires BouncyCastle (bcpkix-jdk15on) to be on the classpath for RSA PSS signing. + +See also: https://www.vaultproject.io/docs/auth/pcf.html[Vault Documentation: Using the pcf auth backend] + [[vault.config.backends]] == Secret Backends diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java index 97c5ab9a..563f8223 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java @@ -19,7 +19,6 @@ package org.springframework.cloud.vault.config; import java.io.ByteArrayInputStream; import java.io.IOException; import java.net.URI; -import java.security.GeneralSecurityException; import java.util.Base64; import java.util.concurrent.atomic.AtomicReference; @@ -69,6 +68,9 @@ import org.springframework.vault.authentication.KubernetesAuthentication; import org.springframework.vault.authentication.KubernetesAuthenticationOptions; import org.springframework.vault.authentication.KubernetesServiceAccountTokenFile; import org.springframework.vault.authentication.MacAddressUserId; +import org.springframework.vault.authentication.PcfAuthentication; +import org.springframework.vault.authentication.PcfAuthenticationOptions; +import org.springframework.vault.authentication.ResourceCredentialSupplier; import org.springframework.vault.authentication.StaticUserId; import org.springframework.vault.authentication.TokenAuthentication; import org.springframework.vault.support.VaultToken; @@ -97,6 +99,104 @@ class ClientAuthenticationFactory { this.externalRestOperations = externalRestOperations; } + /** + * @return a new {@link ClientAuthentication}. + */ + ClientAuthentication createClientAuthentication() { + + switch (this.vaultProperties.getAuthentication()) { + + case APPID: + return appIdAuthentication(this.vaultProperties); + + case APPROLE: + return appRoleAuthentication(this.vaultProperties); + + case AWS_EC2: + return awsEc2Authentication(this.vaultProperties); + + case AWS_IAM: + return awsIamAuthentication(this.vaultProperties); + + case AZURE_MSI: + return azureMsiAuthentication(this.vaultProperties); + + case CERT: + return new ClientCertificateAuthentication(this.restOperations); + + case CUBBYHOLE: + return cubbyholeAuthentication(); + + case GCP_GCE: + return gcpGceAuthentication(this.vaultProperties); + + case GCP_IAM: + return gcpIamAuthentication(this.vaultProperties); + + case KUBERNETES: + return kubernetesAuthentication(this.vaultProperties); + + case PCF: + return pcfAuthentication(this.vaultProperties); + + case TOKEN: + Assert.hasText(this.vaultProperties.getToken(), + "Token (spring.cloud.vault.token) must not be empty"); + return new TokenAuthentication(this.vaultProperties.getToken()); + } + + throw new UnsupportedOperationException( + String.format("Client authentication %s not supported", + this.vaultProperties.getAuthentication())); + } + + private ClientAuthentication appIdAuthentication(VaultProperties vaultProperties) { + + VaultProperties.AppIdProperties appId = vaultProperties.getAppId(); + Assert.hasText(appId.getUserId(), + "UserId (spring.cloud.vault.app-id.user-id) must not be empty"); + + AppIdAuthenticationOptions authenticationOptions = AppIdAuthenticationOptions + .builder().appId(vaultProperties.getApplicationName()) // + .path(appId.getAppIdPath()) // + .userIdMechanism(getClientAuthentication(appId)).build(); + + return new AppIdAuthentication(authenticationOptions, this.restOperations); + } + + private AppIdUserIdMechanism getClientAuthentication( + VaultProperties.AppIdProperties appId) { + + try { + Class userIdClass = ClassUtils.forName(appId.getUserId(), null); + return (AppIdUserIdMechanism) BeanUtils.instantiateClass(userIdClass); + } + catch (ClassNotFoundException ex) { + + switch (appId.getUserId().toUpperCase()) { + + case VaultProperties.AppIdProperties.IP_ADDRESS: + return new IpAddressUserId(); + + case VaultProperties.AppIdProperties.MAC_ADDRESS: + + if (StringUtils.hasText(appId.getNetworkInterface())) { + try { + return new MacAddressUserId( + Integer.parseInt(appId.getNetworkInterface())); + } + catch (NumberFormatException e) { + return new MacAddressUserId(appId.getNetworkInterface()); + } + } + + return new MacAddressUserId(); + default: + return new StaticUserId(appId.getUserId()); + } + } + } + static AppRoleAuthenticationOptions getAppRoleAuthenticationOptions( VaultProperties vaultProperties) { @@ -156,101 +256,6 @@ class ClientAuthenticationFactory { return SecretId.absent(); } - /** - * @return a new {@link ClientAuthentication}. - */ - ClientAuthentication createClientAuthentication() { - - switch (this.vaultProperties.getAuthentication()) { - - case APPID: - return appIdAuthentication(this.vaultProperties); - - case APPROLE: - return appRoleAuthentication(this.vaultProperties); - - case AWS_EC2: - return awsEc2Authentication(this.vaultProperties); - - case AWS_IAM: - return awsIamAuthentication(this.vaultProperties); - - case AZURE_MSI: - return azureMsiAuthentication(this.vaultProperties); - - case CERT: - return new ClientCertificateAuthentication(this.restOperations); - - case CUBBYHOLE: - return cubbyholeAuthentication(); - - case GCP_GCE: - return gcpGceAuthentication(this.vaultProperties); - - case GCP_IAM: - return gcpIamAuthentication(this.vaultProperties); - - case KUBERNETES: - return kubernetesAuthentication(this.vaultProperties); - - case TOKEN: - Assert.hasText(this.vaultProperties.getToken(), - "Token (spring.cloud.vault.token) must not be empty"); - return new TokenAuthentication(this.vaultProperties.getToken()); - } - - throw new UnsupportedOperationException( - String.format("Client authentication %s not supported", - this.vaultProperties.getAuthentication())); - } - - private ClientAuthentication appIdAuthentication(VaultProperties vaultProperties) { - - VaultProperties.AppIdProperties appId = vaultProperties.getAppId(); - Assert.hasText(appId.getUserId(), - "UserId (spring.cloud.vault.app-id.user-id) must not be empty"); - - AppIdAuthenticationOptions authenticationOptions = AppIdAuthenticationOptions - .builder().appId(vaultProperties.getApplicationName()) // - .path(appId.getAppIdPath()) // - .userIdMechanism(getClientAuthentication(appId)).build(); - - return new AppIdAuthentication(authenticationOptions, this.restOperations); - } - - private AppIdUserIdMechanism getClientAuthentication( - VaultProperties.AppIdProperties appId) { - - try { - Class userIdClass = ClassUtils.forName(appId.getUserId(), null); - return (AppIdUserIdMechanism) BeanUtils.instantiateClass(userIdClass); - } - catch (ClassNotFoundException ex) { - - switch (appId.getUserId().toUpperCase()) { - - case VaultProperties.AppIdProperties.IP_ADDRESS: - return new IpAddressUserId(); - - case VaultProperties.AppIdProperties.MAC_ADDRESS: - - if (StringUtils.hasText(appId.getNetworkInterface())) { - try { - return new MacAddressUserId( - Integer.parseInt(appId.getNetworkInterface())); - } - catch (NumberFormatException e) { - return new MacAddressUserId(appId.getNetworkInterface()); - } - } - - return new MacAddressUserId(); - default: - return new StaticUserId(appId.getUserId()); - } - } - } - private ClientAuthentication appRoleAuthentication(VaultProperties vaultProperties) { AppRoleAuthenticationOptions options = getAppRoleAuthenticationOptions( @@ -377,12 +382,7 @@ class ClientAuthenticationFactory { GcpIamAuthenticationOptions options = builder.build(); - try { - return new GcpIamAuthentication(options, this.restOperations); - } - catch (IOException | GeneralSecurityException e) { - throw new IllegalStateException("Cannot create GcpIamAuthentication", e); - } + return new GcpIamAuthentication(options, this.restOperations); } private GoogleCredential getGoogleCredential(GcpIamProperties gcp) @@ -421,6 +421,33 @@ class ClientAuthenticationFactory { return new KubernetesAuthentication(options, this.restOperations); } + private ClientAuthentication pcfAuthentication(VaultProperties vaultProperties) { + + VaultProperties.PcfProperties pcfProperties = vaultProperties.getPcf(); + + Assert.isTrue( + ClassUtils.isPresent("org.bouncycastle.crypto.signers.PSSSigner", + getClass().getClassLoader()), + "BouncyCastle (bcpkix-jdk15on) must be on the classpath"); + Assert.hasText(pcfProperties.getRole(), + "Role (spring.cloud.vault.pcf.role) must not be empty"); + + PcfAuthenticationOptions.PcfAuthenticationOptionsBuilder builder = PcfAuthenticationOptions + .builder().role(pcfProperties.getRole()).path(pcfProperties.getPcfPath()); + + if (pcfProperties.getInstanceCertificate() != null) { + builder.instanceCertificate(new ResourceCredentialSupplier( + pcfProperties.getInstanceCertificate())); + } + + if (pcfProperties.getInstanceKey() != null) { + builder.instanceKey( + new ResourceCredentialSupplier(pcfProperties.getInstanceKey())); + } + + return new PcfAuthentication(builder.build(), this.restOperations); + } + private static class AwsCredentialProvider { private static AWSCredentialsProvider getAwsCredentialsProvider() { 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 5d9cebbb..d23ca80b 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 @@ -111,6 +111,8 @@ public class VaultProperties implements EnvironmentAware { private KubernetesProperties kubernetes = new KubernetesProperties(); + private PcfProperties pcf = new PcfProperties(); + private Ssl ssl = new Ssl(); private Config config = new Config(); @@ -204,6 +206,10 @@ public class VaultProperties implements EnvironmentAware { return this.kubernetes; } + public PcfProperties getPcf() { + return this.pcf; + } + public Ssl getSsl() { return this.ssl; } @@ -292,6 +298,10 @@ public class VaultProperties implements EnvironmentAware { this.kubernetes = kubernetes; } + public void setPcf(PcfProperties pcf) { + this.pcf = pcf; + } + public void setSsl(Ssl ssl) { this.ssl = ssl; } @@ -313,7 +323,7 @@ public class VaultProperties implements EnvironmentAware { */ public enum AuthenticationMethod { - TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, GCP_GCE, GCP_IAM, KUBERNETES + TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, GCP_GCE, GCP_IAM, KUBERNETES, PCF; } @@ -853,6 +863,68 @@ public class VaultProperties implements EnvironmentAware { } + /** + * PCF properties. + */ + public static class PcfProperties { + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String pcfPath = "pcf"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Path to the instance certificate (PEM). Defaults to {@code CF_INSTANCE_CERT} + * env variable. + */ + private Resource instanceCertificate; + + /** + * Path to the instance key (PEM). Defaults to {@code CF_INSTANCE_KEY} env + * variable. + */ + private Resource instanceKey; + + public String getPcfPath() { + return this.pcfPath; + } + + public void setPcfPath(String pcfPath) { + this.pcfPath = pcfPath; + } + + public String getRole() { + return this.role; + } + + public void setRole(String role) { + this.role = role; + } + + public Resource getInstanceCertificate() { + return this.instanceCertificate; + } + + public void setInstanceCertificate(Resource instanceCertificate) { + this.instanceCertificate = instanceCertificate; + } + + public Resource getInstanceKey() { + return this.instanceKey; + } + + public void setInstanceKey(Resource instanceKey) { + this.instanceKey = instanceKey; + } + + } + /** * SSL properties. */ diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ClientAuthenticationFactoryUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ClientAuthenticationFactoryUnitTests.java index b3c98dd2..f8ce2b5f 100644 --- a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ClientAuthenticationFactoryUnitTests.java +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/ClientAuthenticationFactoryUnitTests.java @@ -18,10 +18,14 @@ package org.springframework.cloud.vault.config; import org.junit.Test; +import org.springframework.core.io.ClassPathResource; import org.springframework.vault.authentication.AppRoleAuthenticationOptions; import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.PcfAuthentication; import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -162,4 +166,21 @@ public class ClientAuthenticationFactoryUnitTests { .isInstanceOf(IllegalArgumentException.class); } + @Test + public void shouldSupportPcfAuthentication() { + + VaultProperties properties = new VaultProperties(); + properties.setAuthentication(VaultProperties.AuthenticationMethod.PCF); + properties.getPcf().setRole("my-role"); + properties.getPcf().setInstanceKey(new ClassPathResource("bootstrap.yml")); + properties.getPcf() + .setInstanceCertificate(new ClassPathResource("bootstrap.yml")); + + ClientAuthentication clientAuthentication = new ClientAuthenticationFactory( + properties, new RestTemplate(), new RestTemplate()) + .createClientAuthentication(); + + assertThat(clientAuthentication).isInstanceOf(PcfAuthentication.class); + } + } diff --git a/src/test/bash/install_vault.sh b/src/test/bash/install_vault.sh index 837cecfc..2eee154f 100755 --- a/src/test/bash/install_vault.sh +++ b/src/test/bash/install_vault.sh @@ -1,46 +1,160 @@ -#!/bin/bash +#!/usr/bin/env bash ########################################################################### # Download and Install Vault # # This script is prepared for caching of the download directory # ########################################################################### +set -o errexit -VAULT_VER="${VAULT_VER:-1.2.2}" -UNAME=$(uname -s | tr '[:upper:]' '[:lower:]') -VAULT_ZIP="vault_${VAULT_VER}_${UNAME}_amd64.zip" -IGNORE_CERTS="${IGNORE_CERTS:-no}" +EDITION="${EDITION:-oss}" +VAULT_OSS="${VAULT_OSS:-1.2.2}" +VAULT_ENT="${VAULT_ENT:-0.11.0}" +UNAME=$(uname -s | tr '[:upper:]' '[:lower:]') +VERBOSE=false +VAULT_DIRECTORY=vault +DOWNLOAD_DIRECTORY=download +readonly script_name="$(basename "${BASH_SOURCE[0]}")" -# cleanup -mkdir -p vault -mkdir -p download +function say() { + echo "$@" +} -if [[ ! -f "download/${VAULT_ZIP}" ]] ; then - cd download +function verbose() { + if [[ ${VERBOSE} == true ]]; then + echo "$@" + fi +} + +function initialize() { + # cleanup + mkdir -p ${VAULT_DIRECTORY} + mkdir -p ${DOWNLOAD_DIRECTORY} +} + +function usage() { + cat <