diff --git a/Makefile b/Makefile index 455c308dad..2a7e5966b3 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 \ diff --git a/pkg/rclone/aws.go b/pkg/rclone/aws.go index 3c901ffefd..ec8bf47ca2 100644 --- a/pkg/rclone/aws.go +++ b/pkg/rclone/aws.go @@ -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 +}