Skip to content
This repository has been archived by the owner on Jan 12, 2024. It is now read-only.

Commit

Permalink
Feature/orphan kms key cleanup job (#172)
Browse files Browse the repository at this point in the history
Create orphan clean up job
  • Loading branch information
fieldju authored Aug 16, 2018
1 parent 076f0ad commit e028d10
Show file tree
Hide file tree
Showing 12 changed files with 554 additions and 38 deletions.
2 changes: 1 addition & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -1514,7 +1514,7 @@ This endpoint does not return any secret data but can be used by Cerberus admins

### Trigger Scheduled Job [POST]

Manually trigger a job, e.g. ExpiredTokenCleanUpJob, HystrixMetricsProcessingJob, KmsKeyCleanUpJob, KpiMetricsProcessingJob.
Manually trigger a job, e.g. ExpiredTokenCleanUpJob, HystrixMetricsProcessingJob, InactiveKmsKeyCleanUpJob, KpiMetricsProcessingJob.
A 400 response code is given if the job wasn't found.

+ Request
Expand Down
4 changes: 3 additions & 1 deletion gradle/check.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,14 @@ jacoco {
toolVersion = "0.7.7.201606060606"
reportsDir = file("$buildDir/reports/jacoco")
}

jacocoTestReport {
reports {
xml.enabled true
csv.enabled false
}
}

test.finalizedBy(project.tasks.jacocoTestReport)

task findbugsHtml {
Expand All @@ -57,4 +59,4 @@ task findbugsHtml {
}
findbugsMain.finalizedBy findbugsHtml

tasks.coveralls.dependsOn check
tasks.coveralls.dependsOn check
20 changes: 20 additions & 0 deletions src/main/java/com/nike/cerberus/domain/AuthKmsKeyMetadata.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.nike.cerberus.domain;

import java.time.OffsetDateTime;
import java.util.Objects;

public class AuthKmsKeyMetadata {

Expand Down Expand Up @@ -64,4 +65,23 @@ public AuthKmsKeyMetadata setLastValidatedTs(OffsetDateTime lastValidatedTs) {
this.lastValidatedTs = lastValidatedTs;
return this;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AuthKmsKeyMetadata that = (AuthKmsKeyMetadata) o;
return Objects.equals(awsIamRoleArn, that.awsIamRoleArn) &&
Objects.equals(awsKmsKeyId, that.awsKmsKeyId) &&
Objects.equals(awsRegion, that.awsRegion) &&
Objects.equals(createdTs, that.createdTs) &&
Objects.equals(lastUpdatedTs, that.lastUpdatedTs) &&
Objects.equals(lastValidatedTs, that.lastValidatedTs);
}

@Override
public int hashCode() {

return Objects.hash(awsIamRoleArn, awsKmsKeyId, awsRegion, createdTs, lastUpdatedTs, lastValidatedTs);
}
}
7 changes: 7 additions & 0 deletions src/main/java/com/nike/cerberus/hystrix/HystrixKmsClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.amazonaws.services.kms.model.EncryptResult;
import com.amazonaws.services.kms.model.GetKeyPolicyRequest;
import com.amazonaws.services.kms.model.GetKeyPolicyResult;
import com.amazonaws.services.kms.model.ListKeysRequest;
import com.amazonaws.services.kms.model.ListKeysResult;
import com.amazonaws.services.kms.model.PutKeyPolicyRequest;
import com.amazonaws.services.kms.model.PutKeyPolicyResult;
import com.amazonaws.services.kms.model.ScheduleKeyDeletionRequest;
Expand Down Expand Up @@ -81,6 +83,11 @@ public PutKeyPolicyResult putKeyPolicy(PutKeyPolicyRequest request) {
return execute("KmsPutKeyPolicy", () -> client.putKeyPolicy(request));
}

public ListKeysResult listKeys(ListKeysRequest request) {
// Default AWS limit was X as of July 2018
return execute("ListKeysRequest", () -> client.listKeys(request));
}

/**
* Execute a function that returns a value in a ThreadPool unique to that command.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@
import javax.inject.Inject;
import javax.inject.Named;

public class KmsKeyCleanUpJob extends LockingJob {
/**
* Scans through the data store and deletes in-active KMS CMKs
*/
public class InactiveKmsKeyCleanUpJob extends LockingJob {

private final Logger logger = LoggerFactory.getLogger(getClass());

Expand All @@ -35,10 +38,10 @@ public class KmsKeyCleanUpJob extends LockingJob {
private final int pauseTimeInSeconds;

@Inject
public KmsKeyCleanUpJob(CleanUpService cleanUpService,
@Named("cms.jobs.KmsCleanUpJob.deleteKmsKeysOlderThanNDays")
public InactiveKmsKeyCleanUpJob(CleanUpService cleanUpService,
@Named("cms.jobs.KmsCleanUpJob.deleteKmsKeysOlderThanNDays")
int expirationPeriodInDays,
@Named("cms.jobs.KmsCleanUpJob.batchPauseTimeInSeconds")
@Named("cms.jobs.KmsCleanUpJob.batchPauseTimeInSeconds")
int pauseTimeInSeconds) {

this.cleanUpService = cleanUpService;
Expand Down
146 changes: 146 additions & 0 deletions src/main/java/com/nike/cerberus/jobs/OrphanedKmsKeyCleanUpJob.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.nike.cerberus.jobs;

import com.amazonaws.regions.Regions;
import com.google.common.collect.Sets;
import com.nike.cerberus.domain.AuthKmsKeyMetadata;
import com.nike.cerberus.service.KmsService;
import org.apache.commons.lang.StringUtils;

import javax.inject.Inject;
import javax.inject.Named;
import java.util.*;
import java.util.stream.Collectors;

import static com.nike.cerberus.service.KmsService.SOONEST_A_KMS_KEY_CAN_BE_DELETED;

/**
* This scans through all KMS keys it can access in all regions and parses the Key Policies to see if it was a key
* that was created by CMS for the current running env. If it is a key that was created by itself for this env,
* it cross references it with the data store to check if it is orphaned.
* If it is orphaned (Created by CMS for the current env, but not in the database), it schedules it for deletion.
*
* Orphaned keys can be created due to a race condition from lazily creating KMS CMKs for auth.
*/
public class OrphanedKmsKeyCleanUpJob extends LockingJob {

private final KmsService kmsService;
private final boolean isDeleteOrphanKeysInDryMode;
private final String environmentName;

@Inject
public OrphanedKmsKeyCleanUpJob(KmsService kmsService,
@Named("cms.kms.delete_orphaned_keys_job.dry_mode")
boolean isDeleteOrphanKeysInDryMode,
@Named("cms.env.name") String environmentName) {

this.kmsService = kmsService;
this.isDeleteOrphanKeysInDryMode = isDeleteOrphanKeysInDryMode;
this.environmentName = environmentName;
}

@Override
protected void executeLockableCode() {

log.info("Fetching the the keys that are in the database");
List<AuthKmsKeyMetadata> authKmsKeyMetadataList = kmsService.getAuthenticationKmsMetadata();

Map<String, Set<String>> orphanedKeysByRegion = new HashMap<>();

// For each region that has KMS
Arrays.stream(Regions.values()).forEach(region -> {
String regionName = region.getName();

// skip china
if (regionName.startsWith("cn")) {
log.debug("KMS isn't in china, skipping...");
return;
}
// skip us gov
if (regionName.startsWith("us-gov")) {
log.debug("Cerberus isn't in us-gov, as z requires special credentials, skipping...");
return;
}

orphanedKeysByRegion.put(regionName, processRegion(authKmsKeyMetadataList, regionName));
});

logCompleteSummary(orphanedKeysByRegion);
}

/**
* Downloads all the KMS CMK policies for the keys in the given region to determine what keys it created and compares
* that set to the set of keys it has in the datastore to find and delete orphaned keys
*
* @param authKmsKeyMetadataList The kms metadata from the data store
* @param regionName The region to process and delete orphaned keys in
* @return The set or orphaned keys it found and processed
*/
protected Set<String> processRegion(List<AuthKmsKeyMetadata> authKmsKeyMetadataList, String regionName) {
log.info("Processing region: {}", regionName);
// Get the KMS Key Ids that are in the db for the current region
Set<String> currentKmsCmkIdsForRegion = authKmsKeyMetadataList.stream()
.filter(authKmsKeyMetadata -> StringUtils.equalsIgnoreCase(authKmsKeyMetadata.getAwsRegion(), regionName))
.map(authKmsKeyMetadata -> {
String fullArn = authKmsKeyMetadata.getAwsKmsKeyId();
return fullArn.split("/")[1];
})
.collect(Collectors.toSet());

log.info("Fetching all KMS CMK ids keys for the region: {}", regionName);
Set<String> allKmsCmkIdsForRegion = kmsService.getKmsKeyIdsForRegion(regionName);
log.info("Found {} keys to process for region: {}", allKmsCmkIdsForRegion.size(), regionName);

log.info("Filtering out the keys that were not created by this environment");
Set<String> kmsCmksCreatedByKmsService = kmsService.filterKeysCreatedByKmsService(allKmsCmkIdsForRegion, regionName);
log.info("Found {} keys to created by this environment process for region: {}", kmsCmksCreatedByKmsService.size(), regionName);

log.info("Calculating difference between the set of keys created by this env to the set of keys in the db");
Set<String> orphanedKmsKeysForRegion = Sets.difference(kmsCmksCreatedByKmsService, currentKmsCmkIdsForRegion);
log.info("Found {} keys that were orphaned for region: {}", orphanedKmsKeysForRegion.size(), regionName);

logRegionSummary(regionName, kmsCmksCreatedByKmsService, currentKmsCmkIdsForRegion, orphanedKmsKeysForRegion);

// Delete the orphaned keys
if (! isDeleteOrphanKeysInDryMode) {
orphanedKmsKeysForRegion.forEach(kmsCmkId -> kmsService
.scheduleKmsKeyDeletion(kmsCmkId, regionName, SOONEST_A_KMS_KEY_CAN_BE_DELETED));
}

return orphanedKmsKeysForRegion;
}

/**
* Logs the summary of actions taken for a given region
*/
private void logRegionSummary(String regionName,
Set<String> kmsCmksCreatedByKmsService,
Set<String> currentKmsCmkIdsForRegion,
Set<String> orphanedKmsKeysForRegion) {

log.info("---------- Orphan KMS Key cleanup job summary for region: {} ------------", regionName);
log.debug("The following keys where determined to be created by CMS service for env: {}", environmentName);
kmsCmksCreatedByKmsService.forEach(log::debug);
log.debug("The following keys were in the data-store for this region");
currentKmsCmkIdsForRegion.forEach(log::debug);
log.info("The following keys were determined to be orphaned and have been scheduled for deletion? {}", !isDeleteOrphanKeysInDryMode);
orphanedKmsKeysForRegion.forEach(log::info);
log.info("--------------------------------------------------------------------------------");

}

/**
* Logs a summary for all regions
*/
private void logCompleteSummary(Map<String, Set<String>> orphanedKeysByRegion) {
log.info("----------- Orphan Kms Key Cleanup Job summary ------------");
orphanedKeysByRegion.forEach((regionName, keys) -> {
log.info("Region: {}, number of orphaned keys: {}", regionName, keys.size());
});
if (isDeleteOrphanKeysInDryMode) {
log.info("The job was in dry mode so the keys were not deleted");
} else {
log.info("The job was not in dry mode so the keys have been scheduled for deletion");
}
log.info("--------------------------------------------------------------------------------");
}
}
13 changes: 9 additions & 4 deletions src/main/java/com/nike/cerberus/service/KmsPolicyService.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ public boolean isPolicyValid(String policyJson) {
*/
protected boolean consumerPrincipalIsAnArnAndNotAnId(String policyJson) {
try {
Policy policy = policyReader.createPolicyFromJsonString(policyJson);
Policy policy = getPolicyFromPolicyString(policyJson);
return policy.getStatements()
.stream()
.anyMatch(statement ->
Expand All @@ -127,7 +127,7 @@ protected boolean consumerPrincipalIsAnArnAndNotAnId(String policyJson) {
*/
protected boolean cmsHasKeyDeletePermissions(String policyJson) {
try {
Policy policy = policyReader.createPolicyFromJsonString(policyJson);
Policy policy = getPolicyFromPolicyString(policyJson);
return policy.getStatements()
.stream()
.anyMatch(statement ->
Expand All @@ -151,7 +151,7 @@ protected boolean cmsHasKeyDeletePermissions(String policyJson) {
* @return - The updated JSON KMS policy containing a regenerated statement for CMS
*/
protected String overwriteCMSPolicy(String policyJson) {
Policy policy = policyReader.createPolicyFromJsonString(policyJson);
Policy policy = getPolicyFromPolicyString(policyJson);
removeStatementFromPolicy(policy, CERBERUS_MANAGEMENT_SERVICE_SID);
Collection<Statement> statements = policy.getStatements();
statements.add(generateStandardCMSPolicyStatement());
Expand All @@ -169,7 +169,7 @@ protected String overwriteCMSPolicy(String policyJson) {
* @return - The updated key policy JSON
*/
protected String removeConsumerPrincipalFromPolicy(String policyJson) {
Policy policy = policyReader.createPolicyFromJsonString(policyJson);
Policy policy = getPolicyFromPolicyString(policyJson);
removeStatementFromPolicy(policy, CERBERUS_CONSUMER_SID);
return policy.toJson();
}
Expand Down Expand Up @@ -259,4 +259,9 @@ public String generateStandardKmsPolicy(String iamRoleArn) {

return kmsPolicy.toJson();
}


public Policy getPolicyFromPolicyString(String jsonString) {
return policyReader.createPolicyFromJsonString(jsonString);
}
}
Loading

0 comments on commit e028d10

Please sign in to comment.