@underpostnet/underpost 2.90.4 → 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/.github/workflows/release.cd.yml +7 -7
- package/README.md +7 -8
- package/bin/build.js +6 -1
- package/bin/deploy.js +2 -196
- package/cli.md +154 -80
- 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/rocky-setup.sh +1 -0
- package/scripts/ssh-cluster-info.sh +4 -3
- package/src/cli/cluster.js +1 -1
- package/src/cli/db.js +1143 -201
- package/src/cli/deploy.js +93 -24
- package/src/cli/env.js +2 -2
- package/src/cli/image.js +198 -133
- package/src/cli/index.js +111 -44
- package/src/cli/lxd.js +73 -74
- package/src/cli/monitor.js +20 -9
- package/src/cli/repository.js +212 -5
- package/src/cli/run.js +207 -74
- package/src/cli/ssh.js +642 -14
- package/src/client/components/core/CommonJs.js +0 -1
- package/src/db/mongo/MongooseDB.js +5 -1
- 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 +242 -1
- package/src/server/process.js +6 -1
- package/src/server/start.js +2 -0
- 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
|
@@ -4,7 +4,14 @@
|
|
|
4
4
|
* @namespace UnderpostSSH
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
|
+
import Dns from '../server/dns.js';
|
|
9
|
+
import { pbcopy, shellExec } from '../server/process.js';
|
|
10
|
+
import { loggerFactory } from '../server/logger.js';
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
import UnderpostRootEnv from './env.js';
|
|
13
|
+
|
|
14
|
+
const logger = loggerFactory(import.meta);
|
|
8
15
|
|
|
9
16
|
/**
|
|
10
17
|
* @class UnderpostSSH
|
|
@@ -14,31 +21,652 @@ import { shellExec } from '../server/process.js';
|
|
|
14
21
|
class UnderpostSSH {
|
|
15
22
|
static API = {
|
|
16
23
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* @param {
|
|
22
|
-
* @
|
|
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
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Main callback function for SSH operations including user management, key import/export, and SSH service configuration.
|
|
192
|
+
* @async
|
|
193
|
+
* @function callback
|
|
23
194
|
* @memberof UnderpostSSH
|
|
195
|
+
* @param {Object} options - Configuration options for SSH operations
|
|
196
|
+
* @param {string} [options.deployId=''] - Deployment ID context for SSH operations
|
|
197
|
+
* @param {boolean} [options.generate=false] - Generate new SSH credentials
|
|
198
|
+
* @param {string} [options.user=''] - SSH user name (defaults to 'root')
|
|
199
|
+
* @param {string} [options.password=''] - SSH user password (auto-generated if not provided, overridden by saved config if user exists)
|
|
200
|
+
* @param {string} [options.host=''] - SSH host address (defaults to public IP, overridden by saved config if user exists)
|
|
201
|
+
* @param {string} [options.filter=''] - Filter for user/group listings
|
|
202
|
+
* @param {string} [options.groups=''] - Comma-separated list of groups for the user (defaults to 'wheel')
|
|
203
|
+
* @param {number} [options.port=22] - SSH port number
|
|
204
|
+
* @param {boolean} [options.start=false] - Start SSH service with hardened configuration
|
|
205
|
+
* @param {boolean} [options.userAdd=false] - Add a new SSH user and generate keys
|
|
206
|
+
* @param {boolean} [options.userRemove=false] - Remove an SSH user and cleanup keys
|
|
207
|
+
* @param {boolean} [options.userLs=false] - List all SSH users and groups
|
|
208
|
+
* @param {boolean} [options.reset=false] - Reset SSH configuration (clear authorized_keys and known_hosts)
|
|
209
|
+
* @param {boolean} [options.keysList=false] - List authorized SSH keys
|
|
210
|
+
* @param {boolean} [options.hostsList=false] - List known SSH hosts
|
|
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
|
|
24
217
|
* @returns {Promise<void>}
|
|
218
|
+
* @description
|
|
219
|
+
* Handles various SSH operations:
|
|
220
|
+
* - User creation with automatic key generation and backup
|
|
221
|
+
* - User removal with key cleanup
|
|
222
|
+
* - Key import/export between SSH directory and private backup location
|
|
223
|
+
* - SSH service initialization and hardening
|
|
224
|
+
* - User and group listing with optional filtering
|
|
25
225
|
*/
|
|
26
226
|
callback: async (
|
|
27
227
|
options = {
|
|
228
|
+
deployId: '',
|
|
28
229
|
generate: false,
|
|
230
|
+
user: '',
|
|
231
|
+
password: '',
|
|
232
|
+
host: '',
|
|
233
|
+
filter: '',
|
|
234
|
+
groups: '',
|
|
235
|
+
port: 22,
|
|
236
|
+
start: false,
|
|
237
|
+
userAdd: false,
|
|
238
|
+
userRemove: false,
|
|
239
|
+
userLs: false,
|
|
240
|
+
reset: false,
|
|
241
|
+
keysList: false,
|
|
242
|
+
hostsList: false,
|
|
243
|
+
disablePassword: false,
|
|
244
|
+
keyTest: false,
|
|
245
|
+
stop: false,
|
|
246
|
+
status: false,
|
|
247
|
+
connectUri: false,
|
|
248
|
+
copy: false,
|
|
29
249
|
},
|
|
30
250
|
) => {
|
|
31
|
-
|
|
32
|
-
|
|
251
|
+
let confNode, confNodePath;
|
|
252
|
+
|
|
253
|
+
// Set defaults
|
|
254
|
+
if (!options.user) options.user = 'root';
|
|
255
|
+
if (!options.host) options.host = await Dns.getPublicIp();
|
|
256
|
+
if (!options.password) options.password = options.disablePassword ? '' : generateRandomPasswordSelection(16);
|
|
257
|
+
if (!options.groups) options.groups = 'wheel';
|
|
258
|
+
if (!options.port) options.port = 22; // Handle connect uri
|
|
259
|
+
|
|
260
|
+
const userHome = UnderpostSSH.API.getUserHome(options.user);
|
|
261
|
+
options.userHome = userHome;
|
|
262
|
+
|
|
263
|
+
// Load config and override password and host if user exists in config
|
|
264
|
+
if (options.deployId) {
|
|
265
|
+
const config = UnderpostSSH.API.loadConfigNode(options.deployId);
|
|
266
|
+
confNode = config.confNode;
|
|
267
|
+
confNodePath = config.confNodePath;
|
|
268
|
+
|
|
269
|
+
if (confNode.users && confNode.users[options.user]) {
|
|
270
|
+
if (confNode.users[options.user].host) {
|
|
271
|
+
options.host = confNode.users[options.user].host;
|
|
272
|
+
logger.info(`Using saved host for user ${options.user}: ${options.host}`);
|
|
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;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
logger.info('options', options);
|
|
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
|
|
300
|
+
if (options.reset) {
|
|
301
|
+
shellExec(`> ${userHome}/.ssh/authorized_keys`);
|
|
302
|
+
shellExec(`> ${userHome}/.ssh/known_hosts`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (options.userLs) {
|
|
306
|
+
const filter = options.filter ? `${options.filter}` : '';
|
|
307
|
+
const groupsOut = shellExec(`getent group${filter ? ` | grep '${filter}'` : ''}`, {
|
|
308
|
+
silent: true,
|
|
309
|
+
stdout: true,
|
|
310
|
+
});
|
|
311
|
+
const usersOut = shellExec(`getent passwd${filter ? ` | grep '${filter}'` : ''}`, {
|
|
312
|
+
silent: true,
|
|
313
|
+
stdout: true,
|
|
314
|
+
});
|
|
315
|
+
console.log('Groups'.bold.blue);
|
|
316
|
+
console.log(`group_name : password_x : GID(Internal Group ID) : user_list`.blue);
|
|
317
|
+
console.log(filter ? groupsOut.replaceAll(filter, filter.red) : groupsOut);
|
|
318
|
+
console.log('Users'.bold.blue);
|
|
319
|
+
console.log(`usuario : x : UID : GID : GECOS : home_dir : shell`.blue);
|
|
320
|
+
console.log(filter ? usersOut.replaceAll(filter, filter.red) : usersOut);
|
|
321
|
+
}
|
|
322
|
+
|
|
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}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
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
|
+
|
|
343
|
+
const privateCopyDir = `./engine-private/conf/${options.deployId}/users/${options.user}`;
|
|
344
|
+
if (fs.existsSync(privateCopyDir)) {
|
|
345
|
+
fs.removeSync(privateCopyDir);
|
|
346
|
+
logger.info(`Private key copy removed from ${privateCopyDir}`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
delete confNode.users[options.user];
|
|
350
|
+
UnderpostSSH.API.saveConfigNode(confNodePath, confNode);
|
|
351
|
+
}
|
|
33
352
|
|
|
34
|
-
|
|
35
|
-
|
|
353
|
+
logger.info(`User removed`);
|
|
354
|
+
if (groups) logger.info(`User removed from groups: ${groups}`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
36
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
|
|
377
|
+
if (userExistsInConfig && keysExistInBackup) {
|
|
378
|
+
logger.info(`User ${options.user} already exists in config. Importing existing keys...`);
|
|
379
|
+
|
|
380
|
+
// Create system user if it doesn't exist
|
|
381
|
+
const userExists = UnderpostSSH.API.checkUserExists(options.user);
|
|
382
|
+
if (!userExists) {
|
|
383
|
+
UnderpostSSH.API.createSystemUser(options.user, options.password, options.groups);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const userHome = UnderpostSSH.API.getUserHome(options.user);
|
|
387
|
+
const sshDir = `${userHome}/.ssh`;
|
|
388
|
+
UnderpostSSH.API.ensureSSHDirectory(sshDir);
|
|
389
|
+
|
|
390
|
+
const userKeyPath = `${sshDir}/id_rsa`;
|
|
391
|
+
const userPubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
392
|
+
|
|
393
|
+
// Import keys from backup
|
|
394
|
+
fs.copyFileSync(privateKeyPath, userKeyPath);
|
|
395
|
+
fs.copyFileSync(publicKeyPath, userPubKeyPath);
|
|
396
|
+
|
|
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);
|
|
401
|
+
|
|
402
|
+
logger.info(`Keys imported from ${privateCopyDir} to ${sshDir}`);
|
|
403
|
+
logger.info(`User added with existing keys`);
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// New user or no existing keys - create new user and generate keys
|
|
409
|
+
UnderpostSSH.API.createSystemUser(options.user, options.password, options.groups);
|
|
410
|
+
|
|
411
|
+
const userHome = UnderpostSSH.API.getUserHome(options.user);
|
|
412
|
+
const sshDir = `${userHome}/.ssh`;
|
|
413
|
+
UnderpostSSH.API.ensureSSHDirectory(sshDir);
|
|
414
|
+
|
|
415
|
+
const keyPath = `${sshDir}/id_rsa`;
|
|
416
|
+
const pubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
417
|
+
|
|
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
|
+
}
|
|
423
|
+
|
|
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);
|
|
428
|
+
|
|
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}`;
|
|
432
|
+
fs.ensureDirSync(privateCopyDir);
|
|
433
|
+
|
|
434
|
+
const privateKeyCopyPath = `${privateCopyDir}/id_rsa`;
|
|
435
|
+
const publicKeyCopyPath = `${privateCopyDir}/id_rsa.pub`;
|
|
436
|
+
|
|
437
|
+
fs.copyFileSync(keyPath, privateKeyCopyPath);
|
|
438
|
+
fs.copyFileSync(pubKeyPath, publicKeyCopyPath);
|
|
439
|
+
|
|
440
|
+
logger.info(`Keys backed up to ${privateCopyDir}`);
|
|
441
|
+
|
|
442
|
+
confNode.users[options.user] = {
|
|
443
|
+
...confNode.users[options.user],
|
|
444
|
+
...options,
|
|
445
|
+
keyPath,
|
|
446
|
+
pubKeyPath,
|
|
447
|
+
privateKeyCopyPath,
|
|
448
|
+
publicKeyCopyPath,
|
|
449
|
+
};
|
|
450
|
+
UnderpostSSH.API.saveConfigNode(confNodePath, confNode);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
logger.info(`User added`);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
|
|
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;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (options.userLs && confNode && confNode.users) {
|
|
466
|
+
logger.info(`Users in config:`);
|
|
467
|
+
Object.keys(confNode.users).forEach((user) => {
|
|
468
|
+
logger.info(`- ${user}`);
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Handle generate root keys
|
|
474
|
+
if (options.generate)
|
|
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
|
|
491
|
+
if (options.start) {
|
|
492
|
+
UnderpostSSH.API.chmod({ user: options.user });
|
|
493
|
+
UnderpostSSH.API.initService({ port: options.port });
|
|
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}`);
|
|
520
|
+
},
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Generates new SSH ED25519 key pair and stores copies in multiple locations.
|
|
524
|
+
* @function generateKeys
|
|
525
|
+
* @memberof UnderpostSSH
|
|
526
|
+
* @param {Object} params - Key generation parameters
|
|
527
|
+
* @param {string} params.user - Username for the SSH key comment
|
|
528
|
+
* @param {string} params.password - Password to encrypt the private key
|
|
529
|
+
* @param {string} params.host - Host address for the SSH key comment
|
|
530
|
+
* @returns {void}
|
|
531
|
+
* @description
|
|
532
|
+
* Creates a new SSH ED25519 key pair and distributes it to:
|
|
533
|
+
* - User's ~/.ssh/ directory
|
|
534
|
+
* - ./engine-private/deploy/ directory
|
|
535
|
+
* Cleans up temporary key files after copying.
|
|
536
|
+
*/
|
|
537
|
+
generateKeys: ({ user, password, host }) => {
|
|
538
|
+
shellExec(`sudo rm -rf ./id_rsa`);
|
|
539
|
+
shellExec(`sudo rm -rf ./id_rsa.pub`);
|
|
540
|
+
|
|
541
|
+
shellExec(`ssh-keygen -t ed25519 -f id_rsa -N "${password}" -q -C "${user}@${host}"`);
|
|
542
|
+
|
|
543
|
+
shellExec(`sudo cp ./id_rsa ~/.ssh/id_rsa`);
|
|
544
|
+
shellExec(`sudo cp ./id_rsa.pub ~/.ssh/id_rsa.pub`);
|
|
545
|
+
|
|
546
|
+
shellExec(`sudo cp ./id_rsa ./engine-private/deploy/id_rsa`);
|
|
547
|
+
shellExec(`sudo cp ./id_rsa.pub ./engine-private/deploy/id_rsa.pub`);
|
|
548
|
+
|
|
549
|
+
shellExec(`sudo rm -rf ./id_rsa`);
|
|
550
|
+
shellExec(`sudo rm -rf ./id_rsa.pub`);
|
|
551
|
+
},
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Sets proper permissions and ownership for SSH directories and files.
|
|
555
|
+
* @function chmod
|
|
556
|
+
* @memberof UnderpostSSH
|
|
557
|
+
* @param {Object} params - Permission configuration parameters
|
|
558
|
+
* @param {string} params.user - Username for setting ownership
|
|
559
|
+
* @returns {void}
|
|
560
|
+
* @description
|
|
561
|
+
* Applies secure permissions to SSH files:
|
|
562
|
+
* - ~/.ssh/ directory: 700
|
|
563
|
+
* - ~/.ssh/authorized_keys: 600
|
|
564
|
+
* - ~/.ssh/known_hosts: 644
|
|
565
|
+
* - ~/.ssh/id_rsa: 600
|
|
566
|
+
* - /etc/ssh/ssh_host_ed25519_key: 600
|
|
567
|
+
* Sets ownership to specified user for ~/.ssh/ and contents.
|
|
568
|
+
*/
|
|
569
|
+
chmod: ({ user }) => {
|
|
570
|
+
shellExec(`sudo chmod 700 ~/.ssh/`);
|
|
571
|
+
shellExec(`sudo chmod 600 ~/.ssh/authorized_keys`);
|
|
572
|
+
shellExec(`sudo chmod 644 ~/.ssh/known_hosts`);
|
|
573
|
+
shellExec(`sudo chmod 600 ~/.ssh/id_rsa`);
|
|
574
|
+
shellExec(`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`);
|
|
575
|
+
shellExec(`chown -R ${user}:${user} ~/.ssh`);
|
|
576
|
+
},
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Initializes and hardens SSH service configuration for RHEL-based systems.
|
|
580
|
+
* @function initService
|
|
581
|
+
* @memberof UnderpostSSH
|
|
582
|
+
* @param {Object} params - Service configuration parameters
|
|
583
|
+
* @param {number} params.port - Port number for SSH service
|
|
584
|
+
* @returns {void}
|
|
585
|
+
* @description
|
|
586
|
+
* Configures SSH daemon with hardened security settings:
|
|
587
|
+
* - Disables password authentication (key-only)
|
|
588
|
+
* - Disables root login
|
|
589
|
+
* - Enables ED25519 host key
|
|
590
|
+
* - Disables X11 forwarding and TCP forwarding
|
|
591
|
+
* - Sets client alive intervals to prevent ghost connections
|
|
592
|
+
* - Configures PAM for RHEL/SELinux compatibility
|
|
593
|
+
*
|
|
594
|
+
* After configuration:
|
|
595
|
+
* - Enables sshd service for auto-start on boot
|
|
596
|
+
* - Restarts sshd service to apply changes
|
|
597
|
+
* - Displays service status with colored output
|
|
598
|
+
*/
|
|
599
|
+
initService: ({ port }) => {
|
|
37
600
|
shellExec(
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
601
|
+
`sudo tee /etc/ssh/sshd_config <<EOF
|
|
602
|
+
# ==============================================================
|
|
603
|
+
# RHEL Hardened SSHD Configuration
|
|
604
|
+
# ==============================================================
|
|
605
|
+
|
|
606
|
+
# --- Network Settings ---
|
|
607
|
+
Port ${port ? port : '22'}
|
|
608
|
+
# Explicitly listen on all interfaces (IPv4 and IPv6)
|
|
609
|
+
|
|
610
|
+
# --- Host Keys ---
|
|
611
|
+
# ED25519 is the modern standard (Fast & Secure)
|
|
612
|
+
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
613
|
+
# RSA is kept for compatibility with older clients (Optional)
|
|
614
|
+
# HostKey /etc/ssh/ssh_host_rsa_key
|
|
615
|
+
|
|
616
|
+
# --- Logging ---
|
|
617
|
+
SyslogFacility AUTHPRIV
|
|
618
|
+
# VERBOSE logs the key fingerprint used for login (Audit trail)
|
|
619
|
+
LogLevel VERBOSE
|
|
620
|
+
|
|
621
|
+
# --- Authentication & Security ---
|
|
622
|
+
# STRICTLY KEY-BASED AUTHENTICATION
|
|
623
|
+
PubkeyAuthentication yes
|
|
624
|
+
PasswordAuthentication no
|
|
625
|
+
ChallengeResponseAuthentication no
|
|
626
|
+
PermitEmptyPasswords no
|
|
627
|
+
|
|
628
|
+
# PREVENT ROOT LOGIN
|
|
629
|
+
# Administrators should log in as a standard user and use 'sudo'
|
|
630
|
+
PermitRootLogin no
|
|
631
|
+
|
|
632
|
+
# Security checks on ownership of ~/.ssh/ files
|
|
633
|
+
StrictModes yes
|
|
634
|
+
MaxAuthTries 3
|
|
635
|
+
LoginGraceTime 60
|
|
636
|
+
|
|
637
|
+
# --- PAM (Pluggable Authentication Modules) ---
|
|
638
|
+
# REQUIRED: Must be 'yes' on RHEL for proper session/SELinux handling.
|
|
639
|
+
# Since PasswordAuthentication is 'no', PAM will not ask for passwords.
|
|
640
|
+
UsePAM yes
|
|
641
|
+
|
|
642
|
+
# --- Session & Network Health ---
|
|
643
|
+
# Disconnect idle sessions after 5 minutes (300s * 0) to prevent ghost connections
|
|
644
|
+
ClientAliveInterval 300
|
|
645
|
+
ClientAliveCountMax 0
|
|
646
|
+
|
|
647
|
+
# --- Feature Restrictions ---
|
|
648
|
+
# Disable GUI forwarding unless explicitly needed
|
|
649
|
+
X11Forwarding no
|
|
650
|
+
# Disable DNS checks for faster logins (unless you use Host based auth)
|
|
651
|
+
UseDNS no
|
|
652
|
+
# Disable tunneling unless needed
|
|
653
|
+
PermitTunnel no
|
|
654
|
+
AllowTcpForwarding no
|
|
655
|
+
|
|
656
|
+
# --- Subsystem ---
|
|
657
|
+
Subsystem sftp /usr/libexec/openssh/sftp-server
|
|
658
|
+
EOF`,
|
|
659
|
+
{ disableLog: true },
|
|
41
660
|
);
|
|
661
|
+
shellExec(`sudo systemctl enable sshd`);
|
|
662
|
+
shellExec(`sudo systemctl restart sshd`);
|
|
663
|
+
|
|
664
|
+
const status = shellExec(`sudo systemctl status sshd`, { silent: true, stdout: true });
|
|
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
|
+
}
|
|
42
670
|
},
|
|
43
671
|
};
|
|
44
672
|
}
|