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

Added "keyagents" domain config option. #6190

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Changes from 1 commit
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
Next Next commit
Added "keyagents" domain config option.
HuFlungDu committed Jun 19, 2024
commit 17f86d0bdef3dafa8c4983ac965a260ce911af28
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -339,6 +339,7 @@
"isaml",
"Jitsi",
"jumpcloud",
"keyagents",
"keyfile",
"keygrip",
"keyid",
16 changes: 15 additions & 1 deletion meshagent.js
Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@
"use strict";

// Construct a MeshAgent object, called upon connection
module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain, key) {
const forge = parent.parent.certificateOperations.forge;
const common = parent.parent.common;
parent.agentStats.createMeshAgentCount++;
@@ -29,6 +29,7 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {
obj.remoteaddr = req.clientIp;
obj.remoteaddrport = obj.remoteaddr + ':' + ws._socket.remotePort;
obj.nonce = parent.crypto.randomBytes(48).toString('binary');
obj.key = key
//ws._socket.setKeepAlive(true, 240000); // Set TCP keep alive, 4 minutes
if (args.agentidletimeout != 0) { ws._socket.setTimeout(args.agentidletimeout, function () { obj.close(1); }); } // Inactivity timeout of 2:30 minutes, by default agent will WebSocket ping every 2 minutes and server will pong back.
//obj.nodeid = null;
@@ -519,6 +520,19 @@ module.exports.CreateMeshAgent = function (parent, db, ws, req, args, domain) {

// Check the agent signature if we can
if (obj.agentnonce == null) { obj.unauthsign = msg.substring(4 + certlen); } else {
if (obj.key) {
let _nodekey = 'node/' + domain.id + '/' + obj.unauth.nodeid;
if (!obj.key.nodeid) {
obj.key.nodeid = _nodekey;
obj.key.domain = domain.id
db.Set(obj.key)
} else if (obj.key.nodeid !== _nodekey) {
parent.agentStats.agentBadSignature3Count++;
parent.setAgentIssue(obj, "BadSignature3");
parent.parent.debug('agent', 'Agent connected as a node that does not match its key, holding connection (' + obj.remoteaddrport + ').');
console.log('Agent connected as a node that does not match its key, holding connection (' + obj.remoteaddrport + ').'); return;
}
}
if (processAgentSignature(msg.substring(4 + certlen)) == false) {
parent.agentStats.agentBadSignature2Count++;
parent.setAgentIssue(obj, "BadSignature2");
10 changes: 10 additions & 0 deletions meshcentral-config-schema.json
Original file line number Diff line number Diff line change
@@ -383,6 +383,11 @@
"default": false,
"description": "When enabled, MeshCentral will block all downloads of MeshAgent including install scripts, if the user is not logged in"
},
"keyAgents": {
"type": "boolean",
"default": false,
"description": "When enabled, Meshcentral will key each agent downloaded to a specific node id, which is determined on first agent connection. If an agent attempts to connect without this key, or with a node id that does not match this key, it will be ignored. Deleting a device will remove its key, preventing that agent from connecting again in the future."
},
"ignoreAgentHashCheck": {
"type": [
"boolean",
@@ -1742,6 +1747,11 @@
"default": false,
"description": "When enabled, MeshCentral will block all downloads of MeshAgent including install scripts, if the user is not logged in"
},
"keyAgents": {
"type": "boolean",
"default": false,
"description": "When enabled, Meshcentral will key each agent downloaded to a specific node id, which is determined on first agent connection. If an agent attempts to connect without this key, or with a node id that does not match this key, it will be ignored. Deleting a device will remove its key, preventing that agent from connecting again in the future."
},
"geoLocation": {
"type": "boolean",
"default": false,
2 changes: 1 addition & 1 deletion meshctrl.js
Original file line number Diff line number Diff line change
@@ -1106,7 +1106,7 @@ function displayConfigHelp() {
}

function performConfigOperations(args) {
var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide'];
var domainValues = ['title', 'title2', 'titlepicture', 'trustedcert', 'welcomepicture', 'welcometext', 'userquota', 'meshquota', 'newaccounts', 'usernameisemail', 'newaccountemaildomains', 'newaccountspass', 'newaccountsrights', 'geolocation', 'lockagentdownload', 'keyagents', 'userconsentflags', 'Usersessionidletimeout', 'auth', 'ldapoptions', 'ldapusername', 'ldapuserbinarykey', 'ldapuseremail', 'footer', 'certurl', 'loginKey', 'userallowedip', 'agentallowedip', 'agentnoproxy', 'agentconfig', 'orphanagentuser', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording', 'hide'];
var domainObjectValues = ['ldapoptions', 'httpheaders', 'yubikey', 'passwordrequirements', 'limits', 'amtacmactivation', 'redirects', 'sessionrecording'];
var domainArrayValues = ['newaccountemaildomains', 'newaccountsrights', 'loginkey', 'agentconfig'];
var configChange = false;
9 changes: 8 additions & 1 deletion meshuser.js
Original file line number Diff line number Diff line change
@@ -520,7 +520,7 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if (agentstats.invalidJsonCount > 0) { errorCountersCount++; errorCounters.InvalidJSON = agentstats.invalidJsonCount; }
if (agentstats.unknownAgentActionCount > 0) { errorCountersCount++; errorCounters.UnknownAction = agentstats.unknownAgentActionCount; }
if (agentstats.agentBadWebCertHashCount > 0) { errorCountersCount++; errorCounters.BadWebCertificate = agentstats.agentBadWebCertHashCount; }
if ((agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count) > 0) { errorCountersCount++; errorCounters.BadSignature = (agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count); }
if ((agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count + agentstats.agentBadSignature3Count) > 0) { errorCountersCount++; errorCounters.BadSignature = (agentstats.agentBadSignature1Count + agentstats.agentBadSignature2Count + agentstats.agentBadSignature3Count); }
if (agentstats.agentMaxSessionHoldCount > 0) { errorCountersCount++; errorCounters.MaxSessionsReached = agentstats.agentMaxSessionHoldCount; }
if ((agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count) > 0) { errorCountersCount++; errorCounters.UnknownDeviceGroup = (agentstats.invalidDomainMeshCount + agentstats.invalidDomainMesh2Count); }
if ((agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count) > 0) { errorCountersCount++; errorCounters.InvalidDeviceGroupType = (agentstats.invalidMeshTypeCount + agentstats.invalidMeshType2Count); }
@@ -2794,6 +2794,13 @@ module.exports.CreateMeshUser = function (parent, db, ws, req, args, domain, use
if ((nodes != null) && (nodes.length == 1)) { db.Remove('da' + nodes[0].daid); } // Remove diagnostic agent to real agent link
db.Remove('ra' + node._id); // Remove real agent to diagnostic agent link
});
if (domain.keyagents || parent.parent.config.settings.keyagents) {
db.GetAllTypeNodeFiltered([nodeid], domain.id, 'agentkey', null, function (err, docs) {
for (let _doc of docs) {
db.Remove(_doc._id);
}
})
}

// Remove any user node links
if (node.links != null) {
76 changes: 71 additions & 5 deletions webserver.js
Original file line number Diff line number Diff line change
@@ -369,6 +369,7 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
agentBadWebCertHashCount: 0,
agentBadSignature1Count: 0,
agentBadSignature2Count: 0,
agentBadSignature3Count: 0,
agentMaxSessionHoldCount: 0,
invalidDomainMeshCount: 0,
invalidMeshTypeCount: 0,
@@ -5413,7 +5414,17 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var meshsettings = '';
if (req.query.ac != '4') { // If MeshCentral Assistant Monitor Mode, DONT INCLUDE SERVER DETAILS!
meshsettings += '\r\nMeshName=' + mesh.name + '\r\nMeshType=' + mesh.mtype + '\r\nMeshID=0x' + meshidhex + '\r\nServerID=' + serveridhex + '\r\n';
if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
if (obj.args.lanonly != true) {
let connectString = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx';

if (domain.keyagents || obj.parent.config.settings.keyagents) {
let _key = getRandomLowerCase(128);
let _agentKey = { "type": "agentkey", "nodeid": null, "key": _key, "_id": `agentkey//${_key}` };
db.Set(_agentKey);
connectString += `?key=${_agentKey["key"]}`;
}
meshsettings += 'MeshServer=' + connectString + '\r\n';
} else {
meshsettings += 'MeshServer=local\r\n';
if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
}
@@ -5848,7 +5859,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
if (obj.args.lanonly != true) {
let connectString = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx';
if (domain.keyagents || obj.parent.config.settings.keyagents) {
let _key = getRandomLowerCase(128);
let _agentKey = { "type": "agentkey", "nodeid": null, "key": _key, "_id": `agentkey//${_key}` };
db.Set(_agentKey);
connectString += `?key=${_agentKey["key"]}`;
}
meshsettings += 'MeshServer=' + connectString + '\r\n';
} else {
meshsettings += 'MeshServer=local\r\n';
if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
}
@@ -5995,7 +6015,16 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var httpsPort = ((obj.args.aliasport == null) ? obj.args.port : obj.args.aliasport); // Use HTTPS alias port is specified
if (obj.args.agentport != null) { httpsPort = obj.args.agentport; } // If an agent only port is enabled, use that.
if (obj.args.agentaliasport != null) { httpsPort = obj.args.agentaliasport; } // If an agent alias port is specified, use that.
if (obj.args.lanonly != true) { meshsettings += 'MeshServer=wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx\r\n'; } else {
if (obj.args.lanonly != true) {
let connectString = 'wss://' + serverName + ':' + httpsPort + '/' + xdomain + 'agent.ashx';
if (domain.keyagents || obj.parent.config.settings.keyagents) {
let _key = getRandomLowerCase(128);
let _agentKey = { "type": "agentkey", "nodeid": null, "key": _key, "_id": `agentkey//${_key}` };
db.Set(_agentKey)
connectString += `?key=${_agentKey["key"]}`
}
meshsettings += 'MeshServer=' + connectString + '\r\n';
} else {
meshsettings += 'MeshServer=local\r\n';
if ((obj.args.localdiscovery != null) && (typeof obj.args.localdiscovery.key == 'string') && (obj.args.localdiscovery.key.length > 0)) { meshsettings += 'DiscoveryKey=' + obj.args.localdiscovery.key + '\r\n'; }
}
@@ -6880,8 +6909,27 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
let p = Promise.resolve({cont: true})
if (domain.keyagents || obj.parent.config.settings.keyagents) {
if (req.query.key == null) {
return;
}
p = new Promise((resolve, reject)=>{
db.Get(`agentkey//${req.query.key}`, (err, data)=>{
if (err || data.length === 0) {
parent.debug('web', 'Got agent connection with unknown agent key ' + req.clientIp + ', holding.')
resolve({cont: false})
}
resolve({cont: true, key: data[0]})
})
})
}
//console.log('Agent connect: ' + req.clientIp);
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
p.then((_result)=>{
if (!_result.cont) { return; }
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain, _result.key); } catch (e) { console.log(e); }
})

});

// Setup MQTT broker over websocket
@@ -6911,7 +6959,25 @@ module.exports.CreateWebServer = function (parent, db, args, certificates, doneF
var domain = checkAgentIpAddress(ws, req);
if (domain == null) { parent.debug('web', 'Got agent connection with bad domain or blocked IP address ' + req.clientIp + ', holding.'); return; }
if (domain.agentkey && ((req.query.key == null) || (domain.agentkey.indexOf(req.query.key) == -1))) { return; } // If agent key is required and not provided or not valid, just hold the websocket and do nothing.
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain); } catch (e) { console.log(e); }
let p = Promise.resolve({cont: true})
if (domain.keyagents || obj.parent.config.settings.keyagents) {
if (req.query.key == null) {
return;
}
p = new Promise((resolve, reject)=>{
db.Get(`agentkey//${req.query.key}`, (err, data)=>{
if (err || data.length === 0) {
parent.debug('web', 'Got agent connection with unknown agent key ' + req.clientIp + ', holding.')
resolve({cont: false})
}
resolve({cont: true, key: data[0]})
})
})
}
p.then((_result)=>{
if (!_result.cont) { return; }
try { obj.meshAgentHandler.CreateMeshAgent(obj, obj.db, ws, req, obj.args, domain, _result.key); } catch (e) { console.log(e); }
})
});

// Setup mesh relay on alternative agent-only port