Added basic HashiCorp Vault support.
Ylian Saint-Hilaire committed
Oct 14, 2019 at 13:35 UTC
0e3f60e613db163fe96bc1baaefcbb16081b3ba0
3 files changed
+110
-5
certoperations.js
+2
-1
@@ -229,6 +229,7 @@ module.exports.CertificateOperations = function (parent) {
229
// Load a configuration file
230
obj.fileLoad = function (filename, encoding) {
231
if ((parent.configurationFiles != null) && (parent.configurationFiles[filename] != null)) {
232
+ if (typeof parent.configurationFiles[filename] == 'string') { return fixEndOfLines(parent.configurationFiles[filename]); }
233
return fixEndOfLines(parent.configurationFiles[filename].toString());
234
} else {
235
return fixEndOfLines(obj.fs.readFileSync(parent.getConfigFilePath(filename), encoding));
@@ -571,7 +572,7 @@ module.exports.CertificateOperations = function (parent) {
572
return r;
573
}
574
}
574
- if (parent.configurationFiles != null) { console.log("Error: Database missing some certificates."); process.exit(0); return null; }
575
+ if (parent.configurationFiles != null) { console.log("Error: Vault/Database missing some certificates."); process.exit(0); return null; }
576
577
console.log("Generating certificates, may take a few minutes...");
578
parent.updateServerState("state", "generatingcertificates");
meshcentral.js
+106
-3
@@ -116,7 +116,7 @@ function CreateMeshCentralServer(config, args) {
116
try { require('./pass').hash('test', function () { }, 0); } catch (e) { console.log('Old version of node, must upgrade.'); return; } // TODO: Not sure if this test works or not.
117
118
// Check for invalid arguments
119
- var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode'];
119
+ var validArguments = ['_', 'notls', 'user', 'port', 'aliasport', 'mpsport', 'mpsaliasport', 'redirport', 'cert', 'mpscert', 'deletedomain', 'deletedefaultdomain', 'showall', 'showusers', 'shownodes', 'showmeshes', 'showevents', 'showpower', 'clearpower', 'showiplocations', 'help', 'exactports', 'install', 'uninstall', 'start', 'stop', 'restart', 'debug', 'filespath', 'datapath', 'noagentupdate', 'launch', 'noserverbackup', 'mongodb', 'mongodbcol', 'wanonly', 'lanonly', 'nousers', 'mpspass', 'ciralocalfqdn', 'dbexport', 'dbexportmin', 'dbimport', 'dbmerge', 'dbencryptkey', 'selfupdate', 'tlsoffload', 'userallowedip', 'userblockedip', 'swarmallowedip', 'agentallowedip', 'agentblockedip', 'fastcert', 'swarmport', 'logintoken', 'logintokenkey', 'logintokengen', 'logintokengen', 'mailtokengen', 'admin', 'unadmin', 'sessionkey', 'sessiontime', 'minify', 'minifycore', 'dblistconfigfiles', 'dbshowconfigfile', 'dbpushconfigfiles', 'dbpullconfigfiles', 'dbdeleteconfigfiles', 'vaultpushconfigfiles', 'vaultpullconfigfiles', 'vaultdeleteconfigfiles', 'configkey', 'loadconfigfromdb', 'npmpath', 'memorytracking', 'serverid', 'recordencryptionrecode', 'vault', 'token', 'unsealkey', 'name'];
120
for (var arg in obj.args) { obj.args[arg.toLocaleLowerCase()] = obj.args[arg]; if (validArguments.indexOf(arg.toLocaleLowerCase()) == -1) { console.log('Invalid argument "' + arg + '", use --help.'); return; } }
121
if (obj.args.mongodb == true) { console.log('Must specify: --mongodb [connectionstring] \r\nSee https://docs.mongodb.com/manual/reference/connection-string/ for MongoDB connection string.'); return; }
122
for (i in obj.config.settings) { obj.args[i] = obj.config.settings[i]; } // Place all settings into arguments, arguments have already been placed into settings so arguments take precedence.
@@ -164,7 +164,7 @@ function CreateMeshCentralServer(config, args) {
164
165
// If "--launch" is in the arguments, launch now
166
if (obj.args.launch) {
167
- obj.StartEx();
167
+ if (obj.args.vault) { obj.StartVault(); } else { obj.StartEx(); }
168
} else {
169
// if "--launch" is not specified, launch the server as a child process.
170
var startArgs = [];
@@ -251,6 +251,108 @@ function CreateMeshCentralServer(config, args) {
251
// Initiate server self-update
252
obj.performServerCertUpdate = function () { console.log('Updating server certificates...'); process.exit(200); };
253
254
+ // Start by loading configuration from Vault
255
+ obj.StartVault = function () {
256
+ // Check that the configuration can only be loaded from one place
257
+ if ((obj.args.vault != null) && (obj.args.loadconfigfromdb != null)) { console.log("Can't load configuration from both database and Vault."); process.exit(); return; }
258
+
259
+ // Fix arguments if needed
260
+ if (typeof obj.args.vault == 'string') {
261
+ obj.args.vault = { endpoint: obj.args.vault };
262
+ if (typeof obj.args.token == 'string') { obj.args.vault.token = obj.args.token; }
263
+ if (typeof obj.args.unsealkey == 'string') { obj.args.vault.unsealkey = obj.args.unsealkey; }
264
+ if (typeof obj.args.name == 'string') { obj.args.vault.name = obj.args.name; }
265
+ }
266
+
267
+ // Load configuration for HashiCorp's Vault if needed
268
+ if (obj.args.vault) {
269
+ if (obj.args.vault.endpoint == null) { console.log('Missing Vault endpoint.'); process.exit(); return; }
270
+ if (obj.args.vault.token == null) { console.log('Missing Vault token.'); process.exit(); return; }
271
+ if (obj.args.vault.unsealkey == null) { console.log('Missing Vault unsealkey.'); process.exit(); return; }
272
+ if (obj.args.vault.name == null) { obj.args.vault.name = 'meshcentral'; }
273
+
274
+ // Get new instance of the client
275
+ var vault = require("node-vault")({ endpoint: obj.args.vault.endpoint, token: obj.args.vault.token });
276
+ vault.unseal({ key: obj.args.vault.unsealkey })
277
+ .then(() => {
278
+ if (obj.args.vaultdeleteconfigfiles) {
279
+ vault.delete('secret/data/' + obj.args.vault.name)
280
+ .then(function (r) { console.log('Done.'); process.exit(); })
281
+ .catch(function (x) { console.log(x); process.exit(); });
282
+ } else if (obj.args.vaultpushconfigfiles) {
283
+ // Push configuration files into Vault
284
+ if ((obj.args.vaultpushconfigfiles == '*') || (obj.args.vaultpushconfigfiles === true)) { obj.args.vaultpushconfigfiles = obj.datapath; }
285
+ obj.fs.readdir(obj.args.vaultpushconfigfiles, function (err, files) {
286
+ if (err != null) { console.log('ERROR: Unable to read from folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
287
+ var configFound = false;
288
+ for (var i in files) { if (files[i] == 'config.json') { configFound = true; } }
289
+ if (configFound == false) { console.log('ERROR: No config.json in folder ' + obj.args.vaultpushconfigfiles); process.exit(); return; }
290
+ var configFiles = {};
291
+ for (var i in files) {
292
+ const file = files[i];
293
+ if ((file == 'config.json') || file.endsWith('.key') || file.endsWith('.crt') || (file == 'terms.txt') || file.endsWith('.jpg') || file.endsWith('.png')) {
294
+ const path = obj.path.join(obj.args.vaultpushconfigfiles, files[i]), binary = Buffer.from(obj.fs.readFileSync(path, { encoding: 'binary' }), 'binary');
295
+ console.log('Pushing ' + file + ', ' + binary.length + ' bytes.');
296
+ if (file.endsWith('.json') || file.endsWith('.key') || file.endsWith('.crt')) { configFiles[file] = binary.toString(); } else { configFiles[file] = binary.toString('base64'); }
297
+ }
298
+ }
299
+ vault.write('secret/data/' + obj.args.vault.name, { "data": configFiles })
300
+ .then(function (r) { console.log('Done.'); process.exit(); })
301
+ .catch(function (x) { console.log(x); process.exit(); });
302
+ });
303
+ } else {
304
+ // Read configuration files from Vault
305
+ vault.read('secret/data/' + obj.args.vault.name)
306
+ .then(function (r) {
307
+ if ((r == null) || (r.data == null) || (r.data.data == null)) { console.log('Unable to read configuration from Vault.'); process.exit(); return; }
308
+ var configFiles = obj.configurationFiles = r.data.data;
309
+
310
+ // Decode Base64 when needed
311
+ for (var file in configFiles) { if (!file.endsWith('.json') && !file.endsWith('.key') && !file.endsWith('.crt')) { configFiles[file] = Buffer.from(configFiles[file], 'base64'); } }
312
+
313
+ // Save all of the files
314
+ if (obj.args.vaultpullconfigfiles) {
315
+ for (var i in configFiles) {
316
+ var fullFileName = obj.path.join(obj.args.vaultpullconfigfiles, i);
317
+ try { obj.fs.writeFileSync(fullFileName, configFiles[i]); } catch (ex) { console.log('Unable to write to ' + fullFileName); process.exit(); return; }
318
+ console.log('Pulling ' + i + ', ' + configFiles[i].length + ' bytes.');
319
+ }
320
+ console.log('Done.');
321
+ process.exit();
322
+ }
323
+
324
+ // Parse the new configuration file
325
+ var config2 = null;
326
+ try { config2 = JSON.parse(configFiles['config.json']); } catch (ex) { console.log('Error, unable to parse config.json from Vault.'); process.exit(); return; }
327
+
328
+ // Set the command line arguments to the config file if they are not present
329
+ if (!config2.settings) { config2.settings = {}; }
330
+ for (var i in args) { config2.settings[i] = args[i]; }
331
+ obj.args = args = config2.settings;
332
+
333
+ // Lower case all keys in the config file
334
+ try {
335
+ require('./common.js').objKeysToLower(config2, ["ldapoptions"]);
336
+ } catch (ex) {
337
+ console.log('CRITICAL ERROR: Unable to access the file \"./common.js\".\r\nCheck folder & file permissions.');
338
+ process.exit();
339
+ return;
340
+ }
341
+
342
+ // Grad some of the values from the original config.json file if present.
343
+ if ((config.settings.vault != null) && (config2.settings != null)) { config2.settings.vault = config.settings.vault; }
344
+
345
+ // We got a new config.json from the database, let's use it.
346
+ config = obj.config = config2;
347
+ obj.StartEx();
348
+ })
349
+ .catch(function (x) { console.log(x); process.exit(); });
350
+ }
351
+ }).catch(function (x) { console.log(x); process.exit(); });
352
+ return;
353
+ }
354
+ }
355
+
356
// Look for easy command line instructions and do them here.
357
obj.StartEx = function () {
358
var i;
@@ -532,7 +634,7 @@ function CreateMeshCentralServer(config, args) {
634
obj.StartEx1b();
635
});
636
} else {
535
- config = obj.config = getConfig(true);
637
+ config = obj.config = getConfig(obj.args.vault == null);
638
obj.StartEx1b();
639
}
640
});
@@ -1848,6 +1950,7 @@ function mainStart() {
1950
if (config.letsencrypt != null) { modules.push('greenlock'); modules.push('le-store-certbot'); modules.push('le-challenge-fs'); modules.push('le-acme-core'); } // Add Greenlock Modules
1951
if (config.settings.mqtt != null) { modules.push('aedes'); } // Add MQTT Modules
1952
if (config.settings.mongodb != null) { modules.push('mongodb'); } // Add MongoDB, official driver.
1953
+ if (config.settings.vault != null) { modules.push('node-vault'); } // Add official HashiCorp's Vault module.
1954
else if (config.settings.xmongodb != null) { modules.push('mongojs'); } // Add MongoJS, old driver.
1955
if (config.smtp != null) { modules.push('nodemailer'); } // Add SMTP support
1956
package.json
+2
-1
@@ -1,6 +1,6 @@
1
{
2
"name": "meshcentral",
3
- "version": "0.4.2-i",
3
+ "version": "0.4.2-j",
4
"keywords": [
5
"Remote Management",
6
"Intel AMT",
@@ -42,6 +42,7 @@
42
"multiparty": "^4.2.1",
43
"nedb": "^1.8.0",
44
"node-forge": "^0.8.4",
45
+ "node-vault": "^0.9.11",
46
"ws": "^6.2.1",
47
"xmldom": "^0.1.27",
48
"yauzl": "^2.10.0"