How to access application default credentials from a GCE container without any Google API binding?

4/14/2017

I am writing an application that runs in a container, on Google Container Engine, in a language that doesn't have any binding to the Google API.

I need to access the application default credentials. Unfortunately the official documentation doesn't explain how to do such a thing in production environment without using one of the existing bindings to the Google API.

In development environment (i.e. on my local dev machine) I export the GOOGLE_APPLICATION_CREDENTIALS variable, but it isn't available in the production container. Does it mean I have to use some endpoint from the REST API?

-- Antoine
google-cloud-platform
google-kubernetes-engine

1 Answer

4/15/2017

Ruby's implementation is open source and can be accessed here.

Different locations to check by priority

The get_application_default method clearly shows that:

  1. the GOOGLE_APPLICATION_CREDENTIALS environment variable is checked,
  2. then the PATH is checked,
  3. then the default path /etc/google/auth is checked,
  4. finally, if still nothing and on a compute instance, a new access token is fetched.
def get_application_default(scope = nil, options = {})
  creds = DefaultCredentials.from_env(scope) ||
          DefaultCredentials.from_well_known_path(scope) ||
          DefaultCredentials.from_system_default_path(scope)
  return creds unless creds.nil?
  raise NOT_FOUND_ERROR unless GCECredentials.on_gce?(options)
  GCECredentials.new
end

It is consistent with what says the official documentation:

  1. The environment variable GOOGLE_APPLICATION_CREDENTIALS is checked. If this variable is specified it should point to a file that defines the credentials. [...]

  2. If you have installed the Google Cloud SDK on your machine and have run the command gcloud auth application-default login, your identity can be used as a proxy to test code calling APIs from that machine.

  3. If you are running in Google App Engine production, the built-in service account associated with the application will be used.

  4. If you are running in Google Compute Engine production, the built-in service account associated with the virtual machine instance will be used.

  5. If none of these conditions is true, an error will occur.

Detecting GCE environment

The on_gce? method shows how to check whether we are on GCE by sending a GET/HEAD HTTP request to http://169.254.169.254. If there is a Metadata-Flavor: Google header in the response, then it's probably GCE.

def on_gce?(options = {})
  c = options[:connection] || Faraday.default_connection
  resp = c.get(COMPUTE_CHECK_URI) do |req|
    # Comment from: oauth2client/client.py
    #
    # Note: the explicit `timeout` below is a workaround. The underlying
    # issue is that resolving an unknown host on some networks will take
    # 20-30 seconds; making this timeout short fixes the issue, but
    # could lead to false negatives in the event that we are on GCE, but
    # the metadata resolution was particularly slow. The latter case is
    # "unlikely".
    req.options.timeout = 0.1
  end
  return false unless resp.status == 200
  return false unless resp.headers.key?('Metadata-Flavor')
  return resp.headers['Metadata-Flavor'] == 'Google'
rescue Faraday::TimeoutError, Faraday::ConnectionFailed
  return false
end

Fetching an access token directly from Google

If the default credentials could not be found on the filesystem and the application is running on GCE, we can ask a new access token without any prior authentication. This is possible because of the default service account, that is created automatically when GCE is enabled in a project.

The fetch_access_token method shows how, from a GCE instance, we can get a new access token by simply issuing a GET request to http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token.

def fetch_access_token(options = {})
  c = options[:connection] || Faraday.default_connection
  c.headers = { 'Metadata-Flavor' => 'Google' }
  resp = c.get(COMPUTE_AUTH_TOKEN_URI)
  case resp.status
  when 200
    Signet::OAuth2.parse_credentials(resp.body,
                                     resp.headers['content-type'])
  when 404
    raise(Signet::AuthorizationError, NO_METADATA_SERVER_ERROR)
  else
    msg = "Unexpected error code #{resp.status}" + UNEXPECTED_ERROR_SUFFIX
    raise(Signet::AuthorizationError, msg)
  end
end

Here is a curl command to illustrate:

curl \
  http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token \
  -H 'accept: application/json' \
  -H 'Metadata-Flavor: Google'
-- Antoine
Source: StackOverflow