Skip to content

HBASE-29402: Comprehensive key management for encryption at rest #7111

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 62 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
98ff63d
PBE: POC for initializing STK and a default implementation for key pr…
haridsv Jan 17, 2025
4e426a8
PBE: POC for creating keymeta table and providing admin service
haridsv Jan 28, 2025
537aadb
Fix compilation errors
haridsv Mar 30, 2025
bcee407
Added support for key namespace
haridsv Mar 5, 2025
2d2cdb5
Integrated with the STK cache to cache the DEK key material
haridsv Mar 10, 2025
995640c
Added key listing to do end2end testing for key decryption with STK
haridsv Mar 11, 2025
56401c7
Operation counts
haridsv Mar 12, 2025
236c9cb
Attempting broader test coverage
haridsv Mar 29, 2025
d975e67
Use shutdownMiniHBaseCluster() instead of shutdownMiniCluster
haridsv Mar 31, 2025
99249b5
Got the test working for STK rotation
haridsv Apr 1, 2025
434ff64
Better coverage for default key provider
haridsv Apr 1, 2025
d0db6e8
Minor refactoring
haridsv Apr 2, 2025
5a239f6
Renamed PBEClusterKey to SystemKey everywhere
haridsv Apr 2, 2025
df49852
Renamed "PBE key" to "Managed ey"
haridsv Apr 2, 2025
ca262b5
Renamed "cust spec" to "key cust"
haridsv Apr 3, 2025
e5b82b9
Replaced left over references to PBE
haridsv Apr 3, 2025
4cb8692
Update PBE references in hbase-shell
haridsv Apr 3, 2025
53b8c63
Rename enableManagedKeys to enableKeyManagement, some test coverage
haridsv Apr 11, 2025
20310c0
More test coverage
haridsv Apr 14, 2025
217fa9c
Updated the enable API to return a list of key data
haridsv Apr 16, 2025
bdb07eb
added test coverage
haridsv Apr 21, 2025
4626d2d
added test coverage
haridsv Apr 22, 2025
1d68325
added test coverage
haridsv Apr 23, 2025
5bc64af
Added TestKeymetaTableAccessor
haridsv Apr 24, 2025
81a38a9
Added TestManagedKeyDataCache
haridsv Apr 24, 2025
9597dab
Added TestManagedKeyAccessor
haridsv Apr 25, 2025
dfa316e
Some parameterization
haridsv Apr 28, 2025
5645c8a
More test coverage for system key
haridsv Apr 28, 2025
0ad777a
Small optimizaion
haridsv Apr 28, 2025
80739b2
Filled some gaps and added test coverage
haridsv May 19, 2025
c8e3f9a
Just renamed status to state
haridsv May 20, 2025
a2d7f8e
Address a couple of gaps
haridsv May 27, 2025
49c58df
Fix some naming
haridsv May 28, 2025
d3a72fc
rename
haridsv May 28, 2025
49d3765
Config for dynamic lookup
haridsv Jun 6, 2025
c2d2ddb
Finish an incomplete rename
haridsv Jun 6, 2025
9b0555d
More test coverage
haridsv Jun 9, 2025
dcc9ec0
New test clas
haridsv Jun 9, 2025
5a71fd6
format
haridsv Jun 9, 2025
b006d49
some cleanup
haridsv Jun 18, 2025
4cfaff2
Javadoc
haridsv Jun 18, 2025
4517f6d
Fix build error
haridsv Jun 18, 2025
bcec30f
Fix checkstyle errors for imports
haridsv Jun 18, 2025
b1fd7f5
Fix checkstyle errors
haridsv Jun 18, 2025
51b122d
more checkstyle fixes
haridsv Jun 18, 2025
5de212c
fix spotbugs
haridsv Jun 18, 2025
61b2bbf
Fix NPE caused by changing new HashMap to Map.of
haridsv Jun 19, 2025
ad7d817
javadoc warning
haridsv Jun 19, 2025
47a9a58
Fix test failures and errors
haridsv Jun 19, 2025
846ea59
Add missing license header
haridsv Jun 20, 2025
2256d71
Revert an inadvertent whitespace change
haridsv Jun 20, 2025
65a1c1d
Fix for test failures on cluster ID
haridsv Jun 20, 2025
13f2654
One more file missing license header
haridsv Jun 23, 2025
895749b
Add missing license header
haridsv Jun 23, 2025
015c196
Address most of Rubocop errors
haridsv Jun 25, 2025
cb033c9
Another attempt to organize imports
haridsv Jun 25, 2025
60c9568
Additional checkstyle import fixes
haridsv Jun 25, 2025
0f1f467
Misc. checkstyle fixes
haridsv Jun 25, 2025
346659c
Fix compilation error
haridsv Jun 25, 2025
98183cc
Use single letter qualifier names to reduce storage space
haridsv Jul 7, 2025
588e6b3
Switched to Caffeine cache with a better architecture.
haridsv Jul 9, 2025
670ba17
Improvements to prior Cursor changes.
haridsv Jul 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,9 @@ linklint/
**/*.log
tmp
**/.flattened-pom.xml
.*.sw*
ID
filenametags
tags
.codegenie
.vscode
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* 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
*
* 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 org.apache.hadoop.hbase.keymeta;

import java.io.IOException;
import java.security.KeyException;
import java.util.ArrayList;
import java.util.List;

import org.apache.hadoop.hbase.client.Connection;
import org.apache.hadoop.hbase.io.crypto.ManagedKeyData;
import org.apache.hadoop.hbase.io.crypto.ManagedKeyState;
import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos;
import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeysRequest;
import org.apache.hadoop.hbase.protobuf.generated.ManagedKeysProtos.ManagedKeysResponse;
import org.apache.yetus.audience.InterfaceAudience;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.apache.hbase.thirdparty.com.google.protobuf.ServiceException;

import org.apache.hadoop.hbase.shaded.protobuf.ProtobufUtil;

@InterfaceAudience.Public
public class KeymetaAdminClient implements KeymetaAdmin {
private static final Logger LOG = LoggerFactory.getLogger(KeymetaAdminClient.class);
private ManagedKeysProtos.ManagedKeysService.BlockingInterface stub;

public KeymetaAdminClient(Connection conn) throws IOException {
this.stub = ManagedKeysProtos.ManagedKeysService.newBlockingStub(
conn.getAdmin().coprocessorService());
}

@Override
public List<ManagedKeyData> enableKeyManagement(String keyCust, String keyNamespace)
throws IOException {
try {
ManagedKeysProtos.GetManagedKeysResponse response = stub.enableKeyManagement(null,
ManagedKeysRequest.newBuilder().setKeyCust(keyCust).setKeyNamespace(keyNamespace).build());
return generateKeyDataList(response);
} catch (ServiceException e) {
throw ProtobufUtil.handleRemoteException(e);
}
}

@Override
public List<ManagedKeyData> getManagedKeys(String keyCust, String keyNamespace)
throws IOException, KeyException {
try {
ManagedKeysProtos.GetManagedKeysResponse statusResponse = stub.getManagedKeys(null,
ManagedKeysRequest.newBuilder().setKeyCust(keyCust).setKeyNamespace(keyNamespace).build());
return generateKeyDataList(statusResponse);
} catch (ServiceException e) {
throw ProtobufUtil.handleRemoteException(e);
}
}

private static List<ManagedKeyData> generateKeyDataList(
ManagedKeysProtos.GetManagedKeysResponse stateResponse) {
List<ManagedKeyData> keyStates = new ArrayList<>();
for (ManagedKeysResponse state: stateResponse.getStateList()) {
keyStates.add(new ManagedKeyData(
state.getKeyCustBytes().toByteArray(),
state.getKeyNamespace(), null,
ManagedKeyState.forValue((byte) state.getKeyState().getNumber()),
state.getKeyMetadata(),
state.getRefreshTimestamp(), state.getReadOpCount(), state.getWriteOpCount()));
}
return keyStates;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,21 @@ public static byte[] wrapKey(Configuration conf, byte[] key, String algorithm)
* @return the encrypted key bytes
*/
public static byte[] wrapKey(Configuration conf, String subject, Key key) throws IOException {
return wrapKey(conf, subject, key, null);
}

/**
* Protect a key by encrypting it with the secret key of the given subject or kek. The
* configuration must be set up correctly for key alias resolution. Only one of the
* {@code subject} or {@code kek} needs to be specified and the other one can be {@code null}.
* @param conf configuration
* @param subject subject key alias
* @param key the key
* @param kek the key encryption key
* @return the encrypted key bytes
*/
public static byte[] wrapKey(Configuration conf, String subject, Key key, Key kek)
throws IOException {
// Wrap the key with the configured encryption algorithm.
String algorithm = conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES);
Cipher cipher = Encryption.getCipher(conf, algorithm);
Expand All @@ -100,8 +115,13 @@ public static byte[] wrapKey(Configuration conf, String subject, Key key) throws
builder
.setHash(UnsafeByteOperations.unsafeWrap(Encryption.computeCryptoKeyHash(conf, keyBytes)));
ByteArrayOutputStream out = new ByteArrayOutputStream();
Encryption.encryptWithSubjectKey(out, new ByteArrayInputStream(keyBytes), subject, conf, cipher,
iv);
if (kek != null) {
Encryption.encryptWithGivenKey(kek, out, new ByteArrayInputStream(keyBytes), cipher, iv);
}
else {
Encryption.encryptWithSubjectKey(out, new ByteArrayInputStream(keyBytes), subject, conf,
cipher, iv);
}
builder.setData(UnsafeByteOperations.unsafeWrap(out.toByteArray()));
// Build and return the protobuf message
out.reset();
Expand All @@ -118,6 +138,21 @@ public static byte[] wrapKey(Configuration conf, String subject, Key key) throws
* @return the raw key bytes
*/
public static Key unwrapKey(Configuration conf, String subject, byte[] value)
throws IOException, KeyException {
return unwrapKey(conf, subject, value, null);
}

/**
* Unwrap a key by decrypting it with the secret key of the given subject. The configuration must
* be set up correctly for key alias resolution. Only one of the {@code subject} or {@code kek}
* needs to be specified and the other one can be {@code null}.
* @param conf configuration
* @param subject subject key alias
* @param value the encrypted key bytes
* @param kek the key encryption key
* @return the raw key bytes
*/
public static Key unwrapKey(Configuration conf, String subject, byte[] value, Key kek)
throws IOException, KeyException {
EncryptionProtos.WrappedKey wrappedKey =
EncryptionProtos.WrappedKey.parser().parseDelimitedFrom(new ByteArrayInputStream(value));
Expand All @@ -126,11 +161,12 @@ public static Key unwrapKey(Configuration conf, String subject, byte[] value)
if (cipher == null) {
throw new RuntimeException("Cipher '" + algorithm + "' not available");
}
return getUnwrapKey(conf, subject, wrappedKey, cipher);
return getUnwrapKey(conf, subject, wrappedKey, cipher, kek);
}

private static Key getUnwrapKey(Configuration conf, String subject,
EncryptionProtos.WrappedKey wrappedKey, Cipher cipher) throws IOException, KeyException {
EncryptionProtos.WrappedKey wrappedKey, Cipher cipher, Key kek)
throws IOException, KeyException {
String configuredHashAlgorithm = Encryption.getConfiguredHashAlgorithm(conf);
String wrappedHashAlgorithm = wrappedKey.getHashAlgorithm().trim();
if (!configuredHashAlgorithm.equalsIgnoreCase(wrappedHashAlgorithm)) {
Expand All @@ -143,8 +179,14 @@ private static Key getUnwrapKey(Configuration conf, String subject,
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
byte[] iv = wrappedKey.hasIv() ? wrappedKey.getIv().toByteArray() : null;
Encryption.decryptWithSubjectKey(out, wrappedKey.getData().newInput(), wrappedKey.getLength(),
subject, conf, cipher, iv);
if (kek != null) {
Encryption.decryptWithGivenKey(kek, out, wrappedKey.getData().newInput(),
wrappedKey.getLength(), cipher, iv);
}
else {
Encryption.decryptWithSubjectKey(out, wrappedKey.getData().newInput(), wrappedKey.getLength(),
subject, conf, cipher, iv);
}
byte[] keyBytes = out.toByteArray();
if (wrappedKey.hasHash()) {
if (
Expand Down Expand Up @@ -176,7 +218,7 @@ public static Key unwrapWALKey(Configuration conf, String subject, byte[] value)
if (cipher == null) {
throw new RuntimeException("Cipher '" + algorithm + "' not available");
}
return getUnwrapKey(conf, subject, wrappedKey, cipher);
return getUnwrapKey(conf, subject, wrappedKey, cipher, null);
}

/**
Expand Down
38 changes: 38 additions & 0 deletions hbase-common/src/main/java/org/apache/hadoop/hbase/HConstants.java
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,11 @@ public enum OperationStatusCode {

/** Temporary directory used for table creation and deletion */
public static final String HBASE_TEMP_DIRECTORY = ".tmp";
/**
* Directory used for storing master keys for the cluster
*/
public static final String SYSTEM_KEYS_DIRECTORY = ".system_keys";
public static final String SYSTEM_KEY_FILE_PREFIX = "system_key.";
/**
* The period (in milliseconds) between computing region server point in time metrics
*/
Expand Down Expand Up @@ -1304,6 +1309,39 @@ public enum OperationStatusCode {
/** Configuration key for enabling WAL encryption, a boolean */
public static final String ENABLE_WAL_ENCRYPTION = "hbase.regionserver.wal.encryption";

/** Property used by ManagedKeyStoreKeyProvider class to set the alias that identifies
* the current system key. */
public static final String CRYPTO_MANAGED_KEY_STORE_SYSTEM_KEY_NAME_CONF_KEY =
"hbase.crypto.managed_key_store.system.key.name";
public static final String CRYPTO_MANAGED_KEY_STORE_CONF_KEY_PREFIX =
"hbase.crypto.managed_key_store.cust.";

/** Enables or disables the key management feature. */
public static final String CRYPTO_MANAGED_KEYS_ENABLED_CONF_KEY =
"hbase.crypto.managed_keys.enabled";
public static final boolean CRYPTO_MANAGED_KEYS_DEFAULT_ENABLED = false;

/** The number of keys to retrieve from Key Provider per each custodian and namespace
* combination. */
public static final String CRYPTO_MANAGED_KEYS_PER_CUST_NAMESPACE_ACTIVE_KEY_COUNT =
"hbase.crypto.managed_keys.per_cust_namespace.active_count";
public static final int CRYPTO_MANAGED_KEYS_PER_CUST_NAMESPACE_ACTIVE_KEY_DEFAULT_COUNT = 1;
/** Enables or disables key lookup during data path as an alternative to static injection of keys
* using control path. */
public static final String CRYPTO_MANAGED_KEYS_DYNAMIC_LOOKUP_ENABLED_CONF_KEY =
"hbase.crypto.managed_keys.dynamic_lookup.enabled";
public static final boolean CRYPTO_MANAGED_KEYS_DYNAMIC_LOOKUP_DEFAULT_ENABLED = true;

/** Maximum number of entries in the managed key data cache. */
public static final String CRYPTO_MANAGED_KEYS_L1_CACHE_MAX_ENTRIES_CONF_KEY =
"hbase.crypto.managed_keys.l1_cache.max_entries";
public static final int CRYPTO_MANAGED_KEYS_L1_CACHE_MAX_ENTRIES_DEFAULT = 1000;

/** Maximum number of entries in the managed key active keys cache. */
public static final String CRYPTO_MANAGED_KEYS_L1_ACTIVE_CACHE_MAX_NS_ENTRIES_CONF_KEY =
"hbase.crypto.managed_keys.l1_active_cache.max_ns_entries";
public static final int CRYPTO_MANAGED_KEYS_L1_ACTIVE_CACHE_MAX_NS_ENTRIES_DEFAULT = 100;

/** Configuration key for setting RPC codec class name */
public static final String RPC_CODEC_CONF_KEY = "hbase.client.rpc.codec";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.io.IOUtils;
import org.apache.hadoop.classification.InterfaceAudience.Private;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.hbase.HBaseConfiguration;
import org.apache.hadoop.hbase.HBaseInterfaceAudience;
import org.apache.hadoop.hbase.HConstants;
import org.apache.hadoop.hbase.io.crypto.aes.AES;
import org.apache.hadoop.hbase.util.Bytes;
Expand Down Expand Up @@ -468,6 +470,19 @@ public static void encryptWithSubjectKey(OutputStream out, InputStream in, Strin
if (key == null) {
throw new IOException("No key found for subject '" + subject + "'");
}
encryptWithGivenKey(key, out, in, cipher, iv);
}

/**
* Encrypts a block of plaintext with the specified symmetric key.
* @param key The symmetric key
* @param out ciphertext
* @param in plaintext
* @param cipher the encryption algorithm
* @param iv the initialization vector, can be null
*/
public static void encryptWithGivenKey(Key key, OutputStream out, InputStream in,
Cipher cipher, byte[] iv) throws IOException {
Encryptor e = cipher.getEncryptor();
e.setKey(key);
e.setIv(iv); // can be null
Expand All @@ -490,36 +505,39 @@ public static void decryptWithSubjectKey(OutputStream out, InputStream in, int o
if (key == null) {
throw new IOException("No key found for subject '" + subject + "'");
}
Decryptor d = cipher.getDecryptor();
d.setKey(key);
d.setIv(iv); // can be null
try {
decrypt(out, in, outLen, d);
decryptWithGivenKey(key, out, in, outLen, cipher, iv);
} catch (IOException e) {
// If the current cipher algorithm fails to unwrap, try the alternate cipher algorithm, if one
// is configured
String alternateAlgorithm = conf.get(HConstants.CRYPTO_ALTERNATE_KEY_ALGORITHM_CONF_KEY);
if (alternateAlgorithm != null) {
if (LOG.isDebugEnabled()) {
LOG.debug("Unable to decrypt data with current cipher algorithm '"
+ conf.get(HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES)
LOG.debug("Unable to decrypt data with current cipher algorithm '" + conf.get(
HConstants.CRYPTO_KEY_ALGORITHM_CONF_KEY, HConstants.CIPHER_AES)
+ "'. Trying with the alternate cipher algorithm '" + alternateAlgorithm
+ "' configured.");
}
Cipher alterCipher = Encryption.getCipher(conf, alternateAlgorithm);
if (alterCipher == null) {
throw new RuntimeException("Cipher '" + alternateAlgorithm + "' not available");
}
d = alterCipher.getDecryptor();
d.setKey(key);
d.setIv(iv); // can be null
decrypt(out, in, outLen, d);
} else {
throw new IOException(e);
decryptWithGivenKey(key, out, in, outLen, alterCipher, iv);
}
else {
throw e;
}
}
}

public static void decryptWithGivenKey(Key key, OutputStream out, InputStream in, int outLen,
Cipher cipher, byte[] iv) throws IOException {
Decryptor d = cipher.getDecryptor();
d.setKey(key);
d.setIv(iv); // can be null
decrypt(out, in, outLen, d);
}

private static ClassLoader getClassLoaderForClass(Class<?> c) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
Expand Down Expand Up @@ -561,6 +579,9 @@ public static KeyProvider getKeyProvider(Configuration conf) {
provider = (KeyProvider) ReflectionUtils
.newInstance(getClassLoaderForClass(KeyProvider.class).loadClass(providerClassName), conf);
provider.init(providerParameters);
if (provider instanceof ManagedKeyProvider) {
((ManagedKeyProvider) provider).initConfig(conf);
}
if (LOG.isDebugEnabled()) {
LOG.debug("Installed " + providerClassName + " into key provider cache");
}
Expand All @@ -571,6 +592,11 @@ public static KeyProvider getKeyProvider(Configuration conf) {
}
}

@InterfaceAudience.LimitedPrivate(HBaseInterfaceAudience.UNITTEST)
public static void clearKeyProviderCache() {
keyProviderCache.clear();
}

public static void incrementIv(byte[] iv) {
incrementIv(iv, 1);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,8 @@
@InterfaceAudience.Public
public class KeyStoreKeyProvider implements KeyProvider {

private static final char[] NO_PASSWORD = new char[0];

protected KeyStore store;
protected char[] password; // can be null if no password
protected Properties passwordFile; // can be null if no file provided
Expand Down Expand Up @@ -172,9 +174,15 @@ protected char[] getAliasPassword(String alias) {

@Override
public Key getKey(String alias) {
// First try with no password, as it is more common to have a password only for the store.
try {
return store.getKey(alias, getAliasPassword(alias));
return store.getKey(alias, NO_PASSWORD);
} catch (UnrecoverableKeyException e) {
try {
return store.getKey(alias, getAliasPassword(alias));
} catch (UnrecoverableKeyException|NoSuchAlgorithmException|KeyStoreException e2) {
// Ignore.
}
throw new RuntimeException(e);
} catch (KeyStoreException e) {
throw new RuntimeException(e);
Expand Down
Loading