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.
This commit is contained in:
Mark Paluch
2018-11-09 16:59:27 +01:00
parent 17e1f30cd0
commit d57b877bae
6 changed files with 328 additions and 4 deletions

View File

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

18
pom.xml
View File

@@ -46,6 +46,9 @@
<httpcore.version>4.4.9</httpcore.version>
<netty.version>4.1.30.Final</netty.version>
<okhttp3.version>3.10.0</okhttp3.version>
<google-api-services-iam.version>v1-rev259-1.25.0</google-api-services-iam.version>
<google-auth-library-oauth2-http.version>0.10.0</google-auth-library-oauth2-http.version>
</properties>
<dependencyManagement>
@@ -105,6 +108,21 @@
<artifactId>netty-all</artifactId>
<version>${netty.version}</version>
</dependency>
<!-- GCP SDK -->
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-iam</artifactId>
<version>${google-api-services-iam.version}</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<version>${google-auth-library-oauth2-http.version}</version>
<optional>true</optional>
</dependency>
</dependencies>
</dependencyManagement>

View File

@@ -53,6 +53,18 @@
</exclusions>
</dependency>
<dependency>
<groupId>com.google.apis</groupId>
<artifactId>google-api-services-iam</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.google.auth</groupId>
<artifactId>google-auth-library-oauth2-http</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>

View File

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

View File

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

View File

@@ -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.
*
* <p>
* 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
}
}