AWS ABAC (Attribute-based Access Control) using Hashicorp Vault

Harsimran Singh Maan
ITNEXT
Published in
7 min readMar 1, 2023

--

I had recently posted on LinkedIn that I have received my certifications for AWS Security Specialty and Hashicorp Vault Operations professional. I received a message from a friend and a former colleague asking how Vault can be used to implement Attribute-based Access Control for AWS resources. I have been researching this topic for a few months and have an open upstream proposal to Vault to enable ABAC support on the existing AWS secrets engine. While the proposal is for one use-case that would satisfy aspects of ABAC when requesting AWS STS tokens through Vault, Vault’s identity engine can also be used to sign principal claims that can be validated by AWS and enforce ABAC on resources. We’ll take a look at the latter in this post and revisit the former once/if the upstream proposal is accepted. (If you are interested in the upstream Vault proposal, please share your views on the PR)

Note that this post specifically talks about AWS ABAC and Vault, but other integrations like AWS IAM roles for service accounts(IRSA) can be used to setup ABAC for Kubernetes Service accounts accessing AWS resources in a similar way.

What is Attribute-based Access Control (ABAC)?

Before we talk about the solution, let’s make sure that we have the same basic understanding of ABAC. Unlike the commonly used Role-based Access control(RBAC) where access is granted based on what roles are assigned to a principal (a user or a service), ABAC allows granting access based on principal attributes (called claims). ABAC avoids the need to map roles to principals.

Imagine that you want to protect a resource that can only be accessed by employees in the HR department but not by users in Engineering department, for RBAC you’d setup roles like HR-role and Engineering-role and assign roles to appropriate employees. The authorization server would check the role binding to the principal and grant or deny access to the resource. For ABAC, the authorization server relies on verifiable information provided through principal claims like department=hr or department=engineering and uses it to either grant to deny access to the resource.

RBAC vs ABAC

ABAC for AWS IAM

Now that we understand ABAC, let’s explore it in context of AWS IAM. Using the same example as above, to grant access to a specific path /hr/ or /engineering/ in a S3 bucket named department-storage, you can have two separate roles and bind them (allow assumeRole) to the users in the specific department. The HR IAM policy for the might look like

Policy to grant HR-role access to HR owned objects in S3

A similar policy would be created for the Engineering department with line 12 looking something like "arn:aws:s3:::department-storage/engineering/*" .

Rewriting the same policy for ABAC would look something like

ABAC policy looks at the department attribute of the principal to decide access to S3 resources

The aws:PrincipalTag/department is inferred from the user’s active session with AWS and used as an attribute to check authorization when a request is made to specific S3 paths.

As evident here, ABAC enabled policy can help avoid duplication of policy fragments across multiple policies for same class of access requirements. A single role named department-storage-role can be used to provide access to multiple departments.

Vault as the Attribute signing authority

If you are using Hashicorp Vault in your environment, you can use it to sign claims about a principal and send Vault-issued ID tokens to AWS to exchange for an STS token and then use the token to talk to AWS services. One advantage of having a system like Vault do the claim signing is that it can federate attributes from multiple auth systems like Kubernetes, EC2 instance profile, Gitlab, Github, Azure, GCP, and pretty much any system out there via custom auth plugins. This massively reduces the need to implement bespoke trust relationships with AWS for each ID issuing party.

ABAC enabled AWS Access by federating identities through Vault

To perform the setup, we’ll configure Vault’s OpenID Connect(OIDC) identity engine and add Vault as an OIDC provider on AWS, then generate tokens from Vault to login to AWS and use the attributes from the tokens to establish session tags used for ABAC.

$ # Update vault oidc issuer
$ vault write identity/oidc/config \
issuer=https://a-public-endpoint-that-aws-can-access-to-get-verification-key

Next we create a key for signing principal claims in Vault.

$ vault write identity/oidc/key/signing-key rotation_period=86400 \
verification_ttl=3600 algorithm=RS384 allowed_client_ids='*'

You can get the OIDC document to configure AWS using curl $VAULT_ADDR/v1/identity/oidc/.well-known/openid-configuration .

OIDC configuration from the Vault server

We’ll skip the AWS OIDC setup bits here for simplicity but if you are interested in the exact steps, please feel free to reach out and I can write about that in a separate post.

Sample config that adds Vault as a trusted party on AWS

Once the identity setup is done, the roles using ABAC policies would need appropriate permissions for sts:AssumeRoleWithWebIdentity and sts:TagSession.

AWS IAM role trust relationship for enabling entities to exchange Vault token with STS creds for department-storage-role.

At this stage, we are now ready to issue id tokens from Vault that users and services can use to login to AWS and access appropriate resources as allowed by the ABAC.

Exchange Vault id token for AWS ABAC enabled access

Vault ID tokens can be issued by an entity(user and service) logged into Vault by any of the supported auth methods.

To issue the tokens, we need to create an OIDC role in Vault for which the tokens would be signed by the signing-key created earlier and the audience of sts.amazon.com as specified in the AWS OIDC setup and the AWs role’s trust relationship.

$ # Base64 the token template
$ template=$(base64 <<EOT
{
"aClaimAboutThePrincipal": "aValue",
"https://aws.amazon.com/tags": {
"principal_tags": {
"department": [{{identity.entity.metadata.dept}}],
"anotherAttributeFromMetadata": ["Vancouver"]
},
"transitive_tag_keys": [
"department"
]
}
}
EOT
)
$ # Create an oidc role in Vault
$ vault write identity/oidc/role/test-role client_id="sts.amazon.com" \
name="test-role" \
key="signing-key" \
ttl=3600 \
template=$template
$ # Read back the role config from Vault to confirm the setup
$ vault read identity/oidc/role/test-role
Key Value
--- -----
client_id sts.amazon.com
key signing-key
template {
"aClaimAboutThePrincipal": "aValue",
"https://aws.amazon.com/tags": {
"principal_tags": {
"department": [{{identity.entity.metadata.dept}}],
"anotherAttributeFromMetadata": ["Vancouver"]
},
"transitive_tag_keys": [
"department"
]
}
}
ttl 1h

The template above embeds a principalTag named department for which the value is dynamically inferred from the entity’s metadata value for key dept. The metadata on the entity alias can also be populated when an entity logs into Vault and can provide a number of attributes that can be included for ABAC.

Next, we’ll create an HR user directly in Vault for a quick test.

$ #Create a policy to grant access to mint id tokens
$ vault policy write department-access -<<EOF
path "identity/oidc/token/test-role" {
capabilities = [ "read"]
}
EOF

$ #Enable userpass auth
$ vault auth enable -path="userpass-test" userpass
$ # Record the accessor id for later use
$ vault auth list -format=json \
| jq -r '.["userpass-test/"].accessor' > accessor_test.txt

$ #Create an auth user bob and let it mint token via the policy created above
$ vault write auth/userpass-test/users/bob password="test"\
policies="department-access"

$ #Create an entity for the user and record the entity id
$ vault write -format=json identity/entity name="bob" policies="default" \
metadata=dept="hr" \
| jq -r ".data.id" > entity_id.txt

$ #Create an entity alias for the user bob
$ vault write identity/entity-alias name="bob" \
canonical_id=$(cat entity_id.txt) \
mount_accessor=$(cat accessor_test.txt)

$ # In simple terms, we have created an HR user named Bob

Login with the user to get the Vault issued id token.

$ vault login -format=json -method=userpass -path=userpass-test \
username=bob password=test \
| jq -r ".auth.client_token" > bob_token.txt

$ # Get the id token as bob
$ VAULT_TOKEN=$(cat bob_token.txt) vault read -format=json \
identity/oidc/token/test-role | jq -r ".data.token" >id_token.txt

As shown below, the department is populated dynamically with the value hr .

Sample Vault issued id token payload

The id token can be exchanged for an AWS STS token and then the STS token can be used to access resources on AWS.

To use AWS CLI with the id token and AssumeRoleWithWebIdentity , follow the steps below.

$ # Configure AWS CLI profile, named abac
$ cat >> ~/.aws/config <<EOT
[abac]
role_arn=arn:aws:iam::000000000000:role/department-storage-role
web_identity_token_file=/path/to/file/containing/id/token/id_token.txt
role_session_name=test

EOT

$ # Use the profile to access department-storage/hr/ path
$ aws s3 ls department-storage/hr/ --profile=abac

$ # Expect access denied when accessing other paths in the bucket
$ aws s3 ls department-storage/engineering/ --profile=abac

Summary

In this post, we explored an approach of using Attribute-based Access Control for AWS resources using the Vault identity secrets engine. We covered basics of ABAC, policy creation in AWS using ABAC and then steps to configure Vault to provide session tags via OIDC JWT ID token to use against AWS AssumeRoleWithWebIdentity.

If you’d like to talk about how IAM, AWS ABAC or Hashicorp Vault can be setup securely and in an automated manner for your organization’s specific needs, feel free to connect with me on LinkedIn.

Checkout more stories at https://harsimranmaan.medium.com/ .

--

--