Introduction
WARNING: This guide is intended for educational purposes. It is recommended to thoroughly review and adjust the content before applying it to production environments.
This article aims to provide a high-level overview of how to use Sentinel to parse JSON Web Tokens (JWT) for use with policies in HashiCorp Vault.
Expected Outcome
By the end of the guide you will be able to parse JWTs in Sentinel Policies and use data from the payload to enforce specific rules within Vault.
Prerequisites
- Basic understanding of HashiCorp Vault (Ability to create policies and secrets)
- Vault Enterprise
- Basic understanding of Sentinel Policies
Procedure
Understanding JWT
A JSON Web Token (JWT) is an open standard that defines a compact way for securely transmitting information between parties as a JSON object.
JWTs are made of 3 components. Each component is separated by a period (.) and are Base64URL encoded.
- Header: Metadata about token. I.e. hashing algorithm used
- Payload: Contains the claims. This is where we can store any information we’d like the token to carry.
- Signature: Used to verify the sender and to ensure no tampering of the JWT.
A great site to play around with JWTs and do further research is jwt.io
Example JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Decoding JWT
Base64 decoding requires the string to be divisible by 4. Due to reasons explained in the “Additional Information” section of this article, the JWT parsed in the policy may need to have additional padding added to ensure it can be decoded successfully.
Here is an example function that can be added to a Sentinel policy to pad, decode and return the values in a JWT component.
import "json"
import "base64"
decode_jwt = func(jwt_component)
{
padding_to_add = length(jwt_component) % 4
for range(padding_to_add) as iterator {
jwt_component += "="
}
decoded_payload = base64.urldecode(jwt_component)
return json.unmarshal(decoded_payload)
}
Implementing the Sentinel Policy
Now that we have a brief overview of JWTs and how to decode them we can put this knowledge together into a Policy. In this example we introduce a rule that will deny unless the token parsed contains the claims in the payload: “Name”: “John Doe”
Write the policy
Create a file called “jwt-policy.sentinel” and add the following code:
import "json"
import "base64"
import "strings"
// Ensure token component is divisible by 4 and return json content
decode_jwt = func(jwt_component) {
padding_to_add = 4 - (length(jwt_component) % 4)
if padding_to_add < 4 {
for range(padding_to_add) as iterator {
jwt_component += "="
}
}
decoded_payload = base64.urldecode(jwt_component)
return json.unmarshal(decoded_payload)
}
// Gather specific token from request body using sentinel properties
payload = request.data["jwt"]
// Split via each token component (header, payload, signature)
split = strings.split(payload, ".")
// Decode the second component (payload)
decoded = decode_jwt(split[1])
// Deny request unless token contains name of "John Doe"
main = rule {
decoded.name == "John Doe"
}
Conclusion
We have now created a Sentinel policy to check for specific values from within a JWT token and use the values for evaluating our policy rules. This same approach can be modified to check for a range of data passed through each request. For more information on the topic read about Sentinel with regards to Vault here: Vault Sentinel
Additional Information
Base64Url Decode vs. Base64 Decode:
Base64Url is a variant of Base64 encoding that uses URL-safe characters (replacing ‘+’ with ‘-’ and ‘/’ with ‘_’).
Why JWT Needs Padding:
Because of how Base64 functions, the final token needs to be divisible by 4. To do that additional padding is added (in the form of ‘=’) to ensure it is divisible by 4. This padding can be dropped when not needed (i.e. when sending through requests).
When decoding, we need to add this padding back to ensure we decode the correct data.