|
1 | 1 | # Attaché |
2 | | -Attaché provides an emulation layer for Cloud Provider IMDS APIs |
| 2 | + |
| 3 | +[](http://golang.org) |
| 4 | + |
| 5 | +Attaché provides an emulation layer for cloud provider instance metadata APIs, allowing for seamless multi-cloud IAM using Hashicorp Vault. |
| 6 | + |
| 7 | +<p align="center"> |
| 8 | + <a href="./attache.png"><img src="./attache.png" alt="Attaché" width="800" /></a> |
| 9 | +</p> |
| 10 | + |
| 11 | + |
| 12 | +## How it works |
| 13 | + |
| 14 | +1. Attaché intercepts requests that applications perform to the cloud provider's instance metadata service (IMDS) |
| 15 | +2. Attaché forwards these requests to a pre-configured cloud secrets backend of Hashicorp Vault to retrieve application-scoped cloud credentials |
| 16 | +3. Finally, Attaché returns the requested credentials to the application |
| 17 | + |
| 18 | +## Installation |
| 19 | + |
| 20 | +You can use the pre-built binaries from the [releases page](https://github.com/DataDog/attache/releases) or use the provided Docker image: |
| 21 | + |
| 22 | +``` |
| 23 | +docker run --rm -it docker pull ghcr.io/datadog/attache |
| 24 | +``` |
| 25 | + |
| 26 | +## Sample usage |
| 27 | + |
| 28 | +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. |
| 29 | + |
| 30 | +### 1. Set up a Vault server |
| 31 | + |
| 32 | +```bash |
| 33 | +vault server -dev -dev-root-token-id=local -log-level=DEBUG |
| 34 | +``` |
| 35 | +### 2. Set up your application roles in AWS and Google Cloud |
| 36 | + |
| 37 | +Create an AWS IAM role caled `application-role`, and a Google Cloud service account called `application-role` (they have to match). |
| 38 | + |
| 39 | +### 3. Configure your Vault AWS secrets backend |
| 40 | + |
| 41 | +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: |
| 42 | + |
| 43 | +Create an AWS IAM user: |
| 44 | + |
| 45 | +```bash |
| 46 | +accountId=$(aws sts get-caller-identity --query Account --output text) |
| 47 | +aws iam create-user --user-name vault-demo |
| 48 | +credentials=$(aws iam create-access-key --user-name vault-demo) |
| 49 | +accessKeyId=$(echo "$credentials" | jq -r '.AccessKey.AccessKeyId') |
| 50 | +secretAccessKey=$(echo "$credentials" | jq -r '.AccessKey.SecretAccessKey') |
| 51 | +``` |
| 52 | + |
| 53 | +Allow Vault to assume the role we want to give our application: |
| 54 | + |
| 55 | +```bash |
| 56 | +aws iam put-user-policy --user-name vault-demo --policy-name vault-demo --policy-document '{ |
| 57 | + "Version": "2012-10-17", |
| 58 | + "Statement": [ |
| 59 | + { |
| 60 | + "Effect": "Allow", |
| 61 | + "Action": "sts:AssumeRole", |
| 62 | + "Resource": "arn:aws:iam::'$accountId':role/application-role" |
| 63 | + }, |
| 64 | + { |
| 65 | + "Sid": "AllowVaultToRotateItsOwnCredentials", |
| 66 | + "Effect": "Allow", |
| 67 | + "Action": ["iam:GetUser", "iam:DeleteAccessKey", "iam:CreateAccessKey"], |
| 68 | + "Resource": "arn:aws:iam::'$accountId':user/vault-demo" |
| 69 | + } |
| 70 | + ] |
| 71 | +}' |
| 72 | +``` |
| 73 | + |
| 74 | +Then we configure the Vault AWS credentials backend: |
| 75 | + |
| 76 | +```bash |
| 77 | +# Point the Vault CLI to our local test server |
| 78 | +export VAULT_ADDR="http://127.0.0.1:8200" |
| 79 | +export VAULT_TOKEN="local" |
| 80 | + |
| 81 | +vault secrets enable -path cloud-iam/aws/0123456789012 aws |
| 82 | +vault write cloud-iam/aws/0123456789012/config/root access_key="$accessKeyId" secret_key="$secretAccessKey" |
| 83 | +vault write cloud-iam/aws/0123456789012/roles/application-role credential_type=assumed_role role_arns="arn:aws:iam::${accountId}:role/application-role" |
| 84 | +vault write -f cloud-iam/aws/0123456789012/config/rotate-root # rotate the IAM user access key so Vault only knows its own static credentials |
| 85 | +``` |
| 86 | + |
| 87 | +We can confirm that Vault is able to retrieve our role credentials by using `vault read cloud-iam/aws/0123456789012/creds/application-role`. |
| 88 | + |
| 89 | +### 4. Configure your Vault GCP secrets backend |
| 90 | + |
| 91 | +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: |
| 92 | + |
| 93 | +```bash |
| 94 | +project=$(gcloud config get-value project) |
| 95 | +vaultSa=vault-demo@$project.iam.gserviceaccount.com |
| 96 | +gcloud iam service-accounts create vault-demo |
| 97 | +gcloud iam service-accounts keys create gcp-creds.json --iam-account=$vaultSa |
| 98 | + |
| 99 | +# Allow the Vault service account to impersonate the application service account |
| 100 | +gcloud iam service-accounts add-iam-policy-binding application-role@$project.iam.gserviceaccount.com \ |
| 101 | + --role=roles/iam.serviceAccountTokenCreator \ |
| 102 | + --member=serviceAccount:$vaultSa |
| 103 | +``` |
| 104 | + |
| 105 | +Then we configure the Vault GCP credentials backend, so it can access our prerequisite |
| 106 | + |
| 107 | +```bash |
| 108 | +gcloud |
| 109 | +vault secrets enable -path cloud-iam/gcp/gcp-sandbox gcp |
| 110 | +vault write cloud-iam/gcp/gcp-sandbox/config credentials=@gcp-creds.json |
| 111 | +vault write cloud-iam/gcp/gcp-sandbox/impersonated-account/application-role service_account_email="application-role@gcp-sandbox.iam.gserviceaccount.com" token_scopes="https://www.googleapis.com/auth/cloud-platform" ttl="4h" |
| 112 | +``` |
| 113 | + |
| 114 | +We can verify this worked by running `vault read cloud-iam/gcp/gcp-sandbox/impersonated-account/application-role/token` |
| 115 | + |
| 116 | +### 5. Configure and run Attaché |
| 117 | + |
| 118 | +Let's create a configuration file for Attaché (see [Configuration reference](#configuration-reference)): |
| 119 | + |
| 120 | +```yaml |
| 121 | +## |
| 122 | +# Attaché global configuration |
| 123 | +## |
| 124 | +server: |
| 125 | + bind_address: 127.0.0.1:8080 |
| 126 | + graceful_timeout: 0s |
| 127 | + rate_limit: "" |
| 128 | + |
| 129 | +# We're running locally |
| 130 | +provider: "" |
| 131 | +region: "" |
| 132 | +zone: "" |
| 133 | + |
| 134 | +# AWS configuration |
| 135 | +aws_vault_mount_path: cloud-iam/aws/012345678901 |
| 136 | +iam_role: application-role |
| 137 | +imds_v1_allowed: false |
| 138 | + |
| 139 | +# GCP configuration |
| 140 | +gcp_vault_mount_path: cloud-iam/gcp/gcp-sandbox |
| 141 | +gcp_project_ids: |
| 142 | + cloud-iam/gcp/gcp-sandbox: "712781682929" |
| 143 | + |
| 144 | +# Azure configuration (unused here) |
| 145 | +azure_vault_mount_path: "" |
| 146 | +``` |
| 147 | +
|
| 148 | +Then we can run Attaché: |
| 149 | +
|
| 150 | +``` |
| 151 | +$ export VAULT_ADDR="http://127.0.0.1:8200" |
| 152 | +$ export VAULT_TOKEN="local" |
| 153 | +$ attache ./config.yaml |
| 154 | +2024-06-17T16:51:23.283+0200 DEBUG attache/main.go:35 loading configuration {"path": "./config.yaml"} |
| 155 | +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":""}} |
| 156 | +2024-06-17T16:51:23.284+0200 INFO cloud-iam-server server/server.go:110 server starting {"address": "127.0.0.1:8080"} |
| 157 | +``` |
| 158 | +
|
| 159 | +Note how we're able to manually retrieve credentials as if we were hitting the AWS IMDS, which Attaché emulates: |
| 160 | +
|
| 161 | +```bash |
| 162 | +$ IMDSV2_TOKEN=$(curl -XPUT localhost:8080/latest/api/token -H x-aws-ec2-metadata-token-ttl-seconds:21600) |
| 163 | + |
| 164 | +$ curl -H "X-aws-ec2-metadata-token: $IMDSV2_TOKEN" localhost:8080/latest/meta-data/iam/security-credentials/ |
| 165 | +application role |
| 166 | + |
| 167 | +$ curl -H "X-aws-ec2-metadata-token: $IMDSV2_TOKEN" localhost:8080/latest/meta-data/iam/security-credentials/application-role |
| 168 | +{ |
| 169 | + "AccessKeyId": "ASIAZ3..", |
| 170 | + "Code": "Success", |
| 171 | + "SecretAccessKey": "liqX1...", |
| 172 | + "Token": "IQoJ...", |
| 173 | +} |
| 174 | +``` |
| 175 | + |
| 176 | +Same as if we were hitting the GCP IMDS: |
| 177 | + |
| 178 | +```bash |
| 179 | +$ curl -H Metadata-Flavor:Google localhost:8080/computeMetadata/v1/instance/service-accounts/ |
| 180 | +default/ |
| 181 | +application-role@gcp-sandbox.iam.gserviceaccount.com/ |
| 182 | + |
| 183 | +$ curl -H Metadata-Flavor:Google localhost:8080/computeMetadata/v1/instance/service-accounts/application-role@gcp-sandbox.iam.gserviceaccount.com/ |
| 184 | +default/ |
| 185 | +{ |
| 186 | + "access_token": "ya29.c.c0AY_VpZ...", |
| 187 | + "token_type": "Bearer", |
| 188 | + "expires_in": 3597 |
| 189 | +} |
| 190 | +``` |
| 191 | + |
| 192 | +### 6. Run your application |
| 193 | + |
| 194 | +Let's use the following application that lists AWS S3 and Google Cloud GCS buckets: |
| 195 | + |
| 196 | +```python |
| 197 | +import boto3 |
| 198 | +from google.cloud import storage |
| 199 | + |
| 200 | +def list_s3_buckets(): |
| 201 | + s3 = boto3.client('s3') |
| 202 | + |
| 203 | + response = s3.list_buckets() |
| 204 | + print(f"Found {len(response['Buckets'])} AWS S3 buckets!") |
| 205 | + |
| 206 | +def list_gcs_buckets(): |
| 207 | + client = storage.Client() |
| 208 | + |
| 209 | + buckets = client.list_buckets() |
| 210 | + print(f"Found {len(list(buckets))} GCS buckets!") |
| 211 | + |
| 212 | +list_s3_buckets() |
| 213 | +list_gcs_buckets() |
| 214 | +``` |
| 215 | + |
| 216 | +We can set the required environment variables to point to Attaché: |
| 217 | + |
| 218 | +```bash |
| 219 | +export AWS_EC2_METADATA_SERVICE_ENDPOINT="http://127.0.0.1:8080/" |
| 220 | +export GCE_METADATA_HOST="127.0.0.1:8080" |
| 221 | +``` |
| 222 | + |
| 223 | +... and then run it! |
| 224 | + |
| 225 | +```bash |
| 226 | +pip install boto3 google-cloud-storage |
| 227 | +python app.py |
| 228 | +``` |
| 229 | + |
| 230 | +We see: |
| 231 | + |
| 232 | +```bash |
| 233 | +Found 154 AWS S3 buckets! |
| 234 | +Found 2 GCS buckets! |
| 235 | +``` |
| 236 | + |
| 237 | +And in the Attaché logs: |
| 238 | + |
| 239 | +``` |
| 240 | +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"} |
| 241 | +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"} |
| 242 | +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"} |
| 243 | +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"} |
| 244 | +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"} |
| 245 | +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"} |
| 246 | +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"} |
| 247 | +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"} |
| 248 | +``` |
| 249 | + |
| 250 | +## Considerations for running in production |
| 251 | + |
| 252 | +TBA |
| 253 | + |
| 254 | +## Caching |
| 255 | + |
| 256 | +TBA |
| 257 | + |
| 258 | +## Configuration reference |
| 259 | + |
| 260 | +```yaml |
| 261 | +## |
| 262 | +# Attaché global configuration |
| 263 | +## |
| 264 | +server: |
| 265 | + bind_address: 127.0.0.1:8080 |
| 266 | + graceful_timeout: 0s |
| 267 | + rate_limit: "" |
| 268 | + |
| 269 | +# If applicable, the current cloud environment where attaché is running |
| 270 | +provider: "" |
| 271 | + |
| 272 | +# If applicable, current cloud region (e.g., us-east-1a) where attaché is running |
| 273 | +region: "" |
| 274 | + |
| 275 | +# If applicable, current cloud availability zone (e.g., us-east-1a) where attaché is running |
| 276 | +zone: "" |
| 277 | + |
| 278 | +## |
| 279 | +# AWS configuration |
| 280 | +## |
| 281 | + |
| 282 | +# Vault path where the AWS secrets backend is mounted |
| 283 | +aws_vault_mount_path: cloud-iam/aws/012345678901 |
| 284 | + |
| 285 | +# The AWS IAM role name that Attaché will assume to retrieve AWS credentials |
| 286 | +iam_role: my-role |
| 287 | + |
| 288 | +# Disable IMDSv1 |
| 289 | +imds_v1_allowed: false |
| 290 | + |
| 291 | +## |
| 292 | +# GCP configuration |
| 293 | +## |
| 294 | + |
| 295 | +# Vault pathw here the Google Cloud secrets backend is mounted |
| 296 | +gcp_vault_mount_path: cloud-iam/gcp/my-gcp-sandbox |
| 297 | + |
| 298 | +# Mapping of Vault paths to Google Cloud project IDs |
| 299 | +gcp_project_ids: |
| 300 | + cloud-iam/gcp/datadog-sandbox: "012345678901" |
| 301 | + |
| 302 | +## |
| 303 | +# Azure configuration |
| 304 | +## |
| 305 | +azure_vault_mount_path: cloud-iam/azure/my-azure-role |
| 306 | +``` |
0 commit comments