Accessing an application's default credentials from a container running in GCE
[This is an adapted version of an answer I posted on StackOverflow.]
Containers running inside Google Compute Engine (GCE for short) are already authenticated and can simply ask GCE for a new access token when the last one has expired. The application running inside the container just has to detect whether it’s running from within GCE, and if it does, then fetch the new access token from a special URI.
I analyzed Ruby’s open source implementation, that can be accessed here.
Where to find the access token
GCE’s official documentation states that one can obtain the access token at multiple places, and that these places have to be checked in a specific order:
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.
That’s exactly what the Ruby implementation does in its method
get_application_default
:
- the
GOOGLE_APPLICATION_CREDENTIALS
environment variable is checked, - then the
PATH
is checked, - then the default path
/etc/google/auth
is checked, - 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
Detecting GCE environment
The on_gce?
method shows how to check whether we are on GCE by sending a
GET (or 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'