From 2d95ddaa488575e51712b5a8d31aacdf2144f0e9 Mon Sep 17 00:00:00 2001 From: Sean Lin Date: Wed, 28 Aug 2019 09:36:22 -0700 Subject: [PATCH] Add command to add/rotate JWT secrets (#139) --- .../com/nike/cerberus/ConfigConstants.java | 1 + .../com/nike/cerberus/cli/CerberusRunner.java | 20 +----- .../cli/EnvironmentConfigToArgsMapper.java | 3 + .../command/core/AddJwtSecretCommand.java | 52 ++++++++++++++ .../domain/environment/JwtSecret.java | 69 +++++++++++++++++++ .../domain/environment/JwtSecretData.java | 32 +++++++++ .../operation/core/AddJwtSecretOperation.java | 54 +++++++++++++++ .../nike/cerberus/service/KeyGenerator.java | 38 ++++++++++ .../com/nike/cerberus/store/ConfigStore.java | 59 +++++++++++++++- .../cerberus/service/KeyGeneratorTest.java | 43 ++++++++++++ .../nike/cerberus/store/ConfigStoreTest.java | 10 ++- 11 files changed, 361 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/nike/cerberus/command/core/AddJwtSecretCommand.java create mode 100644 src/main/java/com/nike/cerberus/domain/environment/JwtSecret.java create mode 100644 src/main/java/com/nike/cerberus/domain/environment/JwtSecretData.java create mode 100644 src/main/java/com/nike/cerberus/operation/core/AddJwtSecretOperation.java create mode 100644 src/main/java/com/nike/cerberus/service/KeyGenerator.java create mode 100644 src/test/java/com/nike/cerberus/service/KeyGeneratorTest.java diff --git a/src/main/java/com/nike/cerberus/ConfigConstants.java b/src/main/java/com/nike/cerberus/ConfigConstants.java index 9d2e53c8..7f24a560 100644 --- a/src/main/java/com/nike/cerberus/ConfigConstants.java +++ b/src/main/java/com/nike/cerberus/ConfigConstants.java @@ -33,6 +33,7 @@ public class ConfigConstants { public static final String CERT_PART_PUBKEY = "pubkey.pem"; public static final String CERT_ACME_ACCOUNT_PRIVATE_KEY = "certificates/acme/account-private-key-pkcs1.pem"; public static final String CMS_ENV_CONFIG_PATH = "cms/environment.properties"; + public static final String JWT_SECRETS_PATH = "jwt-secrets.json"; public static final String VERSION_PROPERTY = "cli.version"; public static final String CMS_ADMIN_GROUP_KEY = "cms.admin.group"; public static final String ROOT_USER_ARN_KEY = "root.user.arn"; diff --git a/src/main/java/com/nike/cerberus/cli/CerberusRunner.java b/src/main/java/com/nike/cerberus/cli/CerberusRunner.java index a80746cb..4c0034dd 100644 --- a/src/main/java/com/nike/cerberus/cli/CerberusRunner.java +++ b/src/main/java/com/nike/cerberus/cli/CerberusRunner.java @@ -42,29 +42,12 @@ import com.nike.cerberus.command.composite.*; import com.nike.cerberus.command.certificates.GenerateAndRotateCertificatesCommand; import com.nike.cerberus.command.certificates.RotateCertificatesCommand; -import com.nike.cerberus.command.core.CreateAlbLogAthenaDbAndTableCommand; -import com.nike.cerberus.command.core.InitializeEnvironmentCommand; -import com.nike.cerberus.command.core.SyncConfigCommand; +import com.nike.cerberus.command.core.*; import com.nike.cerberus.command.rds.CleanUpRdsSnapshotsCommand; import com.nike.cerberus.command.rds.CopyRdsSnapshotsCommand; import com.nike.cerberus.command.rds.CreateDatabaseCommand; -import com.nike.cerberus.command.core.CreateEdgeDomainRecordCommand; -import com.nike.cerberus.command.core.CreateLoadBalancerCommand; -import com.nike.cerberus.command.core.CreateRoute53Command; -import com.nike.cerberus.command.core.CreateSecurityGroupsCommand; -import com.nike.cerberus.command.core.CreateVpcCommand; -import com.nike.cerberus.command.core.CreateWafCommand; import com.nike.cerberus.command.certificates.DeleteOldestCertificatesCommand; -import com.nike.cerberus.command.core.DeleteStackCommand; -import com.nike.cerberus.command.core.GenerateCertificateFilesCommand; -import com.nike.cerberus.command.core.PrintStackInfoCommand; -import com.nike.cerberus.command.core.RestoreCerberusBackupCommand; -import com.nike.cerberus.command.core.RebootCmsCommand; -import com.nike.cerberus.command.core.UpdateStackCommand; import com.nike.cerberus.command.certificates.UploadCertificateFilesCommand; -import com.nike.cerberus.command.core.ViewConfigCommand; -import com.nike.cerberus.command.core.WhitelistCidrForVpcAccessCommand; -import com.nike.cerberus.command.core.UpdateStackTagsCommand; import com.nike.cerberus.command.rds.XRegionDatabaseReplicationCommand; import com.nike.cerberus.domain.input.EnvironmentConfig; import com.nike.cerberus.logging.LoggingConfigurer; @@ -257,6 +240,7 @@ private void registerAllCommands() { registerCommand(new CreateAlbLogAthenaDbAndTableCommand()); registerCommand(new CreateCmsResourcesForRegionCommand()); registerCommand(new XRegionDatabaseReplicationCommand()); + registerCommand(new AddJwtSecretCommand()); } /** diff --git a/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java b/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java index daec9762..a4107bd3 100644 --- a/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java +++ b/src/main/java/com/nike/cerberus/cli/EnvironmentConfigToArgsMapper.java @@ -177,6 +177,9 @@ public static List getArgsForCommand(EnvironmentConfig environmentConfig case XRegionDatabaseReplicationCommand.COMMAND_NAME: args = Arrays.asList(passedArgs); break; + case AddJwtSecretCommand.COMMAND_NAME: + args = Arrays.asList(passedArgs); + break; default: break; } diff --git a/src/main/java/com/nike/cerberus/command/core/AddJwtSecretCommand.java b/src/main/java/com/nike/cerberus/command/core/AddJwtSecretCommand.java new file mode 100644 index 00000000..f4988b57 --- /dev/null +++ b/src/main/java/com/nike/cerberus/command/core/AddJwtSecretCommand.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * 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 com.nike.cerberus.command.core; + +import com.beust.jcommander.Parameter; +import com.beust.jcommander.Parameters; +import com.nike.cerberus.command.Command; +import com.nike.cerberus.operation.Operation; +import com.nike.cerberus.operation.core.AddJwtSecretOperation; + +import static com.nike.cerberus.command.core.AddJwtSecretCommand.COMMAND_NAME; + +/** + * Command for add/rotate JWT secret for CMS + */ +@Parameters(commandNames = COMMAND_NAME, commandDescription = "Add/rotate JWT secret for CMS") +public class AddJwtSecretCommand implements Command { + + public static final String COMMAND_NAME = "add-jwt-secret"; + public static final String ACTIVATION_DELAY_LONG_ARG = "--activation-delay"; + + @Override + public String getCommandName() { + return COMMAND_NAME; + } + + @Parameter(names = ACTIVATION_DELAY_LONG_ARG, description = "delay in second before the secret can be used to sign JWT") + private long activationDelay = 5 * 60; + + public long getActivationDelay() { + return activationDelay; + } + + @Override + public Class> getOperationClass() { + return AddJwtSecretOperation.class; + } +} diff --git a/src/main/java/com/nike/cerberus/domain/environment/JwtSecret.java b/src/main/java/com/nike/cerberus/domain/environment/JwtSecret.java new file mode 100644 index 00000000..63f521f9 --- /dev/null +++ b/src/main/java/com/nike/cerberus/domain/environment/JwtSecret.java @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * 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 com.nike.cerberus.domain.environment; + +public class JwtSecret { + private String id; + + private String secret; + + private String algorithm; + + private long effectiveTs; + + private long createdTs; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getSecret() { + return secret; + } + + public void setSecret(String secret) { + this.secret = secret; + } + + public long getEffectiveTs() { + return effectiveTs; + } + + public void setEffectiveTs(long effectiveTs) { + this.effectiveTs = effectiveTs; + } + + public long getCreatedTs() { + return createdTs; + } + + public void setCreatedTs(long createdTs) { + this.createdTs = createdTs; + } + + public String getAlgorithm() { + return algorithm; + } + + public void setAlgorithm(String algorithm) { + this.algorithm = algorithm; + } +} diff --git a/src/main/java/com/nike/cerberus/domain/environment/JwtSecretData.java b/src/main/java/com/nike/cerberus/domain/environment/JwtSecretData.java new file mode 100644 index 00000000..308c3c3e --- /dev/null +++ b/src/main/java/com/nike/cerberus/domain/environment/JwtSecretData.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * 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 com.nike.cerberus.domain.environment; + +import java.util.LinkedList; + + +public class JwtSecretData { + private LinkedList jwtSecrets = new LinkedList<>(); + + public LinkedList getJwtSecrets() { + return jwtSecrets; + } + + public void setJwtSecrets(LinkedList jwtSecrets) { + this.jwtSecrets = jwtSecrets; + } +} diff --git a/src/main/java/com/nike/cerberus/operation/core/AddJwtSecretOperation.java b/src/main/java/com/nike/cerberus/operation/core/AddJwtSecretOperation.java new file mode 100644 index 00000000..f65130d9 --- /dev/null +++ b/src/main/java/com/nike/cerberus/operation/core/AddJwtSecretOperation.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * 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 com.nike.cerberus.operation.core; + +import com.nike.cerberus.command.core.AddJwtSecretCommand; +import com.nike.cerberus.operation.Operation; +import com.nike.cerberus.store.ConfigStore; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.inject.Inject; + +/** + * Operation for add/rotate JWT secrets. + */ +public class AddJwtSecretOperation implements Operation { + + + private final Logger logger = LoggerFactory.getLogger(getClass()); + + private final ConfigStore configStore; + + @Inject + public AddJwtSecretOperation(ConfigStore configStore) { + + this.configStore = configStore; + } + + @Override + public void run(AddJwtSecretCommand command) { + long activationDelay = command.getActivationDelay(); + configStore.addJwtKey(activationDelay); + + } + + @Override + public boolean isRunnable(AddJwtSecretCommand command) { + return true; + } +} diff --git a/src/main/java/com/nike/cerberus/service/KeyGenerator.java b/src/main/java/com/nike/cerberus/service/KeyGenerator.java new file mode 100644 index 00000000..ad16d1b4 --- /dev/null +++ b/src/main/java/com/nike/cerberus/service/KeyGenerator.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * 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 com.nike.cerberus.service; + +import javax.crypto.SecretKey; +import java.security.NoSuchAlgorithmException; + +/** + * Generate a key + */ +public class KeyGenerator { + public static final String HMACSHA512 = "HmacSHA512"; + + + public SecretKey generateKey(String algorithm) { + javax.crypto.KeyGenerator gen; + try { + gen = javax.crypto.KeyGenerator.getInstance(algorithm); + return gen.generateKey(); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("The " + algorithm + " algorithm is not available."); + } + } +} diff --git a/src/main/java/com/nike/cerberus/store/ConfigStore.java b/src/main/java/com/nike/cerberus/store/ConfigStore.java index 79d3dad3..ee7b36a3 100644 --- a/src/main/java/com/nike/cerberus/store/ConfigStore.java +++ b/src/main/java/com/nike/cerberus/store/ConfigStore.java @@ -43,15 +43,19 @@ import com.nike.cerberus.domain.cloudformation.VpcParameters; import com.nike.cerberus.domain.environment.EnvironmentData; import com.nike.cerberus.domain.environment.CertificateInformation; +import com.nike.cerberus.domain.environment.JwtSecret; +import com.nike.cerberus.domain.environment.JwtSecretData; import com.nike.cerberus.domain.environment.RegionData; import com.nike.cerberus.domain.environment.Stack; import com.nike.cerberus.service.AwsClientFactory; import com.nike.cerberus.service.CloudFormationService; import com.nike.cerberus.service.EncryptionService; +import com.nike.cerberus.service.KeyGenerator; import com.nike.cerberus.service.S3StoreService; import com.nike.cerberus.service.SaltGenerator; import com.nike.cerberus.service.StoreService; import com.nike.cerberus.util.CloudFormationObjectMapper; +import com.nike.cerberus.util.UuidSupplier; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.openssl.jcajce.JcaPEMWriter; import org.shredzone.acme4j.util.KeyPairUtils; @@ -65,6 +69,9 @@ import java.io.StringReader; import java.io.StringWriter; import java.security.KeyPair; +import java.time.Instant; +import java.util.Base64; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -78,6 +85,7 @@ import static com.nike.cerberus.module.CerberusModule.CONFIG_OBJECT_MAPPER; import static com.nike.cerberus.module.CerberusModule.CONFIG_REGION; import static com.nike.cerberus.module.CerberusModule.ENV_NAME; +import static com.nike.cerberus.service.KeyGenerator.HMACSHA512; /** * Abstraction for accessing the configuration storage bucket and the environment state stored in it. @@ -105,6 +113,10 @@ public class ConfigStore { private final EncryptionService encryptionService; + private final KeyGenerator keyGenerator; + private UuidSupplier uuidSupplier; + + private Map storeServiceMap = new HashMap<>(); @Inject @@ -116,7 +128,9 @@ public ConfigStore(AwsClientFactory amazonS3ClientFactory, CloudFormationObjectMapper cloudFormationObjectMapper, @Named(ENV_NAME) String environmentName, @Named(CONFIG_REGION) String configRegion, - EncryptionService encryptionService) { + EncryptionService encryptionService, + KeyGenerator keyGenerator, + UuidSupplier uuidSupplier) { this.cloudFormationService = cloudFormationService; this.configObjectMapper = configObjectMapper; @@ -127,6 +141,8 @@ public ConfigStore(AwsClientFactory amazonS3ClientFactory, this.environmentName = environmentName; this.configRegion = Regions.fromName(configRegion); this.encryptionService = encryptionService; + this.keyGenerator = keyGenerator; + this.uuidSupplier = uuidSupplier; } /** @@ -195,6 +211,37 @@ public Optional getAcmeAccountKeyPair() { } } + /** + * Add/rotate JWT secret for CMS + * @param activationDelayInSecond Delay in second before the secret can be used to sign JWT + */ + public void addJwtKey(long activationDelayInSecond) { + Optional jwtKeysFile = storeServiceMap.get(configRegion).get(JWT_SECRETS_PATH); + JwtSecretData jwtSecretData; + if (jwtKeysFile.isPresent()) { + try { + jwtSecretData = configObjectMapper.readValue(encryptionService.decrypt(jwtKeysFile.get()), JwtSecretData.class); + } catch (IOException e) { + throw new IllegalStateException("Unable to read JWT secret data!", e); + } + logger.info("Found existing JWT secret data"); + } else { + logger.info("JWT secret data not found"); + jwtSecretData = new JwtSecretData(); + } + LinkedList jwtSecrets = jwtSecretData.getJwtSecrets(); + + JwtSecret jwtSecret = new JwtSecret(); + jwtSecret.setAlgorithm(HMACSHA512); + jwtSecret.setId(uuidSupplier.get()); + jwtSecret.setCreatedTs(new Date().getTime()); + jwtSecret.setEffectiveTs(Date.from(Instant.now().plusSeconds(activationDelayInSecond)).getTime()); + jwtSecret.setSecret(Base64.getEncoder().encodeToString(keyGenerator.generateKey(HMACSHA512).getEncoded())); + jwtSecrets.add(jwtSecret); + + saveJwtSecretData(jwtSecretData); + } + public void storeAcmeUserKeyPair(KeyPair keyPair) { EnvironmentData environmentData = getDecryptedEnvironmentData(); StringWriter stringWriter = new StringWriter(); @@ -578,6 +625,16 @@ private void saveEnvironmentData(EnvironmentData environmentData) { } } + private void saveJwtSecretData(JwtSecretData jwtSecretData) { + EnvironmentData environmentData = getDecryptedEnvironmentData(); + try { + String serializedPlainTextEnvironmentData = configObjectMapper.writeValueAsString(jwtSecretData); + encryptAndSaveObject(JWT_SECRETS_PATH, serializedPlainTextEnvironmentData, environmentData); + } catch (JsonProcessingException e) { + throw new RuntimeException("Unable to convert JWT secret data to JSON. Aborting save...", e); + } + } + /** * List keys in config bucket under a path as-if it were a folder */ diff --git a/src/test/java/com/nike/cerberus/service/KeyGeneratorTest.java b/src/test/java/com/nike/cerberus/service/KeyGeneratorTest.java new file mode 100644 index 00000000..342231ea --- /dev/null +++ b/src/test/java/com/nike/cerberus/service/KeyGeneratorTest.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2019 Nike, Inc. + * + * 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 com.nike.cerberus.service; + +import org.junit.Test; + +import javax.crypto.SecretKey; +import java.util.Arrays; + +import static org.junit.Assert.*; + +public class KeyGeneratorTest { + + @Test + public void testGenerateKey() { + KeyGenerator generator = new KeyGenerator(); + SecretKey secretKey1 = generator.generateKey(KeyGenerator.HMACSHA512); + SecretKey secretKey2 = generator.generateKey(KeyGenerator.HMACSHA512); + assertEquals("HmacSHA512", secretKey1.getAlgorithm()); + assertEquals(512 / 8, secretKey1.getEncoded().length); + assertFalse(Arrays.equals(secretKey1.getEncoded(), secretKey2.getEncoded())); + } + + @Test(expected = IllegalStateException.class) + public void testBogusAlgorithm() { + KeyGenerator generator = new KeyGenerator(); + generator.generateKey("sleep sort"); + } +} diff --git a/src/test/java/com/nike/cerberus/store/ConfigStoreTest.java b/src/test/java/com/nike/cerberus/store/ConfigStoreTest.java index 0df2fdfa..99c56975 100644 --- a/src/test/java/com/nike/cerberus/store/ConfigStoreTest.java +++ b/src/test/java/com/nike/cerberus/store/ConfigStoreTest.java @@ -23,7 +23,9 @@ import com.nike.cerberus.domain.environment.RegionData; import com.nike.cerberus.module.CerberusModule; import com.nike.cerberus.service.EncryptionService; +import com.nike.cerberus.service.KeyGenerator; import com.nike.cerberus.service.StoreService; +import com.nike.cerberus.util.UuidSupplier; import org.junit.Before; import org.junit.Test; import org.mockito.Mock; @@ -54,6 +56,12 @@ public class ConfigStoreTest { @Spy private StoreService storeServiceUseast2; + @Mock + private KeyGenerator keyGenerator; + + @Mock + private UuidSupplier uuidSupplier; + private ObjectMapper objectMapper; private ConfigStore configStore; private EnvironmentData initEnvironmentdata = new EnvironmentData(); @@ -70,7 +78,7 @@ public void setUp() { configStore = spy(new ConfigStore(null, null, null, null, objectMapper, null, "env1", - "us-west-2", encryptionService)); + "us-west-2", encryptionService, keyGenerator, uuidSupplier)); doReturn(initEnvironmentdata).when(configStore).getDecryptedEnvironmentData(); doReturn(storeServiceUswest2).when(configStore).getStoreServiceForRegion(Regions.US_WEST_2, initEnvironmentdata);