underpost 2.92.0 → 2.95.3

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.
Files changed (35) hide show
  1. package/.github/workflows/pwa-microservices-template-page.cd.yml +5 -4
  2. package/README.md +4 -5
  3. package/bin/build.js +6 -1
  4. package/bin/deploy.js +2 -69
  5. package/cli.md +100 -92
  6. package/manifests/deployment/dd-default-development/deployment.yaml +4 -4
  7. package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
  8. package/package.json +1 -1
  9. package/scripts/disk-clean.sh +216 -0
  10. package/scripts/ssh-cluster-info.sh +4 -3
  11. package/src/cli/cluster.js +1 -1
  12. package/src/cli/db.js +80 -89
  13. package/src/cli/deploy.js +77 -13
  14. package/src/cli/image.js +198 -133
  15. package/src/cli/index.js +60 -81
  16. package/src/cli/lxd.js +73 -74
  17. package/src/cli/monitor.js +20 -9
  18. package/src/cli/repository.js +86 -3
  19. package/src/cli/run.js +167 -63
  20. package/src/cli/ssh.js +351 -134
  21. package/src/index.js +1 -1
  22. package/src/monitor.js +11 -1
  23. package/src/server/backup.js +1 -1
  24. package/src/server/conf.js +1 -1
  25. package/src/server/dns.js +88 -1
  26. package/src/server/process.js +6 -1
  27. package/scripts/snap-clean.sh +0 -26
  28. package/src/client/public/default/plantuml/client-conf.svg +0 -1
  29. package/src/client/public/default/plantuml/client-schema.svg +0 -1
  30. package/src/client/public/default/plantuml/cron-conf.svg +0 -1
  31. package/src/client/public/default/plantuml/cron-schema.svg +0 -1
  32. package/src/client/public/default/plantuml/server-conf.svg +0 -1
  33. package/src/client/public/default/plantuml/server-schema.svg +0 -1
  34. package/src/client/public/default/plantuml/ssr-conf.svg +0 -1
  35. 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.importKeys=false] - Import keys from private backup to user's SSH directory
44
- * @param {boolean} [options.exportKeys=false] - Export keys from user's SSH directory to private backup
45
- * @param {boolean} [options.disablePassword=false] - If true, do not set a password for the user
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
- confNodePath = `./engine-private/conf/${options.deployId}/conf.node.json`;
85
- confNode = fs.existsSync(confNodePath) ? JSON.parse(fs.readFileSync(confNodePath, 'utf8')) : { users: {} };
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
- if (options.deployId) {
132
- // Config already loaded above, just use it
133
- if (!confNode) {
134
- confNodePath = `./engine-private/conf/${options.deployId}/conf.node.json`;
135
- confNode = fs.existsSync(confNodePath) ? JSON.parse(fs.readFileSync(confNodePath, 'utf8')) : { users: {} };
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 (options.userAdd) {
139
- // Check if user already exists in config
140
- const userExistsInConfig = confNode.users && confNode.users[options.user];
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
- const privateKeyPath = `${privateCopyDir}/id_rsa`;
143
- const publicKeyPath = `${privateCopyDir}/id_rsa.pub`;
144
- const keysExistInBackup = fs.existsSync(privateKeyPath) && fs.existsSync(publicKeyPath);
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
- // Check if system user exists
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
- shellExec(`useradd -m -s /bin/bash ${options.user}`);
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 = shellExec(`getent passwd ${options.user} | cut -d: -f6`, {
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
- shellExec(`chmod 600 ${sshDir}/authorized_keys`);
196
- shellExec(`chmod 644 ${sshDir}/known_hosts`);
197
- shellExec(`chmod 600 ${userKeyPath}`);
198
- shellExec(`chmod 644 ${userPubKeyPath}`);
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
- // New user or no existing keys - create new user and generate keys
207
- shellExec(`useradd -m -s /bin/bash ${options.user}`);
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
- if (!fs.existsSync(sshDir)) {
221
- shellExec(`mkdir -p ${sshDir}`);
222
- shellExec(`chmod 700 ${sshDir}`);
223
- }
411
+ const userHome = UnderpostSSH.API.getUserHome(options.user);
412
+ const sshDir = `${userHome}/.ssh`;
413
+ UnderpostSSH.API.ensureSSHDirectory(sshDir);
224
414
 
225
- const keyPath = `${sshDir}/id_rsa`;
226
- const pubKeyPath = `${sshDir}/id_rsa.pub`;
415
+ const keyPath = `${sshDir}/id_rsa`;
416
+ const pubKeyPath = `${sshDir}/id_rsa.pub`;
227
417
 
228
- if (!fs.existsSync(keyPath)) {
229
- shellExec(
230
- `ssh-keygen -t ed25519 -f ${keyPath} -N "${options.password}" -q -C "${options.user}@${options.host}"`,
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
- shellExec(`chmod 600 ${sshDir}/authorized_keys`);
248
- shellExec(`chmod 644 ${sshDir}/known_hosts`);
249
- shellExec(`chmod 600 ${keyPath}`);
250
- shellExec(`chmod 644 ${pubKeyPath}`);
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
- // Save a copy of the keys to the private folder
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
- fs.outputFileSync(confNodePath, JSON.stringify(confNode, null, 4), 'utf8');
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
- // Remove the private key copy folder
281
- const privateCopyDir = `./engine-private/conf/${options.deployId}/users/${options.user}`;
282
- if (fs.existsSync(privateCopyDir)) {
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
- delete confNode.users[options.user];
288
- fs.outputFileSync(confNodePath, JSON.stringify(confNode, null, 4), 'utf8');
289
- logger.info(`User removed`);
290
- if (groups) logger.info(`User removed from groups: ${groups}`);
291
- return;
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
- if (options.userLs) {
294
- logger.info(`Users:`);
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
- status.match('running') ? status.replaceAll(`running`, `running`.green) : `ssh service not running`.red,
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
@@ -36,7 +36,7 @@ class Underpost {
36
36
  * @type {String}
37
37
  * @memberof Underpost
38
38
  */
39
- static version = 'v2.92.0';
39
+ static version = 'v2.95.3';
40
40
  /**
41
41
  * Repository cli API
42
42
  * @static
package/src/monitor.js CHANGED
@@ -19,6 +19,16 @@ const logger = loggerFactory(import.meta);
19
19
 
20
20
  await logger.setUpInfo();
21
21
 
22
- UnderpostMonitor.API.callback(process.argv[2], process.argv[3], { type: 'blue-green', sync: true });
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);
@@ -86,7 +86,7 @@ class BackUp {
86
86
  ` && underpost cmt . backup cron-job '${new Date().toLocaleDateString()}'` +
87
87
  ` && underpost push . ${process.env.GITHUB_USERNAME}/cron-backups`,
88
88
  {
89
- disableLog: true,
89
+ silent: true,
90
90
  },
91
91
  );
92
92
  }
@@ -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/deploy update-default-conf ${deployId}`);
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');