Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(aws): support for IMDSv2 on setting bucket region #3735

Merged
merged 3 commits into from
Feb 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 1 addition & 8 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ fmt: ## Format source code

.PHONY: check
check: ## Perform static code analysis
check: .check-go-version .check-copyright .check-comments .check-errors-wrap \
check: .check-go-version .check-copyright .check-comments \
.check-log-capital-letter .check-timeutc .check-lint .check-vendor

.PHONY: .check-go-version
Expand All @@ -59,13 +59,6 @@ check: .check-go-version .check-copyright .check-comments .check-errors-wrap \
(echo $$f $$e; false); \
done

.PHONY: .check-errors-wrap
.check-errors-wrap:
@set -e; for f in `$(GOFILES)`; do \
! e=`grep -n -E 'errors\.(Errorf|New|Wrap|Wrapf)' $$f | grep -E '("| )fail'` || \
(echo $$f $$e; false); \
done

.PHONY: .check-log-capital-letter
.check-log-capital-letter:
@set -e; for f in `$(GOFILES)`; do \
Expand Down
61 changes: 51 additions & 10 deletions pkg/rclone/aws.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,83 @@ package rclone
import (
"context"
"encoding/json"
"io"
"net/http"
"time"

"github.com/pkg/errors"
"github.com/rclone/rclone/fs"
)

// awsRegionFromMetadataAPI uses instance metadata API to fetch region of the
// running instance see
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html
// awsRegionFromMetadataAPI uses instance metadata API v2 to fetch region of the
// running instance see https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
// Returns empty string if region can't be obtained for whatever reason.
// Fallbacks to IMDSv1 if the session token cannot be obtained.
func awsRegionFromMetadataAPI() string {
const url = "http://169.254.169.254/latest/dynamic/instance-identity/document"
const docURL = "http://169.254.169.254/latest/dynamic/instance-identity/document"

req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, http.NoBody)
// Step 1: Request an IMDSv2 session token
token, err := awsAPIToken()
// fallback to IMDSv1 when error on token retrieval
if err != nil {
fs.Errorf(nil, "%+v", err)
token = ""
}

// Step 2: Use the session token to retrieve instance metadata
reqMetadata, err := http.NewRequestWithContext(context.Background(), http.MethodGet, docURL, http.NoBody)
if err != nil {
fs.Errorf(nil, "create metadata request: %+v", err)
return ""
}
req.Header.Set("User-Agent", UserAgent())
if token != "" {
reqMetadata.Header.Set("X-aws-ec2-metadata-token", token)
}

metadataClient := http.Client{
Timeout: 2 * time.Second,
}
res, err := metadataClient.Do(req)
resMetadata, err := metadataClient.Do(reqMetadata)
if err != nil {
fs.Debugf(nil, "AWS failed to fetch instance identity: %+v", err)
fs.Errorf(nil, "IMDSv2 failed to fetch instance identity: %+v", err)
return ""
}
defer res.Body.Close()
defer resMetadata.Body.Close()

metadata := struct {
Region string `json:"region"`
}{}
if err := json.NewDecoder(res.Body).Decode(&metadata); err != nil {
if err := json.NewDecoder(resMetadata.Body).Decode(&metadata); err != nil {
fs.Errorf(nil, "parse instance region: %+v", err)
return ""
}

return metadata.Region
}

func awsAPIToken() (string, error) {
const tokenURL = "http://169.254.169.254/latest/api/token"

reqToken, err := http.NewRequestWithContext(context.Background(), http.MethodPut, tokenURL, http.NoBody)
if err != nil {
return "", errors.Wrap(err, "create token request")
}
reqToken.Header.Set("X-aws-ec2-metadata-token-ttl-seconds", "21600")
tokenClient := http.Client{
Timeout: 2 * time.Second,
}
resToken, err := tokenClient.Do(reqToken)
if err != nil {
return "", errors.Wrap(err, "IMDSv2 failed to fetch session token")
}
defer resToken.Body.Close()

if resToken.StatusCode != http.StatusOK {
return "", errors.Wrap(err, "failed to retrieve session token")
}
token, err := io.ReadAll(resToken.Body)
if err != nil {
return "", errors.Wrap(err, "failed to read session token")
}
return string(token), nil
}
Loading