The high level design for dynamic credential generation is depicted in the diagram below:
- Domsed is used to inject a side-car in the App Pod.
- The side-car is a Flask App which exposes two endpoints to the
run
container to fetch temporary aws credentials dynamically - The injected side-car communicates with Hashicorp Vault instance using a token mounted as a secret.
- Vault communicates with AWS using a AWS service account with limited privileges to generate credentials using STS but limited by permission boundaries
In document below we will demonstrate the following:
- Install Vault locally and run the side-car as a stand-alone Flask App
- Demonstrate the
run
container functionality by generating credentials via the side-car - Extending the design to a central version of Vault used by a Domino Installation
The use-case this solution addresses is as follows:
- Domino has 3 users namely,
test-user-1
,test-user-2
andtest-user-3
- The customer is named
domino-demo-customer
- In the customer AWS account is a bucket creatively named as
domino-demo-customer-bucket
- The bucket has the following key/value pairs
test-user-1/whoami.txt
=I am test-user-1
test-user-2/whoami.txt
=I am test-user-2
test-user-3/whoami.txt
=I am test-user-3
- All users have access to their own sub-folders
- Users can be provided access to other users sub-folders
In this section we will prepare the foundation for adding Credential Propagation to Domino Apps The overall Vault Getting Started tutorial is a good place to get started with understanding how to use Vault.
-
First install Vault locally
-
Start Vault in Dev mode. The command to run on your localhost terminal is
vault server -dev
In the output note the following. You will need all three (Examples shown below)export VAULT_ADDR=http://127.0.0.1:8200
Unseal Key: G+5edKxySWwPydj8BB3mUBiJxUamo1pKequX5DS26kY=
Unseal Key: G+5edKxySWwPydj8BB3mUBiJxUamo1pKequX5DS26kY=
Root Token: s.mJTaOPfOLMyECRnilupJNSPb
Ensure that you protect the root token and the unseal key. Write to your AWS Secrets Engine in AWS
-
Go to your browser and login to
http://127.0.0.1:8200
using the root token. -
Configure the following environment variables
export VAULT_ADDR=http://127.0.0.1:8200 export VAULT_TOKEN=s.mJTaOPfOLMyECRnilupJNSPb #Your token will be different # export VAULT_NS=domino/my_install #Namespaces are an enterprise feature.You don't need this in Open Source Version
Once Vault is installed add the following environment variables and run ./install.sh
export VAULT_ADDR=http://127.0.0.1:8200 #Replace with your own url for remote vault
export VAULT_TOKEN="ADD HERE"
export VAULT_NS="" #Add your own namespace. Only valid for enterprise version. If so replace with export VAULT_NS="dominoaws" o
export docker_version=beta_v3 #Add the side-car to a docker registry with a tag
./scripts/install.sh $docker_version
Internally the ./scripts/install.sh $docker_version
performs the following steps
We will create this bucket and its contents by invoking the following program
python ./src/admin/create_demo_customer_bucket.py <CONFIG_FILE_NAME> <AD_MAPPING_FILE>
#Ex. python ./src/admin/create_demo_customer_bucket.py ./config/install_config.json ./config/user_names.json
#Invoking the below has the same effect because those paths are defaults
python ./src/admin/create_demo_customer_bucket.py
The ./config/install_config.json
contains the aws iam user vault will use as well as the demo-bucket name
{
"domino_vault_user" : "vault-domino",
"customer_s3_bucket" : "domino-test-customer-bucket"
}
Think of the file ./config/users.json
to be auto-generated by listing the AD roles mapped to AWS roles. It
is a mapping of users to each AWS role they are mapped through via AWS SAML Integration with LDAP/AD
Next create an IAM user whose permissions vault will assume to make STS calls
python ./src/admin/configure_vault_aws_user.py <domino_vault_user> <customer_s3_bucket>
Default values are:
domino_vault_user = vault-domino
customer_s3_bucket = domino-demo-customer-bucket
This call will place a file $PROJECT_ROOT/aws_creds/creds.json
which will contain the
aws credentials of the new user. This allows the above command to be invoked without the
third parameter as long as the file is present as ./aws_creds/creds.json
This user will assume the following permissions:
./aws_policy_templates/VAULT_USER_POLICY_TEMPLATE.json
with nameVAULT_DOMINO_USER
./aws_policy_templates/PERMISSIONS_BOUNDARY_POLICY_TEMPLATE.json
with nameCUSTOMER_BOUNDARY_PERMISSION
The latter policy allows the vault-domino
user full permissions to the bucket domino-demo-customer-bucket
The access keys are placed in the file ./aws_creds/creds.json
To create a list of demo roles and policies review the file ./config/users.json
{
"AD_GROUPS": ["GRP-1","GRP-2","GRP-3"],
"AWS_ROLES": ["sample_domino_customer_role_1","sample_domino_customer_role_2","sample_domino_customer_role_3"],
"AWS_ROLES_TO_POLICIES_MAPPING": {
"sample_domino_customer_role_1": ["vault_test-user-1_policy","vault_test-user-2_policy"],
"sample_domino_customer_role_2": ["vault_test-user-2_policy"],
"sample_domino_customer_role_3": ["vault_test-user-3_policy"]
},
"AD_GROUP_TO_AWS_ROLE_MAPPING": {"GRP-1": "sample_domino_customer_role_1",
"GRP-2": "sample_domino_customer_role_2",
"GRP-3": "sample_domino_customer_role_3"},
"AD_GROUP_TO_USER_MAPPING": {"GRP-1": ["test-user-1","test-user-2"],
"GRP-2": ["test-user-2"],
"GRP-3": ["test-user-3"]}
}
We have the following relationship between AD Group/Roles and Users in the above JSON file
AD Group | AWS Role | AD Members |
---|---|---|
GRP-1 | sample_domino_customer_role_1 | test-user-1 |
test-user-2 | ||
GRP-1 | sample_domino_customer_role_2 | test-user-2 |
GRP-1 | sample_domino_customer_role_3 | test-user-3 |
To create the necessary policies and roles mapped to user (SSO Emulation) run the following command
python ./src/admin/configure_ldap_based_aws_policies_and_roles.py.py
- Customer has Active Directory (AD) groups
AD_GROUPS
. User's in AD are mapped to an AD group based onAD_GROUP_TO_USER_MAPPING
- Imagine the customer has mapped two AD Groups (
AD_GROUPS
) to two AWS Roles (AWS_ROLES
) - AD Group is mapped to AWS Role in a one to one mapping based on
AD_GROUP_TO_AWS_ROLE_MAPPING
- Each of those roles is mapped to one or more policies based on
AWS_ROLES_TO_POLICIES_MAPPING
This file can be auto-generated based on querying Active Directory. All except AWS_ROLES_TO_POLICIES_MAPPING
are needed to automate
the process. AWS_ROLES_TO_POLICIES_MAPPING
is something that happens outside of this solution as part of Role Governance.
Note that the following env variables are configured:
VAULT_ADDR
VAULT_TOKEN
Copy the Access and Secret keys and configure these env variables and run the command ./scripts/configure-vault.sh
export AWS_ACCESS_KEY_ID=$(cat ./aws_creds/creds.json | jq -r '.AccessKeyId')
export AWS_SECRET_ACCESS_KEY=$(cat ./aws_creds/creds.json | jq -r '.SecretAccessKey')
./scripts/configure-vault.sh
This achieves the following:
- Generate an APP TOKEN which allows Domino to communicate with Vault. You need the
VAULT_ADDR
andVAULT_TOKEN
variables set at this point We will use this as the vault token from this point on to communicate# First define the policy (access permissions) for the token we are about to generate # This defines the permission boundaries for the vault token vault policy write domino ./vault_policies/domino.hcl #Make the token valid for 25 hours and renew every 24 hours. This is necessary #because a token can be renewable for upto 768 hours but always expires in 24 hours export VAULT_TOKEN=$(vault token create -orphan -policy=domino -period=25h -ttl 25h -format=json | jq .auth.client_token | sed 's/"//g' ) #Write to a file (Or your AWS Secrets Engine) Securing is important. Token gives access to vault echo $VAULT_TOKEN > ./root/var/vault/token
- Enable the AWS Secrets Engine in Vault
#There are many more (https://www.vaultproject.io/docs/secrets) vault secrets enable aws
- Vault generates AWS credentials by requesting AWS STS for these credentials. The user
vault-domino
was created with the appropriate permissions to generate "AssumeRole", "GetFederationToken" privileges. We now configure Vault with the keys of this user to perform the actions on Domino's behalf.vault write aws/config/root \ access_key=$AWS_ACCESS_KEY_ID \ secret_key=$AWS_SECRET_ACCESS_KEY \ region=us-west-2
- Immediately rotate them. This ensures that now only Vault knows the SecretAccessKey
#Immediately rotate them. Now only vault knows the secret key. The existing ones are replaced vault write -f aws/config/rotate-root
First install Domsed and execute the command
./scripts/install-vault-cred-prop-to-domino.sh $docker_version
This performs the following steps:
- Write token to a k8s secret (Step only makes sense when Vault is not deployed in local mode)
export compute_namespace=domino-compute kubectl delete secret vault-token -n $compute_namespace kubectl create secret generic vault-token --from-literal=token=$VAULT_TOKEN -n $compute_namespace
- Create a k8s config-map to provide connection details to Vault (Step only makes sense when Vault is not deployed in local mode)
export compute_namespace=domino-compute export DOMSED_VAULT_CONFIGMAP_NAME=dynamic-aws-creds-config kubectl delete configmap ${DOMSED_VAULT_CONFIGMAP_NAME} -n $compute_namespace cat <<EOF | kubectl create -n ${compute_namespace} -f - apiVersion: v1 kind: ConfigMap metadata: name: ${DOMSED_VAULT_CONFIGMAP_NAME} data: dynamic-aws-creds-config: |- { "vault_endpoint": "${VAULT_ADDR}", "polling_interval_in_seconds" : 300, "refresh_threshold_in_seconds" : 600, "vault_namespace" : "${VAULT_NAMESPACE}", "lease_increment" : 3600, "default_user" : "default" } EOF
- Deploy the Domsed Mutation to inject the Vault Side Car
#Deploy the DOMSED Mutation to perform AWS Credential Propagation using Dynamic Secrets kubectl delete -f ./mutations/app_cloud_creds_mutation.yaml -n $platform_namespace kubectl create -f ./mutations/app_cloud_creds_mutation.yaml -n $platform_namespace
We can also provide access control by {DOMINO-PROJECT-OWNER}-{DOMINO-PROJECT}-{CALLING-DOMINO-USER}
For this type of control we use "Federation Token". The sample file is located in ./config/vault_role_by_project_and_user.json
{
"users_by_project": {
"test-user-1-quick-start": {
"test-user-1": {
"policies": [
"vault-test-user-1_policy",
"vault-test-user-2_policy"
]
},
"test-user-2": {
"policies": [
"vault-test-user-2_policy"
]
},
"test-user-3": {
"policies": [
"vault-test-user-3_policy"
]
}
},
"test-user-2-quick-start": {
"test-user-2": {
"policies": [
"vault-test-user-2_policy"
]
},
"test-user-3": {
"policies": [
"vault-test-user-3_policy"
]
}
},
"test-user-3-quick-start": {
"test-user-1": {
"policies": [
"vault-test-user-1_policy"
]
},
"test-user-3": {
"policies": [
"vault-test-user-3_policy"
]
}
}
}
}
We have the following projects, owner, collaborators and policy relationship in the above file:
Project | Owner | Collaborators | Policies |
---|---|---|---|
quick-start | test-user-1 | test-user-1 | vault-test-user-1_policy, vault-test-user-2_policy |
test-user-2 | vault-test-user-2_policy | ||
test-user-3 | vault-test-user-3_policy | ||
quick-start | test-user-2 | test-user-2 | vault-test-user-2_policy |
test-user-3 | vault-test-user-3_policy | ||
quick-start | test-user-3 | test-user-3 | vault-test-user-3_policy |
test-user-1 | vault-test-user-1_policy |
Now we need to configure roles inside Vault which will be configured to do one of the following on behalf of the caller :
- AssumeRole - The naming convention is
vault-{DOMINO_USER_NAME}
This type of Vault Role will be attached to a list ofrole_arns
- FederatedUser - The naming convention is
vault-{DOMINO_PROJECT_OWNER}-{DOMINO_PROJECT_NAME}-{DOMINO_USER_NAME}
This type of Vault Role will be attached to a list ofpolicy_arns
oriam_groups
The idea behind the two approaches is -
- AssumeRole - Is similar to federated user. User will get a set of AWS credentials for the roles (AD Groups) they are mapped to and the applicaion will need to pick one for a task at hand
{ "credential_type": "assumed_role", "role_arns": ["arn:aws:iam::xxx:role/sample_domino_customer_role_1","arn:aws:iam::xxx:role/sample_domino_customer_role_2"] }
- FederatedUser - This is a single AWS Credential with permission set equal to all policies attached. The idea is to provide access to user for a project.
{ "credential_type": "federation_token", "policy_arns": ["arn:aws:iam::xxx:policy/vault_test-user-1_policy","arn:aws:iam::xxx:policy/vault_test-user-2_policy"] }
The App using these will know which one is used depending on how it retrieves the credentials.
- AssumeRole - http://127.0.0.1:5010?user-name=test-user-1 Get me AWS Credentials for the calling user indexed by role arn.
- FederatedUser - user-name=test-user-1&project-name=test-user-2-quick-start Get me AWS Credentials indexed by Project-Owner+Project-Name-Calling-User-Name
To generate test roles and policies run the following command:
python src/admin/configure_vault_aws_roles.py
To start the side-car locally run the following command
python src/mutation/app_vault_sidecar.py ./rootlocal 5010
In your APP pod, the same Flask App will run on port 5010. Details of the mutation are in the file
./mutations/app_cloud_creds_mutation.yaml
apiVersion: apps.dominodatalab.com/v1alpha1
kind: Mutation
metadata:
name: app-cloud-creds-mutation
rules:
- # Optional. List of hardware tier ids
#hardwareTierIdSelector: []
# Optional. List of organization names
#organizationSelector: []
# Optional. List of user names
# Insert volume mount into specific containers
labelSelectors:
- "dominodatalab.com/workload-type=App"
# Insert arbitrary container into matching Pod.
insertContainer:
# 'app' or 'init'
containerType: app
# List of label selectors.
# ALL must match.
# Supportes equality and set-based matching
# Arbitrary object
spec:
name: z-vault-cloud-creds
image: quay.io/domino/app-cloud-creds:beta_v0
args: ['/','5010' ]
volumeMounts:
- name: log-volume
mountPath: /var/log/
- mountPath: /var/vault/
name: token
- name: dynamic-aws-creds-config
mountPath: /var/config/
readOnly: true
insertVolumes:
- emptyDir: { }
name: log-volume
- name: token
secret:
secretName: vault-token
- name: dynamic-aws-creds-config
configMap:
name: dynamic-aws-creds-config
Now let us emulate the app with another flask app which runs on port 5100
python src/mutation/app-test.py ./rootlocal
To test how access is permitted for a user invoke the following. This is an AssumeRole
operation
and derives its credentials per role attached to the AD user based on their group membership
curl --location --request GET 'http://127.0.0.1:5100/readS3ByUserLevelAccessControl' \
--header 'X-Script-Name: /test-user-1/quick-start/anything/random' \
--header 'Domino-Username: test-user-1'
We are invoking the app for project quick-start
owned by user test-user-1
by user test-user-1
To emulate the same APP invocation as test-user-2
run the following command
curl --location --request GET 'http://127.0.0.1:5100/readS3ByUserLevelAccessControl' \
--header 'X-Script-Name: /test-user-1/quick-start/anything/random' \
--header 'Domino-Username: test-user-2'
First you need to get all the AWS roles attached to this user. This is obtained by making the call to the side-car
curl --location --request GET 'http://127.0.0.1:8200/v1/aws/roles/vault-test-user-2' \
--header 'X-Vault-Token: s.1XGbhTykFJFUJDl4ZS5V8oXy'
The resulting output shows that the user is attached to two roles-
- arn:aws:iam::<AWS_ACCOUNT_NO>:role/vault-sample_domino_customer_role_1
- arn:aws:iam::<AWS_ACCOUNT_NO>:role/vault-sample_domino_customer_role_2
{
"request_id": "ce44c8de-8b8e-3006-deba-e0c783a36630",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"credential_type": "assumed_role",
"default_sts_ttl": 0,
"iam_groups": null,
"iam_tags": null,
"max_sts_ttl": 0,
"permissions_boundary_arn": "",
"policy_arns": null,
"policy_document": "",
"role_arns": [
"arn:aws:iam::<AWS_ACCOUNT_NO>:role/vault-sample_domino_customer_role_1",
"arn:aws:iam::<AWS_ACCOUNT_NO>:role/vault-sample_domino_customer_role_2"
],
"user_path": ""
},
"wrap_info": null,
"warnings": null,
"auth": null
}
The output of the command below will test access control on all the buckets for each of those roles separately. This is similar to Credential Propagation where you have to pick a profile for the credentials you are using. The permissions are not a "UNION" of the roles attached.
curl --location --request GET 'http://127.0.0.1:8200/v1/aws/roles/vault-test-user-2' \
--header 'X-Vault-Token: s.1XGbhTykFJFUJDl4ZS5V8oXy'
{
"arn:aws:iam::<AWS_ACCOUNT_NO>:role/vault-sample_domino_customer_role_1": [
{
"bucket": "domino-test-customer-bucket",
"key": "test-user-1/whoami.txt",
"value": "I am test-user-1"
},
{
"bucket": "domino-test-customer-bucket",
"key": "test-user-2/whoami.txt",
"value": "I am test-user-2"
},
{
"bucket": "domino-test-customer-bucket",
"key": "test-user-3/whoami.txt",
"value": "An error occurred (AccessDenied) when calling the GetObject operation: Access Denied"
}
],
"arn:aws:iam::<AWS_ACCOUNT_NO>:role/vault-sample_domino_customer_role_2": [
{
"bucket": "domino-test-customer-bucket",
"key": "test-user-1/whoami.txt",
"value": "An error occurred (AccessDenied) when calling the GetObject operation: Access Denied"
},
{
"bucket": "domino-test-customer-bucket",
"key": "test-user-2/whoami.txt",
"value": "I am test-user-2"
},
{
"bucket": "domino-test-customer-bucket",
"key": "test-user-3/whoami.txt",
"value": "An error occurred (AccessDenied) when calling the GetObject operation: Access Denied"
}
]
}
All aws credentials have a time to live (TTL) for 60 mins. And will be cached for 60 mins They will be refreshed if will be valid for less than 10 mins.
To refresh the cache immediately, you need to pass another header parameter Creds-Refresh
as True
curl --location --request GET 'http://127.0.0.1:5100/reads3byusr' \
--header 'X-Script-Name: /test-user-1/quick-start/anything/random' \
--header 'Domino-Username: test-user-2'
--header 'Creds-Refresh: True'
Run the following curl command
curl --location --request GET 'http://127.0.0.1:5100/reads3byprjusr' \
--header 'X-Script-Name: /test-user-1/quick-start/rrr/rrr' \
--header 'Domino-Username: test-user-1'
This will generated a FEDERATED_USER with policies attached to combination of project and invoking user All aws credentials have a time to live (TTL) for 60 mins. And will be cached for 60 mins They will be refreshed if will be valid for less than 10 mins.
To refresh the cache immediately, you need to pass another header parameter Creds-Refresh
as True
An example app can be created using the following artifacts
- Go to the folder
./app_artifacts/mnt
- Create a project in Domino or reuse the
quick-start
project - Add/Replace the contents of
./app_artifacts/mnt
inside you project/mnt/
folder - Start the app
export VAULT_ADDR=http://127.0.0.1:8200 #Replace with your own url for remote vault
export VAULT_TOKEN="ADD HERE"
export VAULT_NS="" #Add your own namespace. Only valid for enterprise version. If so replace with export VAULT_NS="dominoaws" o
export docker_version=beta_v3 #Add the side-car to a docker registry with a tag
./scripts/install.sh $docker_version
python src/mutation/app-test.py ./root
python src/mutation/app_vault_sidecar.py ./root 5010
Follow the same steps for testing. The app_vault_sidecar.py
connects to remote vault now via app-test.py
- Portable - It works for all clouds and even Active Directory based authentication because Vault supports multiple secrets engines
- Simplified User Interface - The endpoint remains the same from the App perspective. The side-car does the heavy lifting
- Keys are short-lived and access is bounded (Tested for AWS using Federation User Token)
- Needs Vault to be installed either in K8s. Vault as a service is available. It can also be deployed on AWS and managed by the customer.
This approach needs an intensive code-review. The writer of the app can emulate any user because the user-name and project-name is passed as plain text in headers and used directly