Attaché provides an emulation layer for cloud provider instance metadata APIs, allowing for seamless multi-cloud IAM using Hashicorp Vault.
More information can be found in the companion talk, Freeing Identity from Infrastructure.
- Attaché intercepts requests that applications perform to the cloud provider's instance metadata service (IMDS)
- Attaché forwards these requests to a pre-configured cloud secrets backend of Hashicorp Vault to retrieve application-scoped cloud credentials
- Finally, Attaché returns the requested credentials to the application
You can use the pre-built binaries from the releases page or use the provided Docker image:
docker run --rm -it docker pull ghcr.io/datadog/attache
In this example, we will use Attaché to have a local application that uses the AWS and Google Cloud SDKs to seamlessly retrieve cloud credentials.
vault server -dev -dev-root-token-id=local -log-level=DEBUGCreate an AWS IAM role caled application-role, and a Google Cloud service account called application-role (they have to match).
Let's mount an AWS secret backend. For this demo, we'll authenticate Vault to AWS using IAM user access keys, which is (to say the least) a bad practice not to follow in production:
Create an AWS IAM user:
accountId=$(aws sts get-caller-identity --query Account --output text)
aws iam create-user --user-name vault-demo
credentials=$(aws iam create-access-key --user-name vault-demo)
accessKeyId=$(echo "$credentials" | jq -r '.AccessKey.AccessKeyId')
secretAccessKey=$(echo "$credentials" | jq -r '.AccessKey.SecretAccessKey')Allow Vault to assume the role we want to give our application:
aws iam put-user-policy --user-name vault-demo --policy-name vault-demo --policy-document '{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": "arn:aws:iam::'$accountId':role/application-role"
},
{
"Sid": "AllowVaultToRotateItsOwnCredentials",
"Effect": "Allow",
"Action": ["iam:GetUser", "iam:DeleteAccessKey", "iam:CreateAccessKey"],
"Resource": "arn:aws:iam::'$accountId':user/vault-demo"
}
]
}'Then we configure the Vault AWS credentials backend:
# Point the Vault CLI to our local test server
export VAULT_ADDR="http://127.0.0.1:8200"
export VAULT_TOKEN="local"
vault secrets enable -path cloud-iam/aws/0123456789012 aws
vault write cloud-iam/aws/0123456789012/config/root access_key="$accessKeyId" secret_key="$secretAccessKey"
vault write cloud-iam/aws/0123456789012/roles/application-role credential_type=assumed_role role_arns="arn:aws:iam::${accountId}:role/application-role"
vault write -f cloud-iam/aws/0123456789012/config/rotate-root # rotate the IAM user access key so Vault only knows its own static credentialsWe can confirm that Vault is able to retrieve our role credentials by using vault read cloud-iam/aws/0123456789012/creds/application-role.
Let's mount a Google Cloud secret backend. For this demo, we'll authenticate Vault to GCP using a service account key, which is also suboptimal in production:
project=$(gcloud config get-value project)
vaultSa=vault-demo@$project.iam.gserviceaccount.com
gcloud iam service-accounts create vault-demo
gcloud iam service-accounts keys create gcp-creds.json --iam-account=$vaultSa
# Allow the Vault service account to impersonate the application service account
gcloud iam service-accounts add-iam-policy-binding application-role@$project.iam.gserviceaccount.com \
--role=roles/iam.serviceAccountTokenCreator \
--member=serviceAccount:$vaultSaThen we configure the Vault GCP credentials backend, so it can access our prerequisite
gcloud
vault secrets enable -path cloud-iam/gcp/gcp-sandbox gcp
vault write cloud-iam/gcp/gcp-sandbox/config [email protected]
vault write cloud-iam/gcp/gcp-sandbox/impersonated-account/application-role service_account_email="[email protected]" token_scopes="https://www.googleapis.com/auth/cloud-platform" ttl="4h"We can verify this worked by running vault read cloud-iam/gcp/gcp-sandbox/impersonated-account/application-role/token
Let's create a configuration file for Attaché (see Configuration reference):
##
# Attaché global configuration
##
server:
bind_address: 127.0.0.1:8080
graceful_timeout: 0s
rate_limit: ""
# We're running locally
provider: ""
region: ""
zone: ""
# AWS configuration
aws_vault_mount_path: cloud-iam/aws/012345678901
iam_role: application-role
imds_v1_allowed: false
# GCP configuration
gcp_vault_mount_path: cloud-iam/gcp/gcp-sandbox
gcp_project_ids:
cloud-iam/gcp/gcp-sandbox: "712781682929"
# Azure configuration (unused here)
azure_vault_mount_path: ""Then we can run Attaché:
$ export VAULT_ADDR="http://127.0.0.1:8200"
$ export VAULT_TOKEN="local"
$ attache ./config.yaml
2024-06-17T16:51:23.283+0200 DEBUG attache/main.go:35 loading configuration {"path": "./config.yaml"}
2024-06-17T16:51:23.283+0200 DEBUG attache/main.go:49 configuration loaded {"configuration": {"IamRole":"application-role","IMDSv1Allowed":false,"GcpVaultMountPath":"cloud-iam/gcp/gcp-sandbox","GcpProjectIds":{"cloud-iam/gcp/gcp-sandbox":"712781682929"},"AwsVaultMountPath":"cloud-iam/aws/012345678901","AzureVaultMountPath":"","ServerConfig":{"BindAddress":"127.0.0.1:8080","GracefulTimeout":0,"RateLimit":""},"Provider":"","Region":"","Zone":""}}
2024-06-17T16:51:23.284+0200 INFO cloud-iam-server server/server.go:110 server starting {"address": "127.0.0.1:8080"}
Note how we're able to manually retrieve credentials as if we were hitting the AWS IMDS, which Attaché emulates:
$ IMDSV2_TOKEN=$(curl -XPUT localhost:8080/latest/api/token -H x-aws-ec2-metadata-token-ttl-seconds:21600)
$ curl -H "X-aws-ec2-metadata-token: $IMDSV2_TOKEN" localhost:8080/latest/meta-data/iam/security-credentials/
application role
$ curl -H "X-aws-ec2-metadata-token: $IMDSV2_TOKEN" localhost:8080/latest/meta-data/iam/security-credentials/application-role
{
"AccessKeyId": "ASIAZ3..",
"Code": "Success",
"SecretAccessKey": "liqX1...",
"Token": "IQoJ...",
}Same as if we were hitting the GCP IMDS:
$ curl -H Metadata-Flavor:Google localhost:8080/computeMetadata/v1/instance/service-accounts/
default/
[email protected]/
$ curl -H Metadata-Flavor:Google localhost:8080/computeMetadata/v1/instance/service-accounts/[email protected]/
default/
{
"access_token": "ya29.c.c0AY_VpZ...",
"token_type": "Bearer",
"expires_in": 3597
}Let's use the following application that lists AWS S3 and Google Cloud GCS buckets:
import boto3
from google.cloud import storage
def list_s3_buckets():
s3 = boto3.client('s3')
response = s3.list_buckets()
print(f"Found {len(response['Buckets'])} AWS S3 buckets!")
def list_gcs_buckets():
client = storage.Client()
buckets = client.list_buckets()
print(f"Found {len(list(buckets))} GCS buckets!")
list_s3_buckets()
list_gcs_buckets()We can set the required environment variables to point to Attaché:
export AWS_EC2_METADATA_SERVICE_ENDPOINT="http://127.0.0.1:8080/"
export GCE_METADATA_HOST="127.0.0.1:8080"... and then run it!
pip install boto3 google-cloud-storage
python app.pyWe see:
Found 154 AWS S3 buckets!
Found 2 GCS buckets!And in the Attaché logs:
2024-06-17T17:23:15.463+0200 INFO cloud-iam-server server/server.go:170 request {"address": "127.0.0.1:8080", "path": "/latest/api/token", "method": "PUT", "userAgent": "Boto3/1.34.77 Python/3.10.13 Darwin/23.5.0 Botocore/1.34.80"}
2024-06-17T17:23:15.463+0200 INFO cloud-iam-server server/server.go:177 response {"address": "127.0.0.1:8080", "path": "/latest/api/token", "method": "PUT", "statusCode": 200, "userAgent": "Boto3/1.34.77 Python/3.10.13 Darwin/23.5.0 Botocore/1.34.80"}
2024-06-17T17:23:15.464+0200 INFO cloud-iam-server server/server.go:170 request {"address": "127.0.0.1:8080", "path": "/latest/meta-data/iam/security-credentials/", "method": "GET", "userAgent": "Boto3/1.34.77 Python/3.10.13 Darwin/23.5.0 Botocore/1.34.80"}
2024-06-17T17:23:15.465+0200 INFO cloud-iam-server server/server.go:177 response {"address": "127.0.0.1:8080", "path": "/latest/meta-data/iam/security-credentials/", "method": "GET", "statusCode": 200, "userAgent": "Boto3/1.34.77 Python/3.10.13 Darwin/23.5.0 Botocore/1.34.80"}
2024-06-17T17:23:15.466+0200 INFO cloud-iam-server server/server.go:170 request {"address": "127.0.0.1:8080", "path": "/latest/meta-data/iam/security-credentials/application-role", "method": "GET", "userAgent": "Boto3/1.34.77 Python/3.10.13 Darwin/23.5.0 Botocore/1.34.80"}
2024-06-17T17:23:15.895+0200 DEBUG token maintainer cache/maintainer.go:188 Updating cached value {"fetcher": "aws-sts-token-vault", "expiration": "2024-06-17T16:23:14.000Z"}
2024-06-17T17:23:15.895+0200 DEBUG token maintainer cache/maintainer.go:201 scheduling value refresh {"fetcher": "aws-sts-token-vault", "delay": "20m22.713030691s"}
2024-06-17T17:23:15.895+0200 INFO cloud-iam-server server/server.go:177 response {"address": "127.0.0.1:8080", "path": "/latest/meta-data/iam/security-credentials/application-role", "method": "GET", "statusCode": 200, "userAgent": "Boto3/1.34.77 Python/3.10.13 Darwin/23.5.0 Botocore/1.34.80"}
TBA
TBA
##
# Attaché global configuration
##
server:
bind_address: 127.0.0.1:8080
graceful_timeout: 0s
rate_limit: ""
# If applicable, the current cloud environment where attaché is running
provider: ""
# If applicable, current cloud region (e.g., us-east-1a) where attaché is running
region: ""
# If applicable, current cloud availability zone (e.g., us-east-1a) where attaché is running
zone: ""
##
# AWS configuration
##
# Vault path where the AWS secrets backend is mounted
aws_vault_mount_path: cloud-iam/aws/012345678901
# The AWS IAM role name that Attaché will assume to retrieve AWS credentials
iam_role: my-role
# Disable IMDSv1
imds_v1_allowed: false
##
# GCP configuration
##
# Vault pathw here the Google Cloud secrets backend is mounted
gcp_vault_mount_path: cloud-iam/gcp/my-gcp-sandbox
# Mapping of Vault paths to Google Cloud project IDs
gcp_project_ids:
cloud-iam/gcp/datadog-sandbox: "012345678901"
##
# Azure configuration
##
azure_vault_mount_path: cloud-iam/azure/my-azure-role