@underpostnet/underpost 2.92.0 → 2.95.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/pwa-microservices-template-page.cd.yml +5 -4
- package/README.md +4 -5
- package/bin/build.js +6 -1
- package/bin/deploy.js +2 -69
- package/cli.md +99 -92
- package/manifests/deployment/dd-default-development/deployment.yaml +4 -4
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/scripts/disk-clean.sh +216 -0
- package/scripts/ssh-cluster-info.sh +4 -3
- package/src/cli/cluster.js +1 -1
- package/src/cli/db.js +71 -80
- package/src/cli/deploy.js +77 -13
- package/src/cli/image.js +198 -133
- package/src/cli/index.js +59 -81
- package/src/cli/lxd.js +73 -74
- package/src/cli/monitor.js +20 -9
- package/src/cli/repository.js +86 -3
- package/src/cli/run.js +167 -63
- package/src/cli/ssh.js +351 -134
- package/src/index.js +1 -1
- package/src/monitor.js +11 -1
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +1 -1
- package/src/server/dns.js +88 -1
- package/src/server/process.js +6 -1
- package/scripts/snap-clean.sh +0 -26
- package/src/client/public/default/plantuml/client-conf.svg +0 -1
- package/src/client/public/default/plantuml/client-schema.svg +0 -1
- package/src/client/public/default/plantuml/cron-conf.svg +0 -1
- package/src/client/public/default/plantuml/cron-schema.svg +0 -1
- package/src/client/public/default/plantuml/server-conf.svg +0 -1
- package/src/client/public/default/plantuml/server-schema.svg +0 -1
- package/src/client/public/default/plantuml/ssr-conf.svg +0 -1
- package/src/client/public/default/plantuml/ssr-schema.svg +0 -1
package/src/cli/ssh.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
|
|
7
7
|
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
8
|
import Dns from '../server/dns.js';
|
|
9
|
-
import { shellExec } from '../server/process.js';
|
|
9
|
+
import { pbcopy, shellExec } from '../server/process.js';
|
|
10
10
|
import { loggerFactory } from '../server/logger.js';
|
|
11
11
|
import fs from 'fs-extra';
|
|
12
|
+
import UnderpostRootEnv from './env.js';
|
|
12
13
|
|
|
13
14
|
const logger = loggerFactory(import.meta);
|
|
14
15
|
|
|
@@ -19,6 +20,173 @@ const logger = loggerFactory(import.meta);
|
|
|
19
20
|
*/
|
|
20
21
|
class UnderpostSSH {
|
|
21
22
|
static API = {
|
|
23
|
+
/**
|
|
24
|
+
* Loads configuration node from disk or returns default empty config.
|
|
25
|
+
* @private
|
|
26
|
+
* @function loadConfigNode
|
|
27
|
+
* @memberof UnderpostSSH
|
|
28
|
+
* @param {string} deployId - Deployment ID for the config path
|
|
29
|
+
* @returns {{confNode: Object, confNodePath: string}} Configuration node and its file path
|
|
30
|
+
* @description Loads or creates a config node with users object structure
|
|
31
|
+
*/
|
|
32
|
+
loadConfigNode: (deployId) => {
|
|
33
|
+
const confNodePath = `./engine-private/conf/${deployId}/conf.node.json`;
|
|
34
|
+
const confNode = fs.existsSync(confNodePath) ? JSON.parse(fs.readFileSync(confNodePath, 'utf8')) : { users: {} };
|
|
35
|
+
return { confNode, confNodePath };
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Saves configuration node to disk.
|
|
40
|
+
* @private
|
|
41
|
+
* @function saveConfigNode
|
|
42
|
+
* @memberof UnderpostSSH
|
|
43
|
+
* @param {string} confNodePath - Path to the configuration file
|
|
44
|
+
* @param {Object} confNode - Configuration object to save
|
|
45
|
+
* @returns {void}
|
|
46
|
+
*/
|
|
47
|
+
saveConfigNode: (confNodePath, confNode) => {
|
|
48
|
+
fs.outputFileSync(confNodePath, JSON.stringify(confNode, null, 4), 'utf8');
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Checks if a system user exists.
|
|
53
|
+
* @private
|
|
54
|
+
* @function checkUserExists
|
|
55
|
+
* @memberof UnderpostSSH
|
|
56
|
+
* @param {string} username - Username to check
|
|
57
|
+
* @returns {boolean} True if user exists, false otherwise
|
|
58
|
+
*/
|
|
59
|
+
checkUserExists: (username) => {
|
|
60
|
+
const result = shellExec(`id -u ${username} 2>/dev/null || echo "not_found"`, {
|
|
61
|
+
silent: true,
|
|
62
|
+
stdout: true,
|
|
63
|
+
}).trim();
|
|
64
|
+
return result !== 'not_found';
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Gets the home directory for a given user.
|
|
69
|
+
* @private
|
|
70
|
+
* @function getUserHome
|
|
71
|
+
* @memberof UnderpostSSH
|
|
72
|
+
* @param {string} username - Username to get home directory for
|
|
73
|
+
* @returns {string} User's home directory path
|
|
74
|
+
*/
|
|
75
|
+
getUserHome: (username) => {
|
|
76
|
+
return shellExec(`getent passwd ${username} | cut -d: -f6`, {
|
|
77
|
+
silent: true,
|
|
78
|
+
stdout: true,
|
|
79
|
+
}).trim();
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Creates a system user with password and groups.
|
|
84
|
+
* @private
|
|
85
|
+
* @function createSystemUser
|
|
86
|
+
* @memberof UnderpostSSH
|
|
87
|
+
* @param {string} username - Username to create
|
|
88
|
+
* @param {string} password - Password for the user
|
|
89
|
+
* @param {string} groups - Comma-separated list of groups
|
|
90
|
+
* @returns {void}
|
|
91
|
+
*/
|
|
92
|
+
createSystemUser: (username, password, groups) => {
|
|
93
|
+
shellExec(`useradd -m -s /bin/bash ${username}`);
|
|
94
|
+
shellExec(`echo "${username}:${password}" | chpasswd`);
|
|
95
|
+
if (groups) {
|
|
96
|
+
for (const group of groups.split(',').map((g) => g.trim())) {
|
|
97
|
+
shellExec(`usermod -aG "${group}" "${username}"`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Ensures SSH directory exists with proper permissions.
|
|
104
|
+
* @private
|
|
105
|
+
* @function ensureSSHDirectory
|
|
106
|
+
* @memberof UnderpostSSH
|
|
107
|
+
* @param {string} sshDir - Path to SSH directory
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*/
|
|
110
|
+
ensureSSHDirectory: (sshDir) => {
|
|
111
|
+
if (!fs.existsSync(sshDir)) {
|
|
112
|
+
shellExec(`mkdir -p ${sshDir}`);
|
|
113
|
+
shellExec(`chmod 700 ${sshDir}`);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Sets proper permissions on SSH files.
|
|
119
|
+
* @private
|
|
120
|
+
* @function setSSHFilePermissions
|
|
121
|
+
* @memberof UnderpostSSH
|
|
122
|
+
* @param {string} sshDir - SSH directory path
|
|
123
|
+
* @param {string} username - Username for ownership
|
|
124
|
+
* @param {string} [keyPath] - Optional private key path
|
|
125
|
+
* @param {string} [pubKeyPath] - Optional public key path
|
|
126
|
+
* @returns {void}
|
|
127
|
+
*/
|
|
128
|
+
setSSHFilePermissions: (sshDir, username, keyPath, pubKeyPath) => {
|
|
129
|
+
shellExec(`chmod 600 ${sshDir}/authorized_keys`);
|
|
130
|
+
shellExec(`chmod 644 ${sshDir}/known_hosts`);
|
|
131
|
+
if (keyPath) shellExec(`chmod 600 ${keyPath}`);
|
|
132
|
+
if (pubKeyPath) shellExec(`chmod 644 ${pubKeyPath}`);
|
|
133
|
+
shellExec(`chown -R ${username}:${username} ${sshDir}`);
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Configures authorized_keys for a user.
|
|
138
|
+
* @private
|
|
139
|
+
* @function configureAuthorizedKeys
|
|
140
|
+
* @memberof UnderpostSSH
|
|
141
|
+
* @param {string} sshDir - SSH directory path
|
|
142
|
+
* @param {string} pubKeyPath - Public key file path
|
|
143
|
+
* @param {boolean} disablePassword - Whether to add no-forwarding restrictions
|
|
144
|
+
* @returns {void}
|
|
145
|
+
*/
|
|
146
|
+
configureAuthorizedKeys: (sshDir, pubKeyPath, disablePassword) => {
|
|
147
|
+
if (disablePassword) {
|
|
148
|
+
shellExec(`cat >> ${sshDir}/authorized_keys <<EOF
|
|
149
|
+
no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${fs.readFileSync(pubKeyPath, 'utf8')}
|
|
150
|
+
EOF`);
|
|
151
|
+
} else {
|
|
152
|
+
shellExec(`cat ${pubKeyPath} >> ${sshDir}/authorized_keys`);
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Configures known_hosts with SSH server keys.
|
|
158
|
+
* @private
|
|
159
|
+
* @function configureKnownHosts
|
|
160
|
+
* @memberof UnderpostSSH
|
|
161
|
+
* @param {string} sshDir - SSH directory path
|
|
162
|
+
* @param {number} port - SSH port number
|
|
163
|
+
* @param {string} [host] - Optional external host to add
|
|
164
|
+
* @returns {void}
|
|
165
|
+
*/
|
|
166
|
+
configureKnownHosts: (sshDir, port, host) => {
|
|
167
|
+
shellExec(`ssh-keyscan -p ${port} -H localhost >> ${sshDir}/known_hosts`);
|
|
168
|
+
shellExec(`ssh-keyscan -p ${port} -H 127.0.0.1 >> ${sshDir}/known_hosts`);
|
|
169
|
+
if (host) shellExec(`ssh-keyscan -p ${port} -H ${host} >> ${sshDir}/known_hosts`);
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Configures sudoers for passwordless sudo or sets user password.
|
|
174
|
+
* @private
|
|
175
|
+
* @function configureSudoAccess
|
|
176
|
+
* @memberof UnderpostSSH
|
|
177
|
+
* @param {string} username - Username to configure
|
|
178
|
+
* @param {string} password - User password
|
|
179
|
+
* @param {boolean} disablePassword - Whether to enable passwordless sudo
|
|
180
|
+
* @returns {void}
|
|
181
|
+
*/
|
|
182
|
+
configureSudoAccess: (username, password, disablePassword) => {
|
|
183
|
+
if (disablePassword) {
|
|
184
|
+
shellExec(`echo '${username} ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/90_${username}`);
|
|
185
|
+
} else {
|
|
186
|
+
shellExec(`echo "${username}:${password}" | sudo chpasswd`);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
22
190
|
/**
|
|
23
191
|
* Main callback function for SSH operations including user management, key import/export, and SSH service configuration.
|
|
24
192
|
* @async
|
|
@@ -40,9 +208,12 @@ class UnderpostSSH {
|
|
|
40
208
|
* @param {boolean} [options.reset=false] - Reset SSH configuration (clear authorized_keys and known_hosts)
|
|
41
209
|
* @param {boolean} [options.keysList=false] - List authorized SSH keys
|
|
42
210
|
* @param {boolean} [options.hostsList=false] - List known SSH hosts
|
|
43
|
-
* @param {boolean} [options.
|
|
44
|
-
* @param {boolean} [options.
|
|
45
|
-
* @param {boolean} [options.
|
|
211
|
+
* @param {boolean} [options.disablePassword=false] - If true, enable passwordless sudo and add SSH restrictions
|
|
212
|
+
* @param {boolean} [options.keyTest=false] - Test SSH key generation
|
|
213
|
+
* @param {boolean} [options.stop=false] - Stop SSH service
|
|
214
|
+
* @param {boolean} [options.status=false] - Check SSH service status
|
|
215
|
+
* @param {boolean} [options.connectUri=false] - Output SSH connection URI
|
|
216
|
+
* @param {boolean} [options.copy=false] - Copy SSH connection URI to clipboard
|
|
46
217
|
* @returns {Promise<void>}
|
|
47
218
|
* @description
|
|
48
219
|
* Handles various SSH operations:
|
|
@@ -70,46 +241,67 @@ class UnderpostSSH {
|
|
|
70
241
|
keysList: false,
|
|
71
242
|
hostsList: false,
|
|
72
243
|
disablePassword: false,
|
|
244
|
+
keyTest: false,
|
|
245
|
+
stop: false,
|
|
246
|
+
status: false,
|
|
247
|
+
connectUri: false,
|
|
248
|
+
copy: false,
|
|
73
249
|
},
|
|
74
250
|
) => {
|
|
75
251
|
let confNode, confNodePath;
|
|
252
|
+
|
|
253
|
+
// Set defaults
|
|
76
254
|
if (!options.user) options.user = 'root';
|
|
77
255
|
if (!options.host) options.host = await Dns.getPublicIp();
|
|
78
256
|
if (!options.password) options.password = options.disablePassword ? '' : generateRandomPasswordSelection(16);
|
|
79
257
|
if (!options.groups) options.groups = 'wheel';
|
|
80
|
-
if (!options.port) options.port = 22;
|
|
258
|
+
if (!options.port) options.port = 22; // Handle connect uri
|
|
259
|
+
|
|
260
|
+
const userHome = UnderpostSSH.API.getUserHome(options.user);
|
|
261
|
+
options.userHome = userHome;
|
|
81
262
|
|
|
82
263
|
// Load config and override password and host if user exists in config
|
|
83
264
|
if (options.deployId) {
|
|
84
|
-
|
|
85
|
-
confNode =
|
|
265
|
+
const config = UnderpostSSH.API.loadConfigNode(options.deployId);
|
|
266
|
+
confNode = config.confNode;
|
|
267
|
+
confNodePath = config.confNodePath;
|
|
86
268
|
|
|
87
269
|
if (confNode.users && confNode.users[options.user]) {
|
|
88
|
-
if (confNode.users[options.user].password) {
|
|
89
|
-
options.password = confNode.users[options.user].password;
|
|
90
|
-
logger.info(`Using saved password for user ${options.user}`);
|
|
91
|
-
}
|
|
92
270
|
if (confNode.users[options.user].host) {
|
|
93
271
|
options.host = confNode.users[options.user].host;
|
|
94
272
|
logger.info(`Using saved host for user ${options.user}: ${options.host}`);
|
|
95
273
|
}
|
|
274
|
+
if (confNode.users[options.user].password === '') {
|
|
275
|
+
options.disablePassword = true;
|
|
276
|
+
options.password = '';
|
|
277
|
+
logger.info(`Using saved empty password for user ${options.user}`);
|
|
278
|
+
} else if (confNode.users[options.user].password) {
|
|
279
|
+
options.disablePassword = false;
|
|
280
|
+
options.password = confNode.users[options.user].password;
|
|
281
|
+
logger.info(`Using saved password for user ${options.user}`);
|
|
282
|
+
}
|
|
283
|
+
options.port = confNode.users[options.user].port || options.port;
|
|
96
284
|
}
|
|
97
285
|
}
|
|
98
286
|
|
|
99
|
-
let userHome = shellExec(`getent passwd ${options.user} | cut -d: -f6`, { silent: true, stdout: true }).trim();
|
|
100
|
-
options.userHome = userHome;
|
|
101
|
-
|
|
102
287
|
logger.info('options', options);
|
|
103
288
|
|
|
289
|
+
// Handle connect uri
|
|
290
|
+
if (options.connectUri) {
|
|
291
|
+
const keyPath = `${userHome}/.ssh/id_rsa`;
|
|
292
|
+
const uri = `ssh ${options.user}@${options.host} -i ${keyPath} -p ${options.port}`;
|
|
293
|
+
if (options.copy) {
|
|
294
|
+
pbcopy(uri);
|
|
295
|
+
} else console.log(uri);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Handle reset operation
|
|
104
300
|
if (options.reset) {
|
|
105
301
|
shellExec(`> ${userHome}/.ssh/authorized_keys`);
|
|
106
302
|
shellExec(`> ${userHome}/.ssh/known_hosts`);
|
|
107
|
-
return;
|
|
108
303
|
}
|
|
109
304
|
|
|
110
|
-
if (options.keysList) shellExec(`cat ${userHome}/.ssh/authorized_keys`);
|
|
111
|
-
if (options.hostsList) shellExec(`cat ${userHome}/.ssh/known_hosts`);
|
|
112
|
-
|
|
113
305
|
if (options.userLs) {
|
|
114
306
|
const filter = options.filter ? `${options.filter}` : '';
|
|
115
307
|
const groupsOut = shellExec(`getent group${filter ? ` | grep '${filter}'` : ''}`, {
|
|
@@ -128,50 +320,72 @@ class UnderpostSSH {
|
|
|
128
320
|
console.log(filter ? usersOut.replaceAll(filter, filter.red) : usersOut);
|
|
129
321
|
}
|
|
130
322
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
323
|
+
// Handle user removal (works with or without deployId)
|
|
324
|
+
if (options.userRemove) {
|
|
325
|
+
const groups = shellExec(`id -Gn ${options.user}`, { silent: true, stdout: true }).trim().replace(/ /g, ', ');
|
|
326
|
+
shellExec(`userdel -r ${options.user}`);
|
|
327
|
+
|
|
328
|
+
// Remove sudoers file if it exists
|
|
329
|
+
const sudoersFile = `/etc/sudoers.d/90_${options.user}`;
|
|
330
|
+
if (fs.existsSync(sudoersFile)) {
|
|
331
|
+
shellExec(`sudo rm -f ${sudoersFile}`);
|
|
332
|
+
logger.info(`Sudoers file removed: ${sudoersFile}`);
|
|
136
333
|
}
|
|
137
334
|
|
|
138
|
-
if
|
|
139
|
-
|
|
140
|
-
|
|
335
|
+
// Remove the private key copy folder and update config only if deployId is provided
|
|
336
|
+
if (options.deployId) {
|
|
337
|
+
if (!confNode) {
|
|
338
|
+
const config = UnderpostSSH.API.loadConfigNode(options.deployId);
|
|
339
|
+
confNode = config.confNode;
|
|
340
|
+
confNodePath = config.confNodePath;
|
|
341
|
+
}
|
|
342
|
+
|
|
141
343
|
const privateCopyDir = `./engine-private/conf/${options.deployId}/users/${options.user}`;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
344
|
+
if (fs.existsSync(privateCopyDir)) {
|
|
345
|
+
fs.removeSync(privateCopyDir);
|
|
346
|
+
logger.info(`Private key copy removed from ${privateCopyDir}`);
|
|
347
|
+
}
|
|
145
348
|
|
|
349
|
+
delete confNode.users[options.user];
|
|
350
|
+
UnderpostSSH.API.saveConfigNode(confNodePath, confNode);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
logger.info(`User removed`);
|
|
354
|
+
if (groups) logger.info(`User removed from groups: ${groups}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Handle user addition (works with or without deployId)
|
|
359
|
+
if (options.userAdd) {
|
|
360
|
+
let privateCopyDir, privateKeyPath, publicKeyPath, keysExistInBackup, userExistsInConfig;
|
|
361
|
+
|
|
362
|
+
// If deployId is provided, check for existing config and backup keys
|
|
363
|
+
if (options.deployId) {
|
|
364
|
+
if (!confNode) {
|
|
365
|
+
const config = UnderpostSSH.API.loadConfigNode(options.deployId);
|
|
366
|
+
confNode = config.confNode;
|
|
367
|
+
confNodePath = config.confNodePath;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
userExistsInConfig = confNode.users && confNode.users[options.user];
|
|
371
|
+
privateCopyDir = `./engine-private/conf/${options.deployId}/users/${options.user}`;
|
|
372
|
+
privateKeyPath = `${privateCopyDir}/id_rsa`;
|
|
373
|
+
publicKeyPath = `${privateCopyDir}/id_rsa.pub`;
|
|
374
|
+
keysExistInBackup = fs.existsSync(privateKeyPath) && fs.existsSync(publicKeyPath);
|
|
375
|
+
|
|
376
|
+
// If user exists in config AND keys exist in backup, import those keys
|
|
146
377
|
if (userExistsInConfig && keysExistInBackup) {
|
|
147
378
|
logger.info(`User ${options.user} already exists in config. Importing existing keys...`);
|
|
148
379
|
|
|
149
|
-
//
|
|
150
|
-
const userExists =
|
|
151
|
-
shellExec(`id -u ${options.user} 2>/dev/null || echo "not_found"`, {
|
|
152
|
-
silent: true,
|
|
153
|
-
stdout: true,
|
|
154
|
-
}).trim() !== 'not_found';
|
|
155
|
-
|
|
380
|
+
// Create system user if it doesn't exist
|
|
381
|
+
const userExists = UnderpostSSH.API.checkUserExists(options.user);
|
|
156
382
|
if (!userExists) {
|
|
157
|
-
|
|
158
|
-
shellExec(`echo "${options.user}:${options.password}" | chpasswd`);
|
|
159
|
-
if (options.groups)
|
|
160
|
-
for (const group of options.groups.split(',').map((g) => g.trim())) {
|
|
161
|
-
shellExec(`usermod -aG "${group}" "${options.user}"`);
|
|
162
|
-
}
|
|
383
|
+
UnderpostSSH.API.createSystemUser(options.user, options.password, options.groups);
|
|
163
384
|
}
|
|
164
385
|
|
|
165
|
-
const userHome =
|
|
166
|
-
silent: true,
|
|
167
|
-
stdout: true,
|
|
168
|
-
}).trim();
|
|
386
|
+
const userHome = UnderpostSSH.API.getUserHome(options.user);
|
|
169
387
|
const sshDir = `${userHome}/.ssh`;
|
|
170
|
-
|
|
171
|
-
if (!fs.existsSync(sshDir)) {
|
|
172
|
-
shellExec(`mkdir -p ${sshDir}`);
|
|
173
|
-
shellExec(`chmod 700 ${sshDir}`);
|
|
174
|
-
}
|
|
388
|
+
UnderpostSSH.API.ensureSSHDirectory(sshDir);
|
|
175
389
|
|
|
176
390
|
const userKeyPath = `${sshDir}/id_rsa`;
|
|
177
391
|
const userPubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
@@ -179,78 +393,42 @@ class UnderpostSSH {
|
|
|
179
393
|
// Import keys from backup
|
|
180
394
|
fs.copyFileSync(privateKeyPath, userKeyPath);
|
|
181
395
|
fs.copyFileSync(publicKeyPath, userPubKeyPath);
|
|
182
|
-
if (options.disablePassword) {
|
|
183
|
-
shellExec(`cat >> ${sshDir}/authorized_keys <<EOF
|
|
184
|
-
no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${fs.readFileSync(userPubKeyPath, 'utf8')}
|
|
185
|
-
EOF`);
|
|
186
|
-
shellExec(`echo '${options.user} ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/90_${options.user}`);
|
|
187
|
-
} else {
|
|
188
|
-
shellExec(`cat ${userPubKeyPath} >> ${sshDir}/authorized_keys`);
|
|
189
|
-
shellExec(`echo "${options.user}:${options.password}" | sudo chpasswd`);
|
|
190
|
-
}
|
|
191
|
-
shellExec(`ssh-keyscan -p ${options.port} -H localhost >> ${sshDir}/known_hosts`);
|
|
192
|
-
shellExec(`ssh-keyscan -p ${options.port} -H 127.0.0.1 >> ${sshDir}/known_hosts`);
|
|
193
|
-
if (options.host) shellExec(`ssh-keyscan -p ${options.port} -H ${options.host} >> ${sshDir}/known_hosts`);
|
|
194
396
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
shellExec(`chown -R ${options.user}:${options.user} ${sshDir}`);
|
|
397
|
+
UnderpostSSH.API.configureAuthorizedKeys(sshDir, userPubKeyPath, options.disablePassword);
|
|
398
|
+
UnderpostSSH.API.configureSudoAccess(options.user, options.password, options.disablePassword);
|
|
399
|
+
UnderpostSSH.API.configureKnownHosts(sshDir, options.port, options.host);
|
|
400
|
+
UnderpostSSH.API.setSSHFilePermissions(sshDir, options.user, userKeyPath, userPubKeyPath);
|
|
200
401
|
|
|
201
402
|
logger.info(`Keys imported from ${privateCopyDir} to ${sshDir}`);
|
|
202
403
|
logger.info(`User added with existing keys`);
|
|
203
404
|
return;
|
|
204
405
|
}
|
|
406
|
+
}
|
|
205
407
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
shellExec(`echo "${options.user}:${options.password}" | chpasswd`);
|
|
209
|
-
if (options.groups)
|
|
210
|
-
for (const group of options.groups.split(',').map((g) => g.trim())) {
|
|
211
|
-
shellExec(`usermod -aG "${group}" "${options.user}"`);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const userHome = shellExec(`getent passwd ${options.user} | cut -d: -f6`, {
|
|
215
|
-
silent: true,
|
|
216
|
-
stdout: true,
|
|
217
|
-
}).trim();
|
|
218
|
-
const sshDir = `${userHome}/.ssh`;
|
|
408
|
+
// New user or no existing keys - create new user and generate keys
|
|
409
|
+
UnderpostSSH.API.createSystemUser(options.user, options.password, options.groups);
|
|
219
410
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
}
|
|
411
|
+
const userHome = UnderpostSSH.API.getUserHome(options.user);
|
|
412
|
+
const sshDir = `${userHome}/.ssh`;
|
|
413
|
+
UnderpostSSH.API.ensureSSHDirectory(sshDir);
|
|
224
414
|
|
|
225
|
-
|
|
226
|
-
|
|
415
|
+
const keyPath = `${sshDir}/id_rsa`;
|
|
416
|
+
const pubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
227
417
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if (options.disablePassword) {
|
|
235
|
-
shellExec(`cat >> ${sshDir}/authorized_keys <<EOF
|
|
236
|
-
no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${fs.readFileSync(pubKeyPath, 'utf8')}
|
|
237
|
-
EOF`);
|
|
238
|
-
shellExec(`echo '${options.user} ALL=(ALL) NOPASSWD: ALL' | sudo tee /etc/sudoers.d/90_${options.user}`);
|
|
239
|
-
} else {
|
|
240
|
-
shellExec(`cat ${pubKeyPath} >> ${sshDir}/authorized_keys`);
|
|
241
|
-
shellExec(`echo "${options.user}:${options.password}" | sudo chpasswd`);
|
|
242
|
-
}
|
|
243
|
-
shellExec(`ssh-keyscan -p ${options.port} -H localhost >> ${sshDir}/known_hosts`);
|
|
244
|
-
shellExec(`ssh-keyscan -p ${options.port} -H 127.0.0.1 >> ${sshDir}/known_hosts`);
|
|
245
|
-
if (options.host) shellExec(`ssh-keyscan -p ${options.port} -H ${options.host} >> ${sshDir}/known_hosts`);
|
|
418
|
+
if (!fs.existsSync(keyPath)) {
|
|
419
|
+
shellExec(
|
|
420
|
+
`ssh-keygen -t ed25519 -f ${keyPath} -N "${options.password}" -q -C "${options.user}@${options.host}"`,
|
|
421
|
+
);
|
|
422
|
+
}
|
|
246
423
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
shellExec(`chown -R ${options.user}:${options.user} ${sshDir}`);
|
|
424
|
+
UnderpostSSH.API.configureAuthorizedKeys(sshDir, pubKeyPath, options.disablePassword);
|
|
425
|
+
UnderpostSSH.API.configureSudoAccess(options.user, options.password, options.disablePassword);
|
|
426
|
+
UnderpostSSH.API.configureKnownHosts(sshDir, options.port, options.host);
|
|
427
|
+
UnderpostSSH.API.setSSHFilePermissions(sshDir, options.user, keyPath, pubKeyPath);
|
|
252
428
|
|
|
253
|
-
|
|
429
|
+
// Save a copy of the keys to the private folder only if deployId is provided
|
|
430
|
+
if (options.deployId) {
|
|
431
|
+
if (!privateCopyDir) privateCopyDir = `./engine-private/conf/${options.deployId}/users/${options.user}`;
|
|
254
432
|
fs.ensureDirSync(privateCopyDir);
|
|
255
433
|
|
|
256
434
|
const privateKeyCopyPath = `${privateCopyDir}/id_rsa`;
|
|
@@ -269,43 +447,78 @@ EOF`);
|
|
|
269
447
|
privateKeyCopyPath,
|
|
270
448
|
publicKeyCopyPath,
|
|
271
449
|
};
|
|
272
|
-
|
|
273
|
-
logger.info(`User added`);
|
|
274
|
-
return;
|
|
450
|
+
UnderpostSSH.API.saveConfigNode(confNodePath, confNode);
|
|
275
451
|
}
|
|
276
|
-
if (options.userRemove) {
|
|
277
|
-
const groups = shellExec(`id -Gn ${options.user}`, { silent: true, stdout: true }).trim().replace(/ /g, ', ');
|
|
278
|
-
shellExec(`userdel -r ${options.user}`);
|
|
279
452
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
fs.removeSync(privateCopyDir);
|
|
284
|
-
logger.info(`Private key copy removed from ${privateCopyDir}`);
|
|
285
|
-
}
|
|
453
|
+
logger.info(`User added`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
286
456
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
457
|
+
// Handle config user listing (only with deployId)
|
|
458
|
+
if (options.deployId) {
|
|
459
|
+
if (!confNode) {
|
|
460
|
+
const config = UnderpostSSH.API.loadConfigNode(options.deployId);
|
|
461
|
+
confNode = config.confNode;
|
|
462
|
+
confNodePath = config.confNodePath;
|
|
292
463
|
}
|
|
293
|
-
|
|
294
|
-
|
|
464
|
+
|
|
465
|
+
if (options.userLs && confNode && confNode.users) {
|
|
466
|
+
logger.info(`Users in config:`);
|
|
295
467
|
Object.keys(confNode.users).forEach((user) => {
|
|
296
468
|
logger.info(`- ${user}`);
|
|
297
469
|
});
|
|
298
|
-
return;
|
|
299
470
|
}
|
|
300
471
|
}
|
|
301
472
|
|
|
473
|
+
// Handle generate root keys
|
|
302
474
|
if (options.generate)
|
|
303
475
|
UnderpostSSH.API.generateKeys({ user: options.user, password: options.password, host: options.host });
|
|
476
|
+
|
|
477
|
+
// Handle list operations
|
|
478
|
+
if (options.keysList) shellExec(`cat ${userHome}/.ssh/authorized_keys`);
|
|
479
|
+
if (options.hostsList) shellExec(`cat ${userHome}/.ssh/known_hosts`);
|
|
480
|
+
|
|
481
|
+
// Handle key test
|
|
482
|
+
if (options.keyTest) {
|
|
483
|
+
const keyPath = `${userHome}/.ssh/id_rsa`;
|
|
484
|
+
shellExec(`ssh-keygen -y -f ${keyPath} -P "${options.password}"`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Handle stop server
|
|
488
|
+
if (options.stop) shellExec('service sshd stop');
|
|
489
|
+
|
|
490
|
+
// Handle start server
|
|
304
491
|
if (options.start) {
|
|
305
492
|
UnderpostSSH.API.chmod({ user: options.user });
|
|
306
493
|
UnderpostSSH.API.initService({ port: options.port });
|
|
307
494
|
}
|
|
495
|
+
|
|
496
|
+
// Handle status server
|
|
497
|
+
if (options.status) shellExec('service sshd status');
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Loads saved SSH credentials from config and sets them in the UnderpostRootEnv API.
|
|
502
|
+
* @async
|
|
503
|
+
* @function setDefautlSshCredentials
|
|
504
|
+
* @memberof UnderpostSSH
|
|
505
|
+
* @param {Object} options - Options for setting default SSH credentials
|
|
506
|
+
* @param {string} options.deployId - Deployment ID for the config path
|
|
507
|
+
* @param {string} options.user - SSH user name
|
|
508
|
+
* @returns {Promise<void>}
|
|
509
|
+
*/
|
|
510
|
+
setDefautlSshCredentials: async (options = { deployId: '', user: '' }) => {
|
|
511
|
+
const confNodePath = `./engine-private/conf/${options.deployId}/conf.node.json`;
|
|
512
|
+
if (fs.existsSync(confNodePath)) {
|
|
513
|
+
const { users } = JSON.parse(fs.readFileSync(confNodePath, 'utf8'));
|
|
514
|
+
const { user, host, keyPath, port } = users[options.user];
|
|
515
|
+
UnderpostRootEnv.API.set('DEFAULT_SSH_USER', user);
|
|
516
|
+
UnderpostRootEnv.API.set('DEFAULT_SSH_HOST', host);
|
|
517
|
+
UnderpostRootEnv.API.set('DEFAULT_SSH_KEY_PATH', keyPath);
|
|
518
|
+
UnderpostRootEnv.API.set('DEFAULT_SSH_PORT', port);
|
|
519
|
+
} else logger.warn(`No SSH config found at ${confNodePath}`);
|
|
308
520
|
},
|
|
521
|
+
|
|
309
522
|
/**
|
|
310
523
|
* Generates new SSH ED25519 key pair and stores copies in multiple locations.
|
|
311
524
|
* @function generateKeys
|
|
@@ -336,6 +549,7 @@ EOF`);
|
|
|
336
549
|
shellExec(`sudo rm -rf ./id_rsa`);
|
|
337
550
|
shellExec(`sudo rm -rf ./id_rsa.pub`);
|
|
338
551
|
},
|
|
552
|
+
|
|
339
553
|
/**
|
|
340
554
|
* Sets proper permissions and ownership for SSH directories and files.
|
|
341
555
|
* @function chmod
|
|
@@ -360,6 +574,7 @@ EOF`);
|
|
|
360
574
|
shellExec(`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`);
|
|
361
575
|
shellExec(`chown -R ${user}:${user} ~/.ssh`);
|
|
362
576
|
},
|
|
577
|
+
|
|
363
578
|
/**
|
|
364
579
|
* Initializes and hardens SSH service configuration for RHEL-based systems.
|
|
365
580
|
* @function initService
|
|
@@ -447,9 +662,11 @@ EOF`,
|
|
|
447
662
|
shellExec(`sudo systemctl restart sshd`);
|
|
448
663
|
|
|
449
664
|
const status = shellExec(`sudo systemctl status sshd`, { silent: true, stdout: true });
|
|
450
|
-
console.log(
|
|
451
|
-
|
|
452
|
-
|
|
665
|
+
if (status.match('running')) console.log(status.replaceAll(`running`, `running`.green));
|
|
666
|
+
else {
|
|
667
|
+
logger.error('SSHD service failed to start');
|
|
668
|
+
console.log(status);
|
|
669
|
+
}
|
|
453
670
|
},
|
|
454
671
|
};
|
|
455
672
|
}
|
package/src/index.js
CHANGED
package/src/monitor.js
CHANGED
|
@@ -19,6 +19,16 @@ const logger = loggerFactory(import.meta);
|
|
|
19
19
|
|
|
20
20
|
await logger.setUpInfo();
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const deployId = process.argv[2];
|
|
23
|
+
const env = process.argv[3] || 'production';
|
|
24
|
+
const replicas = process.argv[4] || '1';
|
|
25
|
+
const namespace = process.argv[5] || 'default';
|
|
26
|
+
|
|
27
|
+
UnderpostMonitor.API.callback(deployId, env, {
|
|
28
|
+
type: 'blue-green',
|
|
29
|
+
sync: true,
|
|
30
|
+
replicas,
|
|
31
|
+
namespace,
|
|
32
|
+
});
|
|
23
33
|
|
|
24
34
|
ProcessController.init(logger);
|
package/src/server/backup.js
CHANGED
package/src/server/conf.js
CHANGED
|
@@ -124,7 +124,7 @@ const Config = {
|
|
|
124
124
|
fs.readFileSync(`./.github/workflows/engine-test.ci.yml`, 'utf8').replaceAll('test', deployId.split('dd-')[1]),
|
|
125
125
|
'utf8',
|
|
126
126
|
);
|
|
127
|
-
shellExec(`node bin
|
|
127
|
+
shellExec(`node bin new --default-conf --deploy-id ${deployId}`);
|
|
128
128
|
|
|
129
129
|
if (!fs.existsSync(`./engine-private/deploy/dd.router`))
|
|
130
130
|
fs.writeFileSync(`./engine-private/deploy/dd.router`, '', 'utf8');
|