In my last post “Spring Boot Configuration and Secret Management Patterns on Kubernetes” I touched on some integration patterns for secret management with Spring Cloud Vault. Along with that I also highlighted that one of the issues I was working on was about enabling AWS STS for S_pring Cloud Vault_. This is now available with spring cloud 2020.0.2!!!
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
Notice the new Release Train versioning naming convention!
AWS Security Token Service (AWS STS)
AWS Security Token Service (AWS STS) is a web service that enables you to request temporary, limited-privilege credentials for AWS Identity and Access Management (IAM) users or for users that you authenticate (federated users). The key purpose of AWS STS is to allow a user or an application to assume a role and obtain access to AWS services or resources. For more information, see Temporary Security Credentials in the IAM User Guide.
For applications it's no different, and we could have the application assume the role and request temporary credentials to AWS resources such as EC2, S3, etc. This is where Spring Cloud Vault combined with AWS Secrets backend on Vault provides the capability for a Spring Boot application to use dynamic credentials.
Vault AWS Secret Backend
The AWS secrets engine generates AWS access credentials dynamically based on IAM policies. The AWS IAM credentials are time-based and are automatically revoked when the Vault lease expires.
Vault supports three different types of credentials to retrieve from AWS:
- iam_user: Vault will create an IAM user for each lease, attach the managed and inline IAM policies as specified in the role to the user, and if a permissions boundary is specified on the role, the permissions boundary will also be attached. Vault will then generate an access key and secret key for the IAM user and return them to the caller. IAM users have no session tokens and so no session token will be returned. Vault will delete the IAM user upon reaching the TTL expiration.
- assumed_role: Vault will call sts:AssumeRole and return the access key, secret key, and session token to the caller.
- federation_token: Vault will call sts:GetFederationToken passing in the supplied AWS policy document and return the access key, secret key, and session token to the caller.
More details on the setup can be found under AWS Secrets Engine.
Spring Cloud Vault
The AWS secret engine can be enabled by adding the spring-cloud-vault-config-aws
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-vault-config-aws</artifactId>
<version>3.0.2</version>
</dependency>
</dependencies>
The AWS secret integration now supports the notion of a credential-type which defaults to iam_user for backward compatibility.
Sample iam_user configuration:
spring.cloud.vault:
aws:
enabled: true
role: readonly
backend: aws
access-key-property: cloud.aws.credentials.accessKey
secret-key-property: cloud.aws.credentials.secretKey
- enabled setting this value to true enables the AWS backend config usage
- role sets the role name of the AWS role definition
- backend sets the path of the AWS mount to use
- access-key-property sets the property name in which the AWS access key is stored
- secret-key-property sets the property name in which the AWS secret key is stored
For AWS STS supported values forcredential-type are assumed_role or federation_token.
Sample assume_role configuration:
spring.cloud.vault:
aws:
enabled: true
role: sts-vault-role
backend: aws
credential-type: assumed_role
access-key-property: cloud.aws.credentials.accessKey
secret-key-property: cloud.aws.credentials.secretKey
session-token-key-property: cloud.aws.credentials.sessionToken
ttl: 3600s
role-arn: arn:aws:iam::${AWS_ACCOUNT}:role/sts-app-role
New additions STS configuration:
- session-token-key-property sets the property name in which the AWS STS security token is stored
- credential-type sets the AWS credential type to use for this backend. Defaults to iam_user
- ttl sets the TTL for the STS token when using assumed_role or federation_token. Defaults to the TTL specified by the vault role. Min/Max values are also limited to what AWS would support for STS.
- role-arn sets the IAM role to assume if more than one is configured for the vault role when using assumed_role
Please read Spring Cloud Vault AWS Backend for more details on this integration.
Lease Rotation and Property Sources
STS credentials default to a TTL of 60 mins. You can adjust the TTL based on your requirement. It's important to note that min/max TTL values allowed are as per what AWS STS would allow for configuration.
For assumed_role we could set that between a minimum of 900 seconds (15 minutes) up to the maximum session duration setting for the role which could be anywhere between 3,600s (1 hour) and 43,200s (12 hours). The default expiration period for federation_token is substantially longer (12 hours instead of one hour compared to assumed_role) and we could specify the duration between 900 seconds (15 minutes) to 129,600 seconds (36 hours).
Spring Cloud Vault managed leases can either be RENEWED (if they are renewable) or ROTATED based on the vault lifecycle configuration.
Sample Vault lifecycle:
vault:
enabled: true
host: 127.0.0.1
port: 8200
scheme: http
uri: [http://127.0.0.1:8200/](http://127.0.0.1:8200/)
config:
lifecycle:
min-renewal: 1m
expiry-threshold: 5m
min-renewal makes sure that the leases are not renewed/rotated too frequently and at least stick around for the configured duration. expiry-threshold is the configured duration before the lease expiry that vault will renew/rotate a lease.
Spring Cloud Vault and the LeaseContainer will make sure the property sources are updated with the new set of credentials upon a Lease Expiry. However, it is the responsibility of the application to make sure any properties updated under the property sources and environment are propagated through any spring beans initialized with credentials.
Let us assume a spring boot application that managed AWS creds through a ConfigurationProperties class such as below:
package example.springboot.config; | |
import static example.springboot.config.AwsConfigurationProperties.PREFIX; | |
import org.springframework.boot.context.properties.ConfigurationProperties; | |
import org.springframework.stereotype.Component; | |
/** | |
* Sample ConfigurationProperties for AWS STS | |
* | |
* @author iyerk | |
* | |
*/ | |
@ConfigurationProperties(PREFIX) | |
@Component | |
public class AwsConfigurationProperties { | |
/** | |
* Prefix for configuration properties. | |
*/ | |
public static final String PREFIX = "cloud.aws.credentials"; | |
private String accessKey; | |
private String secretKey; | |
private String sessionToken; | |
public String getAccesskey() { | |
return accessKey; | |
} | |
public void setAccesskey(String accessKey) { | |
this.accessKey = accessKey; | |
} | |
public String getSecretKey() { | |
return secretKey; | |
} | |
public void setSecretKey(String secretKey) { | |
this.secretKey = secretKey; | |
} | |
public String getSessionToken() { | |
return sessionToken; | |
} | |
public void setSessionToken(String sessionToken) { | |
this.sessionToken = sessionToken; | |
} | |
@Override | |
public String toString() { | |
return "AwsConfigurationProperties [accessKey=" + accessKey + ", secretKey=" + secretKey + ", sessionToken=" | |
+ sessionToken + "]"; | |
} | |
} |
Let's also assume that there was another Refresh Scope bean that has an autowired dependency for AwsConfigurationProperties.
package example.springboot.config; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
import org.springframework.cloud.context.config.annotation.RefreshScope; | |
import org.springframework.context.annotation.Bean; | |
import org.springframework.context.annotation.Configuration; | |
import com.amazonaws.auth.AWSCredentials; | |
import com.amazonaws.auth.AWSSessionCredentials; | |
import com.amazonaws.auth.AWSStaticCredentialsProvider; | |
import com.amazonaws.auth.BasicSessionCredentials; | |
import com.amazonaws.services.s3.AmazonS3; | |
import com.amazonaws.services.s3.AmazonS3ClientBuilder; | |
/** | |
* AWS configuration class to initialize aws client sdk. | |
* | |
* @author iyerk | |
* | |
*/ | |
@Configuration | |
@ConditionalOnProperty(name="spring.cloud.vault.aws.enabled") | |
@RefreshScope | |
public class AWSConfiguration { | |
@Autowired | |
AwsConfigurationProperties awsConfigurationProperties; | |
@Value("${cloud.aws.region}") | |
private String region; | |
@Bean | |
@RefreshScope | |
public AWSSessionCredentials basicAWSCredentials() { | |
AWSSessionCredentials credentials = new BasicSessionCredentials(awsConfigurationProperties.getAccesskey(), | |
awsConfigurationProperties.getSecretKey(), awsConfigurationProperties.getSessionToken()); | |
return credentials; | |
} | |
@Bean | |
@RefreshScope | |
public AmazonS3 amazonS3Client(AWSCredentials awsCredentials) { | |
AmazonS3 s3client = AmazonS3ClientBuilder | |
.standard() | |
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials)) | |
.withRegion(region) | |
.build(); | |
return s3client; | |
} | |
} |
In this scenario, it becomes important to listen to SecretLeaseCreatedEvent and rebind/refresh the respective configuration properties and any other refresh scoped beans within the application that may need updated properties, such as AWS credentials injected. Let us review how we can achieve this next.
package example.springboot.config; | |
import javax.annotation.PostConstruct; | |
import org.slf4j.Logger; | |
import org.slf4j.LoggerFactory; | |
import org.springframework.beans.BeansException; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.beans.factory.annotation.Value; | |
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; | |
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; | |
import org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder; | |
import org.springframework.cloud.context.scope.refresh.RefreshScope; | |
import org.springframework.context.ApplicationContext; | |
import org.springframework.context.ApplicationContextAware; | |
import org.springframework.context.annotation.Configuration; | |
import org.springframework.core.env.ConfigurableEnvironment; | |
import org.springframework.vault.core.lease.SecretLeaseContainer; | |
import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent; | |
/** | |
* Configuration for Vault AWS secret backend for STS | |
* | |
* @author iyerk | |
* | |
*/ | |
@Configuration | |
@ConditionalOnProperty(name="spring.cloud.vault.aws.enabled") | |
@ConditionalOnBean(SecretLeaseContainer.class) | |
public class VaultAWSConfiguration implements ApplicationContextAware { | |
private static final Logger logger = LoggerFactory.getLogger(VaultAWSConfiguration.class); | |
private static final String STS_PATH = "/sts/"; | |
ApplicationContext context; | |
@Autowired | |
SecretLeaseContainer container; | |
@Autowired ConfigurableEnvironment configurableEnvironment; | |
@Autowired ConfigurationPropertiesRebinder rebinder; | |
@Value("${spring.cloud.vault.aws.backend}") | |
String vaultAwsBackend; | |
@Value("${spring.cloud.vault.aws.role}") | |
String vaultAwsRole; | |
@PostConstruct | |
private void postConstruct() { | |
logger.info("Register lease listener"); | |
container.addLeaseListener(leaseEvent -> { | |
if (leaseEvent.getSource().getPath().contains(vaultAwsBackend + STS_PATH + vaultAwsRole) | |
&& leaseEvent instanceof SecretLeaseCreatedEvent) { | |
// rebind aws configuration for this app | |
rebind("awsConfigurationProperties"); | |
// refresh additional bean dependencies as needed | |
refresh("AWSConfiguration"); | |
refresh("basicAWSCredentials"); | |
refresh("amazonS3Client"); | |
logger.info("SecretLeaseCreatedEvent received and applied for: "+leaseEvent.getSource().getPath()); | |
} | |
}); | |
} | |
private void rebind(String bean) { | |
try { | |
boolean success = this.rebinder.rebind(bean); | |
if (logger.isInfoEnabled()) { | |
logger.info(String.format( | |
"Attempted to rebind bean '%s' with updated AWS secrets from vault, success: %s", | |
bean, success)); | |
} | |
} | |
catch (Exception ex) { | |
logger.error("Exception rebinding "+bean,ex); | |
} | |
} | |
private void refresh(String bean) { | |
try { | |
boolean success = this.context.getBean(RefreshScope.class).refresh(bean); | |
if (logger.isInfoEnabled()) { | |
logger.info(String.format( | |
"Attempted to refresh bean '%s' with updated AWS secrets from vault, success: %s", | |
bean, success)); | |
} | |
} | |
catch (Exception ex) { | |
logger.error("Exception rebinding "+bean,ex); | |
} | |
} | |
@Override | |
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { | |
this.context = applicationContext; | |
} | |
} |
- VaultAwsConfiguration shown above registers a lease listener during postConstruct()
- Rebinds any ConfigurationProperties (AwsConfigurationProperties) using a ConfigurationPropertiesRebinder
- Refreshes any refresh scoped beans (AwsConfiguration, basicAWSCredentials, amazonS3Client) on the ApplicationContext upon receiving a SecretLeaseCreatedEvent.
Wondering why not just use spring cloud aws? If you are, you are absolutely right! At the moment, Spring Cloud AWS v2.3.0 only supports AWS access key and secrets. It also does not integrate with vault and lease events at the moment. I do have an issue logged for supporting STS Session Token and it will be a nice addition for Spring Cloud AWS to integrate with Spring Cloud Vault for its credential manager implementation.
Graceful Shutdown
Spring Cloud Vault performs a revoke on any active lease as the application container shuts down. The application will need to configure appropriate permissions on the vault role to perform sys/leases/revoke so that spring cloud vault could revoke leases.
Something I ran across with Spring Boot 2.4 and legacy bootstrap is that /actuator/refresh ends up closing the context and thus also triggers destroy() on the LeaseContainer resulting in a revoke. There isn’t a fix or a workaround for this yet under legacy bootstrap, but the recommendation is to cut over to using Config Data API. Note that spring config imports are processed in reverse order. For instance, if we were using multiple sources such as vault and consul (with ACL) for imports, and would like vault secrets to be resolved and imported before others, they will have to be set up as:
spring:
config:
import: consul://,vault://
Unrelated to STS and vault I have a spring boot issue raised for ordered dependency resolution with config data API where we need property sources updated to be honored before we process imports (Such as the consul ACL token from the vault consul backend).
Known Issues
With Spring cloud v2020.0.2 there is a known issue (java.lang.NoSuchMethodError) that stems from spring-cloud-configdue to an incorrect dependency resolution for spring-vault-core. See Vault core dependency resolution causing java.lang.NoSuchMethodError for more details. This will be corrected in a subsequent release but in the meanwhile, you could implement a workaround to override spring-vault-core version such as:
<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>2.3.2</version>
</dependency>
I hope you enjoyed the read and this helps you on your journey to build secured cloud applications using temporary credentials with AWS STS!
Thank you and stay safe!
Thanks to Attila Vágó, Darragh Grace, and Francislâiny Campos for their feedback on this post!
Top comments (0)