Skip to content
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

CLDSRV-559 AWS KMS backend + account level encryption key #5682

Merged
merged 6 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion config.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,5 +86,12 @@
"type": "dummy",
"host": "localhost:6000"
}
]
],
"defaultEncryptionKeyPerAccount": true,
"kmsAWS": {
"region": "us-east-1",
"endpoint": "http://127.0.0.1:8080",
"ak": "tbd",
"sk": "tbd"
}
}
76 changes: 75 additions & 1 deletion lib/Config.js
Original file line number Diff line number Diff line change
Expand Up @@ -453,6 +453,60 @@ class Config extends EventEmitter {
this._configureBackends();
}

_parseKmsAWS(config) {
if (!config.kmsAWS) {
return {};
}
let kmsAWS = {};

const { region, endpoint, ak, sk, tls } = config.kmsAWS;

assert(endpoint, 'Configuration Error: endpoint must be defined in kmsAWS');
assert(ak, 'Configuration Error: ak must be defined in kmsAWS');
assert(sk, 'Configuration Error: sk must be defined in kmsAWS');

kmsAWS = {
endpoint,
ak,
sk,
};

if (region) {
kmsAWS.region = region;
}

if (tls) {
kmsAWS.tls = {};
if (tls.rejectUnauthorized !== undefined) {
assert(typeof tls.rejectUnauthorized === 'boolean');
kmsAWS.tls.rejectUnauthorized = tls.rejectUnauthorized;
}
// min & max TLS: One of 'TLSv1.3', 'TLSv1.2', 'TLSv1.1', or 'TLSv1'
// (see https://nodejs.org/api/tls.html#tlscreatesecurecontextoptions)
if (tls.minVersion !== undefined) {
assert(typeof tls.minVersion === 'string',
'bad config: KMS AWS TLS minVersion must be a string');
kmsAWS.tls.minVersion = tls.minVersion;
}
if (tls.maxVersion !== undefined) {
assert(typeof tls.maxVersion === 'string',
'bad config: KMS AWS TLS maxVersion must be a string');
kmsAWS.tls.maxVersion = tls.maxVersion;
}
if (tls.ca !== undefined) {
kmsAWS.tls.ca = this._loadTlsFileArray(tls.ca);
}
if (tls.cert !== undefined) {
kmsAWS.tls.cert = this._loadTlsFileArray(tls.cert);
}
if (tls.key !== undefined) {
kmsAWS.tls.key = this._loadTlsFileArray(tls.key);
}
}

return kmsAWS;
}

_getLocationConfig() {
let locationConfig;
try {
Expand Down Expand Up @@ -491,7 +545,7 @@ class Config extends EventEmitter {
}
const tlsFilePath = (tlsFileName[0] === '/')
? tlsFileName
: path.join(this._basepath, tlsFileName);
: path.join(this._basePath, tlsFileName);
let tlsFileContent;
try {
tlsFileContent = fs.readFileSync(tlsFilePath);
Expand All @@ -502,6 +556,19 @@ class Config extends EventEmitter {
return tlsFileContent;
}

// Load TLS file or array of files
// if tlsFilename is a string, result will be a Buffer containing the file content
// if tlsFilename is an array of string, result will be an array of Buffer
_loadTlsFileArray(tlsFileName) {
let res;
if (Array.isArray(tlsFileName)) {
res = tlsFileName.map(tlsFile => this._loadTlsFile(tlsFile));
} else {
res = this._loadTlsFile(tlsFileName);
}
return res;
}

/**
* Parse list of endpoints.
* @param {string[] | undefined} listenOn - List of string of the form "ip:port"
Expand Down Expand Up @@ -1092,6 +1159,13 @@ class Config extends EventEmitter {
}
}

this.kmsAWS = this._parseKmsAWS(config);

const defaultEncryptionKeyPerAccount = config.defaultEncryptionKeyPerAccount;
this.defaultEncryptionKeyPerAccount = defaultEncryptionKeyPerAccount || false;
assert(typeof this.defaultEncryptionKeyPerAccount === 'boolean',
'config.defaultEncryptionKeyPerAccount must be a boolean');

this.healthChecks = defaultHealthChecks;
if (config.healthChecks && config.healthChecks.allowFrom) {
assert(config.healthChecks.allowFrom instanceof Array,
Expand Down
8 changes: 4 additions & 4 deletions lib/api/apiUtils/bucket/bucketCreation.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,16 +125,16 @@ function cleanUpBucket(bucketMD, canonicalID, log, callback) {
/**
* Manage the server side encryption on bucket creation, as a side effect
* a bucket key is created in the kms
* @param {string} bucketName - name of bucket
* @param {BucketInfo} bucket - bucket info
* @param {object} headers - request headers
* @param {function} log - Werelogs logger
* @param {function} cb - called on completion
* @returns {undefined}
* @callback called with (err, sseInfo: object)
*/
function bucketLevelServerSideEncryption(bucketName, headers, log, cb) {
function bucketLevelServerSideEncryption(bucket, headers, log, cb) {
kms.bucketLevelEncryption(
bucketName, headers, log, (err, sseInfo) => {
bucket, headers, log, (err, sseInfo) => {
if (err) {
log.debug('error getting bucket encryption info', {
error: err,
Expand Down Expand Up @@ -231,7 +231,7 @@ function createBucket(authInfo, bucketName, headers,
if (existingBucketMD === 'NoBucketYet') {
const sseConfig = parseBucketEncryptionHeaders(headers);
return bucketLevelServerSideEncryption(
bucketName, sseConfig, log,
bucket, sseConfig, log,
(err, sseInfo) => {
if (err) {
return cb(err);
Expand Down
11 changes: 9 additions & 2 deletions lib/api/apiUtils/bucket/bucketDeletion.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,15 @@ function deleteBucket(authInfo, bucketMD, bucketName, canonicalID, request, log,
return cb(err);
}
const serverSideEncryption = bucketMD.getServerSideEncryption();
if (serverSideEncryption &&
serverSideEncryption.algorithm === 'AES256') {
const isScalityManagedEncryptionKey = serverSideEncryption && serverSideEncryption.algorithm === 'AES256';
const isAccountEncryptionEnabled = bucketMD.isAccountEncryptionEnabled();

/**
* If all of the following conditions are met, delete the master encryption key:
* - The encryption key is managed by Scality (not externally managed).
* - The encryption is bucket-specific (to prevent deleting default account encryption key).
*/
if (isScalityManagedEncryptionKey && !isAccountEncryptionEnabled) {
const masterKeyId = serverSideEncryption.masterKeyId;
return kms.destroyBucketKey(masterKeyId, log, cb);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/api/apiUtils/bucket/bucketEncryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ function parseObjectEncryptionHeaders(headers) {
*/
function createDefaultBucketEncryptionMetadata(bucket, log, cb) {
return kms.bucketLevelEncryption(
bucket.getName(),
bucket,
{ algorithm: 'AES256', mandatory: false },
log,
(error, sseConfig) => {
Expand Down Expand Up @@ -236,7 +236,7 @@ function getObjectSSEConfiguration(headers, bucket, log, cb) {

// If the bucket has a default encryption config, and it is mandatory
// (created with putBucketEncryption or legacy headers)
// pass it through
// pass it through.
if (bucketSSE && bucketSSE.mandatory) {
return cb(null, bucketSSE);
}
Expand Down
1 change: 1 addition & 0 deletions lib/api/bucketDeleteEncryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ function bucketDeleteEncryption(authInfo, request, log, callback) {
algorithm: sseConfig.algorithm,
cryptoScheme: sseConfig.cryptoScheme,
masterKeyId: sseConfig.masterKeyId,
configuredMasterKeyId: sseConfig.configuredMasterKeyId,
};

bucket.setServerSideEncryption(updatedConfig);
Expand Down
11 changes: 9 additions & 2 deletions lib/api/bucketPutEncryption.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ function bucketPutEncryption(authInfo, request, log, callback) {
},
(bucket, encryptionConfig, next) => {
const existingConfig = bucket.getServerSideEncryption();
if (existingConfig === null) {
return kms.bucketLevelEncryption(bucket.getName(), encryptionConfig, log,
// Check if encryption is not configured or if a default master key has not been created yet.
if (existingConfig === null || !existingConfig.masterKeyId) {
return kms.bucketLevelEncryption(bucket, encryptionConfig, log,
(err, updatedConfig) => {
if (err) {
return next(err);
Expand All @@ -51,13 +52,19 @@ function bucketPutEncryption(authInfo, request, log, callback) {
});
}

// If encryption is already configured and a default master key exists

// If the request does not specify a custom key, reuse the existing default master key id
// This ensures that a new default master key is not generated every time
// `putBucketEncryption` is called, avoiding unnecessary key creation
const updatedConfig = {
mandatory: true,
algorithm: encryptionConfig.algorithm,
cryptoScheme: existingConfig.cryptoScheme,
masterKeyId: existingConfig.masterKeyId,
};

// If the request specifies a custom master key id, store it in the updated configuration
const { configuredMasterKeyId } = encryptionConfig;
if (configuredMasterKeyId) {
updatedConfig.configuredMasterKeyId = configuredMasterKeyId;
Expand Down
4 changes: 2 additions & 2 deletions lib/kms/file/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ const backend = {

/**
*
* @param {string} bucketName - bucket name
* @param {BucketInfo} bucket - bucket info
* @param {object} log - logger object
* @param {function} cb - callback
* @returns {undefined}
* @callback called with (err, masterKeyId: string)
*/
createBucketKey: function createBucketKeyMem(bucketName, log, cb) {
createBucketKey: function createBucketKeyMem(bucket, log, cb) {
process.nextTick(() => {
// Using createDataKey here for purposes of createBucketKeyMem
// so that we do not need a separate function.
Expand Down
6 changes: 4 additions & 2 deletions lib/kms/in_memory/backend.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ const backend = {
* Target implementation will be async. let's mimic it
*/

supportsDefaultKeyPerAccount: false,

/**
*
* @param {string} bucketName - bucket name
* @param {BucketInfo} bucket - bucket info
* @param {object} log - logger object
* @param {function} cb - callback
* @returns {undefined}
* @callback called with (err, masterKeyId: string)
*/
createBucketKey: function createBucketKeyMem(bucketName, log, cb) {
createBucketKey: function createBucketKeyMem(bucket, log, cb) {
process.nextTick(() => {
// Using createDataKey here for purposes of createBucketKeyMem
// so that we do not need a separate function.
Expand Down
Loading
Loading