underpost 2.90.1 → 2.92.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/release.cd.yml +7 -7
- package/README.md +6 -6
- package/bin/deploy.js +16 -127
- package/cli.md +123 -30
- package/examples/QUICK-REFERENCE.md +499 -0
- package/examples/README.md +447 -0
- package/examples/STATIC-GENERATOR-GUIDE.md +807 -0
- package/examples/ssr-components/CustomPage.js +579 -0
- package/examples/static-config-example.json +183 -0
- package/examples/static-config-simple.json +57 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/dd-test-development/deployment.yaml +2 -2
- package/package.json +1 -1
- package/scripts/rocky-setup.sh +1 -0
- package/src/api/document/document.model.js +7 -0
- package/src/api/document/document.service.js +4 -1
- package/src/cli/db.js +1148 -197
- package/src/cli/deploy.js +17 -12
- package/src/cli/env.js +2 -2
- package/src/cli/index.js +191 -15
- package/src/cli/repository.js +127 -3
- package/src/cli/run.js +41 -12
- package/src/cli/ssh.js +424 -13
- package/src/cli/static.js +785 -49
- package/src/client/components/core/CommonJs.js +0 -1
- package/src/client/components/core/Input.js +6 -4
- package/src/client/components/core/Modal.js +13 -18
- package/src/client/components/core/Panel.js +26 -6
- package/src/client/components/core/PanelForm.js +67 -52
- package/src/db/mongo/MongooseDB.js +5 -1
- package/src/index.js +1 -1
- package/src/server/dns.js +154 -0
- package/src/server/start.js +2 -0
package/src/cli/ssh.js
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
* @namespace UnderpostSSH
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
|
+
import Dns from '../server/dns.js';
|
|
7
9
|
import { shellExec } from '../server/process.js';
|
|
10
|
+
import { loggerFactory } from '../server/logger.js';
|
|
11
|
+
import fs from 'fs-extra';
|
|
12
|
+
|
|
13
|
+
const logger = loggerFactory(import.meta);
|
|
8
14
|
|
|
9
15
|
/**
|
|
10
16
|
* @class UnderpostSSH
|
|
@@ -14,30 +20,435 @@ import { shellExec } from '../server/process.js';
|
|
|
14
20
|
class UnderpostSSH {
|
|
15
21
|
static API = {
|
|
16
22
|
/**
|
|
17
|
-
*
|
|
18
|
-
* @
|
|
19
|
-
*
|
|
20
|
-
* then initiate the SSH connection process.
|
|
21
|
-
* @param {object} [options={ generate: false }] - Options for the SSH callback.
|
|
22
|
-
* @param {boolean} [options.generate=false] - If true, generates a new SSH key pair. Otherwise, it imports the existing one.
|
|
23
|
+
* Main callback function for SSH operations including user management, key import/export, and SSH service configuration.
|
|
24
|
+
* @async
|
|
25
|
+
* @function callback
|
|
23
26
|
* @memberof UnderpostSSH
|
|
27
|
+
* @param {Object} options - Configuration options for SSH operations
|
|
28
|
+
* @param {string} [options.deployId=''] - Deployment ID context for SSH operations
|
|
29
|
+
* @param {boolean} [options.generate=false] - Generate new SSH credentials
|
|
30
|
+
* @param {string} [options.user=''] - SSH user name (defaults to 'root')
|
|
31
|
+
* @param {string} [options.password=''] - SSH user password (auto-generated if not provided, overridden by saved config if user exists)
|
|
32
|
+
* @param {string} [options.host=''] - SSH host address (defaults to public IP, overridden by saved config if user exists)
|
|
33
|
+
* @param {string} [options.filter=''] - Filter for user/group listings
|
|
34
|
+
* @param {string} [options.groups=''] - Comma-separated list of groups for the user (defaults to 'wheel')
|
|
35
|
+
* @param {number} [options.port=22] - SSH port number
|
|
36
|
+
* @param {boolean} [options.start=false] - Start SSH service with hardened configuration
|
|
37
|
+
* @param {boolean} [options.userAdd=false] - Add a new SSH user and generate keys
|
|
38
|
+
* @param {boolean} [options.userRemove=false] - Remove an SSH user and cleanup keys
|
|
39
|
+
* @param {boolean} [options.userLs=false] - List all SSH users and groups
|
|
40
|
+
* @param {boolean} [options.reset=false] - Reset SSH configuration (clear authorized_keys and known_hosts)
|
|
41
|
+
* @param {boolean} [options.keysList=false] - List authorized SSH keys
|
|
42
|
+
* @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
|
|
24
46
|
* @returns {Promise<void>}
|
|
47
|
+
* @description
|
|
48
|
+
* Handles various SSH operations:
|
|
49
|
+
* - User creation with automatic key generation and backup
|
|
50
|
+
* - User removal with key cleanup
|
|
51
|
+
* - Key import/export between SSH directory and private backup location
|
|
52
|
+
* - SSH service initialization and hardening
|
|
53
|
+
* - User and group listing with optional filtering
|
|
25
54
|
*/
|
|
26
55
|
callback: async (
|
|
27
56
|
options = {
|
|
57
|
+
deployId: '',
|
|
28
58
|
generate: false,
|
|
59
|
+
user: '',
|
|
60
|
+
password: '',
|
|
61
|
+
host: '',
|
|
62
|
+
filter: '',
|
|
63
|
+
groups: '',
|
|
64
|
+
port: 22,
|
|
65
|
+
start: false,
|
|
66
|
+
userAdd: false,
|
|
67
|
+
userRemove: false,
|
|
68
|
+
userLs: false,
|
|
69
|
+
reset: false,
|
|
70
|
+
keysList: false,
|
|
71
|
+
hostsList: false,
|
|
72
|
+
disablePassword: false,
|
|
29
73
|
},
|
|
30
74
|
) => {
|
|
31
|
-
|
|
32
|
-
|
|
75
|
+
let confNode, confNodePath;
|
|
76
|
+
if (!options.user) options.user = 'root';
|
|
77
|
+
if (!options.host) options.host = await Dns.getPublicIp();
|
|
78
|
+
if (!options.password) options.password = options.disablePassword ? '' : generateRandomPasswordSelection(16);
|
|
79
|
+
if (!options.groups) options.groups = 'wheel';
|
|
80
|
+
if (!options.port) options.port = 22;
|
|
81
|
+
|
|
82
|
+
// Load config and override password and host if user exists in config
|
|
83
|
+
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: {} };
|
|
86
|
+
|
|
87
|
+
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
|
+
if (confNode.users[options.user].host) {
|
|
93
|
+
options.host = confNode.users[options.user].host;
|
|
94
|
+
logger.info(`Using saved host for user ${options.user}: ${options.host}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let userHome = shellExec(`getent passwd ${options.user} | cut -d: -f6`, { silent: true, stdout: true }).trim();
|
|
100
|
+
options.userHome = userHome;
|
|
101
|
+
|
|
102
|
+
logger.info('options', options);
|
|
103
|
+
|
|
104
|
+
if (options.reset) {
|
|
105
|
+
shellExec(`> ${userHome}/.ssh/authorized_keys`);
|
|
106
|
+
shellExec(`> ${userHome}/.ssh/known_hosts`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (options.keysList) shellExec(`cat ${userHome}/.ssh/authorized_keys`);
|
|
111
|
+
if (options.hostsList) shellExec(`cat ${userHome}/.ssh/known_hosts`);
|
|
112
|
+
|
|
113
|
+
if (options.userLs) {
|
|
114
|
+
const filter = options.filter ? `${options.filter}` : '';
|
|
115
|
+
const groupsOut = shellExec(`getent group${filter ? ` | grep '${filter}'` : ''}`, {
|
|
116
|
+
silent: true,
|
|
117
|
+
stdout: true,
|
|
118
|
+
});
|
|
119
|
+
const usersOut = shellExec(`getent passwd${filter ? ` | grep '${filter}'` : ''}`, {
|
|
120
|
+
silent: true,
|
|
121
|
+
stdout: true,
|
|
122
|
+
});
|
|
123
|
+
console.log('Groups'.bold.blue);
|
|
124
|
+
console.log(`group_name : password_x : GID(Internal Group ID) : user_list`.blue);
|
|
125
|
+
console.log(filter ? groupsOut.replaceAll(filter, filter.red) : groupsOut);
|
|
126
|
+
console.log('Users'.bold.blue);
|
|
127
|
+
console.log(`usuario : x : UID : GID : GECOS : home_dir : shell`.blue);
|
|
128
|
+
console.log(filter ? usersOut.replaceAll(filter, filter.red) : usersOut);
|
|
129
|
+
}
|
|
130
|
+
|
|
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: {} };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (options.userAdd) {
|
|
139
|
+
// Check if user already exists in config
|
|
140
|
+
const userExistsInConfig = confNode.users && confNode.users[options.user];
|
|
141
|
+
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);
|
|
145
|
+
|
|
146
|
+
if (userExistsInConfig && keysExistInBackup) {
|
|
147
|
+
logger.info(`User ${options.user} already exists in config. Importing existing keys...`);
|
|
148
|
+
|
|
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
|
+
|
|
156
|
+
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
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const userHome = shellExec(`getent passwd ${options.user} | cut -d: -f6`, {
|
|
166
|
+
silent: true,
|
|
167
|
+
stdout: true,
|
|
168
|
+
}).trim();
|
|
169
|
+
const sshDir = `${userHome}/.ssh`;
|
|
170
|
+
|
|
171
|
+
if (!fs.existsSync(sshDir)) {
|
|
172
|
+
shellExec(`mkdir -p ${sshDir}`);
|
|
173
|
+
shellExec(`chmod 700 ${sshDir}`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const userKeyPath = `${sshDir}/id_rsa`;
|
|
177
|
+
const userPubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
178
|
+
|
|
179
|
+
// Import keys from backup
|
|
180
|
+
fs.copyFileSync(privateKeyPath, userKeyPath);
|
|
181
|
+
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
|
+
|
|
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}`);
|
|
200
|
+
|
|
201
|
+
logger.info(`Keys imported from ${privateCopyDir} to ${sshDir}`);
|
|
202
|
+
logger.info(`User added with existing keys`);
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
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`;
|
|
219
|
+
|
|
220
|
+
if (!fs.existsSync(sshDir)) {
|
|
221
|
+
shellExec(`mkdir -p ${sshDir}`);
|
|
222
|
+
shellExec(`chmod 700 ${sshDir}`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const keyPath = `${sshDir}/id_rsa`;
|
|
226
|
+
const pubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
227
|
+
|
|
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`);
|
|
246
|
+
|
|
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}`);
|
|
252
|
+
|
|
253
|
+
// Save a copy of the keys to the private folder
|
|
254
|
+
fs.ensureDirSync(privateCopyDir);
|
|
255
|
+
|
|
256
|
+
const privateKeyCopyPath = `${privateCopyDir}/id_rsa`;
|
|
257
|
+
const publicKeyCopyPath = `${privateCopyDir}/id_rsa.pub`;
|
|
258
|
+
|
|
259
|
+
fs.copyFileSync(keyPath, privateKeyCopyPath);
|
|
260
|
+
fs.copyFileSync(pubKeyPath, publicKeyCopyPath);
|
|
261
|
+
|
|
262
|
+
logger.info(`Keys backed up to ${privateCopyDir}`);
|
|
33
263
|
|
|
34
|
-
|
|
35
|
-
|
|
264
|
+
confNode.users[options.user] = {
|
|
265
|
+
...confNode.users[options.user],
|
|
266
|
+
...options,
|
|
267
|
+
keyPath,
|
|
268
|
+
pubKeyPath,
|
|
269
|
+
privateKeyCopyPath,
|
|
270
|
+
publicKeyCopyPath,
|
|
271
|
+
};
|
|
272
|
+
fs.outputFileSync(confNodePath, JSON.stringify(confNode, null, 4), 'utf8');
|
|
273
|
+
logger.info(`User added`);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
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}`);
|
|
36
279
|
|
|
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
|
+
}
|
|
286
|
+
|
|
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;
|
|
292
|
+
}
|
|
293
|
+
if (options.userLs) {
|
|
294
|
+
logger.info(`Users:`);
|
|
295
|
+
Object.keys(confNode.users).forEach((user) => {
|
|
296
|
+
logger.info(`- ${user}`);
|
|
297
|
+
});
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (options.generate)
|
|
303
|
+
UnderpostSSH.API.generateKeys({ user: options.user, password: options.password, host: options.host });
|
|
304
|
+
if (options.start) {
|
|
305
|
+
UnderpostSSH.API.chmod({ user: options.user });
|
|
306
|
+
UnderpostSSH.API.initService({ port: options.port });
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
/**
|
|
310
|
+
* Generates new SSH ED25519 key pair and stores copies in multiple locations.
|
|
311
|
+
* @function generateKeys
|
|
312
|
+
* @memberof UnderpostSSH
|
|
313
|
+
* @param {Object} params - Key generation parameters
|
|
314
|
+
* @param {string} params.user - Username for the SSH key comment
|
|
315
|
+
* @param {string} params.password - Password to encrypt the private key
|
|
316
|
+
* @param {string} params.host - Host address for the SSH key comment
|
|
317
|
+
* @returns {void}
|
|
318
|
+
* @description
|
|
319
|
+
* Creates a new SSH ED25519 key pair and distributes it to:
|
|
320
|
+
* - User's ~/.ssh/ directory
|
|
321
|
+
* - ./engine-private/deploy/ directory
|
|
322
|
+
* Cleans up temporary key files after copying.
|
|
323
|
+
*/
|
|
324
|
+
generateKeys: ({ user, password, host }) => {
|
|
325
|
+
shellExec(`sudo rm -rf ./id_rsa`);
|
|
326
|
+
shellExec(`sudo rm -rf ./id_rsa.pub`);
|
|
327
|
+
|
|
328
|
+
shellExec(`ssh-keygen -t ed25519 -f id_rsa -N "${password}" -q -C "${user}@${host}"`);
|
|
329
|
+
|
|
330
|
+
shellExec(`sudo cp ./id_rsa ~/.ssh/id_rsa`);
|
|
331
|
+
shellExec(`sudo cp ./id_rsa.pub ~/.ssh/id_rsa.pub`);
|
|
332
|
+
|
|
333
|
+
shellExec(`sudo cp ./id_rsa ./engine-private/deploy/id_rsa`);
|
|
334
|
+
shellExec(`sudo cp ./id_rsa.pub ./engine-private/deploy/id_rsa.pub`);
|
|
335
|
+
|
|
336
|
+
shellExec(`sudo rm -rf ./id_rsa`);
|
|
337
|
+
shellExec(`sudo rm -rf ./id_rsa.pub`);
|
|
338
|
+
},
|
|
339
|
+
/**
|
|
340
|
+
* Sets proper permissions and ownership for SSH directories and files.
|
|
341
|
+
* @function chmod
|
|
342
|
+
* @memberof UnderpostSSH
|
|
343
|
+
* @param {Object} params - Permission configuration parameters
|
|
344
|
+
* @param {string} params.user - Username for setting ownership
|
|
345
|
+
* @returns {void}
|
|
346
|
+
* @description
|
|
347
|
+
* Applies secure permissions to SSH files:
|
|
348
|
+
* - ~/.ssh/ directory: 700
|
|
349
|
+
* - ~/.ssh/authorized_keys: 600
|
|
350
|
+
* - ~/.ssh/known_hosts: 644
|
|
351
|
+
* - ~/.ssh/id_rsa: 600
|
|
352
|
+
* - /etc/ssh/ssh_host_ed25519_key: 600
|
|
353
|
+
* Sets ownership to specified user for ~/.ssh/ and contents.
|
|
354
|
+
*/
|
|
355
|
+
chmod: ({ user }) => {
|
|
356
|
+
shellExec(`sudo chmod 700 ~/.ssh/`);
|
|
357
|
+
shellExec(`sudo chmod 600 ~/.ssh/authorized_keys`);
|
|
358
|
+
shellExec(`sudo chmod 644 ~/.ssh/known_hosts`);
|
|
359
|
+
shellExec(`sudo chmod 600 ~/.ssh/id_rsa`);
|
|
360
|
+
shellExec(`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`);
|
|
361
|
+
shellExec(`chown -R ${user}:${user} ~/.ssh`);
|
|
362
|
+
},
|
|
363
|
+
/**
|
|
364
|
+
* Initializes and hardens SSH service configuration for RHEL-based systems.
|
|
365
|
+
* @function initService
|
|
366
|
+
* @memberof UnderpostSSH
|
|
367
|
+
* @param {Object} params - Service configuration parameters
|
|
368
|
+
* @param {number} params.port - Port number for SSH service
|
|
369
|
+
* @returns {void}
|
|
370
|
+
* @description
|
|
371
|
+
* Configures SSH daemon with hardened security settings:
|
|
372
|
+
* - Disables password authentication (key-only)
|
|
373
|
+
* - Disables root login
|
|
374
|
+
* - Enables ED25519 host key
|
|
375
|
+
* - Disables X11 forwarding and TCP forwarding
|
|
376
|
+
* - Sets client alive intervals to prevent ghost connections
|
|
377
|
+
* - Configures PAM for RHEL/SELinux compatibility
|
|
378
|
+
*
|
|
379
|
+
* After configuration:
|
|
380
|
+
* - Enables sshd service for auto-start on boot
|
|
381
|
+
* - Restarts sshd service to apply changes
|
|
382
|
+
* - Displays service status with colored output
|
|
383
|
+
*/
|
|
384
|
+
initService: ({ port }) => {
|
|
37
385
|
shellExec(
|
|
38
|
-
`
|
|
39
|
-
|
|
40
|
-
|
|
386
|
+
`sudo tee /etc/ssh/sshd_config <<EOF
|
|
387
|
+
# ==============================================================
|
|
388
|
+
# RHEL Hardened SSHD Configuration
|
|
389
|
+
# ==============================================================
|
|
390
|
+
|
|
391
|
+
# --- Network Settings ---
|
|
392
|
+
Port ${port ? port : '22'}
|
|
393
|
+
# Explicitly listen on all interfaces (IPv4 and IPv6)
|
|
394
|
+
|
|
395
|
+
# --- Host Keys ---
|
|
396
|
+
# ED25519 is the modern standard (Fast & Secure)
|
|
397
|
+
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
398
|
+
# RSA is kept for compatibility with older clients (Optional)
|
|
399
|
+
# HostKey /etc/ssh/ssh_host_rsa_key
|
|
400
|
+
|
|
401
|
+
# --- Logging ---
|
|
402
|
+
SyslogFacility AUTHPRIV
|
|
403
|
+
# VERBOSE logs the key fingerprint used for login (Audit trail)
|
|
404
|
+
LogLevel VERBOSE
|
|
405
|
+
|
|
406
|
+
# --- Authentication & Security ---
|
|
407
|
+
# STRICTLY KEY-BASED AUTHENTICATION
|
|
408
|
+
PubkeyAuthentication yes
|
|
409
|
+
PasswordAuthentication no
|
|
410
|
+
ChallengeResponseAuthentication no
|
|
411
|
+
PermitEmptyPasswords no
|
|
412
|
+
|
|
413
|
+
# PREVENT ROOT LOGIN
|
|
414
|
+
# Administrators should log in as a standard user and use 'sudo'
|
|
415
|
+
PermitRootLogin no
|
|
416
|
+
|
|
417
|
+
# Security checks on ownership of ~/.ssh/ files
|
|
418
|
+
StrictModes yes
|
|
419
|
+
MaxAuthTries 3
|
|
420
|
+
LoginGraceTime 60
|
|
421
|
+
|
|
422
|
+
# --- PAM (Pluggable Authentication Modules) ---
|
|
423
|
+
# REQUIRED: Must be 'yes' on RHEL for proper session/SELinux handling.
|
|
424
|
+
# Since PasswordAuthentication is 'no', PAM will not ask for passwords.
|
|
425
|
+
UsePAM yes
|
|
426
|
+
|
|
427
|
+
# --- Session & Network Health ---
|
|
428
|
+
# Disconnect idle sessions after 5 minutes (300s * 0) to prevent ghost connections
|
|
429
|
+
ClientAliveInterval 300
|
|
430
|
+
ClientAliveCountMax 0
|
|
431
|
+
|
|
432
|
+
# --- Feature Restrictions ---
|
|
433
|
+
# Disable GUI forwarding unless explicitly needed
|
|
434
|
+
X11Forwarding no
|
|
435
|
+
# Disable DNS checks for faster logins (unless you use Host based auth)
|
|
436
|
+
UseDNS no
|
|
437
|
+
# Disable tunneling unless needed
|
|
438
|
+
PermitTunnel no
|
|
439
|
+
AllowTcpForwarding no
|
|
440
|
+
|
|
441
|
+
# --- Subsystem ---
|
|
442
|
+
Subsystem sftp /usr/libexec/openssh/sftp-server
|
|
443
|
+
EOF`,
|
|
444
|
+
{ disableLog: true },
|
|
445
|
+
);
|
|
446
|
+
shellExec(`sudo systemctl enable sshd`);
|
|
447
|
+
shellExec(`sudo systemctl restart sshd`);
|
|
448
|
+
|
|
449
|
+
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,
|
|
41
452
|
);
|
|
42
453
|
},
|
|
43
454
|
};
|