From f5a969bc2780c6be44c03e6039edb961be5927d7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 31 Jul 2019 16:26:56 +0200 Subject: [PATCH] Add support for PCF authentication. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support PCF authentication leveraging PCF's App and Container Identity Assurance. Instance certificate and key default to CF_INSTANCE_CERT respective CF_INSTANCE_KEY environment variables. PcfAuthenticationOptions options = PcfAuthenticationOptions.builder().role(…).build(); PcfAuthentication authentication = new PcfAuthentication(options, restOperations); VaultToken token = authentication.login(); PcfAuthentication requires BouncyCastle for RSA-PSS signing. Update KubernetesJwtSupplier and KubernetesServiceAccountTokenFile to inherit from generic Resource-based credential suppliers. Closes gh-440. --- spring-vault-core/pom.xml | 6 + .../authentication/CredentialSupplier.java | 56 ++++ .../KubernetesAuthentication.java | 2 +- .../authentication/KubernetesJwtSupplier.java | 28 +- .../KubernetesServiceAccountTokenFile.java | 45 +-- .../authentication/PcfAuthentication.java | 182 ++++++++++++ .../PcfAuthenticationOptions.java | 262 ++++++++++++++++++ .../ResourceCredentialSupplier.java | 105 +++++++ .../vault/client/RestTemplateBuilder.java | 8 +- .../vault/support/Certificate.java | 4 +- .../vault/support/KeystoreUtil.java | 33 ++- .../vault/support/PemObject.java | 81 ++++++ .../PcfAuthenticationOptionsUnitTests.java | 38 +++ .../PcfAuthenticationUnitTests.java | 139 ++++++++++ .../VaultIntegrationTestConfiguration.java | 3 +- .../VaultTemplateTransitIntegrationTests.java | 8 +- .../vault/util/TestWebClientFactory.java | 2 +- spring-vault-dependencies/pom.xml | 13 +- src/main/asciidoc/new-features.adoc | 1 + .../asciidoc/reference/authentication.adoc | 46 +++ 20 files changed, 975 insertions(+), 87 deletions(-) create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthentication.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthenticationOptions.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java create mode 100644 spring-vault-core/src/main/java/org/springframework/vault/support/PemObject.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationOptionsUnitTests.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationUnitTests.java diff --git a/spring-vault-core/pom.xml b/spring-vault-core/pom.xml index 438e0b83..bc23c35d 100644 --- a/spring-vault-core/pom.xml +++ b/spring-vault-core/pom.xml @@ -191,6 +191,12 @@ true + + org.bouncycastle + bcpkix-jdk15on + true + + diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java new file mode 100644 index 00000000..aaf5abcc --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/CredentialSupplier.java @@ -0,0 +1,56 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.authentication; + +import java.util.function.Supplier; + +/** + * Interface to obtain an arbitrary credential that is uses in + * {@link ClientAuthentication} or {@link AuthenticationSteps} methods. Typically, + * implementations obtain their credential from a file. + * + * @author Mark Paluch + * @since 2.2 + * @see ResourceCredentialSupplier + */ +@FunctionalInterface +public interface CredentialSupplier extends Supplier { + + /** + * Get a credential to be used with an authentication mechanism. + * + * @return the credential. + */ + @Override + String get(); + + /** + * Retrieve a cached {@link CredentialSupplier} that obtains the credential early and + * reuses the token for each {@link #get()} call. This is useful to prevent I/O + * operations in e.g. reactive usage. + *

+ * Reusing a cached token can lead to authentication failures if the credential + * expires. + * + * @return a caching {@link CredentialSupplier}. + */ + default CredentialSupplier cached() { + + String credential = get(); + + return () -> credential; + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthentication.java index a292cb46..f663a78d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthentication.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthentication.java @@ -61,7 +61,7 @@ public class KubernetesAuthentication public KubernetesAuthentication(KubernetesAuthenticationOptions options, RestOperations restOperations) { - Assert.notNull(options, "KubeAuthenticationOptions must not be null"); + Assert.notNull(options, "KubernetesAuthenticationOptions must not be null"); Assert.notNull(restOperations, "RestOperations must not be null"); this.options = options; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java index 63a8f091..f8b11967 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java @@ -15,8 +15,6 @@ */ package org.springframework.vault.authentication; -import java.util.function.Supplier; - /** * Interface to obtain a Kubernetes Service Account Token for Kubernetes authentication. * Implementations are used by {@link KubernetesAuthentication}. @@ -27,30 +25,6 @@ import java.util.function.Supplier; * @see KubernetesAuthentication */ @FunctionalInterface -public interface KubernetesJwtSupplier extends Supplier { +public interface KubernetesJwtSupplier extends CredentialSupplier { - /** - * Get a JWT for Kubernetes authentication. - * - * @return the Kubernetes Service Account JWT. - */ - @Override - String get(); - - /** - * Retrieve a cached {@link KubernetesJwtSupplier} that obtains the JWT early and - * reuses the token for each {@link #get()} call. This is useful to prevent I/O - * operations in e.g. reactive usage. - *

- * Reusing a cached token can lead to authentication failures if the token expires. - * - * @return a caching {@link KubernetesJwtSupplier}. - * @since 2.2 - */ - default KubernetesJwtSupplier cached() { - - String jwt = get(); - - return () -> jwt; - } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java index 6a2a4cd5..4a886fe6 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java @@ -16,15 +16,9 @@ package org.springframework.vault.authentication; import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.util.Assert; -import org.springframework.util.StreamUtils; -import org.springframework.vault.VaultException; /** * Mechanism to retrieve a Kubernetes service account token. @@ -37,15 +31,14 @@ import org.springframework.vault.VaultException; * @since 2.0 * @see KubernetesJwtSupplier */ -public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier { +public class KubernetesServiceAccountTokenFile extends ResourceCredentialSupplier + implements KubernetesJwtSupplier { /** * Default path to the service account token file. */ public static final String DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"; - private final Resource resource; - /** * Create a new {@link KubernetesServiceAccountTokenFile} pointing to the * {@link #DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE}. Construction fails with an @@ -88,38 +81,6 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier * @throws IllegalArgumentException if the{@code path} does not exist. */ public KubernetesServiceAccountTokenFile(Resource resource) { - - Assert.isTrue(resource.exists(), - () -> String.format("Resource %s does not exist", resource)); - - this.resource = resource; - } - - @Override - public String get() { - - try { - return new String(readToken(this.resource), StandardCharsets.US_ASCII); - } - catch (IOException e) { - throw new VaultException(String - .format("Kube JWT token retrieval from %s failed", this.resource), e); - } - } - - /** - * Read the token from {@link Resource}. - * - * @param resource the resource to read from, must not be {@literal null}. - * @return the new byte array that has been copied to (possibly empty). - * @throws IOException in case of I/O errors. - */ - private static byte[] readToken(Resource resource) throws IOException { - - Assert.notNull(resource, "Resource must not be null"); - - try (InputStream is = resource.getInputStream()) { - return StreamUtils.copyToByteArray(is); - } + super(resource); } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthentication.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthentication.java new file mode 100644 index 00000000..02dfad65 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthentication.java @@ -0,0 +1,182 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.authentication; + +import java.nio.charset.StandardCharsets; +import java.security.spec.RSAPrivateKeySpec; +import java.time.Clock; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bouncycastle.crypto.CryptoException; +import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.engines.RSAEngine; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.bouncycastle.crypto.signers.PSSSigner; + +import org.springframework.util.Assert; +import org.springframework.util.Base64Utils; +import org.springframework.vault.VaultException; +import org.springframework.vault.support.PemObject; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestOperations; + +/** + * PCF implementation of {@link ClientAuthentication}. {@link PcfAuthentication} uses a + * PCF instance certificate and key to login into Vault. + *

+ * Requires BouncyCastle to generate a RSA PSS signature. + * + * @author Mark Paluch + * @since 2.2 + * @see PcfAuthenticationOptions + * @see RestOperations + * @see Auth Backend: PCF + */ +public class PcfAuthentication + implements ClientAuthentication, AuthenticationStepsFactory { + + private static final Log logger = LogFactory.getLog(PcfAuthentication.class); + + private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter + .ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); + + // SHA256 hash and a salt length of 222 + private static final int SALT_LENGTH = 222; + + private final PcfAuthenticationOptions options; + + private final RestOperations restOperations; + + /** + * Create a {@link PcfAuthentication} using {@link PcfAuthenticationOptions} and + * {@link RestOperations}. + * + * @param options must not be {@literal null}. + * @param restOperations must not be {@literal null}. + */ + public PcfAuthentication(PcfAuthenticationOptions options, + RestOperations restOperations) { + + Assert.notNull(options, "PcfAuthenticationOptions must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.options = options; + this.restOperations = restOperations; + } + + /** + * Creates a {@link AuthenticationSteps} for pcf authentication given + * {@link PcfAuthenticationOptions}. + * + * @param options must not be {@literal null}. + * @return {@link AuthenticationSteps} for pcf authentication. + */ + public static AuthenticationSteps createAuthenticationSteps( + PcfAuthenticationOptions options) { + + Assert.notNull(options, "PcfAuthenticationOptions must not be null"); + + String instanceCert = options.getInstanceCertSupplier().get(); + String instanceKey = options.getInstanceKeySupplier().get(); + return AuthenticationSteps + .fromSupplier(() -> getPcfLogin(options.getRole(), options.getClock(), + instanceCert, instanceKey)) // + .login("auth/{mount}/login", options.getPath()); + } + + @Override + public VaultToken login() throws VaultException { + + Map login = getPcfLogin(options.getRole(), options.getClock(), + options.getInstanceCertSupplier().get(), + options.getInstanceKeySupplier().get()); + + try { + VaultResponse response = restOperations.postForObject("auth/{mount}/login", + login, VaultResponse.class, options.getPath()); + + Assert.state(response != null && response.getAuth() != null, + "Auth field must not be null"); + + logger.debug("Login successful using PCF authentication"); + + return LoginTokenUtil.from(response.getAuth()); + } + catch (RestClientException e) { + throw VaultLoginException.create("PCF", e); + } + } + + @Override + public AuthenticationSteps getAuthenticationSteps() { + return createAuthenticationSteps(this.options); + } + + private static Map getPcfLogin(String role, Clock clock, + String instanceCert, String instanceKey) { + + Assert.hasText(role, "Role must not be empty"); + + String signingTime = TIME_FORMAT.format(LocalDateTime.now(clock)); + String message = getMessage(role, signingTime, instanceCert); + String signature = sign(message, instanceKey); + Map login = new HashMap<>(); + + login.put("role", role); + login.put("cf_instance_cert", instanceCert); + login.put("signing_time", signingTime); + login.put("signature", signature); + + return login; + } + + private static String sign(String message, String privateKeyPem) { + + try { + return doSign(message.getBytes(StandardCharsets.US_ASCII), privateKeyPem); + } + catch (CryptoException e) { + throw new VaultException("Cannot sign PCF login", e); + } + } + + private static String getMessage(String role, String signingTime, + String instanceCertPem) { + return signingTime + instanceCertPem + role; + } + + private static String doSign(byte[] message, String instanceKeyPem) + throws CryptoException { + + RSAPrivateKeySpec privateKey = PemObject.fromKey(instanceKeyPem).getRSAKeySpec(); + PSSSigner signer = new PSSSigner(new RSAEngine(), new SHA256Digest(), + SALT_LENGTH); + + signer.init(true, new RSAKeyParameters(true, privateKey.getModulus(), + privateKey.getPrivateExponent())); + signer.update(message, 0, message.length); + + byte[] signature = signer.generateSignature(); + return Base64Utils.encodeToUrlSafeString(signature); + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthenticationOptions.java new file mode 100644 index 00000000..1448db75 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/PcfAuthenticationOptions.java @@ -0,0 +1,262 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.authentication; + +import java.time.Clock; +import java.util.function.Supplier; + +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * Authentication options for {@link PcfAuthentication}. + *

+ * Authentication options provide the path, {@link Clock} and instance key/instance + * certificate {@link Supplier}s. {@link PcfAuthenticationOptions} can be constructed + * using {@link #builder()}. Instances of this class are immutable once constructed. + *

+ * Defaults to a cached instance certificate/key by resolving {@code CF_INSTANCE_CERT} and + * {@code CF_INSTANCE_KEY} env variables. + * + * @author Mark Paluch + * @see CredentialSupplier + * @see ResourceCredentialSupplier + * @see #builder() + */ +public class PcfAuthenticationOptions { + + public static final String DEFAULT_PCF_AUTHENTICATION_PATH = "pcf"; + + /** + * Path of the pcf authentication backend mount. + */ + private final String path; + + /** + * Name of the role against which the login is being attempted. + */ + private final String role; + + private final Clock clock; + + /** + * Supplier instance to obtain the instance certificate. + */ + private final Supplier instanceCertSupplier; + + /** + * Supplier instance to obtain the instance key. + */ + private final Supplier instanceKeySupplier; + + private PcfAuthenticationOptions(String path, String role, Clock clock, + Supplier instanceCertSupplier, Supplier instanceKeySupplier) { + this.path = path; + this.role = role; + this.clock = clock; + this.instanceCertSupplier = instanceCertSupplier; + this.instanceKeySupplier = instanceKeySupplier; + } + + /** + * @return a new {@link PcfAuthenticationOptionsBuilder}. + */ + public static PcfAuthenticationOptionsBuilder builder() { + return new PcfAuthenticationOptionsBuilder(); + } + + /** + * @return the path of the pcf authentication backend mount. + */ + public String getPath() { + return path; + } + + /** + * @return name of the role against which the login is being attempted. + */ + public String getRole() { + return role; + } + + /** + * @return the {@link Clock}. + */ + public Clock getClock() { + return clock; + } + + /** + * @return the instance certificate {@link Supplier}. + */ + public Supplier getInstanceCertSupplier() { + return instanceCertSupplier; + } + + /** + * @return the instance key {@link Supplier}. + */ + public Supplier getInstanceKeySupplier() { + return instanceKeySupplier; + } + + /** + * Builder for {@link PcfAuthenticationOptions}. + */ + public static class PcfAuthenticationOptionsBuilder { + + private String path = DEFAULT_PCF_AUTHENTICATION_PATH; + + private Clock clock = Clock.systemUTC(); + + @Nullable + private String role; + + @Nullable + private Supplier instanceCertSupplier; + + @Nullable + private Supplier instanceKeySupplier; + + PcfAuthenticationOptionsBuilder() { + } + + /** + * Configure the mount path. + * + * @param path must not be empty or {@literal null}. + * @return {@code this} {@link PcfAuthenticationOptionsBuilder}. + * @see #DEFAULT_PCF_AUTHENTICATION_PATH + */ + public PcfAuthenticationOptionsBuilder path(String path) { + + Assert.hasText(path, "Path must not be empty"); + + this.path = path; + return this; + } + + /** + * Configure the role. + * + * @param role name of the role against which the login is being attempted, must + * not be {@literal null} or empty. + * @return {@code this} {@link PcfAuthenticationOptionsBuilder}. + */ + public PcfAuthenticationOptionsBuilder role(String role) { + + Assert.hasText(role, "Role must not be empty"); + + this.role = role; + return this; + } + + /** + * Configure the {@link Clock}. + * + * @param clock must not be {@literal null}. + * @return {@code this} {@link PcfAuthenticationOptionsBuilder}. + */ + public PcfAuthenticationOptionsBuilder clock(Clock clock) { + + Assert.notNull(clock, "Clock must not be null"); + + this.clock = clock; + return this; + } + + /** + * Configure the {@link Supplier} to obtain the instance certificate. + * + * @param instanceCertSupplier the supplier, must not be {@literal null}. + * @return {@code this} {@link PcfAuthenticationOptionsBuilder}. + * @see ResourceCredentialSupplier + */ + public PcfAuthenticationOptionsBuilder instanceCertificate( + Supplier instanceCertSupplier) { + + Assert.notNull(instanceCertSupplier, + "Instance certificate supplier must not be null"); + + this.instanceCertSupplier = instanceCertSupplier; + return this; + } + + /** + * Configure the {@link Supplier} to obtain the instance key. + * + * @param instanceKeySupplier the supplier, must not be {@literal null}. + * @return {@code this} {@link PcfAuthenticationOptionsBuilder}. + * @see ResourceCredentialSupplier + */ + public PcfAuthenticationOptionsBuilder instanceKey( + Supplier instanceKeySupplier) { + + Assert.notNull(instanceKeySupplier, + "Instance certificate supplier must not be null"); + + this.instanceKeySupplier = instanceKeySupplier; + return this; + } + + /** + * Build a new {@link PcfAuthenticationOptions} instance. + *

+ * Falls back to the instance certificate at {@code CF_INSTANCE_CERT} if + * {@link #instanceCertificate(Supplier)} is not configured respective + * {@code CF_INSTANCE_KEY} if {@link #instanceKey(Supplier)} is not configured. + * + * @return a new {@link PcfAuthenticationOptions}. + * @throws IllegalStateException if {@link #instanceCertificate(Supplier)} or + * {@link #instanceKey(Supplier)} are not set and the corresponding + * environment variable {@code CF_INSTANCE_CERT} respective + * {@code CF_INSTANCE_KEY} is not set. + */ + public PcfAuthenticationOptions build() { + + Assert.notNull(role, "Role must not be null"); + + Supplier instanceCertSupplier = this.instanceCertSupplier; + + if (instanceCertSupplier == null) { + instanceCertSupplier = new ResourceCredentialSupplier( + resolveEnvVariable("CF_INSTANCE_CERT")).cached(); + } + + Supplier instanceKeySupplier = this.instanceKeySupplier; + if (instanceKeySupplier == null) { + instanceKeySupplier = new ResourceCredentialSupplier( + resolveEnvVariable("CF_INSTANCE_KEY")).cached(); + } + + return new PcfAuthenticationOptions(path, role, clock, instanceCertSupplier, + instanceKeySupplier); + } + + private static String resolveEnvVariable(String name) { + + String value = System.getenv(name); + + if (StringUtils.isEmpty(value)) { + throw new IllegalStateException( + String.format("Environment variable %s not set", name)); + } + + return value; + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java new file mode 100644 index 00000000..19627068 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/ResourceCredentialSupplier.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.authentication; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.vault.VaultException; + +/** + * Mechanism to retrieve a credential from a {@link Resource}. + * + * @author Mark Paluch + * @since 2.2 + * @see CredentialSupplier + */ +public class ResourceCredentialSupplier implements CredentialSupplier { + + private final Resource resource; + + /** + * Create a new {@link ResourceCredentialSupplier} {@link ResourceCredentialSupplier} + * from a {@code path}. + * + * @param path path to the file holding the credential. + * @throws IllegalArgumentException if the{@code path} does not exist. + */ + public ResourceCredentialSupplier(String path) { + this(new FileSystemResource(path)); + } + + /** + * Create a new {@link ResourceCredentialSupplier} {@link ResourceCredentialSupplier} + * from a {@link File} handle. + * + * @param file path to the file holding the credential. + * @throws IllegalArgumentException if the{@code path} does not exist. + */ + public ResourceCredentialSupplier(File file) { + this(new FileSystemResource(file)); + } + + /** + * Create a new {@link ResourceCredentialSupplier} {@link ResourceCredentialSupplier} + * from a {@link Resource} handle. + * + * @param resource resource pointing to the resource holding the credential. + * @throws IllegalArgumentException if the {@link Resource} does not exist. + */ + public ResourceCredentialSupplier(Resource resource) { + + Assert.isTrue(resource.exists(), + () -> String.format("Resource %s does not exist", resource)); + + this.resource = resource; + } + + @Override + public String get() { + + try { + return new String(readToken(this.resource), StandardCharsets.US_ASCII); + } + catch (IOException e) { + throw new VaultException( + String.format("Credential retrieval from %s failed", this.resource), + e); + } + } + + /** + * Read the token from {@link Resource}. + * + * @param resource the resource to read from, must not be {@literal null}. + * @return the new byte array that has been copied to (possibly empty). + * @throws IOException in case of I/O errors. + */ + private static byte[] readToken(Resource resource) throws IOException { + + Assert.notNull(resource, "Resource must not be null"); + + try (InputStream is = resource.getInputStream()) { + return StreamUtils.copyToByteArray(is); + } + } +} diff --git a/spring-vault-core/src/main/java/org/springframework/vault/client/RestTemplateBuilder.java b/spring-vault-core/src/main/java/org/springframework/vault/client/RestTemplateBuilder.java index 5136e6eb..b05a792a 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/client/RestTemplateBuilder.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/client/RestTemplateBuilder.java @@ -264,7 +264,13 @@ public class RestTemplateBuilder { ClientHttpRequest request = requestFactory.createRequest(uri, httpMethod); HttpHeaders headers = request.getHeaders(); - this.defaultHeaders.forEach(headers::addIfAbsent); + this.defaultHeaders.forEach((key, value) -> { + + if (!headers.containsKey(key)) { + headers.add(key, value); + } + + }); this.requestCustomizers.forEach(it -> it.customize(request)); return request; diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java b/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java index 764389c3..e71f585c 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/Certificate.java @@ -106,7 +106,7 @@ public class Certificate { byte[] bytes = Base64Utils.decodeFromString(getCertificate()); return KeystoreUtil.getCertificate(bytes); } - catch (IOException | CertificateException e) { + catch (CertificateException e) { throw new VaultException("Cannot create Certificate from certificate", e); } } @@ -123,7 +123,7 @@ public class Certificate { byte[] bytes = Base64Utils.decodeFromString(getIssuingCaCertificate()); return KeystoreUtil.getCertificate(bytes); } - catch (IOException | CertificateException e) { + catch (CertificateException e) { throw new VaultException( "Cannot create Certificate from issuing CA certificate", e); } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java b/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java index d79a1522..57206694 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/KeystoreUtil.java @@ -22,6 +22,7 @@ import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.KeyFactory; import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -40,6 +41,27 @@ import java.util.List; */ class KeystoreUtil { + private static final CertificateFactory CERTIFICATE_FACTORY; + + private static final KeyFactory KEY_FACTORY; + + static { + + try { + CERTIFICATE_FACTORY = CertificateFactory.getInstance("X.509"); + } + catch (CertificateException e) { + throw new IllegalStateException("No X.509 Certificate available", e); + } + + try { + KEY_FACTORY = KeyFactory.getInstance("RSA"); + } + catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("No RSA KeyFactory available", e); + } + } + /** * Create a {@link KeyStore} containing the {@link KeySpec} and {@link X509Certificate * certificates} using the given {@code keyAlias}. @@ -54,8 +76,7 @@ class KeystoreUtil { X509Certificate... certificates) throws GeneralSecurityException, IOException { - KeyFactory kf = KeyFactory.getInstance("RSA"); - PrivateKey privateKey = kf.generatePrivate(privateKeySpec); + PrivateKey privateKey = KEY_FACTORY.generatePrivate(privateKeySpec); KeyStore keyStore = createKeyStore(); @@ -93,11 +114,9 @@ class KeystoreUtil { } static X509Certificate getCertificate(byte[] source) - throws CertificateException, IOException { + throws CertificateException { - CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); - - List certificates = getCertificates(certificateFactory, source); + List certificates = getCertificates(CERTIFICATE_FACTORY, source); return certificates.stream().findFirst().orElseThrow( () -> new IllegalArgumentException("No X509Certificate found")); @@ -120,7 +139,7 @@ class KeystoreUtil { } private static List getCertificates(CertificateFactory cf, - byte[] source) throws CertificateException, IOException { + byte[] source) throws CertificateException { List x509Certificates = new ArrayList<>(); diff --git a/spring-vault-core/src/main/java/org/springframework/vault/support/PemObject.java b/spring-vault-core/src/main/java/org/springframework/vault/support/PemObject.java new file mode 100644 index 00000000..03a5d103 --- /dev/null +++ b/spring-vault-core/src/main/java/org/springframework/vault/support/PemObject.java @@ -0,0 +1,81 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.support; + +import java.io.IOException; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.util.Base64Utils; + +/** + * Represents a PEM object that is internally decoded to a DER object. Typically used to + * obtain a {@link RSAPrivateCrtKeySpec}. + *

+ * Mainly for use within the framework. + * + * @author Mark Paluch + * @since 2.2 + */ +public class PemObject { + + private static final Pattern KEY_PATTERN = Pattern + .compile("-+BEGIN\\s+.*PRIVATE\\s+KEY[^-]*-+(?:\\s|\\r|\\n)+" + // Header + "([a-z0-9+/=\\r\\n]+)" + // Base64 text + "-+END\\s+.*PRIVATE\\s+KEY[^-]*-+", // Footer + Pattern.CASE_INSENSITIVE); + + private final byte[] content; + + private PemObject(String content) { + + String sanitized = content.replaceAll("\r", "").replaceAll("\n", ""); + this.content = Base64Utils.decodeFromString(sanitized); + } + + /** + * Create a{@link PemObject} from PEM {@code content} that is enclosed with + * {@code -BEGIN PRIVATE KEY-} and {@code -END PRIVATE KEY-}. + * + * @param content the PEM content. + * @return the {@link PemObject} from PEM {@code content}. + */ + public static PemObject fromKey(String content) { + + Matcher m = KEY_PATTERN.matcher(content); + if (!m.find()) { + throw new IllegalArgumentException("Could not find a PKCS #8 private key"); + } + + return new PemObject(m.group(1)); + } + + /** + * Retrieve a {@link RSAPrivateCrtKeySpec}. + * + * @return the {@link RSAPrivateCrtKeySpec}. + */ + public RSAPrivateCrtKeySpec getRSAKeySpec() { + + try { + return KeystoreUtil.getRSAKeySpec(this.content); + } + catch (IOException e) { + throw new IllegalArgumentException("Cannot obtain PrivateKey", e); + } + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationOptionsUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationOptionsUnitTests.java new file mode 100644 index 00000000..3db4ab1d --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationOptionsUnitTests.java @@ -0,0 +1,38 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.authentication; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Unit tests for {@link PcfAuthenticationOptions}. + * + * @author Mark Paluch + */ +class PcfAuthenticationOptionsUnitTests { + + @Test + void emptyEnvVariableShouldFailWithMeaningfulMessage() { + + assertThatThrownBy( + () -> PcfAuthenticationOptions.builder().role("my-role").build()) + .isInstanceOf(IllegalStateException.class) // + .hasMessageContaining( + "Environment variable CF_INSTANCE_CERT not set"); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationUnitTests.java new file mode 100644 index 00000000..bafd3d64 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/PcfAuthenticationUnitTests.java @@ -0,0 +1,139 @@ +/* + * Copyright 2019 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 + * + * https://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.vault.authentication; + +import java.time.Clock; +import java.time.Instant; +import java.time.ZoneId; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.vault.client.VaultClients; +import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.RestTemplate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; + +/** + * Unit tests for {@link PcfAuthentication}. + * + * @author Mark Paluch + */ +class PcfAuthenticationUnitTests { + + RestTemplate restTemplate; + + MockRestServiceServer mockRest; + + String instanceKey = "-----BEGIN RSA PRIVATE KEY-----\n" + + "MIIEpAIBAAKCAQEAzhDAw7m5EuCcQkT7cesJF9J/0FWeSICLg4F/3R8EpkjPfZSW\n" + + "BeIjSs6v+wUjPCdBomZpYnrphYvSdiDrHWIDKrFNcWMFms0t6A0jEyGG/k9xDf6u\n" + + "VjDw4Gi4LJ0o0sNK0y9ULgdrViwRkSKNdhZx+34l/aeWyrg9WG6KAdjbPQE2J4Vi\n" + + "3x7yJVEh/Ya0uR/UtN/8hB8Makuaz6SRcLvooUO0FXYpy20olA/nlmowCl1PxpVv\n" + + "smZZG7qDJGL/P3C3M9YRX4VPXLTiyvEFwIgWNz6QIhX/Enm0q+xyw+gd5hRQzF4Q\n" + + "Fs03VrzHsQX9H3GMd/HoqsM1vD+PPfpKI7re7wIDAQABAoIBAQCk83oq8wd4Wf4b\n" + + "ejbBWQB9Zk5UCcVbijKjwU0GR2ckaNJXV1LEQOI5ZrwuN02eQFpk0o/3eiZmdaey\n" + + "UeWDLssUKLuyUS7SXP4rbCCwlr0F47e/GSia7DBVot4TMHbWR+gkpxU+h0ffwgUJ\n" + + "5dvRNGRnifKFWtr1SYgpusqUcfAFovvcbLZ4X+VkV7uazHAbtor5ol2UuRN04Hnu\n" + + "i5gxKBvr/j4ULDnZDJZjJKPVyaS9x4ewVJx241mFbE3aOWxRl6QwouqXTqhIs/Lv\n" + + "85MR67nuVlEWMToR/sx0/5Xg/BJDHLUCRM5MNx+wD+Fyj6DlJn2A63Ly4HQZZuYG\n" + + "Z48BGku5AoGBAOFmlJQITfp5xtu2oy10bwtDElHV3HBAtcJ7OHgu9ZniNNhgZ7ud\n" + + "pw9VC/bL8qZ8iToooKA7AgecSmaDeX0qWPgE9uaheaT76x/6Kn3Ue/ZfV2pSrStu\n" + + "YlFc5UxG3Z3klAx/Y+DA7rLb2Y0U6olLx5A7Fg+V/feD8mYxnyZy2r/bAoGBAOoK\n" + + "NVIyzcv13S7OqiCo4nhF2CcDWekGTzmsFygwvNwiM1CipzFifQQw6Z+0rl1FwxN0\n" + + "uGs3x41iTmkVOyASO78Li9aPQHZWhRWQI82689kYcnChQnDZGLfIv/WVxGy3DgKd\n" + + "FdJIOSLagTwhUOnZ4kaR6Mj2Jc4RXYjwpoQUJON9AoGBALOqP75rjDSegvs5boJZ\n" + + "7/WLJfwjOw4jFn6KF638yHo7zCG5XpY3CSX4hYvYb3dzhzLblYWC45BLbSafn+Q8\n" + + "MCSqWF/n0H3I7FdV4i7gg1sUDirK8gvPdgEiygdt6VLlE3mOxX8ualYZViTVyklc\n" + + "JRt7bY9I4OI9w6bf4NsV6/XHAoGAISd5Dj/sL2yQ/MSCDUZfbrJWQJCU+BHQv1bF\n" + + "oQfmeTjPFCk2jiRpmWJkdh9eZBAx5luulGG+fyTh/rjnO0/Z7uJv2OFKPHldOQTG\n" + + "TaqiSKrR62qswte+TKq/psakoNH9xhkCsltQ3MMfc6k0kSwwhda9p1pXWK3VFkUh\n" + + "EazY3PECgYAoo8jvvQTKlXBmnVU1R//16fCklJXqYcEeOAO0CgyPCxHuAULK++M/\n" + + "HRHd+6FippoH4ppSACEqQO5TwBTYxgOCwOcYZaRDvYZqEbgPNlf3oZ73kRyoIeAK\n" + + "zvaXPNUuUEoW4E9Y2M+9SzF+975TTjqwjBgoCIF3xd+xQfgV9vkB6w==\n" + + "-----END RSA PRIVATE KEY-----"; + + Clock clock = Clock.fixed(Instant.parse("2007-12-03T10:15:30.00Z"), ZoneId.of("UTC")); + + @BeforeEach + void before() { + + RestTemplate restTemplate = VaultClients.createRestTemplate(); + restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler()); + + this.mockRest = MockRestServiceServer.createServer(restTemplate); + this.restTemplate = restTemplate; + } + + @Test + void loginShouldObtainToken() { + + PcfAuthenticationOptions options = PcfAuthenticationOptions.builder() + .instanceCertificate(() -> "foo") // + .instanceKey(() -> instanceKey) // + .role("dev-role") // + .clock(clock) // + .build(); + + PcfAuthentication authentication = new PcfAuthentication(options, restTemplate); + + expectLoginRequest(); + + VaultToken login = authentication.login(); + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + } + + @Test + void loginWithStepsShouldObtainToken() { + + PcfAuthenticationOptions options = PcfAuthenticationOptions.builder() + .instanceCertificate(() -> "foo") // + .instanceKey(() -> instanceKey) // + .role("dev-role") // + .clock(clock) // + .build(); + + expectLoginRequest(); + + AuthenticationStepsExecutor authentication = new AuthenticationStepsExecutor( + PcfAuthentication.createAuthenticationSteps(options), restTemplate); + + VaultToken login = authentication.login(); + assertThat(login).isInstanceOf(LoginToken.class); + assertThat(login.getToken()).isEqualTo("my-token"); + } + + private void expectLoginRequest() { + + mockRest.expect(requestTo("/auth/pcf/login")).andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.role").value("dev-role")) + .andExpect(jsonPath("$.signature").exists()) + .andExpect(jsonPath("$.cf_instance_cert").value("foo")) + .andExpect(jsonPath("$.signing_time").value("2007-12-03T10:15:30Z")) + .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON) + .body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}")); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java index 28499cf5..38396d02 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultIntegrationTestConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.vault.client.VaultEndpoint; import org.springframework.vault.config.AbstractReactiveVaultConfiguration; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.util.Settings; +import org.springframework.vault.util.TestRestTemplateFactory; /** * Test configuration for Vault integration tests. @@ -34,7 +35,7 @@ public class VaultIntegrationTestConfiguration @Override public VaultEndpoint vaultEndpoint() { - return new VaultEndpoint(); + return TestRestTemplateFactory.TEST_VAULT_ENDPOINT; } @Override diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransitIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransitIntegrationTests.java index f580bc71..e8105433 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransitIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateTransitIntegrationTests.java @@ -18,7 +18,6 @@ package org.springframework.vault.core; import java.util.Collections; import java.util.List; -import org.apache.commons.codec.binary.Base64; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,6 +26,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.Base64Utils; import org.springframework.vault.support.VaultMount; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultTransitKeyConfiguration; @@ -103,7 +103,7 @@ class VaultTemplateTransitIntegrationTests extends IntegrationTestSupport { VaultResponse response = vaultOperations.write("transit/encrypt/mykey", Collections.singletonMap("plaintext", - Base64.encodeBase64String("that message is secret".getBytes()))); + Base64Utils.encodeToString("that message is secret".getBytes()))); assertThat((String) response.getRequiredData().get("ciphertext")).isNotEmpty(); } @@ -113,13 +113,13 @@ class VaultTemplateTransitIntegrationTests extends IntegrationTestSupport { VaultResponse response = vaultOperations.write("transit/encrypt/mykey", Collections.singletonMap("plaintext", - Base64.encodeBase64String("that message is secret".getBytes()))); + Base64Utils.encodeToString("that message is secret".getBytes()))); VaultResponse decrypted = vaultOperations.write("transit/decrypt/mykey", Collections.singletonMap("ciphertext", response.getRequiredData().get("ciphertext"))); assertThat((String) decrypted.getRequiredData().get("plaintext")).isEqualTo( - Base64.encodeBase64String("that message is secret".getBytes())); + Base64Utils.encodeToString("that message is secret".getBytes())); } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java b/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java index ba255001..3228c986 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/util/TestWebClientFactory.java @@ -29,7 +29,7 @@ import org.springframework.web.reactive.function.client.WebClient; */ public class TestWebClientFactory { - private static final VaultEndpoint TEST_VAULT_ENDPOINT = new VaultEndpoint(); + private static final VaultEndpoint TEST_VAULT_ENDPOINT = TestRestTemplateFactory.TEST_VAULT_ENDPOINT; /** * Create a new {@link WebClient} using the {@link SslConfiguration}. See diff --git a/spring-vault-dependencies/pom.xml b/spring-vault-dependencies/pom.xml index 8a1aba94..bad7c6e5 100644 --- a/spring-vault-dependencies/pom.xml +++ b/spring-vault-dependencies/pom.xml @@ -66,6 +66,7 @@ 1.11.605 v1-rev20190704-1.30.1 0.16.2 + 1.62 @@ -150,6 +151,15 @@ true + + + + org.bouncycastle + bcpkix-jdk15on + ${bcpkix-jdk15on.version} + true + + @@ -209,7 +219,8 @@ sonatype-nexus-staging Nexus Release Repository - https://oss.sonatype.org/service/local/staging/deploy/maven2/ + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 95da39f3..331384f5 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -8,6 +8,7 @@ * SpEL support in `@Secret`. * Add support for Jetty as reactive HttpClient. * `LifecycleAwareSessionManager` and `ReactiveLifecycleAwareSessionManager` emit now ``AuthenticationEvent``s. +* <>. * Deprecation of `AppIdAuthentication`. Use `AppRoleAuthentication` instead as recommended by HashiCorp Vault. [[new-features.2-1-0]] diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc index abaac6d9..6ee8920b 100644 --- a/src/main/asciidoc/reference/authentication.adoc +++ b/src/main/asciidoc/reference/authentication.adoc @@ -531,6 +531,52 @@ See also: * https://www.vaultproject.io/docs/auth/gcp.html[Vault Documentation: Using the GCP auth backend] * https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt[GCP Documentation: projects.serviceAccounts.signJwt][[vault.authentication.gcpiam]] +[[vault.authentication.pcf]] +== PCF authentication + +The https://www.vaultproject.io/docs/auth/pcf.html[pcf] +auth backend allows Vault login for PCF instances. +It leverages https://content.pivotal.io/blog/new-in-pcf-2-1-app-container-identity-assurance-via-automatic-cert-rotation[PCF's App and Container Identity Assurance]. + +PCF authentication uses the instance key and certificate to create a signature that is validated by Vault. +If the signature matches, and potentially bound organization/space/application Id's match, Vault issues an appropriately-scoped token. + +Instance credentials are available from files at `CF_INSTANCE_CERT` and +`CF_INSTANCE_KEY` variables. + +==== +[source,java] +---- +@Configuration +class AppConfig extends AbstractVaultConfiguration { + + // … + + @Override + public ClientAuthentication clientAuthentication() { + + PcfAuthenticationOptions options = PcfAuthenticationOptions.builder() + .role(…).build(); + + PcfAuthentication authentication = new PcfAuthentication(options, + restOperations()); + } + + // … +} +---- +==== + +`PcfAuthenticationOptions` requires the https://www.bouncycastle.org/latest_releases.html[BouncyCastle] +library for creating RSA-PSS signatures. + +You can configure the authentication via `PcfAuthenticationOptions`. + +See also: + +* https://www.vaultproject.io/docs/auth/pcf.html[Vault Documentation: +Using the PCF auth backend] + [[vault.authentication.clientcert]] == TLS certificate authentication