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?
Ruby's implementation is open source and can be accessed here.
The get_application_default
method clearly shows that:
/etc/google/auth
is checked,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:
The environment variable GOOGLE_APPLICATION_CREDENTIALS is checked. If this variable is specified it should point to a file that defines the credentials. [...]
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.
If you are running in Google App Engine production, the built-in service account associated with the application will be used.
If you are running in Google Compute Engine production, the built-in service account associated with the virtual machine instance will be used.
If none of these conditions is true, an error will occur.
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
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'