Skip to content

Commit f9f27ce

Browse files
committed
Merge branch 'improvement/CLDSRV-636-kms-cherry-pick' into q/7.70
2 parents 4a7134e + 6f8b630 commit f9f27ce

27 files changed

+709
-165
lines changed

config.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,9 @@
8888
}
8989
],
9090
"defaultEncryptionKeyPerAccount": true,
91+
"kmsHideScalityArn": false,
9192
"kmsAWS": {
93+
"providerName": "aws",
9294
"region": "us-east-1",
9395
"endpoint": "http://127.0.0.1:8080",
9496
"ak": "tbd",

lib/Config.js

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ const { azureAccountNameRegex, base64Regex,
1818
const { utapiVersion } = require('utapi');
1919
const { versioning } = require('arsenal');
2020
const constants = require('../constants');
21+
const {
22+
KmsType,
23+
KmsProtocol,
24+
isValidProvider,
25+
isValidType,
26+
isValidProtocol,
27+
} = require('arsenal/build/lib/network/KMSInterface');
2128

2229
const versionIdUtils = versioning.VersionID;
2330

@@ -449,8 +456,9 @@ class Config extends EventEmitter {
449456

450457
// Read config automatically
451458
this._getLocationConfig();
452-
this._getConfig();
459+
const config = this._getConfig();
453460
this._configureBackends();
461+
this._sseMigration(config);
454462
}
455463

456464
_parseKmsAWS(config) {
@@ -459,13 +467,19 @@ class Config extends EventEmitter {
459467
}
460468
let kmsAWS = {};
461469

462-
const { region, endpoint, ak, sk, tls } = config.kmsAWS;
470+
const { providerName, region, endpoint, ak, sk, tls, noAwsArn } = config.kmsAWS;
463471

472+
assert(providerName, 'Configuration Error: providerName must be defined in kmsAWS');
473+
assert(isValidProvider(providerName),
474+
'Configuration Error: kmsAWS.providerNamer must be lowercase alphanumeric only');
464475
assert(endpoint, 'Configuration Error: endpoint must be defined in kmsAWS');
465476
assert(ak, 'Configuration Error: ak must be defined in kmsAWS');
466477
assert(sk, 'Configuration Error: sk must be defined in kmsAWS');
478+
assert(['undefined', 'boolean'].some(type => type === typeof noAwsArn),
479+
'Configuration Error:: kmsAWS.noAwsArn must be a boolean or not set');
467480

468481
kmsAWS = {
482+
providerName,
469483
endpoint,
470484
ak,
471485
sk,
@@ -475,6 +489,10 @@ class Config extends EventEmitter {
475489
kmsAWS.region = region;
476490
}
477491

492+
if (noAwsArn) {
493+
kmsAWS.noAwsArn = noAwsArn;
494+
}
495+
478496
if (tls) {
479497
kmsAWS.tls = {};
480498
if (tls.rejectUnauthorized !== undefined) {
@@ -589,6 +607,10 @@ class Config extends EventEmitter {
589607
transport: this._parseKmipTransport({}),
590608
};
591609
if (config.kmip) {
610+
assert(config.kmip.providerName, 'config.kmip.providerName must be defined');
611+
assert(isValidProvider(config.kmip.providerName),
612+
'config.kmip.providerName must be lowercase alphanumeric only');
613+
this.kmip.providerName = config.kmip.providerName;
592614
if (config.kmip.client) {
593615
if (config.kmip.client.compoundCreateActivate) {
594616
assert(typeof config.kmip.client.compoundCreateActivate ===
@@ -1145,8 +1167,12 @@ class Config extends EventEmitter {
11451167

11461168
this.kms = {};
11471169
if (config.kms) {
1170+
assert(config.kms.providerName, 'config.kms.providerName must be provided');
1171+
assert(isValidProvider(config.kms.providerName),
1172+
'config.kms.providerName must be lowercase alphanumeric only');
11481173
assert(typeof config.kms.userName === 'string');
11491174
assert(typeof config.kms.password === 'string');
1175+
this.kms.providerName = config.kms.providerName;
11501176
this.kms.userName = config.kms.userName;
11511177
this.kms.password = config.kms.password;
11521178
if (config.kms.helperProgram !== undefined) {
@@ -1176,6 +1202,11 @@ class Config extends EventEmitter {
11761202
assert(typeof this.defaultEncryptionKeyPerAccount === 'boolean',
11771203
'config.defaultEncryptionKeyPerAccount must be a boolean');
11781204

1205+
this.kmsHideScalityArn = Object.hasOwnProperty.call(config, 'kmsHideScalityArn')
1206+
? config.kmsHideScalityArn
1207+
: true; // By default hide scality arn to keep backward compatibility and simplicity
1208+
assert.strictEqual(typeof this.kmsHideScalityArn, 'boolean');
1209+
11791210
this.healthChecks = defaultHealthChecks;
11801211
if (config.healthChecks && config.healthChecks.allowFrom) {
11811212
assert(config.healthChecks.allowFrom instanceof Array,
@@ -1380,6 +1411,7 @@ class Config extends EventEmitter {
13801411
'bad config: maxScannedLifecycleListingEntries must be greater than 2');
13811412
this.maxScannedLifecycleListingEntries = config.maxScannedLifecycleListingEntries;
13821413
}
1414+
return config;
13831415
}
13841416

13851417
_configureBackends() {
@@ -1455,6 +1487,61 @@ class Config extends EventEmitter {
14551487
};
14561488
}
14571489

1490+
_sseMigration(config) {
1491+
if (config.sseMigration) {
1492+
/**
1493+
* For data that was encrypted internally by default and a new external provider is setup.
1494+
* This config helps detect the existing encryption key to decrypt with the good provider.
1495+
* The key format will be migrated automatically on GET/HEADs to include provider details.
1496+
*/
1497+
this.sseMigration = {};
1498+
const { previousKeyType, previousKeyProtocol, previousKeyProvider } = config.sseMigration;
1499+
if (!previousKeyType) {
1500+
assert.fail(
1501+
'NotImplemented: No dynamic KMS key migration. Set sseMigration.previousKeyType');
1502+
}
1503+
1504+
// If previousKeyType is provided it's used as static value to migrate the format of the key
1505+
// without additional dynamic evaluation if the key provider is unknown.
1506+
assert(isValidType(previousKeyType),
1507+
'ssenMigration.previousKeyType must be "internal" or "external"');
1508+
this.sseMigration.previousKeyType = previousKeyType;
1509+
1510+
let expectedProtocol;
1511+
if (previousKeyType === KmsType.internal) {
1512+
// For internal key type default protocol is file and provider is scality
1513+
this.sseMigration.previousKeyProtocol = previousKeyProtocol || KmsProtocol.file;
1514+
this.sseMigration.previousKeyProvider = previousKeyProvider || 'scality';
1515+
expectedProtocol = [KmsProtocol.scality, KmsProtocol.mem, KmsProtocol.file];
1516+
} else if (previousKeyType === KmsType.external) {
1517+
// No defaults allowed for external provider
1518+
assert(previousKeyProtocol,
1519+
'sseMigration.previousKeyProtocol must be defined for external provider');
1520+
this.sseMigration.previousKeyProtocol = previousKeyProtocol;
1521+
assert(previousKeyProvider,
1522+
'sseMigration.previousKeyProvider must be defined for external provider');
1523+
this.sseMigration.previousKeyProvider = previousKeyProvider;
1524+
expectedProtocol = [KmsProtocol.kmip, KmsProtocol.aws_kms];
1525+
}
1526+
1527+
assert(isValidProtocol(previousKeyType, this.sseMigration.previousKeyProtocol),
1528+
`sseMigration.previousKeyProtocol must be one of ${expectedProtocol}`);
1529+
assert(isValidProvider(previousKeyProvider),
1530+
'sseMigration.previousKeyProvider must be lowercase alphanumeric only');
1531+
1532+
if (this.sseMigration.previousKeyType === KmsType.external) {
1533+
if ([KmsProtocol.file, KmsProtocol.mem].includes(this.backends.kms)) {
1534+
assert.fail(
1535+
`sseMigration.previousKeyType "external" can't migrate to "internal" KMS provider ${
1536+
this.backends.kms}`
1537+
);
1538+
}
1539+
// We'd have to compare protocol & providerName
1540+
assert.fail('sseMigration.previousKeyType "external" is not yet available');
1541+
}
1542+
}
1543+
}
1544+
14581545
setAuthDataAccounts(accounts) {
14591546
this.authData.accounts = accounts;
14601547
this.emit('authdata-update');

lib/api/apiUtils/bucket/bucketEncryption.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { errors, errorInstances } = require('arsenal');
22
const metadata = require('../../../metadata/wrapper');
33
const kms = require('../../../kms/wrapper');
44
const { parseString } = require('xml2js');
5+
const { isScalityKmsArn } = require('arsenal/build/lib/network/KMSInterface');
56

67
/**
78
* ServerSideEncryptionInfo - user configuration for server side encryption
@@ -95,6 +96,12 @@ function parseEncryptionXml(xml, log, cb) {
9596
}
9697

9798
result.configuredMasterKeyId = encConfig.KMSMasterKeyID[0];
99+
// If key is not in a scality arn format include a scality arn prefix
100+
// of the currently selected KMS client.
101+
// To keep track of KMS type, protocol and provider used
102+
if (!isScalityKmsArn(result.configuredMasterKeyId)) {
103+
result.configuredMasterKeyId = `${kms.arnPrefix}${result.configuredMasterKeyId}`;
104+
}
98105
}
99106
return cb(null, result);
100107
});
@@ -119,7 +126,12 @@ function hydrateEncryptionConfig(algorithm, configuredMasterKeyId, mandatory = n
119126
const sseConfig = { algorithm, mandatory };
120127

121128
if (algorithm === 'aws:kms' && configuredMasterKeyId) {
122-
sseConfig.configuredMasterKeyId = configuredMasterKeyId;
129+
// If key is not in a scality arn format include a scality arn prefix
130+
// of the currently selected KMS client.
131+
// To keep track of KMS type, protocol and provider used
132+
sseConfig.configuredMasterKeyId = isScalityKmsArn(configuredMasterKeyId)
133+
? configuredMasterKeyId
134+
: `${kms.arnPrefix}${configuredMasterKeyId}`;
123135
}
124136

125137
if (mandatory !== null) {
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
const { getVersionSpecificMetadataOptions } = require('../object/versioning');
2+
// const getReplicationInfo = require('../object/getReplicationInfo');
3+
const { config } = require('../../../Config');
4+
const kms = require('../../../kms/wrapper');
5+
const metadata = require('../../../metadata/wrapper');
6+
const { isScalityKmsArn, makeScalityArnPrefix } = require('arsenal/build/lib/network/KMSInterface');
7+
8+
// Bucket need a key from the new KMS, not a simple reformating
9+
function updateBucketEncryption(bucket, log, cb) {
10+
const sse = bucket.getServerSideEncryption();
11+
12+
if (!sse) {
13+
return cb(null, bucket);
14+
}
15+
16+
const masterKey = sse.masterKeyId;
17+
const configuredKey = sse.configuredMasterKeyId;
18+
19+
// Note: if migration is from an external to an external, absence of arn is not enough
20+
// a comparison of arn will be necessary but config validation blocks this for now
21+
const updateMaster = masterKey && !isScalityKmsArn(masterKey);
22+
const updateConfigured = configuredKey && !isScalityKmsArn(configuredKey);
23+
24+
if (!updateMaster && !updateConfigured) {
25+
return cb(null, bucket);
26+
}
27+
log.debug('trying to update bucket encryption', { oldKey: masterKey || configuredKey });
28+
// this should trigger vault account key update as well
29+
return kms.createBucketKey(bucket, log, (err, newSse) => {
30+
if (err) {
31+
return cb(err, bucket);
32+
}
33+
// if both keys needs migration, it is ok the use the same KMS key
34+
// as the configured one should be used and the only way to use the
35+
// masterKeyId is to PutBucketEncryption to AES256 but then nothing
36+
// will break and the same KMS key will continue to be used.
37+
// And the key is managed (created) by Scality, not passed from input.
38+
if (updateMaster) {
39+
sse.masterKeyId = newSse.masterKeyArn;
40+
}
41+
if (updateConfigured) {
42+
sse.configuredMasterKeyId = newSse.masterKeyArn;
43+
}
44+
// KMS account key will not be deleted when bucket is deleted
45+
if (newSse.isAccountEncryptionEnabled) {
46+
sse.isAccountEncryptionEnabled = newSse.isAccountEncryptionEnabled;
47+
}
48+
49+
log.info('updating bucket encryption', {
50+
oldKey: masterKey || configuredKey,
51+
newKey: newSse.masterKeyArn,
52+
isAccount: newSse.isAccountEncryptionEnabled,
53+
});
54+
return metadata.updateBucket(bucket.getName(), bucket, log, err => cb(err, bucket));
55+
});
56+
}
57+
58+
// Only reformat the key, don't generate a new one.
59+
// Use opts.skipObjectUpdate to only prepare objMD without sending the update to metadata
60+
// if a metadata.putObjectMD is expected later in call flow. (Downside: update skipped if error)
61+
function updateObjectEncryption(bucket, objMD, objectKey, log, keyArnPrefix, opts, cb) {
62+
if (!objMD) {
63+
return cb(null, bucket, objMD);
64+
}
65+
66+
const key = objMD['x-amz-server-side-encryption-aws-kms-key-id'];
67+
68+
if (!key || isScalityKmsArn(key)) {
69+
return cb(null, bucket, objMD);
70+
}
71+
const newKey = `${keyArnPrefix}${key}`;
72+
// eslint-disable-next-line no-param-reassign
73+
objMD['x-amz-server-side-encryption-aws-kms-key-id'] = newKey;
74+
// Doesn't seem to be used but update as well
75+
for (const dataLocator of objMD.location || []) {
76+
if (dataLocator.masterKeyId) {
77+
dataLocator.masterKeyId = `${keyArnPrefix}${dataLocator.masterKeyId}`;
78+
}
79+
}
80+
// eslint-disable-next-line no-param-reassign
81+
objMD.originOp = 's3:ObjectCreated:Copy';
82+
// Copy should be tested for 9.5 in INTGR-1038
83+
// to make sure it does not impact backbeat CRR / bucket notif
84+
const params = getVersionSpecificMetadataOptions(objMD, config.nullVersionCompatMode);
85+
86+
log.info('reformating object encryption key', { oldKey: key, newKey, skipUpdate: opts.skipObjectUpdate });
87+
if (opts.skipObjectUpdate) {
88+
return cb(null, bucket, objMD);
89+
}
90+
return metadata.putObjectMD(bucket.getName(), objectKey, objMD, params,
91+
log, err => cb(err, bucket, objMD));
92+
}
93+
94+
/**
95+
* Update encryption of bucket and object if kms provider changed
96+
*
97+
* @param {Error} err - error coming from metadata validate before the action handling
98+
* @param {BucketInfo} bucket - bucket
99+
* @param {Object} [objMD] - object metadata
100+
* @param {string} objectKey - objectKey from request.
101+
* @param {Logger} log - request logger
102+
* @param {Object} opts - options for sseMigration
103+
* @param {boolean} [opts.skipObject] - ignore object update
104+
* @param {boolean} [opts.skipObjectUpdate] - don't update metadata but prepare objMD for later update
105+
* @param {Function} cb - callback (err, bucket, objMD)
106+
* @returns {undefined}
107+
*/
108+
function updateEncryption(err, bucket, objMD, objectKey, log, opts, cb) {
109+
// Error passed here to call the function inbetween the metadataValidate and its callback
110+
if (err) {
111+
return cb(err);
112+
}
113+
// if objMD missing, still try updateBucketEncryption
114+
if (!config.sseMigration) {
115+
return cb(null, bucket, objMD);
116+
}
117+
118+
const { previousKeyType, previousKeyProtocol, previousKeyProvider } = config.sseMigration;
119+
// previousKeyType is required and validated in Config.js
120+
// for now it is the only implementation we need.
121+
// See TAD Seamless decryption with internal and external KMS: https://scality.atlassian.net/wiki/x/EgADu
122+
// for other method of migration without a previousKeyType
123+
124+
const keyArnPrefix = makeScalityArnPrefix(previousKeyType, previousKeyProtocol, previousKeyProvider);
125+
126+
return updateBucketEncryption(bucket, log, (err, bucket) => {
127+
// Any error in updating encryption at bucket or object level is returned to client.
128+
// Other possibilities: ignore error, include sse migration notice in error message.
129+
if (err) {
130+
return cb(err, bucket, objMD);
131+
}
132+
if (opts.skipObject) {
133+
return cb(err, bucket, objMD);
134+
}
135+
return updateObjectEncryption(bucket, objMD, objectKey, log, keyArnPrefix, opts, cb);
136+
});
137+
}
138+
139+
module.exports = {
140+
updateEncryption,
141+
};
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const { config } = require('../../../Config');
2+
const { getKeyIdFromArn } = require('arsenal/build/lib/network/KMSInterface');
3+
4+
function setSSEHeaders(headers, algo, kmsKey) {
5+
if (algo) {
6+
// eslint-disable-next-line no-param-reassign
7+
headers['x-amz-server-side-encryption'] = algo;
8+
if (kmsKey && algo === 'aws:kms') {
9+
// eslint-disable-next-line no-param-reassign
10+
headers['x-amz-server-side-encryption-aws-kms-key-id'] =
11+
config.kmsHideScalityArn ? getKeyIdFromArn(kmsKey) : kmsKey;
12+
}
13+
}
14+
}
15+
16+
module.exports = {
17+
setSSEHeaders,
18+
};

lib/api/bucketGetEncryption.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ const collectCorsHeaders = require('../utilities/collectCorsHeaders');
66
const { checkExpectedBucketOwner } = require('./apiUtils/authorization/bucketOwner');
77
const { standardMetadataValidateBucket } = require('../metadata/metadataUtils');
88
const escapeForXml = s3middleware.escapeForXml;
9+
const { config } = require('../Config');
10+
const { getKeyIdFromArn } = require('arsenal/build/lib/network/KMSInterface');
911

1012
/**
1113
* Bucket Get Encryption - Get bucket SSE configuration
@@ -60,7 +62,11 @@ function bucketGetEncryption(authInfo, request, log, callback) {
6062
];
6163

6264
if (sseInfo.configuredMasterKeyId) {
63-
xml.push(`<KMSMasterKeyID>${escapeForXml(sseInfo.configuredMasterKeyId)}</KMSMasterKeyID>`);
65+
xml.push(`<KMSMasterKeyID>${escapeForXml(
66+
config.kmsHideScalityArn
67+
? getKeyIdFromArn(sseInfo.configuredMasterKeyId)
68+
: sseInfo.configuredMasterKeyId
69+
)}</KMSMasterKeyID>`);
6470
}
6571

6672
xml.push(

0 commit comments

Comments
 (0)