Skip to content

Commit e1a8c9c

Browse files
authored
Merge pull request #6 from DataDog/docs
Add readme
2 parents b7eb591 + 28345a6 commit e1a8c9c

File tree

2 files changed

+305
-1
lines changed

2 files changed

+305
-1
lines changed

README.md

Lines changed: 305 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,306 @@
11
# Attaché
2-
Attaché provides an emulation layer for Cloud Provider IMDS APIs
2+
3+
[![made-with-Go](https://img.shields.io/badge/Made%20with-Go-1f425f.svg)](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+
```

attache.png

55.9 KB
Loading

0 commit comments

Comments
 (0)