Make secretmanager primary storage for keyring (#1124)

* Make secretmanager primary storage for keyring

Also removed the migrate_kms_keyring command.
This commit is contained in:
Weimin Yu 2021-05-10 11:11:26 -04:00 committed by GitHub
parent b64a49597c
commit fe4d72be89
6 changed files with 28 additions and 147 deletions

View file

@ -21,9 +21,6 @@ import static google.registry.model.common.EntityGroupRoot.getCrossTldKey;
import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm; import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import com.google.common.flogger.FluentLogger; import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key; import com.googlecode.objectify.Key;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
@ -37,7 +34,6 @@ import google.registry.privileges.secretmanager.KeyringSecretStore;
import java.io.IOException; import java.io.IOException;
import java.util.Arrays; import java.util.Arrays;
import java.util.Optional; import java.util.Optional;
import java.util.stream.Stream;
import javax.inject.Inject; import javax.inject.Inject;
import org.bouncycastle.openpgp.PGPException; import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPKeyPair; import org.bouncycastle.openpgp.PGPKeyPair;
@ -51,6 +47,7 @@ import org.bouncycastle.openpgp.PGPPublicKey;
* @see <a href="https://cloud.google.com/kms/docs/">Google Cloud Key Management Service * @see <a href="https://cloud.google.com/kms/docs/">Google Cloud Key Management Service
* Documentation</a> * Documentation</a>
*/ */
// TODO(2021-06-01): rename this class to SecretManagerKeyring and delete KmsSecretRevision
public class KmsKeyring implements Keyring { public class KmsKeyring implements Keyring {
private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@ -221,8 +218,7 @@ public class KmsKeyring implements Keyring {
try { try {
return kmsConnection.decrypt(keyName, encryptedData); return kmsConnection.decrypt(keyName, encryptedData);
} catch (Exception e) { } catch (Exception e) {
throw new KeyringException( return new byte[0];
String.format("CloudKMS decrypt operation failed for secret %s", keyName), e);
} }
} }
@ -230,7 +226,7 @@ public class KmsKeyring implements Keyring {
try { try {
return secretStore.getSecret(keyName); return secretStore.getSecret(keyName);
} catch (Exception e) { } catch (Exception e) {
return new byte[0]; throw new KeyringException("Failed to retrieve secret for " + keyName, e);
} }
} }
@ -243,44 +239,6 @@ public class KmsKeyring implements Keyring {
return secretStoreData; return secretStoreData;
} }
logger.atWarning().log("Values for %s in Datastore and Secret Manager do not match.", keyName); logger.atWarning().log("Values for %s in Datastore and Secret Manager do not match.", keyName);
return dsData; return secretStoreData;
}
/**
* Generates the tasks to migrate secrets from Datastore to Secret Manager.
*
* <p>The keys in the returned {@link ImmutableMap} are the names of the secrets that need
* migration. The values in the map are {@link Runnable Runnables} that copy secret data from
* Datastore to Secret Manager for their corresponding keys. Only secrets that are absent in
* Secret Manager or have inconsistent values are included in the returned map.
*/
public ImmutableMap<String, Runnable> migrationPlan() {
ImmutableMap.Builder<String, Runnable> tasks = new ImmutableMap.Builder<>();
ImmutableList<String> labels =
Streams.concat(
Stream.of(PrivateKeyLabel.values()).map(PrivateKeyLabel::getLabel),
Stream.of(PublicKeyLabel.values()).map(PublicKeyLabel::getLabel),
Stream.of(StringKeyLabel.values()).map(StringKeyLabel::getLabel))
.collect(ImmutableList.toImmutableList());
for (String keyName : labels) {
byte[] dsData;
try {
dsData = getDecryptedDataFromDatastore(keyName);
} catch (IllegalStateException e) {
logger.atWarning().log("Cannot load %s from Datastore. Skipping...", keyName);
continue;
}
byte[] secretStoreData = getDataFromSecretStore(keyName);
if (Arrays.equals(dsData, secretStoreData)) {
logger.atInfo().log("%s is already up to date.\n", keyName);
continue;
}
logger.atInfo().log("%s needs to be migrated.\n", keyName);
tasks.put(keyName, () -> secretStore.createOrUpdateSecret(keyName, dsData));
}
return tasks.build();
} }
} }

View file

@ -36,6 +36,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull; import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeySerializer; import google.registry.keyring.api.KeySerializer;
import google.registry.keyring.kms.KmsKeyring.PrivateKeyLabel; import google.registry.keyring.kms.KmsKeyring.PrivateKeyLabel;
@ -57,7 +58,9 @@ import org.bouncycastle.openpgp.PGPPublicKey;
* The {@link KmsUpdater} accumulates updates to a {@link KmsKeyring} and persists them to KMS and * The {@link KmsUpdater} accumulates updates to a {@link KmsKeyring} and persists them to KMS and
* Datastore when closed. * Datastore when closed.
*/ */
// TODO(2021-06-01): rename this class to SecretManagerKeyringUpdater
public final class KmsUpdater { public final class KmsUpdater {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final KmsConnection kmsConnection; private final KmsConnection kmsConnection;
private final KeyringSecretStore secretStore; private final KeyringSecretStore secretStore;
@ -126,29 +129,30 @@ public final class KmsUpdater {
} }
/** /**
* Generates new encryption keys in KMS, encrypts the updated secrets with them, and persists the * Persists the secrets in the Secret Manager (primary) and the Datastore (secondary).
* encrypted secrets to Datastore.
* *
* <p>The operations in this method are organized so that existing {@link KmsSecretRevision} * <p>Updates to the Secret Manager are not transactional. If an error happens, the successful
* entities remain primary and decryptable if a failure occurs. * updates are not reverted; unwritten updates are aborted. This is not a problem right now, since
* this class is only used by the {@code UpdateKmsKeyringCommand}, which is invoked manually and
* only updates one secret at a time.
*/ */
public void update() { public void update() {
checkState(!secretValues.isEmpty(), "At least one Keyring value must be persisted"); checkState(!secretValues.isEmpty(), "At least one Keyring value must be persisted");
persistEncryptedValues(encryptValues(secretValues));
// Errors when writing to secret store can be thrown to the top, since writes are always
// executed by a human user using the UpdateKmsKeyringCommand.
try { try {
secretValues for (Map.Entry<String, byte[]> e : secretValues.entrySet()) {
.entrySet() secretStore.createOrUpdateSecret(e.getKey(), e.getValue());
.forEach(e -> secretStore.createOrUpdateSecret(e.getKey(), e.getValue())); logger.atInfo().log("Secret %s updated.", e.getKey());
}
} catch (RuntimeException e) { } catch (RuntimeException e) {
throw new RuntimeException( throw new RuntimeException(
"Failed to persist secrets to Secret Manager. " "Failed to persist secrets to Secret Manager. "
+ "Please check the status of Secret Manager and re-run the command.", + "Please check the status of Secret Manager and re-run the command.",
e); e);
} }
// TODO(2021-06-01): remove the writes to Datastore
persistEncryptedValues(encryptValues(secretValues));
} }
/** /**

View file

@ -1,78 +0,0 @@
// Copyright 2021 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.tools;
import static com.google.common.base.Preconditions.checkState;
import com.beust.jcommander.Parameters;
import google.registry.keyring.api.Keyring;
import google.registry.keyring.kms.KmsKeyring;
import google.registry.privileges.secretmanager.KeyringSecretStore;
import java.util.Map;
import javax.inject.Inject;
/** Migrates secrets from the KMS keyring to the Secret Manager. */
@Parameters(
separators = " =",
commandDescription = "Migrate values of secrets in KmsKeyring to Secret Manager.")
public class MigrateKmsKeyringCommand extends ConfirmingCommand implements CommandWithRemoteApi {
@Inject Keyring keyring;
@Inject KeyringSecretStore secretStore;
Map<String, Runnable> migrationTasks;
@Inject
MigrateKmsKeyringCommand() {}
@Override
protected void init() {
checkState(
keyring instanceof KmsKeyring,
"Expecting KmsKeyring, found %s",
keyring.getClass().getSimpleName());
migrationTasks = ((KmsKeyring) keyring).migrationPlan();
}
@Override
protected boolean dontRunCommand() {
return migrationTasks.isEmpty();
}
@Override
protected String prompt() {
if (migrationTasks.isEmpty()) {
return "All keys are up to date.";
}
return String.format("Migrate %s keys?", migrationTasks.size());
}
@Override
protected String execute() {
int errors = 0;
for (Map.Entry<String, Runnable> entry : migrationTasks.entrySet()) {
try {
entry.getValue().run();
} catch (Exception e) {
System.err.printf("Failed to migrate %s: %s", entry.getKey(), e.getMessage());
errors++;
}
}
return errors == 0 ? "Success!" : "Failed to migrate " + errors + "keys.";
}
}

View file

@ -103,7 +103,6 @@ public final class RegistryTool {
.put("lock_domain", LockDomainCommand.class) .put("lock_domain", LockDomainCommand.class)
.put("login", LoginCommand.class) .put("login", LoginCommand.class)
.put("logout", LogoutCommand.class) .put("logout", LogoutCommand.class)
.put("migrate_kms_keyring", MigrateKmsKeyringCommand.class)
.put("pending_escrow", PendingEscrowCommand.class) .put("pending_escrow", PendingEscrowCommand.class)
.put("populate_null_registrar_fields", PopulateNullRegistrarFieldsCommand.class) .put("populate_null_registrar_fields", PopulateNullRegistrarFieldsCommand.class)
.put("registrar_contact", RegistrarContactCommand.class) .put("registrar_contact", RegistrarContactCommand.class)

View file

@ -135,8 +135,6 @@ interface RegistryToolComponent {
void inject(LogoutCommand command); void inject(LogoutCommand command);
void inject(MigrateKmsKeyringCommand command);
void inject(PendingEscrowCommand command); void inject(PendingEscrowCommand command);
void inject(RenewDomainCommand command); void inject(RenewDomainCommand command);

View file

@ -44,12 +44,12 @@ class KmsKeyringTest {
AppEngineExtension.builder().withDatastoreAndCloudSql().build(); AppEngineExtension.builder().withDatastoreAndCloudSql().build();
private KmsKeyring keyring; private KmsKeyring keyring;
private KeyringSecretStore fakeSecretStore =
new KeyringSecretStore(new FakeSecretManagerClient());
@BeforeEach @BeforeEach
void beforeEach() { void beforeEach() {
keyring = keyring = new KmsKeyring(new FakeKmsConnection(), fakeSecretStore);
new KmsKeyring(
new FakeKmsConnection(), new KeyringSecretStore(new FakeSecretManagerClient()));
} }
@TestOfyAndSql @TestOfyAndSql
@ -154,7 +154,7 @@ class KmsKeyringTest {
assertThat(jsonCredential).isEqualTo("json-credential-stringmoo"); assertThat(jsonCredential).isEqualTo("json-credential-stringmoo");
} }
private static void persistSecret(String secretName, byte[] secretValue) { private void persistSecret(String secretName, byte[] secretValue) {
KmsConnection kmsConnection = new FakeKmsConnection(); KmsConnection kmsConnection = new FakeKmsConnection();
KmsSecretRevision secretRevision = KmsSecretRevision secretRevision =
@ -165,22 +165,22 @@ class KmsKeyringTest {
.build(); .build();
KmsSecret secret = KmsSecret.create(secretName, secretRevision); KmsSecret secret = KmsSecret.create(secretName, secretRevision);
tm().transact(() -> tm().putAll(secretRevision, secret)); tm().transact(() -> tm().putAll(secretRevision, secret));
fakeSecretStore.createOrUpdateSecret(secretName, secretValue);
} }
private static void saveCleartextSecret(String secretName) { private void saveCleartextSecret(String secretName) {
persistSecret(secretName, KeySerializer.serializeString(secretName + "moo")); persistSecret(secretName, KeySerializer.serializeString(secretName + "moo"));
} }
private static void savePublicKeySecret(String publicKeyName) throws Exception { private void savePublicKeySecret(String publicKeyName) throws Exception {
persistSecret(publicKeyName, KeySerializer.serializePublicKey(KmsTestHelper.getPublicKey())); persistSecret(publicKeyName, KeySerializer.serializePublicKey(KmsTestHelper.getPublicKey()));
} }
private static void savePrivateKeySecret(String privateKeyName) throws Exception { private void savePrivateKeySecret(String privateKeyName) throws Exception {
persistSecret(privateKeyName, KeySerializer.serializeKeyPair(KmsTestHelper.getKeyPair())); persistSecret(privateKeyName, KeySerializer.serializeKeyPair(KmsTestHelper.getKeyPair()));
} }
private static void saveKeyPairSecret(String publicKeyName, String privateKeyName) private void saveKeyPairSecret(String publicKeyName, String privateKeyName) throws Exception {
throws Exception {
savePublicKeySecret(publicKeyName); savePublicKeySecret(publicKeyName);
savePrivateKeySecret(privateKeyName); savePrivateKeySecret(privateKeyName);
} }