class Google::Auth::ExternalAccount::AwsCredentials

This module handles the retrieval of credentials from Google Cloud by utilizing the AWS EC2 metadata service and then exchanging the credentials for a short-lived Google Cloud access token.

Constants

IMDSV2_TOKEN_EXPIRATION_IN_SECONDS

Constant for imdsv2 session token expiration in seconds

Attributes

client_id[R]

Will always be nil, but method still gets used.

Public Class Methods

new(options = {}) click to toggle source
# File lib/googleauth/external_account/aws_credentials.rb, line 37
def initialize options = {}
  base_setup options

  @audience = options[:audience]
  @credential_source = options[:credential_source] || {}
  @environment_id = @credential_source[:environment_id]
  @region_url = @credential_source[:region_url]
  @credential_verification_url = @credential_source[:url]
  @regional_cred_verification_url = @credential_source[:regional_cred_verification_url]
  @imdsv2_session_token_url = @credential_source[:imdsv2_session_token_url]

  # These will be lazily loaded when needed, or will raise an error if not provided
  @region = nil
  @request_signer = nil
  @imdsv2_session_token = nil
  @imdsv2_session_token_expiry = nil
end

Public Instance Methods

retrieve_subject_token!() click to toggle source

Retrieves the subject token using the credential_source object. The subject token is a serialized [AWS GetCallerIdentity signed request](

https://cloud.google.com/iam/docs/access-resources-aws#exchange-token).

The logic is summarized as:

Retrieve the AWS region from the AWS_REGION or AWS_DEFAULT_REGION environment variable or from the AWS metadata server availability-zone if not found in the environment variable.

Check AWS credentials in environment variables. If not found, retrieve from the AWS metadata server security-credentials endpoint.

When retrieving AWS credentials from the metadata server security-credentials endpoint, the AWS role needs to be determined by # calling the security-credentials endpoint without any argument. Then the credentials can be retrieved via: security-credentials/role_name

Generate the signed request to AWS STS GetCallerIdentity action.

Inject x-goog-cloud-target-resource into header and serialize the signed request. This will be the subject-token to pass to GCP STS.

@return [string] The retrieved subject token.

# File lib/googleauth/external_account/aws_credentials.rb, line 78
def retrieve_subject_token!
  if @request_signer.nil?
    @region = region
    @request_signer = AwsRequestSigner.new @region
  end

  request = {
    method: "POST",
    url: @regional_cred_verification_url.sub("{region}", @region)
  }

  request_options = @request_signer.generate_signed_request fetch_security_credentials, request

  request_headers = request_options[:headers]
  request_headers["x-goog-cloud-target-resource"] = @audience

  aws_signed_request = {
    headers: [],
    method: request_options[:method],
    url: request_options[:url]
  }

  aws_signed_request[:headers] = request_headers.keys.sort.map do |key|
    { key: key, value: request_headers[key] }
  end

  uri_escape aws_signed_request.to_json
end

Private Instance Methods

fetch_metadata_role_name() click to toggle source

Retrieves the AWS role currently attached to the current AWS workload by querying the AWS metadata server. This is needed for the AWS metadata server security credentials endpoint in order to retrieve the AWS security credentials needed to sign requests to AWS APIs.

# File lib/googleauth/external_account/aws_credentials.rb, line 184
def fetch_metadata_role_name
  unless @credential_verification_url
    raise "Unable to determine the AWS metadata server security credentials endpoint"
  end

  get_aws_resource(@credential_verification_url, "IAM Role").body
end
fetch_metadata_security_credentials(role_name) click to toggle source

Retrieves the AWS security credentials required for signing AWS requests from the AWS metadata server.

# File lib/googleauth/external_account/aws_credentials.rb, line 193
def fetch_metadata_security_credentials role_name
  response = get_aws_resource "#{@credential_verification_url}/#{role_name}", "credentials"
  MultiJson.load response.body
end
fetch_security_credentials() click to toggle source

Retrieves the AWS security credentials required for signing AWS requests from either the AWS security credentials environment variables or from the AWS metadata server.

# File lib/googleauth/external_account/aws_credentials.rb, line 157
def fetch_security_credentials
  env_aws_access_key_id = ENV[CredentialsLoader::AWS_ACCESS_KEY_ID_VAR]
  env_aws_secret_access_key = ENV[CredentialsLoader::AWS_SECRET_ACCESS_KEY_VAR]
  # This is normally not available for permanent credentials.
  env_aws_session_token = ENV[CredentialsLoader::AWS_SESSION_TOKEN_VAR]

  if env_aws_access_key_id && env_aws_secret_access_key
    return {
      access_key_id: env_aws_access_key_id,
      secret_access_key: env_aws_secret_access_key,
      session_token: env_aws_session_token
    }
  end

  role_name = fetch_metadata_role_name
  credentials = fetch_metadata_security_credentials role_name

  {
    access_key_id: credentials["AccessKeyId"],
    secret_access_key: credentials["SecretAccessKey"],
    session_token: credentials["Token"]
  }
end
get_aws_resource(url, name, data: nil, headers: {}) click to toggle source
# File lib/googleauth/external_account/aws_credentials.rb, line 130
def get_aws_resource url, name, data: nil, headers: {}
  begin
    headers["x-aws-ec2-metadata-token"] = imdsv2_session_token
    response = if data
                 headers["Content-Type"] = "application/json"
                 connection.post url, data, headers
               else
                 connection.get url, nil, headers
               end

    raise Faraday::Error unless response.success?
    response
  rescue Faraday::Error
    raise "Failed to retrieve AWS #{name}."
  end
end
imdsv2_session_token() click to toggle source
# File lib/googleauth/external_account/aws_credentials.rb, line 109
def imdsv2_session_token
  return @imdsv2_session_token unless imdsv2_session_token_invalid?
  raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
  begin
    response = connection.put @imdsv2_session_token_url do |req|
      req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
    end
  rescue Faraday::Error => e
    raise "Fetching AWS IMDSV2 token error: #{e}"
  end
  raise Faraday::Error unless response.success?
  @imdsv2_session_token = response.body
  @imdsv2_session_token_expiry = Time.now + IMDSV2_TOKEN_EXPIRATION_IN_SECONDS
  @imdsv2_session_token
end
imdsv2_session_token_invalid?() click to toggle source
# File lib/googleauth/external_account/aws_credentials.rb, line 125
def imdsv2_session_token_invalid?
  return true if @imdsv2_session_token.nil?
  @imdsv2_session_token_expiry.nil? || @imdsv2_session_token_expiry < Time.now
end
region() click to toggle source
# File lib/googleauth/external_account/aws_credentials.rb, line 198
def region
  @region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]

  unless @region
    raise "region_url or region must be set for external account credentials" unless @region_url

    @region ||= get_aws_resource(@region_url, "region").body[0..-2]
  end

  @region
end
uri_escape(string) click to toggle source
# File lib/googleauth/external_account/aws_credentials.rb, line 147
def uri_escape string
  if string.nil?
    nil
  else
    CGI.escape(string.encode("UTF-8")).gsub("+", "%20").gsub("%7E", "~")
  end
end