diff --git a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
index cb258dfa74dc..8a870299fb72 100644
--- a/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
+++ b/hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java
@@ -216,6 +216,28 @@ public final class HddsConfigKeys {
public static final String HDDS_X509_ROOTCA_PRIVATE_KEY_FILE_DEFAULT =
"";
+ public static final String HDDS_SECRET_KEY_FILE =
+ "hdds.secret.key.file.name";
+ public static final String HDDS_SECRET_KEY_FILE_DEFAULT = "secret_keys.json";
+
+ public static final String HDDS_SECRET_KEY_EXPIRY_DURATION =
+ "hdds.secret.key.expiry.duration";
+ public static final String HDDS_SECRET_KEY_EXPIRY_DURATION_DEFAULT = "7d";
+
+ public static final String HDDS_SECRET_KEY_ROTATE_DURATION =
+ "hdds.secret.key.rotate.duration";
+ public static final String HDDS_SECRET_KEY_ROTATE_DURATION_DEFAULT = "1d";
+
+ public static final String HDDS_SECRET_KEY_ALGORITHM =
+ "hdds.secret.key.algorithm";
+ public static final String HDDS_SECRET_KEY_ALGORITHM_DEFAULT =
+ "HmacSHA256";
+
+ public static final String HDDS_SECRET_KEY_ROTATE_CHECK_DURATION =
+ "hdds.secret.key.rotate.check.duration";
+ public static final String HDDS_SECRET_KEY_ROTATE_CHECK_DURATION_DEFAULT
+ = "10m";
+
/**
* Do not instantiate.
*/
diff --git a/hadoop-hdds/common/src/main/resources/ozone-default.xml b/hadoop-hdds/common/src/main/resources/ozone-default.xml
index d26da2a2ca0d..6926b3986359 100644
--- a/hadoop-hdds/common/src/main/resources/ozone-default.xml
+++ b/hadoop-hdds/common/src/main/resources/ozone-default.xml
@@ -3602,4 +3602,51 @@
history from compaction DAG. Uses millisecond by default when no time unit is specified.
+
+ hdds.secret.key.file.name
+ secret_keys.json
+ SCM, SECURITY
+
+ Name of file which stores symmetric secret keys for token signatures.
+
+
+
+ hdds.secret.key.expiry.duration
+ 7d
+ SCM, SECURITY
+
+ The duration for which symmetric secret keys issued by SCM are valid.
+ This default value, in combination with hdds.secret.key.rotate.duration=1d, results in 7 secret keys (for the
+ last 7 days) are kept valid at any point of time.
+
+
+
+ hdds.secret.key.rotate.duration
+ 1d
+ SCM, SECURITY
+
+ The duration that SCM periodically generate a new symmetric secret keys.
+
+
+
+ hdds.secret.key.rotate.check.duration
+ 10m
+ SCM, SECURITY
+
+ The duration that SCM periodically checks if it's time to generate new symmetric secret keys.
+ This config has an impact on the practical correctness of secret key expiry and rotation period. For example,
+ if hdds.secret.key.rotate.duration=1d and hdds.secret.key.rotate.check.duration=10m, the actual key rotation
+ will happen each 1d +/- 10m.
+
+
+
+ hdds.secret.key.algorithm
+ HmacSHA256
+ SCM, SECURITY
+
+ The algorithm that SCM uses to generate symmetric secret keys.
+ A valid algorithm is the one supported by KeyGenerator, as described at
+ https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyGenerator.
+
+
diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/LocalSecretKeyStore.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/LocalSecretKeyStore.java
new file mode 100644
index 000000000000..48cc633b67b9
--- /dev/null
+++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/LocalSecretKeyStore.java
@@ -0,0 +1,199 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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
+ *
+ * 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.apache.hadoop.hdds.security.symmetric;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.KeyGenerator;
+import java.security.NoSuchAlgorithmException;
+import java.time.Duration;
+import java.time.Instant;
+import java.util.List;
+import java.util.UUID;
+import java.util.concurrent.TimeoutException;
+
+import static java.time.Duration.between;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * This component manages symmetric SecretKey life-cycle, including generation,
+ * rotation and destruction.
+ */
+public class SecretKeyManager {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(SecretKeyManager.class);
+
+ private final SecretKeyState state;
+ private final Duration rotationDuration;
+ private final Duration validityDuration;
+ private final SecretKeyStore keyStore;
+
+ private final KeyGenerator keyGenerator;
+
+ public SecretKeyManager(SecretKeyState state,
+ SecretKeyStore keyStore,
+ Duration rotationDuration,
+ Duration validityDuration,
+ String algorithm) {
+ this.state = requireNonNull(state);
+ this.rotationDuration = requireNonNull(rotationDuration);
+ this.validityDuration = requireNonNull(validityDuration);
+ this.keyStore = requireNonNull(keyStore);
+ this.keyGenerator = createKeyGenerator(algorithm);
+ }
+
+ public SecretKeyManager(SecretKeyState state,
+ SecretKeyStore keyStore,
+ SecretKeyConfig config) {
+ this(state, keyStore, config.getRotateDuration(),
+ config.getExpiryDuration(), config.getAlgorithm());
+ }
+
+ /**
+ * If the SecretKey state is not initialized, initialize it from by loading
+ * SecretKeys from local file, or generate new keys if the file doesn't
+ * exist.
+ */
+ public synchronized void checkAndInitialize() throws TimeoutException {
+ if (isInitialized()) {
+ return;
+ }
+
+ LOG.info("Initializing SecretKeys.");
+
+ // Load and filter expired keys.
+ List allKeys = keyStore.load()
+ .stream()
+ .filter(x -> !x.isExpired())
+ .collect(toList());
+
+ if (allKeys.isEmpty()) {
+ // if no valid key present , generate new key as the current key.
+ // This happens at first start or restart after being down for
+ // a significant time.
+ ManagedSecretKey newKey = generateSecretKey();
+ allKeys.add(newKey);
+ LOG.info("No valid key has been loaded. " +
+ "A new key is generated: {}", newKey);
+ } else {
+ LOG.info("Keys reloaded: {}", allKeys);
+ }
+
+ state.updateKeys(allKeys);
+ }
+
+ public boolean isInitialized() {
+ return state.getCurrentKey() != null;
+ }
+
+ /**
+ * Check and rotate the keys.
+ *
+ * @return true if rotation actually happens, false if it doesn't.
+ */
+ public synchronized boolean checkAndRotate() throws TimeoutException {
+ // Initialize the state if it's not initialized already.
+ checkAndInitialize();
+
+ ManagedSecretKey currentKey = state.getCurrentKey();
+ if (shouldRotate(currentKey)) {
+ ManagedSecretKey newCurrentKey = generateSecretKey();
+ List updatedKeys = state.getSortedKeys()
+ .stream().filter(x -> !x.isExpired())
+ .collect(toList());
+ updatedKeys.add(newCurrentKey);
+
+ LOG.info("SecretKey rotation is happening, new key generated {}",
+ newCurrentKey);
+ state.updateKeys(updatedKeys);
+ return true;
+ }
+ return false;
+ }
+
+ private boolean shouldRotate(ManagedSecretKey currentKey) {
+ Duration established = between(currentKey.getCreationTime(), Instant.now());
+ return established.compareTo(rotationDuration) >= 0;
+ }
+
+ private ManagedSecretKey generateSecretKey() {
+ Instant now = Instant.now();
+ return new ManagedSecretKey(
+ UUID.randomUUID(),
+ now,
+ now.plus(validityDuration),
+ keyGenerator.generateKey()
+ );
+ }
+
+ private KeyGenerator createKeyGenerator(String algorithm) {
+ try {
+ return KeyGenerator.getInstance(algorithm);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalArgumentException("Error creating KeyGenerator for " +
+ "algorithm " + algorithm, e);
+ }
+ }
+}
diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyState.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyState.java
new file mode 100644
index 000000000000..7be70b4b029b
--- /dev/null
+++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyState.java
@@ -0,0 +1,52 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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
+ *
+ * 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.apache.hadoop.hdds.security.symmetric;
+
+import org.apache.hadoop.hdds.scm.metadata.Replicate;
+
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+
+/**
+ * This component holds the state of managed SecretKeys, including the
+ * current key and all active keys.
+ */
+public interface SecretKeyState {
+ /**
+ * Get the current active key, which is used for signing tokens. This is
+ * also the latest key managed by this state.
+ *
+ * @return the current active key, or null if the state is not initialized.
+ */
+ ManagedSecretKey getCurrentKey();
+
+ /**
+ * Get the keys that managed by this manager.
+ * The returned keys are sorted by creation time, in the order of latest
+ * to oldest.
+ */
+ List getSortedKeys();
+
+ /**
+ * Update the SecretKeys.
+ * This method replicates SecretKeys across all SCM instances.
+ */
+ @Replicate
+ void updateKeys(List newKeys) throws TimeoutException;
+}
diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyStateImpl.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyStateImpl.java
new file mode 100644
index 000000000000..d5c886fd99e6
--- /dev/null
+++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyStateImpl.java
@@ -0,0 +1,108 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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
+ *
+ * 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.apache.hadoop.hdds.security.symmetric;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.locks.ReadWriteLock;
+import java.util.concurrent.locks.ReentrantReadWriteLock;
+
+import static java.util.Comparator.comparing;
+import static java.util.Objects.requireNonNull;
+import static java.util.stream.Collectors.toList;
+
+/**
+ * Default implementation of {@link SecretKeyState}.
+ */
+public final class SecretKeyStateImpl implements SecretKeyState {
+ private static final Logger LOG =
+ LoggerFactory.getLogger(SecretKeyStateImpl.class);
+
+ private final ReadWriteLock lock = new ReentrantReadWriteLock();
+
+ private List sortedKeys;
+ private ManagedSecretKey currentKey;
+
+ private final SecretKeyStore keyStore;
+
+ /**
+ * Instantiate a state with no keys. This state object needs to be backed by
+ * a proper replication proxy so that the @Replication method works.
+ */
+ public SecretKeyStateImpl(SecretKeyStore keyStore) {
+ this.keyStore = requireNonNull(keyStore);
+ }
+
+ /**
+ * Get the current active key, which is used for signing tokens. This is
+ * also the latest key managed by this state.
+ */
+ @Override
+ public ManagedSecretKey getCurrentKey() {
+ lock.readLock().lock();
+ try {
+ return currentKey;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Get the keys that managed by this manager.
+ * The returned keys are sorted by creation time, in the order of latest
+ * to oldest.
+ */
+ @Override
+ public List getSortedKeys() {
+ lock.readLock().lock();
+ try {
+ return sortedKeys;
+ } finally {
+ lock.readLock().unlock();
+ }
+ }
+
+ /**
+ * Update the SecretKeys.
+ * This method replicates SecretKeys across all SCM instances.
+ */
+ @Override
+ public void updateKeys(List newKeys) {
+ LOG.info("Updating keys with {}", newKeys);
+ lock.writeLock().lock();
+ try {
+ // Store sorted keys in order of latest to oldest and make it
+ // immutable so that can be used to answer queries directly.
+ sortedKeys = Collections.unmodifiableList(
+ newKeys.stream()
+ .sorted(comparing(ManagedSecretKey::getCreationTime).reversed())
+ .collect(toList())
+ );
+ currentKey = sortedKeys.get(0);
+ LOG.info("Current key updated {}", currentKey);
+ keyStore.save(sortedKeys);
+ } finally {
+ lock.writeLock().unlock();
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyStore.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyStore.java
new file mode 100644
index 000000000000..c851c3683d33
--- /dev/null
+++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/SecretKeyStore.java
@@ -0,0 +1,35 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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
+ *
+ * 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.apache.hadoop.hdds.security.symmetric;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * Interface for SecretKey storage component, which is responsible for saving
+ * the SecretKeys states persistently to ensure they're not lost during
+ * restarts.
+ *
+ * This interface allows new persistent storage to be plugged in easily.
+ */
+public interface SecretKeyStore {
+ List load();
+
+ void save(Collection secretKeys);
+}
diff --git a/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/package-info.java b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/package-info.java
new file mode 100644
index 000000000000..2997fe0a26bb
--- /dev/null
+++ b/hadoop-hdds/framework/src/main/java/org/apache/hadoop/hdds/security/symmetric/package-info.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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
+ *
+ * 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.
+ */
+
+/**
+ * In secure mode, Ozone uses symmetric key algorithm to sign all its issued
+ * tokens, such as block or container tokens. These tokens are then verified
+ * by datanodes to ensure their authenticity and integrity.
+ *
+ *
+ * That process requires symmetric {@link javax.crypto.SecretKey} to be
+ * generated, managed, and distributed to different Ozone components.
+ * For example, the token signer (Ozone Manager and SCM) and the
+ * verifier (datanode) need to use the same SecretKey.
+ *
+ *
+ * This package encloses the logic to manage symmetric secret keys
+ * lifecycle. In details, it consists of the following components:
+ *
+ *
+ * The definition of manage secret key which is shared between SCM,
+ * OM and datanodes, see
+ * {@link org.apache.hadoop.hdds.security.symmetric.ManagedSecretKey}.
+ *
+ *
+ *
+ * The definition of secret key states, which is designed to get replicated
+ * across all SCM instances, see
+ * {@link org.apache.hadoop.hdds.security.symmetric.SecretKeyState}
+ *
+ *
+ *
+ * The definition and implementation of secret key persistent storage, to
+ * help retain SecretKey after restarts, see
+ * {@link org.apache.hadoop.hdds.security.symmetric.SecretKeyStore} and
+ * {@link org.apache.hadoop.hdds.security.symmetric.LocalSecretKeyStore}.
+ *
+ *
+ *
+ * The basic logic to manage secret key lifecycle, see
+ * {@link org.apache.hadoop.hdds.security.symmetric.SecretKeyManager}
+ *
+ *
+ *
+ *
+ * The original overall design can be found at
+ * HDDS-7733.
+ */
+package org.apache.hadoop.hdds.security.symmetric;
diff --git a/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/symmetric/LocalSecretKeyStoreTest.java b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/symmetric/LocalSecretKeyStoreTest.java
new file mode 100644
index 000000000000..c406ce2b08f6
--- /dev/null
+++ b/hadoop-hdds/framework/src/test/java/org/apache/hadoop/hdds/security/symmetric/LocalSecretKeyStoreTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF 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
+ *