From 6e2afade1bd0017b0fc79ddf5b354ed97fa88335 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 Nov 2018 16:59:27 +0100 Subject: [PATCH] Add support for GCP authentication. We now support GCP Compute and GCP IAM authentication. We also provide different RestTemplate's for Vault and for external service communication. Closes gh-249. --- .../src/main/asciidoc/spring-cloud-vault.adoc | 119 ++++++++++++++++++ pom.xml | 18 +++ spring-cloud-vault-config/pom.xml | 12 ++ .../config/ClientAuthenticationFactory.java | 89 ++++++++++++- .../config/VaultBootstrapConfiguration.java | 13 +- .../cloud/vault/config/VaultProperties.java | 81 +++++++++++- 6 files changed, 328 insertions(+), 4 deletions(-) diff --git a/docs/src/main/asciidoc/spring-cloud-vault.adoc b/docs/src/main/asciidoc/spring-cloud-vault.adoc index c7ec148b..3286bea8 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault.adoc @@ -485,6 +485,125 @@ See also: * https://www.vaultproject.io/docs/secrets/cubbyhole/index.html[Vault Documentation: Cubbyhole Secret Backend] * https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping] +[[vault.config.authentication.gcpgce]] +== GCP-GCE authentication + +The https://www.vaultproject.io/docs/auth/gcp.html[gcp] +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials. + +GCP GCE (Google Compute Engine) authentication creates a signature in the form of a +JSON Web Token (JWT) for a service account. A JWT for a Compute Engine instance +is obtained from the GCE metadata service using https://cloud.google.com/compute/docs/instances/verifying-instance-identity[Instance identification]. +This API creates a JSON Web Token that can be used to confirm the instance identity. + +Unlike most Vault authentication backends, this backend +does not require first-deploying, or provisioning security-sensitive +credentials (tokens, username/password, client certificates, etc.). +Instead, it treats GCP as a Trusted Third Party and uses the +cryptographically signed dynamic metadata information that uniquely +represents each GCP service account. + +.bootstrap.yml with required GCP-GCE Authentication properties +==== +[source,yaml] +---- +spring.cloud.vault: + authentication: GCP_GCE + gcp-gce: + role: my-dev-role +---- +==== + +.bootstrap.yml with all GCP-GCE Authentication properties +==== +[source,yaml] +---- +spring.cloud.vault: + authentication: GCP_GCE + gcp-gce: + gcp-path: gcp + role: my-dev-role + service-account: my-service@projectid.iam.gserviceaccount.com +---- +==== + +* `role` sets the name of the role against which the login is being attempted. +* `gcp-path` sets the path of the GCP mount to use +* `service-account` allows overriding the service account Id to a specific value. Defaults to the `default` service account. + +See also: + +* https://www.vaultproject.io/docs/auth/gcp.html[Vault Documentation: Using the GCP auth backend] +* https://cloud.google.com/compute/docs/instances/verifying-instance-identity[GCP Documentation: Verifying the Identity of Instances] + +[[vault.config.authentication.gcpiam]] +== GCP-IAM authentication + +The https://www.vaultproject.io/docs/auth/gcp.html[gcp] +auth backend allows Vault login by using existing GCP (Google Cloud Platform) IAM and GCE credentials. + +GCP IAM authentication creates a signature in the form of a JSON Web Token (JWT) +for a service account. A JWT for a service account is obtained by +calling GCP IAM's https://cloud.google.com/iam/reference/rest/v1/projects.serviceAccounts/signJwt[`projects.serviceAccounts.signJwt`] API. The caller authenticates against GCP IAM +and proves thereby its identity. This Vault backend treats GCP as a Trusted Third Party. + +IAM credentials can be obtained from either the runtime environment +, specifically the https://cloud.google.com/docs/authentication/production[`GOOGLE_APPLICATION_CREDENTIALS`] +environment variable, the Google Compute metadata service, +or supplied externally as e.g. JSON or base64 encoded. +JSON is the preferred form as it carries the project id and +service account identifier required for calling ``projects.serviceAccounts.signJwt``. + +.bootstrap.yml with required GCP-IAM Authentication properties +==== +[source,yaml] +---- +spring.cloud.vault: + authentication: GCP_IAM + gcp-iam: + role: my-dev-role +---- +==== + +.bootstrap.yml with all GCP-IAM Authentication properties +==== +[source,yaml] +---- +spring.cloud.vault: + authentication: GCP_IAM + gcp-iam: + credentials: + location: classpath:credentials.json + encoded-key: e+KApn0= + gcp-path: gcp + jwt-validity: 15m + project-id: my-project-id + role: my-dev-role + service-account: my-service@projectid.iam.gserviceaccount.com +---- +==== + +* `role` sets the name of the role against which the login is being attempted. +* `credentials.location` path to the credentials resource that contains Google credentials in JSON format. +* `credentials.encoded-key` the base64 encoded contents of an OAuth2 account private key in the JSON format. +* `gcp-path` sets the path of the GCP mount to use +* `jwt-validity` configures the JWT token validity. Defaults to 15 minutes. +* `project-id` allows overriding the project Id to a specific value. Defaults to the project Id from the obtained credential. +* `service-account` allows overriding the service account Id to a specific value. Defaults to the service account from the obtained credential. + +GCP IAM authentication requires the Google Cloud Java SDK dependency +(`com.google.apis:google-api-services-iam` and `com.google.auth:google-auth-library-oauth2-http`) +as the authentication implementation uses Google APIs for credentials and JWT signing. + +NOTE: Google credentials require an OAuth 2 token maintaining the token lifecycle. All API +is synchronous therefore, `GcpIamAuthentication` does not support `AuthenticationSteps` which is +required for reactive usage. + +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.config.authentication.kubernetes]] === Kubernetes authentication diff --git a/pom.xml b/pom.xml index 6f7f34ca..687a413b 100644 --- a/pom.xml +++ b/pom.xml @@ -46,6 +46,9 @@ 4.4.9 4.1.30.Final 3.10.0 + + v1-rev259-1.25.0 + 0.10.0 @@ -105,6 +108,21 @@ netty-all ${netty.version} + + + + com.google.apis + google-api-services-iam + ${google-api-services-iam.version} + true + + + + com.google.auth + google-auth-library-oauth2-http + ${google-auth-library-oauth2-http.version} + true + diff --git a/spring-cloud-vault-config/pom.xml b/spring-cloud-vault-config/pom.xml index 012f9d0b..d5a0cb23 100644 --- a/spring-cloud-vault-config/pom.xml +++ b/spring-cloud-vault-config/pom.xml @@ -53,6 +53,18 @@ + + com.google.apis + google-api-services-iam + true + + + + com.google.auth + google-auth-library-oauth2-http + true + + com.squareup.okhttp3 okhttp 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 b61f10b4..c698401d 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 @@ -15,18 +15,26 @@ */ 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; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSCredentialsProvider; import com.amazonaws.auth.DefaultAWSCredentialsProviderChain; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import lombok.RequiredArgsConstructor; +import lombok.extern.apachecommons.CommonsLog; import org.springframework.beans.BeanUtils; import org.springframework.cloud.vault.config.VaultProperties.AppRoleProperties; import org.springframework.cloud.vault.config.VaultProperties.AwsIamProperties; import org.springframework.cloud.vault.config.VaultProperties.AzureMsiProperties; +import org.springframework.cloud.vault.config.VaultProperties.GcpCredentials; +import org.springframework.cloud.vault.config.VaultProperties.GcpIamProperties; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -36,6 +44,8 @@ import org.springframework.vault.authentication.AppRoleAuthenticationOptions.Rol import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; import org.springframework.vault.authentication.AwsEc2AuthenticationOptions.Nonce; import org.springframework.vault.authentication.AwsIamAuthenticationOptions.AwsIamAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.GcpComputeAuthenticationOptions.GcpComputeAuthenticationOptionsBuilder; +import org.springframework.vault.authentication.GcpIamAuthenticationOptions.GcpIamAuthenticationOptionsBuilder; import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestOperations; @@ -48,12 +58,15 @@ import org.springframework.web.client.RestOperations; * @since 1.1 */ @RequiredArgsConstructor +@CommonsLog class ClientAuthenticationFactory { private final VaultProperties vaultProperties; private final RestOperations restOperations; + private final RestOperations externalRestOperations; + /** * @return a new {@link ClientAuthentication}. */ @@ -82,6 +95,12 @@ class ClientAuthenticationFactory { case CUBBYHOLE: return cubbyholeAuthentication(); + case GCP_GCE: + return gcpGceAuthentication(vaultProperties); + + case GCP_IAM: + return gcpIamAuthentication(vaultProperties); + case KUBERNETES: return kubernetesAuthentication(vaultProperties); @@ -224,7 +243,7 @@ class ClientAuthenticationFactory { .build(); return new AwsEc2Authentication(authenticationOptions, restOperations, - restOperations); + externalRestOperations); } private ClientAuthentication awsIamAuthentication(VaultProperties vaultProperties) { @@ -264,7 +283,7 @@ class ClientAuthenticationFactory { AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder() .role(azureMsi.getRole()).build(); - return new AzureMsiAuthentication(options, restOperations); + return new AzureMsiAuthentication(options, restOperations, externalRestOperations); } private ClientAuthentication cubbyholeAuthentication() { @@ -280,6 +299,72 @@ class ClientAuthenticationFactory { return new CubbyholeAuthentication(options, restOperations); } + private ClientAuthentication gcpGceAuthentication(VaultProperties vaultProperties) { + + VaultProperties.GcpGceProperties gcp = vaultProperties.getGcpGce(); + + Assert.hasText(gcp.getRole(), + "Role (spring.cloud.vault.gcp-gce.role) must not be empty"); + + GcpComputeAuthenticationOptionsBuilder builder = GcpComputeAuthenticationOptions + .builder().path(gcp.getGcpPath()).role(gcp.getRole()); + + if (StringUtils.hasText(gcp.getServiceAccount())) { + builder.serviceAccount(gcp.getServiceAccount()); + } + + return new GcpComputeAuthentication(builder.build(), restOperations, + externalRestOperations); + } + + private ClientAuthentication gcpIamAuthentication(VaultProperties vaultProperties) { + + VaultProperties.GcpIamProperties gcp = vaultProperties.getGcpIam(); + + Assert.hasText(gcp.getRole(), + "Role (spring.cloud.vault.gcp-iam.role) must not be empty"); + + GcpIamAuthenticationOptionsBuilder builder = GcpIamAuthenticationOptions + .builder().path(gcp.getGcpPath()).role(gcp.getRole()) + .jwtValidity(gcp.getJwtValidity()); + + if (StringUtils.hasText(gcp.getProjectId())) { + builder.projectId(gcp.getProjectId()); + } + + if (StringUtils.hasText(gcp.getServiceAccountId())) { + builder.serviceAccountId(gcp.getServiceAccountId()); + } + + GcpCredentialSupplier supplier = () -> getGoogleCredential(gcp); + builder.credential(supplier.get()); + + GcpIamAuthenticationOptions options = builder.build(); + + try { + return new GcpIamAuthentication(options, restOperations); + } + catch (IOException | GeneralSecurityException e) { + throw new IllegalStateException("Cannot create GcpIamAuthentication", e); + } + } + + private GoogleCredential getGoogleCredential(GcpIamProperties gcp) throws IOException { + + GcpCredentials credentialProperties = gcp.getCredentials(); + if (credentialProperties.getLocation() != null) { + return GoogleCredential.fromStream(credentialProperties.getLocation() + .getInputStream()); + } + + if (StringUtils.hasText(credentialProperties.getEncodedKey())) { + return GoogleCredential.fromStream(new ByteArrayInputStream(Base64 + .getDecoder().decode(credentialProperties.getEncodedKey()))); + } + + return GoogleCredential.getApplicationDefault(); + } + private ClientAuthentication kubernetesAuthentication(VaultProperties vaultProperties) { VaultProperties.KubernetesProperties kubernetes = vaultProperties.getKubernetes(); diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java index eb79728c..8c67dd15 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java @@ -49,6 +49,7 @@ import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.support.ClientOptions; import org.springframework.vault.support.SslConfiguration; import org.springframework.web.client.RestOperations; +import org.springframework.web.client.RestTemplate; /** * {@link EnableAutoConfiguration Auto-configuration} for Spring Vault support. @@ -68,8 +69,16 @@ public class VaultBootstrapConfiguration implements InitializingBean { private final VaultEndpointProvider endpointProvider; + /** + * Used for Vault communication. + */ private RestOperations restOperations; + /** + * Used for external (AWS, GCP) communication. + */ + private RestOperations externalRestOperations; + public VaultBootstrapConfiguration(ConfigurableApplicationContext applicationContext, VaultProperties vaultProperties, ObjectProvider endpointProvider) { @@ -96,6 +105,8 @@ public class VaultBootstrapConfiguration implements InitializingBean { this.restOperations = VaultClients.createRestTemplate(endpointProvider, clientHttpRequestFactory); + + this.externalRestOperations = new RestTemplate(clientHttpRequestFactory); } /** @@ -190,7 +201,7 @@ public class VaultBootstrapConfiguration implements InitializingBean { public ClientAuthentication clientAuthentication() { ClientAuthenticationFactory factory = new ClientAuthenticationFactory( - vaultProperties, restOperations); + vaultProperties, restOperations, externalRestOperations); return factory.createClientAuthentication(); } 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 57406333..5fc55482 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 @@ -15,6 +15,8 @@ */ package org.springframework.cloud.vault.config; +import java.time.Duration; + import javax.validation.constraints.NotEmpty; import lombok.Data; @@ -100,6 +102,10 @@ public class VaultProperties implements EnvironmentAware { private AzureMsiProperties azureMsi = new AzureMsiProperties(); + private GcpGceProperties gcpGce = new GcpGceProperties(); + + private GcpIamProperties gcpIam = new GcpIamProperties(); + private KubernetesProperties kubernetes = new KubernetesProperties(); private Ssl ssl = new Ssl(); @@ -265,6 +271,79 @@ public class VaultProperties implements EnvironmentAware { private String role = ""; } + @Data + public static class GcpGceProperties { + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String gcpPath = "gcp"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Optional service account id. Using the default id if left unconfigured. + */ + private String serviceAccount = ""; + } + + @Data + public static class GcpIamProperties { + + /** + * Mount path of the Kubernetes authentication backend. + */ + @NotEmpty + private String gcpPath = "gcp"; + + /** + * Name of the role against which the login is being attempted. + */ + private String role = ""; + + /** + * Overrides the GCP project Id. + */ + private String projectId = ""; + + /** + * Overrides the GCP service account Id. + */ + private String serviceAccountId = ""; + + /** + * Validity of the JWT token. + */ + private Duration jwtValidity = Duration.ofMinutes(15); + + /** + * Credentials configuration. + */ + private final GcpCredentials credentials = new GcpCredentials(); + } + + @Data + public static class GcpCredentials { + + /** + * Location of the OAuth2 credentials private key. + * + *

+ * Since this is a Resource, the private key can be in a multitude of locations, + * such as a local file system, classpath, URL, etc. + */ + private Resource location; + + /** + * The base64 encoded contents of an OAuth2 account private key in JSON format. + */ + private String encodedKey; + } + @Data public static class KubernetesProperties { @@ -345,6 +424,6 @@ public class VaultProperties implements EnvironmentAware { } public enum AuthenticationMethod { - TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, KUBERNETES + TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, AZURE_MSI, CERT, CUBBYHOLE, GCP_GCE, GCP_IAM, KUBERNETES } }