Skip to content

Commit

Permalink
CLDSRV-559 AWS KMS backend
Browse files Browse the repository at this point in the history
  • Loading branch information
nicolas2bert committed Sep 25, 2024
1 parent 853102f commit 3f9a7fb
Show file tree
Hide file tree
Showing 19 changed files with 1,187 additions and 155 deletions.
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"
}
}
117 changes: 60 additions & 57 deletions 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 @@ -508,7 +562,7 @@ class Config extends EventEmitter {
_loadTlsFileArray(tlsFileName) {
let res;
if (Array.isArray(tlsFileName)) {
res = tlsFileName.map(this._loadTlsFile);
res = tlsFileName.map(tlsFile => this._loadTlsFile(tlsFile));
} else {
res = this._loadTlsFile(tlsFileName);
}
Expand Down Expand Up @@ -1105,63 +1159,12 @@ class Config extends EventEmitter {
}
}

// Use env variables as default values.
// We use the same env variables as the AWS CLI does but prefixed with "KMS_",
// allowing distinct endpoints betweens AWS compatibility components.
// Please note that if no config is specified here, the AWS Client
// seems to fallback on the local AWS configuration files
// (those contained in ~/.aws directory)
this.kmsAWS = {
region: process.env.KMS_AWS_REGION || process.env.KMS_AWS_DEFAULT_REGION,
endpoint: process.env.KMS_AWS_ENDPOINT_URL_KMS || process.env.KMS_AWS_ENDPOINT_URL,
ak: process.env.KMS_AWS_ACCESS_KEY_ID,
sk: process.env.KMS_AWS_SECRET_ACCESS_KEY,
};
if (config.kmsAWS) {
const { region, endpoint, ak, sk, tls } = config.kmsAWS;
if (region) {
this.kmsAWS.region = region;
}
if (endpoint) {
this.kmsAWS.endpoint = endpoint;
}
/* Configure credentials.
Currently only support AK+SK authentication, both must be supplied.
*/
if (ak && sk) {
this.kmsAWS.ak = ak;
this.kmsAWS.sk = sk;
}
this.kmsAWS = this._parseKmsAWS(config);

if (tls) {
this.kmsAWS.tls = {};
if (tls.rejectUnauthorized !== undefined) {
assert(typeof tls.rejectUnauthorized === 'boolean');
this.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');
this.kmsAWS.tls.minVersion = tls.minVersion;
}
if (tls.maxVersion !== undefined) {
assert(typeof tls.maxVersion === 'string',
'bad config: KMS AWS TLS maxVersion must be a string');
this.kmsAWS.tls.maxVersion = tls.maxVersion;
}
if (tls.ca !== undefined) {
this.kmsAWS.tls.ca = this._loadTlsFileArray(tls.ca);
}
if (tls.cert !== undefined) {
this.kmsAWS.tls.cert = this._loadTlsFileArray(tls.cert);
}
if (tls.key !== undefined) {
this.kmsAWS.tls.key = this._loadTlsFileArray(tls.key);
}
}
}
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) {
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

0 comments on commit 3f9a7fb

Please sign in to comment.