GCS Repository: Add secure storage of credentials (#24697)

This commit adds gcs credential settings to the elasticsearch keystore.
The setting name follows the same pattern as the s3 client settings,
beginning with `gcs.client.`, followed by the client name, and then the
setting name, in this case, `credentials_file`. Using the legacy service
file setting is also deprecated.
This commit is contained in:
Ryan Ernst 2017-05-16 17:17:37 -07:00 committed by GitHub
parent f80799acc2
commit d74760c306
7 changed files with 216 additions and 74 deletions

View File

@ -41,34 +41,19 @@ The bucket should now be created.
The plugin supports two authentication modes:
* the built-in <<repository-gcs-using-compute-engine, Compute Engine authentication>>. This mode is
* The built-in <<repository-gcs-using-compute-engine, Compute Engine authentication>>. This mode is
recommended if your elasticsearch node is running on a Compute Engine virtual machine.
* the <<repository-gcs-using-service-account, Service Account>> authentication mode.
* Specifying <<repository-gcs-using-service-account, Service Account>> credentials.
[[repository-gcs-using-compute-engine]]
===== Using Compute Engine
When running on Compute Engine, the plugin use the Google's built-in authentication mechanism to
When running on Compute Engine, the plugin use Google's built-in authentication mechanism to
authenticate on the Storage service. Compute Engine virtual machines are usually associated to a
default service account. This service account can be found in the VM instance details in the
https://console.cloud.google.com/compute/[Compute Engine console].
To indicate that a repository should use the built-in authentication,
the repository `service_account` setting must be set to `_default_`:
[source,js]
----
PUT _snapshot/my_gcs_repository_on_compute_engine
{
"type": "gcs",
"settings": {
"bucket": "my_bucket",
"service_account": "_default_"
}
}
----
// CONSOLE
// TEST[skip:we don't have gcs setup while testing this]
This is the default authentication mode and requires no configuration.
NOTE: The Compute Engine VM must be allowed to use the Storage service. This can be done only at VM
creation time, when "Storage" access can be configured to "Read/Write" permission. Check your
@ -76,7 +61,7 @@ instance details at the section "Cloud API access scopes".
[[repository-gcs-using-service-account]]
===== Using a Service Account
If your elasticsearch node is not running on Compute Engine, or if you don't want to use Google
If your elasticsearch node is not running on Compute Engine, or if you don't want to use Google's
built-in authentication mechanism, you can authenticate on the Storage service using a
https://cloud.google.com/iam/docs/overview#service_account[Service Account] file.
@ -107,10 +92,14 @@ A service account file looks like this:
----
// NOTCONSOLE
This file must be copied in the `config` directory of the elasticsearch installation and on
every node of the cluster.
This file must be stored in the <<secure-settings, elasticsearch keystore>>, under a setting name
of the form `gcs.client.NAME.credentials_file`, where `NAME` is the name of the client congiguration.
The default client name is `default`, but a different client name can be specified in repository
settings using `client`.
To indicate that a repository should use a service account file:
For example, if specifying the credentials file in the keystore under
`gcs.client.my_alternate_client.credentials_file`, you can configure a repository to use these
credentials like this:
[source,js]
----
@ -119,7 +108,7 @@ PUT _snapshot/my_gcs_repository
"type": "gcs",
"settings": {
"bucket": "my_bucket",
"service_account": "service_account.json"
"client": "my_alternate_client"
}
}
----
@ -150,8 +139,7 @@ PUT _snapshot/my_gcs_repository
{
"type": "gcs",
"settings": {
"bucket": "my_bucket",
"service_account": "service_account.json"
"bucket": "my_bucket"
}
}
----
@ -164,10 +152,10 @@ The following settings are supported:
The name of the bucket to be used for snapshots. (Mandatory)
`service_account`::
`client`::
The service account to use. It can be a relative path to a service account JSON file
or the value `_default_` that indicate to use built-in Compute Engine service account.
The client congfiguration to use. This controls which credentials are used to connect
to Compute Engine.
`base_path`::

View File

@ -22,10 +22,12 @@ package org.elasticsearch.repositories.gcs;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import com.google.api.client.auth.oauth2.TokenRequest;
import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.json.GoogleJsonError;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpHeaders;
@ -39,6 +41,8 @@ import com.google.api.services.storage.model.Bucket;
import com.google.api.services.storage.model.Objects;
import com.google.api.services.storage.model.StorageObject;
import org.elasticsearch.SpecialPermission;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.Plugin;
@ -108,9 +112,15 @@ public class GoogleCloudStoragePlugin extends Plugin implements RepositoryPlugin
});
}
private final Map<String, GoogleCredential> credentials;
public GoogleCloudStoragePlugin(Settings settings) {
credentials = GoogleCloudStorageService.loadClientCredentials(settings);
}
// overridable for tests
protected GoogleCloudStorageService createStorageService(Environment environment) {
return new GoogleCloudStorageService.InternalGoogleCloudStorageService(environment);
return new GoogleCloudStorageService.InternalGoogleCloudStorageService(environment, credentials);
}
@Override
@ -118,4 +128,9 @@ public class GoogleCloudStoragePlugin extends Plugin implements RepositoryPlugin
return Collections.singletonMap(GoogleCloudStorageRepository.TYPE,
(metadata) -> new GoogleCloudStorageRepository(metadata, env, namedXContentRegistry, createStorageService(env)));
}
@Override
public List<Setting<?>> getSettings() {
return Collections.singletonList(GoogleCloudStorageService.CREDENTIALS_FILE_SETTING);
}
}

View File

@ -48,25 +48,26 @@ class GoogleCloudStorageRepository extends BlobStoreRepository {
static final ByteSizeValue MIN_CHUNK_SIZE = new ByteSizeValue(1, ByteSizeUnit.BYTES);
static final ByteSizeValue MAX_CHUNK_SIZE = new ByteSizeValue(100, ByteSizeUnit.MB);
public static final String TYPE = "gcs";
static final String TYPE = "gcs";
public static final TimeValue NO_TIMEOUT = timeValueMillis(-1);
static final TimeValue NO_TIMEOUT = timeValueMillis(-1);
public static final Setting<String> BUCKET =
static final Setting<String> BUCKET =
simpleString("bucket", Property.NodeScope, Property.Dynamic);
public static final Setting<String> BASE_PATH =
static final Setting<String> BASE_PATH =
simpleString("base_path", Property.NodeScope, Property.Dynamic);
public static final Setting<Boolean> COMPRESS =
static final Setting<Boolean> COMPRESS =
boolSetting("compress", false, Property.NodeScope, Property.Dynamic);
public static final Setting<ByteSizeValue> CHUNK_SIZE =
static final Setting<ByteSizeValue> CHUNK_SIZE =
byteSizeSetting("chunk_size", MAX_CHUNK_SIZE, MIN_CHUNK_SIZE, MAX_CHUNK_SIZE, Property.NodeScope, Property.Dynamic);
public static final Setting<String> APPLICATION_NAME =
static final Setting<String> APPLICATION_NAME =
new Setting<>("application_name", GoogleCloudStoragePlugin.NAME, Function.identity(), Property.NodeScope, Property.Dynamic);
public static final Setting<String> SERVICE_ACCOUNT =
simpleString("service_account", Property.NodeScope, Property.Dynamic);
public static final Setting<TimeValue> HTTP_READ_TIMEOUT =
static final Setting<String> SERVICE_ACCOUNT =
new Setting<>("service_account", "_default_", Function.identity(), Property.NodeScope, Property.Dynamic, Property.Deprecated);
static final Setting<String> CLIENT_NAME = new Setting<>("client", "default", Function.identity());
static final Setting<TimeValue> HTTP_READ_TIMEOUT =
timeSetting("http.read_timeout", NO_TIMEOUT, Property.NodeScope, Property.Dynamic);
public static final Setting<TimeValue> HTTP_CONNECT_TIMEOUT =
static final Setting<TimeValue> HTTP_CONNECT_TIMEOUT =
timeSetting("http.connect_timeout", NO_TIMEOUT, Property.NodeScope, Property.Dynamic);
private final ByteSizeValue chunkSize;
@ -81,7 +82,8 @@ class GoogleCloudStorageRepository extends BlobStoreRepository {
String bucket = getSetting(BUCKET, metadata);
String application = getSetting(APPLICATION_NAME, metadata);
String serviceAccount = getSetting(SERVICE_ACCOUNT, metadata);
String serviceAccount = SERVICE_ACCOUNT.get(metadata.settings());
String clientName = CLIENT_NAME.get(metadata.settings());
String basePath = BASE_PATH.get(metadata.settings());
if (Strings.hasLength(basePath)) {
@ -113,7 +115,7 @@ class GoogleCloudStorageRepository extends BlobStoreRepository {
logger.debug("using bucket [{}], base_path [{}], chunk_size [{}], compress [{}], application [{}]",
bucket, basePath, chunkSize, compress, application);
Storage client = storageService.createClient(serviceAccount, application, connectTimeout, readTimeout);
Storage client = storageService.createClient(serviceAccount, clientName, application, connectTimeout, readTimeout);
this.blobStore = new GoogleCloudStorageBlobStore(settings, bucket, client);
}

View File

@ -35,28 +35,43 @@ import com.google.api.services.storage.StorageScopes;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.SecureSetting;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.env.Environment;
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
interface GoogleCloudStorageService {
String SETTINGS_PREFIX = "gcs.client.";
/** A json credentials file loaded from secure settings. */
Setting.AffixSetting<InputStream> CREDENTIALS_FILE_SETTING = Setting.affixKeySetting(SETTINGS_PREFIX, "credentials_file",
key -> SecureSetting.secureFile(key, null));
/**
* Creates a client that can be used to manage Google Cloud Storage objects.
*
* @param serviceAccount path to service account file
* @param clientName name of client settings to use from secure settings
* @param application name of the application
* @param connectTimeout connection timeout for HTTP requests
* @param readTimeout read timeout for HTTP requests
* @return a Client instance that can be used to manage objects
*/
Storage createClient(String serviceAccount, String application, TimeValue connectTimeout, TimeValue readTimeout) throws Exception;
Storage createClient(String serviceAccount, String clientName, String application,
TimeValue connectTimeout, TimeValue readTimeout) throws Exception;
/**
* Default implementation
@ -67,58 +82,60 @@ interface GoogleCloudStorageService {
private final Environment environment;
InternalGoogleCloudStorageService(Environment environment) {
/** Credentials identified by client name. */
private final Map<String, GoogleCredential> credentials;
InternalGoogleCloudStorageService(Environment environment, Map<String, GoogleCredential> credentials) {
super(environment.settings());
this.environment = environment;
this.credentials = credentials;
}
@Override
public Storage createClient(String serviceAccount, String application, TimeValue connectTimeout, TimeValue readTimeout)
throws Exception {
public Storage createClient(String serviceAccountFile, String clientName, String application,
TimeValue connectTimeout, TimeValue readTimeout) throws Exception {
try {
GoogleCredential credentials = (DEFAULT.equalsIgnoreCase(serviceAccount)) ? loadDefault() : loadCredentials(serviceAccount);
GoogleCredential credential = getCredential(serviceAccountFile, clientName);
NetHttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
Storage.Builder storage = new Storage.Builder(httpTransport, JacksonFactory.getDefaultInstance(),
new DefaultHttpRequestInitializer(credentials, connectTimeout, readTimeout));
new DefaultHttpRequestInitializer(credential, connectTimeout, readTimeout));
storage.setApplicationName(application);
logger.debug("initializing client with service account [{}/{}]",
credentials.getServiceAccountId(), credentials.getServiceAccountUser());
credential.getServiceAccountId(), credential.getServiceAccountUser());
return storage.build();
} catch (IOException e) {
throw new ElasticsearchException("Error when loading Google Cloud Storage credentials file", e);
}
}
/**
* HTTP request initializer that loads credentials from the service account file
* and manages authentication for HTTP requests
*/
private GoogleCredential loadCredentials(String serviceAccount) throws IOException {
if (serviceAccount == null) {
throw new ElasticsearchException("Cannot load Google Cloud Storage service account file from a null path");
}
Path account = environment.configFile().resolve(serviceAccount);
if (Files.exists(account) == false) {
throw new ElasticsearchException("Unable to find service account file [" + serviceAccount
// pkg private for tests
GoogleCredential getCredential(String serviceAccountFile, String clientName) throws IOException {
if (DEFAULT.equalsIgnoreCase(serviceAccountFile) == false) {
deprecationLogger.deprecated("Using GCS service account file from disk is deprecated. " +
"Move the file into the elasticsearch keystore.");
Path account = environment.configFile().resolve(serviceAccountFile);
if (Files.exists(account) == false) {
throw new IllegalArgumentException("Unable to find service account file [" + serviceAccountFile
+ "] defined for repository");
}
try (InputStream is = Files.newInputStream(account)) {
GoogleCredential credential = GoogleCredential.fromStream(is);
if (credential.createScopedRequired()) {
credential = credential.createScoped(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL));
}
return credential;
try (InputStream is = Files.newInputStream(account)) {
GoogleCredential credential = GoogleCredential.fromStream(is);
if (credential.createScopedRequired()) {
credential = credential.createScoped(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL));
}
return credential;
}
} else if (credentials.containsKey(clientName)) {
return credentials.get(clientName);
}
return getDefaultCredential();
}
/**
* HTTP request initializer that loads default credentials when running on Compute Engine
*/
private GoogleCredential loadDefault() throws IOException {
// pkg private for tests
GoogleCredential getDefaultCredential() throws IOException {
return GoogleCredential.getApplicationDefault();
}
@ -172,4 +189,23 @@ interface GoogleCloudStorageService {
}
}
}
/** Load all secure credentials from the settings. */
static Map<String, GoogleCredential> loadClientCredentials(Settings settings) {
Set<String> clientNames = settings.getGroups(SETTINGS_PREFIX).keySet();
Map<String, GoogleCredential> credentials = new HashMap<>();
for (String clientName : clientNames) {
Setting<InputStream> concreteSetting = CREDENTIALS_FILE_SETTING.getConcreteSettingForNamespace(clientName);
try (InputStream credStream = concreteSetting.get(settings)) {
GoogleCredential credential = GoogleCredential.fromStream(credStream);
if (credential.createScopedRequired()) {
credential = credential.createScoped(Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL));
}
credentials.put(clientName, credential);
} catch (IOException e) {
throw new UncheckedIOException(e);
}
}
return credentials;
}
}

View File

@ -67,6 +67,9 @@ public class GoogleCloudStorageBlobStoreRepositoryTests extends ESBlobStoreRepos
}
public static class MockGoogleCloudStoragePlugin extends GoogleCloudStoragePlugin {
public MockGoogleCloudStoragePlugin() {
super(Settings.EMPTY);
}
@Override
protected GoogleCloudStorageService createStorageService(Environment environment) {
return new MockGoogleCloudStorageService();
@ -75,7 +78,8 @@ public class GoogleCloudStorageBlobStoreRepositoryTests extends ESBlobStoreRepos
public static class MockGoogleCloudStorageService implements GoogleCloudStorageService {
@Override
public Storage createClient(String serviceAccount, String application, TimeValue connectTimeout, TimeValue readTimeout) throws
public Storage createClient(String serviceAccount, String accountName, String application,
TimeValue connectTimeout, TimeValue readTimeout) throws
Exception {
return storage.get();
}

View File

@ -0,0 +1,85 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.repositories.gcs;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collections;
import java.util.Map;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.repositories.gcs.GoogleCloudStorageService.InternalGoogleCloudStorageService;
import org.elasticsearch.test.ESTestCase;
import static org.hamcrest.Matchers.containsString;
public class GoogleCloudStorageServiceTests extends ESTestCase {
private InputStream getDummyCredentialStream() throws IOException {
return GoogleCloudStorageServiceTests.class.getResourceAsStream("/dummy-account.json");
}
public void testDefaultCredential() throws Exception {
Environment env = new Environment(Settings.builder().put("path.home", createTempDir()).build());
GoogleCredential cred = GoogleCredential.fromStream(getDummyCredentialStream());
InternalGoogleCloudStorageService service = new InternalGoogleCloudStorageService(env, Collections.emptyMap()) {
@Override
GoogleCredential getDefaultCredential() throws IOException {
return cred;
}
};
assertSame(cred, service.getCredential("_default_", "default"));
}
public void testFileCredentialBackcompat() throws Exception {
Path home = createTempDir();
Path config = home.resolve("config");
Files.createDirectories(config);
Settings settings = Settings.builder()
.put("path.home", home).build();
Environment env = new Environment(settings);
Files.copy(getDummyCredentialStream(), config.resolve("test-cred.json"));
InternalGoogleCloudStorageService service = new InternalGoogleCloudStorageService(env, Collections.emptyMap());
GoogleCredential cred = service.getCredential("test-cred.json", "default");
assertEquals("some-project-name@appspot.gserviceaccount.com", cred.getServiceAccountId());
assertWarnings("Using GCS service account file from disk is deprecated. Move the file into the elasticsearch keystore.");
}
public void testFileCredentialMissing() throws Exception {
Environment env = new Environment(Settings.builder().put("path.home", createTempDir()).build());
InternalGoogleCloudStorageService service = new InternalGoogleCloudStorageService(env, Collections.emptyMap());
IllegalArgumentException e = expectThrows(IllegalArgumentException.class, () ->
service.getCredential("test-cred.json", "default"));
assertThat(e.getMessage(), containsString("Unable to find service account file"));
assertWarnings("Using GCS service account file from disk is deprecated. Move the file into the elasticsearch keystore.");
}
public void testClientCredential() throws Exception {
GoogleCredential cred = GoogleCredential.fromStream(getDummyCredentialStream());
Map<String, GoogleCredential> credentials = Collections.singletonMap("clientname", cred);
Environment env = new Environment(Settings.builder().put("path.home", createTempDir()).build());
InternalGoogleCloudStorageService service = new InternalGoogleCloudStorageService(env, credentials);
assertSame(cred, service.getCredential("_default_", "clientname"));
}
}

View File

@ -0,0 +1,12 @@
{
"type": "service_account",
"project_id": "some-project-name",
"private_key_id": "c7cefcb7c72a2880ecce49cb9d1095de5a61aff0",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDa+7r0RE1YykXC\n+d+DXlN3Dg3aL1YOfYuhy5PF/Vi0FFQHyXuPtAvkVHZD2NxMDZq2DxTu3AVLh1UE\nt2hMrWjDQDuArPl8FezpyYQwde04Qlx1YpQ1xUjTaFWd0hrOZEfsxY00h3ilxR3G\nJsofR3PZBKYI11VGruNemCgjiJg5hcoJDxLXUgcfpKJaiPeHutczCeZ1RANQwQF1\n/dPXhqbtWiaS/iu5so64P54TsrVX5DcXmbGr6hQAReIcI6cjA8QhSu6QBtdvEPhv\n27uTuSu4XRtTh3djVGFzFV9pamGZeGELkTiHVSDI8IkQ32s8yuP5Zys/4bFJk7nn\niqJpe0/DAgMBAAECggEAEexQnPWKLx4/H3o8JRBvXGs2DwmYzY7RAukaqzXVMMgJ\nKKoBBv4Biyquk1cIkOD8LLKHUBWKCWiGOOCaFMyMqo5zUFDYCqPwxCHOQ/ki9VvZ\nHXJ4Fv6Su1rqxwQPVZ03ldWFfSspYMgFa9Z47J54iOasgES/og1mZrOldWMUsoBu\nCKf0fH+vIsxWPwmRtyxKCMwqenqdc22nGGLhmpm8tuw1eQp6XtTXagqkPtAVMMga\nmgC0EGqhZA/IklGW1JuGWELjXVMgS/tLIPq+hYsmY14y6Ie032YoSMWkz6Z5p7i0\n/JwCzVZNO1mD0MwVj7nDmokXOpoyM7Qcbx8r1E4Q4QKBgQDxqAZ6D+A671mCNU0J\n6Qzc3cOZq7MBj4y7M/2qPXHC7i/DdbmnM7PPPriaBBch2nX7jZRlRmVDBsmrC4OG\n3m5+HAx7YPVbefwe6h5ki5O9wg1pLcgYY9uvgLSlD85lVZKAzO7QK2W5zfM19kPD\nSckIa+U7DKFbwKhtCsxcP6ARJwKBgQDn+zAPHepGP2Zf78pOLlVXcQqVA1uOu+bW\nrG4G+lPrytB0C4QdwWdBV3Lcqmnw/ae5PkQBs0dCbtWG8+MT8gA6k5kleflaZrAY\ngdUJIUP6J7ocWYxVTfqGFyFF1n5VT8/jbVucaT7izBZfZvlGyf7Vz7ewQzgWQWlK\nCQ0qstV2BQKBgQCajAQAYlDcQCC1dlMbqHDye91RVQ65S84MF1b+Xid4LA5d6dde\nyGERhKJY1Y7ZtrZHt6cVEe1G7XtiKY3nXi+59URCT6L66svEFaR0VxOYgxdCkeXr\nO0nPNvfQrIgqJIz6VJXSij6XktAdTa7OoUyxVxeWKSC05kSQ4BwMTyCWdwKBgQCW\noqlmZ4qE6w5TJaY8diG8kg7JDFEbsjAHHhikN1DfP+d0MzYrDDc8WsifOZlpf4y1\n4RTP9dZD8Sx+YUgG35H+d3FuwHGGnj+i6kunjg5SFhHn7s4NZoFTKRnV+541T4oy\nqARg4IaRRu0QLhGYQfpUZHlm339AFGGGTbJbE51A8QKBgQDTEN5O+3bRG3Fa1J6z\nU9PMrjjs6l8xhXFso10YEYG5KRnfhzCFujyWNiLE6WrlUL8invVBaCxsZr51GDgA\nhyEEdm4kXCRrv4JyhOvIuGxNcAIiQK/e91UQEM6u1t6hUI1rE7ZOyJQzBxj9hFlV\n7OvhBlHXQUtAOdq0XLHr9GzdSA==\n-----END PRIVATE KEY-----\n",
"client_email": "some-project-name@appspot.gserviceaccount.com",
"client_id": "123456789101112130594",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/some-project-name%40appspot.gserviceaccount.com"
}