Add support for PCF authentication.

We now support PCF authentication:

spring.cloud.vault:
    authentication: PCF
    pcf:
        role: my-dev-role

Closes gh-354.
This commit is contained in:
Mark Paluch
2019-09-09 14:45:14 +02:00
parent 2f60c97ef3
commit 8569e78e6f
6 changed files with 406 additions and 196 deletions

View File

@@ -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'

View File

@@ -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

View File

@@ -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() {

View File

@@ -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.
*/

View File

@@ -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);
}
}

View File

@@ -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 <<EOF
Usage: ${script_name} [OPTION]...
Download and extract HashiCorp Vault
Options:
-h|--help Displays this help
-v|--version Vault version number
-e|--edition oss|enterprise Vault Edition
EOF
}
function parse_options() {
local option
while [[ $# -gt 0 ]]; do
option="$1"
shift
case ${option} in
-h | -H | --help)
usage
exit 0
;;
--verbose)
VERBOSE=true
;;
-v | --version)
VAULT_VER="$1"
verbose "VAULT_VER=${VAULT_VER}"
shift
;;
-e | --edition)
EDITION="$1"
verbose "EDITION=${EDITION}"
shift
;;
*)
script_exit "Invalid argument was provided: ${option}" 2
;;
esac
done
}
function unpack() {
cd ${VAULT_DIRECTORY}
if [[ -f vault ]]; then
rm vault
fi
say "Unzipping ${VAULT_FILE}..."
verbose " unzip ../${DOWNLOAD_DIRECTORY}/${VAULT_FILE}"
if [[ ${VERBOSE} == true ]]; then
unzip "../${DOWNLOAD_DIRECTORY}/${VAULT_FILE}"
else
unzip -q "../${DOWNLOAD_DIRECTORY}/${VAULT_FILE}"
fi
chmod a+x vault
# check
./vault --version
cd ..
}
function download() {
if [[ ! -f "${DOWNLOAD_DIRECTORY}/${VAULT_FILE}" ]]; then
cd ${DOWNLOAD_DIRECTORY}
# install Vault
if [[ "${IGNORE_CERTS}" == "no" ]] ; then
echo "Downloading Vault with certs verification"
wget "https://releases.hashicorp.com/vault/${VAULT_VER}/${VAULT_ZIP}"
say "Downloading Vault from ${VAULT_URL}"
verbose "wget ${VAULT_URL} -O ${VAULT_FILE}"
if [[ ${VERBOSE} == true ]]; then
wget "${VAULT_URL}" -O "${VAULT_FILE}"
else
echo "WARNING... Downloading Vault WITHOUT certs verification"
wget "https://releases.hashicorp.com/vault/${VAULT_VER}/${VAULT_ZIP}" --no-check-certificate
wget "${VAULT_URL}" -q -O "${VAULT_FILE}"
fi
if [[ $? != 0 ]] ; then
if [[ $? != 0 ]]; then
echo "Cannot download Vault"
exit 1
fi
cd ..
fi
fi
}
cd vault
function download_oss() {
if [[ -f vault ]] ; then
rm vault
fi
VAULT_VER="${VAULT_VER:-${VAULT_OSS}}"
VAULT_ZIP="vault_${VAULT_VER}_${UNAME}_amd64.zip"
VAULT_FILE=${VAULT_ZIP}
VAULT_URL="https://releases.hashicorp.com/vault/${VAULT_VER}/${VAULT_ZIP}"
unzip ../download/${VAULT_ZIP}
chmod a+x vault
download
unpack
}
# check
./vault --version
function download_enterprise() {
VAULT_VER="${VAULT_VER:-${VAULT_ENT}}"
VAULT_ZIP="vault-enterprise_${VAULT_VER}%2Bent_${UNAME}_amd64.zip"
VAULT_FILE="vault-enterprise_${VAULT_VER}+ent_${UNAME}_amd64.zip"
VAULT_URL="http://hc-enterprise-binaries.s3.amazonaws.com/vault/ent/${VAULT_VER}/${VAULT_ZIP}"
download
unpack
}
function main() {
initialize
parse_options "$@"
if [[ ${EDITION} == 'oss' ]]; then
download_oss
elif [[ ${EDITION} == 'enterprise' ]]; then
download_enterprise
else
say "Ignoring edition option: ${EDITION} - oss and enterprise supported only"
exit 1
fi
}
main "$@"