Skip to content

Commit 89487e7

Browse files
committed
HDDS-7734. Implement symmetric SecretKeys lifescycle management in SCM (apache#4194)
1 parent ce41858 commit 89487e7

22 files changed

Lines changed: 1621 additions & 1 deletion

File tree

hadoop-hdds/common/src/main/java/org/apache/hadoop/hdds/HddsConfigKeys.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,28 @@ public final class HddsConfigKeys {
216216
public static final String HDDS_X509_ROOTCA_PRIVATE_KEY_FILE_DEFAULT =
217217
"";
218218

219+
public static final String HDDS_SECRET_KEY_FILE =
220+
"hdds.secret.key.file.name";
221+
public static final String HDDS_SECRET_KEY_FILE_DEFAULT = "secret_keys.json";
222+
223+
public static final String HDDS_SECRET_KEY_EXPIRY_DURATION =
224+
"hdds.secret.key.expiry.duration";
225+
public static final String HDDS_SECRET_KEY_EXPIRY_DURATION_DEFAULT = "7d";
226+
227+
public static final String HDDS_SECRET_KEY_ROTATE_DURATION =
228+
"hdds.secret.key.rotate.duration";
229+
public static final String HDDS_SECRET_KEY_ROTATE_DURATION_DEFAULT = "1d";
230+
231+
public static final String HDDS_SECRET_KEY_ALGORITHM =
232+
"hdds.secret.key.algorithm";
233+
public static final String HDDS_SECRET_KEY_ALGORITHM_DEFAULT =
234+
"HmacSHA256";
235+
236+
public static final String HDDS_SECRET_KEY_ROTATE_CHECK_DURATION =
237+
"hdds.secret.key.rotate.check.duration";
238+
public static final String HDDS_SECRET_KEY_ROTATE_CHECK_DURATION_DEFAULT
239+
= "10m";
240+
219241
/**
220242
* Do not instantiate.
221243
*/

hadoop-hdds/common/src/main/resources/ozone-default.xml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3637,4 +3637,52 @@
36373637
without using the optimised DAG based pruning approach
36383638
</description>
36393639
</property>
3640+
3641+
<property>
3642+
<name>hdds.secret.key.file.name</name>
3643+
<value>secret_keys.json</value>
3644+
<tag>SCM, SECURITY</tag>
3645+
<description>
3646+
Name of file which stores symmetric secret keys for token signatures.
3647+
</description>
3648+
</property>
3649+
<property>
3650+
<name>hdds.secret.key.expiry.duration</name>
3651+
<value>7d</value>
3652+
<tag>SCM, SECURITY</tag>
3653+
<description>
3654+
The duration for which symmetric secret keys issued by SCM are valid.
3655+
This default value, in combination with hdds.secret.key.rotate.duration=1d, results in 7 secret keys (for the
3656+
last 7 days) are kept valid at any point of time.
3657+
</description>
3658+
</property>
3659+
<property>
3660+
<name>hdds.secret.key.rotate.duration</name>
3661+
<value>1d</value>
3662+
<tag>SCM, SECURITY</tag>
3663+
<description>
3664+
The duration that SCM periodically generate a new symmetric secret keys.
3665+
</description>
3666+
</property>
3667+
<property>
3668+
<name>hdds.secret.key.rotate.check.duration</name>
3669+
<value>10m</value>
3670+
<tag>SCM, SECURITY</tag>
3671+
<description>
3672+
The duration that SCM periodically checks if it's time to generate new symmetric secret keys.
3673+
This config has an impact on the practical correctness of secret key expiry and rotation period. For example,
3674+
if hdds.secret.key.rotate.duration=1d and hdds.secret.key.rotate.check.duration=10m, the actual key rotation
3675+
will happen each 1d +/- 10m.
3676+
</description>
3677+
</property>
3678+
<property>
3679+
<name>hdds.secret.key.algorithm</name>
3680+
<value>HmacSHA256</value>
3681+
<tag>SCM, SECURITY</tag>
3682+
<description>
3683+
The algorithm that SCM uses to generate symmetric secret keys.
3684+
A valid algorithm is the one supported by KeyGenerator, as described at
3685+
https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#KeyGenerator.
3686+
</description>
3687+
</property>
36403688
</configuration>
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
* <p>
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
* <p>
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
package org.apache.hadoop.hdds.security.symmetric;
20+
21+
import com.fasterxml.jackson.databind.MappingIterator;
22+
import com.fasterxml.jackson.databind.ObjectMapper;
23+
import com.fasterxml.jackson.databind.ObjectReader;
24+
import com.fasterxml.jackson.databind.SequenceWriter;
25+
import com.fasterxml.jackson.databind.SerializationFeature;
26+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
27+
import org.slf4j.Logger;
28+
import org.slf4j.LoggerFactory;
29+
30+
import javax.crypto.SecretKey;
31+
import javax.crypto.spec.SecretKeySpec;
32+
import java.io.IOException;
33+
import java.nio.file.Files;
34+
import java.nio.file.Path;
35+
import java.nio.file.attribute.PosixFilePermission;
36+
import java.time.Instant;
37+
import java.util.Collection;
38+
import java.util.Collections;
39+
import java.util.List;
40+
import java.util.Set;
41+
import java.util.UUID;
42+
43+
import static com.google.common.collect.Sets.newHashSet;
44+
import static java.nio.file.Files.createDirectories;
45+
import static java.nio.file.Files.createFile;
46+
import static java.nio.file.Files.exists;
47+
import static java.nio.file.attribute.PosixFilePermission.OWNER_READ;
48+
import static java.nio.file.attribute.PosixFilePermission.OWNER_WRITE;
49+
import static java.util.Objects.requireNonNull;
50+
import static java.util.stream.Collectors.toList;
51+
52+
/**
53+
* A {@link SecretKeyStore} that saves and loads SecretKeys from/to a
54+
* JSON file on local file system.
55+
*/
56+
public class LocalSecretKeyStore implements SecretKeyStore {
57+
private static final Set<PosixFilePermission> SECRET_KEYS_PERMISSIONS =
58+
newHashSet(OWNER_READ, OWNER_WRITE);
59+
private static final Logger LOG =
60+
LoggerFactory.getLogger(LocalSecretKeyStore.class);
61+
62+
private final Path secretKeysFile;
63+
private final ObjectMapper mapper;
64+
65+
public LocalSecretKeyStore(Path secretKeysFile) {
66+
this.secretKeysFile = requireNonNull(secretKeysFile);
67+
this.mapper = new ObjectMapper()
68+
.registerModule(new JavaTimeModule())
69+
.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
70+
}
71+
72+
@Override
73+
public synchronized List<ManagedSecretKey> load() {
74+
if (!secretKeysFile.toFile().exists()) {
75+
return Collections.emptyList();
76+
}
77+
78+
ObjectReader reader = mapper.readerFor(ManagedSecretKeyDto.class);
79+
try (MappingIterator<ManagedSecretKeyDto> iterator =
80+
reader.readValues(secretKeysFile.toFile())) {
81+
List<ManagedSecretKeyDto> dtos = iterator.readAll();
82+
List<ManagedSecretKey> result = dtos.stream()
83+
.map(ManagedSecretKeyDto::toObject)
84+
.collect(toList());
85+
LOG.info("Loaded {} from {}", result, secretKeysFile);
86+
return result;
87+
} catch (IOException e) {
88+
throw new IllegalStateException("Error reading SecretKeys from "
89+
+ secretKeysFile, e);
90+
}
91+
}
92+
93+
@Override
94+
public synchronized void save(Collection<ManagedSecretKey> secretKeys) {
95+
createSecretKeyFiles();
96+
97+
List<ManagedSecretKeyDto> dtos = secretKeys.stream()
98+
.map(ManagedSecretKeyDto::new)
99+
.collect(toList());
100+
101+
try (SequenceWriter writer =
102+
mapper.writer().writeValues(secretKeysFile.toFile())) {
103+
writer.init(true);
104+
writer.writeAll(dtos);
105+
} catch (IOException e) {
106+
throw new IllegalStateException("Error saving SecretKeys to file "
107+
+ secretKeysFile, e);
108+
}
109+
LOG.info("Saved {} to file {}", secretKeys, secretKeysFile);
110+
}
111+
112+
private void createSecretKeyFiles() {
113+
try {
114+
if (!exists(secretKeysFile)) {
115+
Path parent = secretKeysFile.getParent();
116+
if (parent != null && !exists(parent)) {
117+
createDirectories(parent);
118+
}
119+
createFile(secretKeysFile);
120+
}
121+
Files.setPosixFilePermissions(secretKeysFile, SECRET_KEYS_PERMISSIONS);
122+
} catch (IOException e) {
123+
throw new IllegalStateException("Error setting secret keys file" +
124+
" permission: " + secretKeysFile, e);
125+
}
126+
}
127+
128+
/**
129+
* Just a simple DTO that allows serializing/deserializing the immutable
130+
* {@link ManagedSecretKey} objects.
131+
*/
132+
private static class ManagedSecretKeyDto {
133+
private UUID id;
134+
private Instant creationTime;
135+
private Instant expiryTime;
136+
private String algorithm;
137+
private byte[] encoded;
138+
139+
/**
140+
* Used by Jackson when deserializing.
141+
*/
142+
ManagedSecretKeyDto() {
143+
}
144+
145+
ManagedSecretKeyDto(ManagedSecretKey object) {
146+
id = object.getId();
147+
creationTime = object.getCreationTime();
148+
expiryTime = object.getExpiryTime();
149+
algorithm = object.getSecretKey().getAlgorithm();
150+
encoded = object.getSecretKey().getEncoded();
151+
}
152+
153+
public ManagedSecretKey toObject() {
154+
SecretKey secretKey = new SecretKeySpec(this.encoded, this.algorithm);
155+
return new ManagedSecretKey(id, creationTime,
156+
expiryTime, secretKey);
157+
}
158+
159+
public UUID getId() {
160+
return id;
161+
}
162+
163+
public void setId(UUID id) {
164+
this.id = id;
165+
}
166+
167+
public Instant getCreationTime() {
168+
return creationTime;
169+
}
170+
171+
public void setCreationTime(Instant creationTime) {
172+
this.creationTime = creationTime;
173+
}
174+
175+
public Instant getExpiryTime() {
176+
return expiryTime;
177+
}
178+
179+
public void setExpiryTime(Instant expiryTime) {
180+
this.expiryTime = expiryTime;
181+
}
182+
183+
public String getAlgorithm() {
184+
return algorithm;
185+
}
186+
187+
public void setAlgorithm(String algorithm) {
188+
this.algorithm = algorithm;
189+
}
190+
191+
public byte[] getEncoded() {
192+
return encoded;
193+
}
194+
195+
public void setEncoded(byte[] encoded) {
196+
this.encoded = encoded;
197+
}
198+
}
199+
}

0 commit comments

Comments
 (0)