cognitect-labs / aws-api

AWS, data driven
Apache License 2.0
726 stars 99 forks source link

Support EC2 instance metadata service v2 (IMDSv2) #165

Open gws opened 3 years ago

gws commented 3 years ago

The instance metadata service v2 (IMDSv2), released in November 2019, offers some defense in depth protections over the previous version. EC2 provides control over the version of the metadata service used, and some security-conscious environments are disabling IMDSv1.

All current AWS SDKs support interacting with the IMDSv2 with a fallback to IMDSv1, and this request is for aws-api to behave similarly.

eneroth commented 1 year ago

This is a working (but rough; code review left as exercise for reader) solution, mostly copied from the v1 version in the lib:

(ns some-ns
  (:require
    [clojure.core.async :as a]
    [clojure.data.json :as json]
    [clojure.string :as str]
    [cognitect.aws.client.shared :as client-shared]
    [cognitect.aws.credentials :as credentials]
    [cognitect.aws.http :as http]
    [cognitect.aws.retry :as retry]
    [cognitect.aws.util :as u])
  (:import
    [java.net URI]))

(def token-timeout
  "Declares the lifetime of the AWS token before it should be refreshed."
  21600)

(defn- request-map
  ([address]
   (request-map address nil))
  ([address more]
   (let [uri (URI. address)]
     (merge-with (fn merge-if-map 
                   [x y]
                   (if (and (map? x) (map? y))
                     (merge x y)
                     y))
       {:scheme         (.getScheme uri)
        :server-name    (.getHost uri)
        :server-port    (cond
                          (pos? (.getPort uri)) (.getPort uri)
                          (= :https (.getScheme uri)) 443
                          :else 80)
        :uri            (.getPath uri)
        :request-method :get
        :headers        {:accept "*/*"}}
       more))))

(defn- http-request
  [http-client request-map]
  (let [response (a/<!! (retry/with-retry
                          #(http/submit http-client request-map)
                          (a/promise-chan)
                          retry/default-retriable?
                          retry/default-backoff))]
    ;; TODO: handle unhappy paths -JS
    (when (= 200 (:status response))
      (u/bbuf->str (:body response)))))

(defn- get-token
  [http-client]
  (http-request http-client
    (request-map "http://169.254.169.254/latest/api/token"
      {:headers        {"X-aws-ec2-metadata-token-ttl-seconds" (str token-timeout)}
       :request-method :put})))

(defn- get-credential-names
  [http-client token]
  (some-> (http-request http-client
            (request-map "http://169.254.169.254/latest/meta-data/iam/security-credentials"
              {:headers {"X-aws-ec2-metadata-token" token}}))
    (str/split-lines)))

(defn- get-credentials-string
  [http-client token credentials-name]
  (http-request http-client
    (request-map (str "http://169.254.169.254/latest/meta-data/iam/security-credentials/" credentials-name)
      {:headers {"X-aws-ec2-metadata-token" token}})))

(defn- get-imds-v2-credentials
  [http-client]
  (when-let [token (get-token http-client)]
    (first 
      (eduction
        (map (partial get-credentials-string http-client token))
        (filter string?)
        (map #(json/read-str % :key-fn keyword))
        (take 1)
        (get-credential-names http-client token)))))

(defn imds-v2-credentials-provider
  "Attempt to retrieve credentials from Amazon Metadata Service v2."
  [http-client]
  (credentials/cached-credentials-with-auto-refresh
    (reify credentials/CredentialsProvider
      (fetch
        [_]
        (when-let [creds (get-imds-v2-credentials http-client)]
          (credentials/valid-credentials
            {:aws/access-key-id             (:AccessKeyId creds)
             :aws/secret-access-key         (:SecretAccessKey creds)
             :aws/session-token             (:Token creds)
             :cognitect.aws.credentials/ttl (credentials/calculate-ttl creds)}
            "ec2 instance"))))))

(defn default-credentials-provider
  "Returns a chain-credentials-provider with (in order):
    environment-credentials-provider
    system-property-credentials-provider
    profile-credentials-provider
    container-credentials-provider
    instance-profile-credentials-provider
    imds-v2-credentials-provider
  Alpha. Subject to change."
  []
  (let [http-client (client-shared/http-client)]
    (credentials/chain-credentials-provider
      [(credentials/environment-credentials-provider)
       (credentials/system-property-credentials-provider)
       (credentials/profile-credentials-provider)
       (credentials/container-credentials-provider http-client)
       (credentials/instance-profile-credentials-provider http-client)
       (imds-v2-credentials-provider http-client)])))

For example;

(def s3
  (aws/client
    {:api                  :s3
     :credentials-provider (some-ns/default-credentials-provider)}))
iarenaza commented 9 months ago

Are there any plans to implement this? We were just bitten hard by this one and took us quite a bit to discover why the calls where failing :smile:

pieterbreed commented 1 month ago

Hi all,

Kindly reference this AWS blog post (Nov '23) https://aws.amazon.com/blogs/aws/amazon-ec2-instance-metadata-service-imdsv2-by-default/

[Mid 2024] Newly released Amazon EC2 instance types will use IMDSv2 only by default...

It seems as if AWS is moving away from IMDSv1, making it harder and harder to remain on v1. For example: In mid-July 2024 the public ubuntu AMIs are now defaulting to IMSDv2.