Skip to content

Commit 6f10afc

Browse files
authored
Merge pull request #124 from kubescape/feature/aws-imds
Adding cloudmetadata through IMDS
2 parents ee5bd6b + 190cc71 commit 6f10afc

File tree

1 file changed

+149
-0
lines changed

1 file changed

+149
-0
lines changed

cloudmetadata/awsimds.go

+149
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package cloudmetadata
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"time"
10+
11+
apitypes "github.com/armosec/armoapi-go/armotypes"
12+
)
13+
14+
const (
15+
metadataEndpoint = "http://169.254.169.254"
16+
defaultTimeout = 2 * time.Second
17+
tokenPath = "/latest/api/token"
18+
tokenTTL = "21600" // 6 hours in seconds
19+
)
20+
21+
type MetadataClient struct {
22+
client *http.Client
23+
useIMDSv2 bool
24+
}
25+
26+
// NewMetadataClient creates a new client for fetching EC2 metadata
27+
func NewMetadataClient(useIMDSv2 bool) *MetadataClient {
28+
return &MetadataClient{
29+
client: &http.Client{
30+
Timeout: defaultTimeout,
31+
},
32+
useIMDSv2: useIMDSv2,
33+
}
34+
}
35+
36+
// getToken fetches an IMDSv2 token
37+
func (m *MetadataClient) getToken(ctx context.Context) (string, error) {
38+
req, err := http.NewRequestWithContext(ctx, "PUT", metadataEndpoint+tokenPath, nil)
39+
if err != nil {
40+
return "", fmt.Errorf("creating token request: %w", err)
41+
}
42+
43+
req.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", tokenTTL)
44+
45+
resp, err := m.client.Do(req)
46+
if err != nil {
47+
return "", fmt.Errorf("requesting token: %w", err)
48+
}
49+
defer resp.Body.Close()
50+
51+
if resp.StatusCode != http.StatusOK {
52+
return "", fmt.Errorf("token request failed with status: %d", resp.StatusCode)
53+
}
54+
55+
token, err := io.ReadAll(resp.Body)
56+
if err != nil {
57+
return "", fmt.Errorf("reading token response: %w", err)
58+
}
59+
60+
return string(token), nil
61+
}
62+
63+
// get makes a GET request to the metadata service
64+
func (m *MetadataClient) get(ctx context.Context, path string) ([]byte, error) {
65+
req, err := http.NewRequestWithContext(ctx, "GET", metadataEndpoint+path, nil)
66+
if err != nil {
67+
return nil, fmt.Errorf("creating request: %w", err)
68+
}
69+
70+
if m.useIMDSv2 {
71+
token, err := m.getToken(ctx)
72+
if err != nil {
73+
return nil, fmt.Errorf("getting IMDSv2 token: %w", err)
74+
}
75+
req.Header.Set("X-aws-ec2-metadata-token", token)
76+
}
77+
78+
resp, err := m.client.Do(req)
79+
if err != nil {
80+
return nil, fmt.Errorf("making request: %w", err)
81+
}
82+
defer resp.Body.Close()
83+
84+
if resp.StatusCode != http.StatusOK {
85+
return nil, fmt.Errorf("request failed with status: %d", resp.StatusCode)
86+
}
87+
88+
data, err := io.ReadAll(resp.Body)
89+
if err != nil {
90+
return nil, fmt.Errorf("reading response: %w", err)
91+
}
92+
93+
return data, nil
94+
}
95+
96+
// getMetadataValue is a helper function to get a single metadata value
97+
func (m *MetadataClient) getMetadataValue(ctx context.Context, path string) (string, error) {
98+
data, err := m.get(ctx, "/latest/meta-data/"+path)
99+
if err != nil {
100+
return "", err
101+
}
102+
return string(data), nil
103+
}
104+
105+
// GetMetadata fetches all available instance metadata
106+
func (m *MetadataClient) GetMetadata(ctx context.Context) (*apitypes.CloudMetadata, error) {
107+
// Get instance identity document first
108+
data, err := m.get(ctx, "/latest/dynamic/instance-identity/document")
109+
if err != nil {
110+
return nil, fmt.Errorf("fetching instance identity document: %w", err)
111+
}
112+
113+
var identityDoc struct {
114+
Region string `json:"region"`
115+
InstanceID string `json:"instanceId"`
116+
InstanceType string `json:"instanceType"`
117+
AccountID string `json:"accountId"`
118+
PrivateIP string `json:"privateIp"`
119+
AvailabilityZone string `json:"availabilityZone"`
120+
}
121+
122+
if err := json.Unmarshal(data, &identityDoc); err != nil {
123+
return nil, fmt.Errorf("parsing identity document: %w", err)
124+
}
125+
126+
// Get public IP (might not be available)
127+
publicIP, _ := m.getMetadataValue(ctx, "public-ipv4")
128+
129+
// Get hostname
130+
hostname, err := m.getMetadataValue(ctx, "hostname")
131+
if err != nil {
132+
// Fallback to local-hostname
133+
hostname, _ = m.getMetadataValue(ctx, "local-hostname")
134+
}
135+
136+
metadata := &apitypes.CloudMetadata{
137+
Provider: ProviderAWS,
138+
InstanceID: identityDoc.InstanceID,
139+
InstanceType: identityDoc.InstanceType,
140+
Region: identityDoc.Region,
141+
Zone: identityDoc.AvailabilityZone,
142+
PrivateIP: identityDoc.PrivateIP,
143+
PublicIP: publicIP,
144+
Hostname: hostname,
145+
AccountID: identityDoc.AccountID,
146+
}
147+
148+
return metadata, nil
149+
}

0 commit comments

Comments
 (0)