Add support for Azure authentication.
We now support Azure as authentication backend allowing applications to authenticate that run on VM instances with a bound managed service identity. Closes gh-284.
This commit is contained in:
@@ -0,0 +1,232 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.vault.VaultException;
|
||||
import org.springframework.vault.authentication.AuthenticationSteps.HttpRequestBuilder;
|
||||
import org.springframework.vault.authentication.AuthenticationSteps.Node;
|
||||
import org.springframework.vault.support.VaultResponse;
|
||||
import org.springframework.vault.support.VaultToken;
|
||||
import org.springframework.web.client.RestClientException;
|
||||
import org.springframework.web.client.RestOperations;
|
||||
|
||||
/**
|
||||
* Azure MSI (Managed Service Identity) authentication using Azure as trusted third party.
|
||||
* <p>
|
||||
* Azure MSI authentication uses {@link AzureVmEnvironment} and the MSI OAuth2 token
|
||||
* (referenced as JWT token in Vault docs) to log into Vault. VM environment and OAuth2
|
||||
* token are fetched from the Azure Instance Metadata service. Instances of this class are
|
||||
* immutable once constructed.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
* @see AzureMsiAuthenticationOptions
|
||||
* @see RestOperations
|
||||
* @see <a href="https://www.vaultproject.io/docs/auth/azure.html">Auth Backend: azure</a>
|
||||
* @link <a href=
|
||||
* "https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service"
|
||||
* >Azure Instance Metadata service</a>
|
||||
*/
|
||||
public class AzureMsiAuthentication implements ClientAuthentication {
|
||||
|
||||
private static final Log logger = LogFactory.getLog(AzureMsiAuthentication.class);
|
||||
|
||||
private static final HttpEntity<Void> METADATA_HEADERS;
|
||||
|
||||
static {
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.add("Metadata", "true");
|
||||
METADATA_HEADERS = new HttpEntity<>(headers);
|
||||
}
|
||||
|
||||
private final AzureMsiAuthenticationOptions options;
|
||||
|
||||
private final RestOperations vaultRestOperations;
|
||||
|
||||
private final RestOperations azureMetadataRestOperations;
|
||||
|
||||
/**
|
||||
* Create a new {@link AzureMsiAuthentication}.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @param restOperations must not be {@literal null}.
|
||||
*/
|
||||
public AzureMsiAuthentication(AzureMsiAuthenticationOptions options,
|
||||
RestOperations restOperations) {
|
||||
this(options, restOperations, restOperations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@link AzureMsiAuthentication} specifying
|
||||
* {@link AzureMsiAuthenticationOptions}, a Vault and an Azure-Metadata-specific
|
||||
* {@link RestOperations}.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @param vaultRestOperations must not be {@literal null}.
|
||||
* @param azureMetadataRestOperations must not be {@literal null}.
|
||||
*/
|
||||
public AzureMsiAuthentication(AzureMsiAuthenticationOptions options,
|
||||
RestOperations vaultRestOperations, RestOperations azureMetadataRestOperations) {
|
||||
|
||||
Assert.notNull(options, "AzureAuthenticationOptions must not be null");
|
||||
Assert.notNull(vaultRestOperations, "Vault RestOperations must not be null");
|
||||
Assert.notNull(azureMetadataRestOperations,
|
||||
"Azure Instance Metadata RestOperations must not be null");
|
||||
|
||||
this.options = options;
|
||||
this.vaultRestOperations = vaultRestOperations;
|
||||
this.azureMetadataRestOperations = azureMetadataRestOperations;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link AuthenticationSteps} for Azure authentication given
|
||||
* {@link AzureMsiAuthenticationOptions}.
|
||||
*
|
||||
* @param options must not be {@literal null}.
|
||||
* @return {@link AuthenticationSteps} for Azure authentication.
|
||||
*/
|
||||
public static AuthenticationSteps createAuthenticationSteps(
|
||||
AzureMsiAuthenticationOptions options) {
|
||||
|
||||
Assert.notNull(options, "AzureMsiAuthenticationOptions must not be null");
|
||||
|
||||
return createAuthenticationSteps(options, options.getVmEnvironment());
|
||||
}
|
||||
|
||||
protected static AuthenticationSteps createAuthenticationSteps(
|
||||
AzureMsiAuthenticationOptions options,
|
||||
@Nullable AzureVmEnvironment environment) {
|
||||
|
||||
Node<String> msiToken = AuthenticationSteps.fromHttpRequest(
|
||||
HttpRequestBuilder.get(options.getIdentityTokenServiceUri())
|
||||
.with(METADATA_HEADERS).as(Map.class)) //
|
||||
.map(token -> (String) token.get("access_token"));
|
||||
|
||||
Node<AzureVmEnvironment> environmentSteps;
|
||||
|
||||
if (environment == null) {
|
||||
|
||||
environmentSteps = AuthenticationSteps.fromHttpRequest(
|
||||
HttpRequestBuilder.get(options.getInstanceMetadataServiceUri())
|
||||
.with(METADATA_HEADERS).as(Map.class)) //
|
||||
.map(AzureMsiAuthentication::toAzureVmEnvironment);
|
||||
}
|
||||
else {
|
||||
environmentSteps = AuthenticationSteps.fromSupplier(() -> environment);
|
||||
}
|
||||
|
||||
return environmentSteps
|
||||
.zipWith(msiToken)
|
||||
.map(tuple -> getAzureLogin(options.getRole(), tuple.getLeft(),
|
||||
tuple.getRight())) //
|
||||
.login("auth/{mount}/login", options.getPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public VaultToken login() throws VaultException {
|
||||
return createTokenUsingAzureMsiCompute();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private VaultToken createTokenUsingAzureMsiCompute() {
|
||||
|
||||
Map<String, String> login = getAzureLogin(options.getRole(), getVmEnvironment(),
|
||||
getAccessToken());
|
||||
|
||||
try {
|
||||
|
||||
VaultResponse response = this.vaultRestOperations.postForObject(
|
||||
"auth/{mount}/login", login, VaultResponse.class, options.getPath());
|
||||
|
||||
Assert.state(response != null && response.getAuth() != null,
|
||||
"Auth field must not be null");
|
||||
|
||||
if (logger.isDebugEnabled()) {
|
||||
logger.debug("Login successful using Azure authentication");
|
||||
}
|
||||
|
||||
return LoginTokenUtil.from(response.getAuth());
|
||||
}
|
||||
catch (RestClientException e) {
|
||||
throw VaultLoginException.create("Azure", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, String> getAzureLogin(String role,
|
||||
AzureVmEnvironment vmEnvironment, String jwt) {
|
||||
|
||||
Map<String, String> loginBody = new LinkedHashMap<>();
|
||||
loginBody.put("resource_group_name", vmEnvironment.getResourceGroupName());
|
||||
loginBody.put("vm_name", vmEnvironment.getVmName());
|
||||
loginBody.put("subscription_id", vmEnvironment.getSubscriptionId());
|
||||
loginBody.put("jwt", jwt);
|
||||
loginBody.put("role", role);
|
||||
|
||||
return loginBody;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private String getAccessToken() {
|
||||
|
||||
ResponseEntity<Map> response = this.azureMetadataRestOperations.exchange(
|
||||
options.getIdentityTokenServiceUri(), HttpMethod.GET, METADATA_HEADERS,
|
||||
Map.class);
|
||||
|
||||
return (String) response.getBody().get("access_token");
|
||||
}
|
||||
|
||||
private AzureVmEnvironment getVmEnvironment() {
|
||||
|
||||
AzureVmEnvironment vmEnvironment = options.getVmEnvironment();
|
||||
|
||||
return vmEnvironment != null ? vmEnvironment : fetchAzureVmEnvironment();
|
||||
}
|
||||
|
||||
private AzureVmEnvironment fetchAzureVmEnvironment() {
|
||||
|
||||
ResponseEntity<Map> response = this.azureMetadataRestOperations.exchange(
|
||||
options.getInstanceMetadataServiceUri(), HttpMethod.GET,
|
||||
METADATA_HEADERS, Map.class);
|
||||
|
||||
return toAzureVmEnvironment(response.getBody());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static AzureVmEnvironment toAzureVmEnvironment(
|
||||
Map<String, Object> instanceMetadata) {
|
||||
|
||||
Map<String, String> compute = (Map) instanceMetadata.get("compute");
|
||||
|
||||
String subscriptionId = compute.get("subscriptionId");
|
||||
String vmName = compute.get("name");
|
||||
String resourceGroupName = compute.get("resourceGroupName");
|
||||
|
||||
return new AzureVmEnvironment(subscriptionId, resourceGroupName, vmName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.net.URI;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Authentication options for {@link AzureMsiAuthentication}.
|
||||
* <p>
|
||||
* Authentication options provide the path, role, an optional {@link AzureVmEnvironment},
|
||||
* and instance metadata/OAuth2 token URIs. {@link AzureMsiAuthenticationOptions} can be
|
||||
* constructed using {@link #builder()}. Instances of this class are immutable once
|
||||
* constructed.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
* @see AzureMsiAuthenticationOptions
|
||||
* @see #builder()
|
||||
*/
|
||||
public class AzureMsiAuthenticationOptions {
|
||||
|
||||
public static final String DEFAULT_AZURE_AUTHENTICATION_PATH = "azure";
|
||||
|
||||
public static final URI DEFAULT_INSTANCE_METADATA_SERVICE_URI = URI
|
||||
.create("http://169.254.169.254/metadata/instance?api-version=2017-08-01");
|
||||
|
||||
public static final URI DEFAULT_IDENTITY_TOKEN_SERVICE_URI = URI
|
||||
.create("http://169.254.169.254/metadata/identity/oauth2/token?resource=https://vault.hashicorp.com&api-version=2018-02-01");
|
||||
|
||||
/**
|
||||
* Path of the azure authentication backend mount.
|
||||
*/
|
||||
private final String path;
|
||||
|
||||
/**
|
||||
* Name of the role against which the login is being attempted.
|
||||
*/
|
||||
private final String role;
|
||||
|
||||
/*
|
||||
* {@link URI} to the instance metadata endpoint.
|
||||
*/
|
||||
private final URI instanceMetadataServiceUri;
|
||||
|
||||
/*
|
||||
* {@link URI} to the token service for the managed identity.
|
||||
*/
|
||||
private final URI identityTokenServiceUri;
|
||||
|
||||
/**
|
||||
* Optional {@link AzureVmEnvironment}.
|
||||
*/
|
||||
@Nullable
|
||||
private final AzureVmEnvironment vmEnvironment;
|
||||
|
||||
private AzureMsiAuthenticationOptions(String path, String role,
|
||||
URI instanceMetadataServiceUri, URI identityTokenServiceUri,
|
||||
@Nullable AzureVmEnvironment vmEnvironment) {
|
||||
|
||||
this.path = path;
|
||||
this.role = role;
|
||||
this.instanceMetadataServiceUri = instanceMetadataServiceUri;
|
||||
this.identityTokenServiceUri = identityTokenServiceUri;
|
||||
this.vmEnvironment = vmEnvironment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return a new {@link AzureMsiAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public static AzureMsiAuthenticationOptionsBuilder builder() {
|
||||
return new AzureMsiAuthenticationOptionsBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the path of the azure authentication backend mount.
|
||||
*/
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the role against which the login is being attempted.
|
||||
*/
|
||||
public String getRole() {
|
||||
return role;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the {@link AzureVmEnvironment}. If {@literal null}, the environment is
|
||||
* retrieved from the {@link #getInstanceMetadataServiceUri() VM instance metadata
|
||||
* service}.
|
||||
*/
|
||||
@Nullable
|
||||
public AzureVmEnvironment getVmEnvironment() {
|
||||
return vmEnvironment;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link URI} to the instance metadata endpoint.
|
||||
*/
|
||||
public URI getInstanceMetadataServiceUri() {
|
||||
return instanceMetadataServiceUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@link URI} to the token service for the managed identity.
|
||||
*/
|
||||
public URI getIdentityTokenServiceUri() {
|
||||
return identityTokenServiceUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for {@link AzureMsiAuthenticationOptions}.
|
||||
*/
|
||||
public static class AzureMsiAuthenticationOptionsBuilder {
|
||||
|
||||
private String path = DEFAULT_AZURE_AUTHENTICATION_PATH;
|
||||
|
||||
@Nullable
|
||||
private String role;
|
||||
|
||||
@Nullable
|
||||
private AzureVmEnvironment vmEnvironment;
|
||||
|
||||
private URI instanceMetadataServiceUri = DEFAULT_INSTANCE_METADATA_SERVICE_URI;
|
||||
|
||||
private URI identityTokenServiceUri = DEFAULT_IDENTITY_TOKEN_SERVICE_URI;
|
||||
|
||||
AzureMsiAuthenticationOptionsBuilder() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the mount path, defaults to {@literal azure}.
|
||||
*
|
||||
* @param path must not be empty or {@literal null}.
|
||||
* @return {@code this} {@link AzureMsiAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public AzureMsiAuthenticationOptionsBuilder path(String path) {
|
||||
|
||||
Assert.hasText(path, "Path must not be empty");
|
||||
|
||||
this.path = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the name of the role against which the login is being attempted.
|
||||
*
|
||||
* @param role must not be empty or {@literal null}.
|
||||
* @return {@code this} {@link AzureMsiAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public AzureMsiAuthenticationOptionsBuilder role(String role) {
|
||||
|
||||
Assert.hasText(role, "Role must not be null or empty");
|
||||
|
||||
this.role = role;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure a VM environment (subscriptionId, resource group name, VM name).
|
||||
* Environment details are passed to Vault as login body. If left unconfigured,
|
||||
* {@link AzureMsiAuthentication} looks up the details from the instance metadata
|
||||
* service.
|
||||
*
|
||||
* @param vmEnvironment must not be {@literal null}.
|
||||
* @return {@code this} {@link AzureMsiAuthenticationOptionsBuilder}.
|
||||
*/
|
||||
public AzureMsiAuthenticationOptionsBuilder vmEnvironment(
|
||||
AzureVmEnvironment vmEnvironment) {
|
||||
|
||||
Assert.notNull(vmEnvironment, "AzureVmEnvironment must not be null");
|
||||
|
||||
this.vmEnvironment = vmEnvironment;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the instance metadata {@link URI}.
|
||||
*
|
||||
* @param instanceMetadataServiceUri must not be {@literal null}.
|
||||
* @return {@code this} {@link AzureMsiAuthenticationOptionsBuilder}.
|
||||
* @see #DEFAULT_IDENTITY_TOKEN_SERVICE_URI
|
||||
*/
|
||||
public AzureMsiAuthenticationOptionsBuilder instanceMetadataUri(
|
||||
URI instanceMetadataServiceUri) {
|
||||
|
||||
Assert.notNull(identityTokenServiceUri,
|
||||
"Instance metadata service URI must not be null");
|
||||
|
||||
this.instanceMetadataServiceUri = instanceMetadataServiceUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure the managed identity service token {@link URI}.
|
||||
*
|
||||
* @param identityTokenServiceUri must not be {@literal null}.
|
||||
* @return {@code this} {@link AzureMsiAuthenticationOptionsBuilder}.
|
||||
* @see #DEFAULT_IDENTITY_TOKEN_SERVICE_URI
|
||||
*/
|
||||
public AzureMsiAuthenticationOptionsBuilder identityTokenServiceUri(
|
||||
URI identityTokenServiceUri) {
|
||||
|
||||
Assert.notNull(identityTokenServiceUri,
|
||||
"Identity token service URI must not be null");
|
||||
|
||||
this.identityTokenServiceUri = identityTokenServiceUri;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a new {@link AzureMsiAuthenticationOptions} instance.
|
||||
*
|
||||
* @return a new {@link AzureMsiAuthenticationOptions}.
|
||||
*/
|
||||
public AzureMsiAuthenticationOptions build() {
|
||||
|
||||
Assert.hasText(role, "Role must not be null or empty");
|
||||
|
||||
return new AzureMsiAuthenticationOptions(path, role,
|
||||
instanceMetadataServiceUri, identityTokenServiceUri, vmEnvironment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* Value object representing a VM environment consisting of the subscription Id, the
|
||||
* resource group name and the VM name.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
* @since 2.1
|
||||
* @see AzureMsiAuthentication
|
||||
* @see AzureMsiAuthenticationOptions
|
||||
* @link <a href=
|
||||
* "https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service"
|
||||
* >Azure Instance Metadata service</a>
|
||||
*/
|
||||
public class AzureVmEnvironment {
|
||||
|
||||
private final String subscriptionId;
|
||||
|
||||
private final String resourceGroupName;
|
||||
|
||||
private final String vmName;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AzureVmEnvironment}.
|
||||
*
|
||||
* @param subscriptionId must not be {@literal null}.
|
||||
* @param resourceGroupName must not be {@literal null}.
|
||||
* @param vmName must not be {@literal null}.
|
||||
*/
|
||||
public AzureVmEnvironment(String subscriptionId, String resourceGroupName,
|
||||
String vmName) {
|
||||
|
||||
Assert.notNull(subscriptionId, "SubscriptionId must not be null");
|
||||
Assert.notNull(resourceGroupName, "Resource group name must not be null");
|
||||
Assert.notNull(vmName, "VM name must not be null");
|
||||
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.resourceGroupName = resourceGroupName;
|
||||
this.vmName = vmName;
|
||||
}
|
||||
|
||||
public String getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public String getResourceGroupName() {
|
||||
return resourceGroupName;
|
||||
}
|
||||
|
||||
public String getVmName() {
|
||||
return vmName;
|
||||
}
|
||||
}
|
||||
@@ -32,6 +32,8 @@ import org.springframework.vault.authentication.AppRoleAuthentication;
|
||||
import org.springframework.vault.authentication.AppRoleAuthenticationOptions;
|
||||
import org.springframework.vault.authentication.AwsEc2Authentication;
|
||||
import org.springframework.vault.authentication.AwsEc2AuthenticationOptions;
|
||||
import org.springframework.vault.authentication.AzureMsiAuthentication;
|
||||
import org.springframework.vault.authentication.AzureMsiAuthenticationOptions;
|
||||
import org.springframework.vault.authentication.ClientAuthentication;
|
||||
import org.springframework.vault.authentication.ClientCertificateAuthentication;
|
||||
import org.springframework.vault.authentication.CubbyholeAuthentication;
|
||||
@@ -96,7 +98,7 @@ import org.springframework.web.client.RestOperations;
|
||||
* </li>
|
||||
* <li>Authentication method: {@code vault.authentication} (defaults to {@literal TOKEN},
|
||||
* supported authentication methods are:
|
||||
* {@literal TOKEN, APPID, APPROLE, AWS_EC2, CERT, CUBBYHOLE})</li>
|
||||
* {@literal TOKEN, APPID, APPROLE, AZURE, AWS_EC2, CERT, CUBBYHOLE})</li>
|
||||
* <li>Token authentication
|
||||
* <ul>
|
||||
* <li>Vault Token: {@code vault.token}</li>
|
||||
@@ -118,6 +120,10 @@ import org.springframework.web.client.RestOperations;
|
||||
* <li>RoleId: {@code vault.aws-ec2.role-id}</li>
|
||||
* <li>Identity Document URL: {@code vault.aws-ec2.identity-document} (optional)</li>
|
||||
* </ul>
|
||||
* <li>Azure MSI authentication
|
||||
* <ul>
|
||||
* <li>Role: {@code vault.azure-msi.role}</li>
|
||||
* </ul>
|
||||
* <li>Client Certificate authentication
|
||||
* <ul>
|
||||
* <li>(no configuration options)</li>
|
||||
@@ -226,6 +232,8 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration im
|
||||
return appRoleAuthentication();
|
||||
case AWS_EC2:
|
||||
return awsEc2Authentication();
|
||||
case AZURE:
|
||||
return azureMsiAuthentication();
|
||||
case CERT:
|
||||
return new ClientCertificateAuthentication(restOperations());
|
||||
case CUBBYHOLE:
|
||||
@@ -318,6 +326,18 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration im
|
||||
restOperations());
|
||||
}
|
||||
|
||||
protected ClientAuthentication azureMsiAuthentication() {
|
||||
|
||||
String roleId = getProperty("vault.azure-msi.role");
|
||||
Assert.hasText(roleId,
|
||||
"Vault Azure MSI authentication: Role (vault.azure-msi.role) must not be empty");
|
||||
|
||||
AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder()
|
||||
.role(roleId).build();
|
||||
|
||||
return new AzureMsiAuthentication(options, restOperations());
|
||||
}
|
||||
|
||||
protected ClientAuthentication cubbyholeAuthentication() {
|
||||
|
||||
String token = getEnvironment().getProperty("vault.token");
|
||||
@@ -370,6 +390,6 @@ public class EnvironmentVaultConfiguration extends AbstractVaultConfiguration im
|
||||
}
|
||||
|
||||
enum AuthenticationMethod {
|
||||
TOKEN, APPID, APPROLE, AWS_EC2, CERT, CUBBYHOLE, KUBERNETES;
|
||||
TOKEN, APPID, APPROLE, AZURE, AWS_EC2, CERT, CUBBYHOLE, KUBERNETES;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
/*
|
||||
* Copyright 2018 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
|
||||
*
|
||||
* http://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.Before;
|
||||
import org.junit.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.header;
|
||||
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 AzureMsiAuthentication}.
|
||||
*
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
public class AzureMsiAuthenticationUnitTests {
|
||||
|
||||
private RestTemplate restTemplate;
|
||||
private MockRestServiceServer mockRest;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
|
||||
RestTemplate restTemplate = VaultClients.createRestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler());
|
||||
|
||||
this.mockRest = MockRestServiceServer.createServer(restTemplate);
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginShouldObtainTokenAndFetchMetadata() {
|
||||
|
||||
AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder()
|
||||
.role("dev-role") //
|
||||
.build();
|
||||
|
||||
expectMetadataRequest();
|
||||
expectIdentityTokenRequest();
|
||||
expectLoginRequest();
|
||||
|
||||
AzureMsiAuthentication authentication = new AzureMsiAuthentication(options,
|
||||
restTemplate);
|
||||
|
||||
VaultToken login = authentication.login();
|
||||
assertThat(login).isInstanceOf(LoginToken.class);
|
||||
assertThat(login.getToken()).isEqualTo("my-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginShouldObtainToken() {
|
||||
|
||||
AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions
|
||||
.builder()
|
||||
.role("dev-role")
|
||||
.vmEnvironment(
|
||||
new AzureVmEnvironment("foobar-subscription", "vault",
|
||||
"vault-client")).build();
|
||||
|
||||
expectIdentityTokenRequest();
|
||||
expectLoginRequest();
|
||||
|
||||
AzureMsiAuthentication authentication = new AzureMsiAuthentication(options,
|
||||
restTemplate);
|
||||
|
||||
VaultToken login = authentication.login();
|
||||
assertThat(login).isInstanceOf(LoginToken.class);
|
||||
assertThat(login.getToken()).isEqualTo("my-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWithStepsShouldObtainTokenAndFetchMetadata() {
|
||||
|
||||
AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder()
|
||||
.role("dev-role") //
|
||||
.build();
|
||||
|
||||
expectMetadataRequest();
|
||||
expectIdentityTokenRequest();
|
||||
expectLoginRequest();
|
||||
|
||||
AuthenticationStepsExecutor authentication = new AuthenticationStepsExecutor(
|
||||
AzureMsiAuthentication.createAuthenticationSteps(options), restTemplate);
|
||||
|
||||
VaultToken login = authentication.login();
|
||||
assertThat(login).isInstanceOf(LoginToken.class);
|
||||
assertThat(login.getToken()).isEqualTo("my-token");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void loginWithStepsShouldObtainToken() {
|
||||
|
||||
AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions
|
||||
.builder()
|
||||
.role("dev-role")
|
||||
.vmEnvironment(
|
||||
new AzureVmEnvironment("foobar-subscription", "vault",
|
||||
"vault-client")).build();
|
||||
|
||||
expectIdentityTokenRequest();
|
||||
expectLoginRequest();
|
||||
|
||||
AuthenticationStepsExecutor authentication = new AuthenticationStepsExecutor(
|
||||
AzureMsiAuthentication.createAuthenticationSteps(options), restTemplate);
|
||||
|
||||
VaultToken login = authentication.login();
|
||||
assertThat(login).isInstanceOf(LoginToken.class);
|
||||
assertThat(login.getToken()).isEqualTo("my-token");
|
||||
}
|
||||
|
||||
private void expectMetadataRequest() {
|
||||
|
||||
mockRest.expect(
|
||||
requestTo(AzureMsiAuthenticationOptions.DEFAULT_INSTANCE_METADATA_SERVICE_URI))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andExpect(header("Metadata", "true"))
|
||||
.andRespond(
|
||||
withSuccess()
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.body("{\n"
|
||||
+ " \"compute\": {\n"
|
||||
+ " \"name\": \"vault-client\",\n"
|
||||
+ " \"resourceGroupName\": \"vault\",\n"
|
||||
+ " \"subscriptionId\": \"foobar-subscription\"\n"
|
||||
+ " }\n" + "}"));
|
||||
}
|
||||
|
||||
private void expectIdentityTokenRequest() {
|
||||
|
||||
mockRest.expect(
|
||||
requestTo(AzureMsiAuthenticationOptions.DEFAULT_IDENTITY_TOKEN_SERVICE_URI))
|
||||
.andExpect(method(HttpMethod.GET))
|
||||
.andExpect(header("Metadata", "true"))
|
||||
.andRespond(
|
||||
withSuccess().contentType(MediaType.APPLICATION_JSON).body(
|
||||
"{\"access_token\": \"my-token\" }"));
|
||||
|
||||
}
|
||||
|
||||
private void expectLoginRequest() {
|
||||
|
||||
mockRest.expect(requestTo("/auth/azure/login"))
|
||||
.andExpect(method(HttpMethod.POST))
|
||||
.andExpect(jsonPath("$.role").value("dev-role"))
|
||||
.andExpect(jsonPath("$.jwt").value("my-token"))
|
||||
.andExpect(jsonPath("$.subscription_id").value("foobar-subscription"))
|
||||
.andExpect(jsonPath("$.resource_group_name").value("vault"))
|
||||
.andExpect(jsonPath("$.vm_name").value("vault-client"))
|
||||
.andRespond(
|
||||
withSuccess().contentType(MediaType.APPLICATION_JSON).body(
|
||||
"{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
[[new-features.2-1-0]]
|
||||
=== What's new in Spring Vault 2.1
|
||||
|
||||
* <<vault.authentication.gcpgce,GCP Compute>> and <<vault.authentication.gcpiam,GCP IAM>> authentication.
|
||||
* <<vault.authentication.gcpgce,GCP Compute>>, <<vault.authentication.gcpiam,GCP IAM>>, and <<vault.authentication.azuremsi, Azure>> authentication.
|
||||
* Template API support for versioned and unversioned Key/Value backends and for Vault wrapping operations.
|
||||
* Support full pull mode in reactive AppRole authentication.
|
||||
* Improved Exception hierarchy for Vault login failures.
|
||||
|
||||
@@ -375,6 +375,52 @@ See also:
|
||||
* https://www.vaultproject.io/docs/auth/aws.html[Vault Documentation: Using the AWS auth backend]
|
||||
* http://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html[AWS Documentation: STS GetCallerIdentity]
|
||||
|
||||
[[vault.authentication.azuremsi]]
|
||||
== Azure (MSI) authentication
|
||||
|
||||
The https://www.vaultproject.io/docs/auth/azure.html[azure]
|
||||
auth backend provides a secure introduction mechanism
|
||||
for Azure VM 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.).
|
||||
Instead, it treats Azure as a Trusted Third Party and uses the
|
||||
managed service identity and instance metadata information that can be
|
||||
bound to a VM instance
|
||||
|
||||
====
|
||||
[source,java]
|
||||
----
|
||||
@Configuration
|
||||
class AppConfig extends AbstractVaultConfiguration {
|
||||
|
||||
// …
|
||||
|
||||
@Override
|
||||
public ClientAuthentication clientAuthentication() {
|
||||
|
||||
AzureMsiAuthenticationOptions options = AzureMsiAuthenticationOptions.builder()
|
||||
.role(…).build();
|
||||
|
||||
return new AzureMsiAuthentication(options, restOperations());
|
||||
}
|
||||
|
||||
// …
|
||||
}
|
||||
----
|
||||
====
|
||||
|
||||
Azure authentication requires details about the VM environment (subscription Id,
|
||||
resource group name, VM name). These details can be either configured through
|
||||
`AzureMsiAuthenticationOptionsBuilder`.
|
||||
If left unconfigured, `AzureMsiAuthentication` queries Azure's instance metadata service to
|
||||
obtain these details.
|
||||
|
||||
See also:
|
||||
|
||||
* https://www.vaultproject.io/docs/auth/azure.html[Vault Documentation: Using the Azure auth backend]
|
||||
* https://docs.microsoft.com/en-us/azure/active-directory/managed-service-identity/overview[Azure Documentation: Managed Service Identity]
|
||||
|
||||
[[vault.authentication.gcpgce]]
|
||||
== GCP-GCE authentication
|
||||
|
||||
|
||||
Reference in New Issue
Block a user