Skip to content

Commit 37162e0

Browse files
authored
Merge pull request #5788 from scality/improvement/CLDSRV-636-kms-provider
CLDSRV-636: SSE with both internal & external KMS (HF 9.2.0.12)
2 parents 39af6bc + c2f045f commit 37162e0

29 files changed

+696
-167
lines changed

.github/workflows/release.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ on:
1010

1111
jobs:
1212
build-federation-image:
13-
runs-on: ubuntu-20.04
13+
runs-on: ubuntu-latest
1414
steps:
1515
- name: Checkout
1616
uses: actions/checkout@v4
@@ -34,7 +34,7 @@ jobs:
3434
cache-to: type=gha,mode=max,scope=federation
3535

3636
build-image:
37-
runs-on: ubuntu-20.04
37+
runs-on: ubuntu-latest
3838
steps:
3939
- name: Checkout
4040
uses: actions/checkout@v4

.github/workflows/tests.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ jobs:
125125
if: always()
126126

127127
build:
128-
runs-on: ubuntu-20.04
128+
runs-on: ubuntu-latest
129129
steps:
130130
- name: Checkout
131131
uses: actions/checkout@v4
@@ -158,7 +158,7 @@ jobs:
158158
cache-to: type=gha,mode=max,scope=pykmip
159159

160160
build-federation-image:
161-
runs-on: ubuntu-20.04
161+
runs-on: ubuntu-latest
162162
steps:
163163
- name: Checkout
164164
uses: actions/checkout@v4

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
@@ -17,6 +17,13 @@ const { azureAccountNameRegex, base64Regex,
1717
} = require('../constants');
1818
const { utapiVersion } = require('utapi');
1919
const { versioning } = require('arsenal');
20+
const {
21+
KmsType,
22+
KmsProtocol,
23+
isValidProvider,
24+
isValidType,
25+
isValidProtocol,
26+
} = require('arsenal/build/lib/network/KMSInterface');
2027

2128
const versionIdUtils = versioning.VersionID;
2229

@@ -401,8 +408,9 @@ class Config extends EventEmitter {
401408

402409
// Read config automatically
403410
this._getLocationConfig();
404-
this._getConfig();
411+
const config = this._getConfig();
405412
this._configureBackends();
413+
this._sseMigration(config);
406414
}
407415

408416
_parseKmsAWS(config) {
@@ -411,13 +419,19 @@ class Config extends EventEmitter {
411419
}
412420
let kmsAWS = {};
413421

414-
const { region, endpoint, ak, sk, tls } = config.kmsAWS;
422+
const { providerName, region, endpoint, ak, sk, tls, noAwsArn } = config.kmsAWS;
415423

424+
assert(providerName, 'Configuration Error: providerName must be defined in kmsAWS');
425+
assert(isValidProvider(providerName),
426+
'Configuration Error: kmsAWS.providerNamer must be lowercase alphanumeric only');
416427
assert(endpoint, 'Configuration Error: endpoint must be defined in kmsAWS');
417428
assert(ak, 'Configuration Error: ak must be defined in kmsAWS');
418429
assert(sk, 'Configuration Error: sk must be defined in kmsAWS');
430+
assert(['undefined', 'boolean'].some(type => type === typeof noAwsArn),
431+
'Configuration Error:: kmsAWS.noAwsArn must be a boolean or not set');
419432

420433
kmsAWS = {
434+
providerName,
421435
endpoint,
422436
ak,
423437
sk,
@@ -427,6 +441,10 @@ class Config extends EventEmitter {
427441
kmsAWS.region = region;
428442
}
429443

444+
if (noAwsArn) {
445+
kmsAWS.noAwsArn = noAwsArn;
446+
}
447+
430448
if (tls) {
431449
kmsAWS.tls = {};
432450
if (tls.rejectUnauthorized !== undefined) {
@@ -1095,8 +1113,12 @@ class Config extends EventEmitter {
10951113

10961114
this.kms = {};
10971115
if (config.kms) {
1116+
assert(config.kms.providerName, 'config.kms.providerName must be provided');
1117+
assert(isValidProvider(config.kms.providerName),
1118+
'config.kms.providerName must be lowercase alphanumeric only');
10981119
assert(typeof config.kms.userName === 'string');
10991120
assert(typeof config.kms.password === 'string');
1121+
this.kms.providerName = config.kms.providerName;
11001122
this.kms.userName = config.kms.userName;
11011123
this.kms.password = config.kms.password;
11021124
if (config.kms.helperProgram !== undefined) {
@@ -1163,6 +1185,10 @@ class Config extends EventEmitter {
11631185
},
11641186
};
11651187
if (config.kmip) {
1188+
assert(config.kmip.providerName, 'config.kmip.providerName must be defined');
1189+
assert(isValidProvider(config.kmip.providerName),
1190+
'config.kmip.providerName must be lowercase alphanumeric only');
1191+
this.kmip.providerName = config.kmip.providerName;
11661192
if (config.kmip.client) {
11671193
if (config.kmip.client.compoundCreateActivate) {
11681194
assert(typeof config.kmip.client.compoundCreateActivate ===
@@ -1229,6 +1255,11 @@ class Config extends EventEmitter {
12291255
assert(typeof this.defaultEncryptionKeyPerAccount === 'boolean',
12301256
'config.defaultEncryptionKeyPerAccount must be a boolean');
12311257

1258+
this.kmsHideScalityArn = Object.hasOwnProperty.call(config, 'kmsHideScalityArn')
1259+
? config.kmsHideScalityArn
1260+
: true; // By default hide scality arn to keep backward compatibility and simplicity
1261+
assert.strictEqual(typeof this.kmsHideScalityArn, 'boolean');
1262+
12321263
this.healthChecks = defaultHealthChecks;
12331264
if (config.healthChecks && config.healthChecks.allowFrom) {
12341265
assert(config.healthChecks.allowFrom instanceof Array,
@@ -1410,6 +1441,7 @@ class Config extends EventEmitter {
14101441
}
14111442

14121443
this.lifecycleRoleName = config.lifecycleRoleName || null;
1444+
return config;
14131445
}
14141446

14151447
_configureBackends() {
@@ -1485,6 +1517,61 @@ class Config extends EventEmitter {
14851517
};
14861518
}
14871519

1520+
_sseMigration(config) {
1521+
if (config.sseMigration) {
1522+
/**
1523+
* For data that was encrypted internally by default and a new external provider is setup.
1524+
* This config helps detect the existing encryption key to decrypt with the good provider.
1525+
* The key format will be migrated automatically on GET/HEADs to include provider details.
1526+
*/
1527+
this.sseMigration = {};
1528+
const { previousKeyType, previousKeyProtocol, previousKeyProvider } = config.sseMigration;
1529+
if (!previousKeyType) {
1530+
assert.fail(
1531+
'NotImplemented: No dynamic KMS key migration. Set sseMigration.previousKeyType');
1532+
}
1533+
1534+
// If previousKeyType is provided it's used as static value to migrate the format of the key
1535+
// without additional dynamic evaluation if the key provider is unknown.
1536+
assert(isValidType(previousKeyType),
1537+
'ssenMigration.previousKeyType must be "internal" or "external"');
1538+
this.sseMigration.previousKeyType = previousKeyType;
1539+
1540+
let expectedProtocol;
1541+
if (previousKeyType === KmsType.internal) {
1542+
// For internal key type default protocol is file and provider is scality
1543+
this.sseMigration.previousKeyProtocol = previousKeyProtocol || KmsProtocol.file;
1544+
this.sseMigration.previousKeyProvider = previousKeyProvider || 'scality';
1545+
expectedProtocol = [KmsProtocol.scality, KmsProtocol.mem, KmsProtocol.file];
1546+
} else if (previousKeyType === KmsType.external) {
1547+
// No defaults allowed for external provider
1548+
assert(previousKeyProtocol,
1549+
'sseMigration.previousKeyProtocol must be defined for external provider');
1550+
this.sseMigration.previousKeyProtocol = previousKeyProtocol;
1551+
assert(previousKeyProvider,
1552+
'sseMigration.previousKeyProvider must be defined for external provider');
1553+
this.sseMigration.previousKeyProvider = previousKeyProvider;
1554+
expectedProtocol = [KmsProtocol.kmip, KmsProtocol.aws_kms];
1555+
}
1556+
1557+
assert(isValidProtocol(previousKeyType, this.sseMigration.previousKeyProtocol),
1558+
`sseMigration.previousKeyProtocol must be one of ${expectedProtocol}`);
1559+
assert(isValidProvider(previousKeyProvider),
1560+
'sseMigration.previousKeyProvider must be lowercase alphanumeric only');
1561+
1562+
if (this.sseMigration.previousKeyType === KmsType.external) {
1563+
if ([KmsProtocol.file, KmsProtocol.mem].includes(this.backends.kms)) {
1564+
assert.fail(
1565+
`sseMigration.previousKeyType "external" can't migrate to "internal" KMS provider ${
1566+
this.backends.kms}`
1567+
);
1568+
}
1569+
// We'd have to compare protocol & providerName
1570+
assert.fail('sseMigration.previousKeyType "external" is not yet available');
1571+
}
1572+
}
1573+
}
1574+
14881575
_verifyRedisPassword(password) {
14891576
return typeof password === 'string';
14901577
}

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 } = 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+
};

0 commit comments

Comments
 (0)