underpost 3.2.80 → 3.3.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/ghpkg.ci.yml +7 -1
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +1 -1
- package/.github/workflows/release.cd.yml +1 -9
- package/CHANGELOG.md +291 -1
- package/CLI-HELP.md +174 -23
- package/README.md +5 -2
- package/bin/build.js +7 -5
- package/bin/deploy.js +19 -17
- package/deploy/lib/logging.sh +96 -0
- package/deploy/pwa-microservices-template/deploy.sh +72 -0
- package/deploy/release/deploy.sh +62 -0
- package/docker-compose.yml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +5 -1
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +1 -1
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +52 -0
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/deployment/playwright/deployment.yaml +1 -1
- package/manifests/mongodb/kustomization.yaml +4 -1
- package/manifests/mongodb/statefulset.yaml +4 -0
- package/manifests/mongodb/storage-class.yaml +9 -2
- package/package.json +19 -19
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/coverall-test.sh +24 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +18 -15
- package/scripts/kubeadm-node-setup.sh +12 -23
- package/scripts/link-local-underpost-cli.sh +0 -0
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/nat-iptables.sh +12 -4
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/test-monitor.sh +4 -3
- package/src/cli/baremetal.js +1 -2
- package/src/cli/cloud-init.js +1 -1
- package/src/cli/cluster.js +786 -96
- package/src/cli/db.js +11 -4
- package/src/cli/deploy.js +1698 -177
- package/src/cli/docker-compose.js +19 -178
- package/src/cli/env.js +1 -1
- package/src/cli/image.js +15 -7
- package/src/cli/index.js +245 -44
- package/src/cli/ipfs.js +82 -11
- package/src/cli/lxd.js +1 -1
- package/src/cli/monitor.js +2 -2
- package/src/cli/release.js +57 -22
- package/src/cli/repository.js +12 -10
- package/src/cli/run.js +2195 -427
- package/src/cli/secrets.js +969 -0
- package/src/cli/ssh.js +206 -105
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +1 -1
- package/src/cli/vultr.js +583 -0
- package/src/cli/wireguard.js +2125 -0
- package/src/client-builder/client-build.js +102 -13
- package/src/client-builder/ssr.js +27 -73
- package/src/db/mongo/MongoBootstrap.js +295 -54
- package/src/db/mongo/MongooseDB.js +51 -32
- package/src/index.js +25 -1
- package/src/projects/underpost/catalog-underpost.js +4 -1
- package/src/server/backup.js +1 -1
- package/src/server/conf.js +1216 -168
- package/src/server/cri.js +70 -0
- package/src/server/cron.js +249 -51
- package/src/server/dns.js +100 -6
- package/src/server/environment.js +98 -0
- package/src/server/forward-proxy.js +549 -0
- package/src/server/middlewares.js +56 -1
- package/src/server/process.js +0 -1
- package/src/server/selinux.js +185 -0
- package/src/server/systemd.js +205 -0
- package/src/server/underpost-compression.js +186 -0
- package/src/server/underpost-gateway.js +1083 -0
- package/src/server/underpost-ingress.js +380 -0
- package/test/cluster-instances.test.js +435 -0
- package/test/deploy-node-placement.test.js +45 -0
- package/test/instance-traffic-plan.test.js +710 -0
- package/test/selinux.test.js +71 -0
- package/test/sops-secret-store.test.js +612 -0
- package/test/underpost-gateway.test.js +510 -0
- package/test/underpost-ingress.test.js +305 -0
- package/test/wireguard-edge.test.js +1177 -0
package/src/cli/ssh.js
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
8
|
import { pbcopy, shellExec } from '../server/process.js';
|
|
9
9
|
import { loggerFactory } from '../server/logger.js';
|
|
10
|
+
import { waitForPort } from '../server/conf.js';
|
|
11
|
+
import {
|
|
12
|
+
runSELinuxCommands,
|
|
13
|
+
selinuxPackagesCommandFactory,
|
|
14
|
+
selinuxRestoreconCommandFactory,
|
|
15
|
+
selinuxSshContextCommandsFactory,
|
|
16
|
+
selinuxSshPortCommandsFactory,
|
|
17
|
+
shellArgumentFactory,
|
|
18
|
+
} from '../server/selinux.js';
|
|
10
19
|
import fs from 'fs-extra';
|
|
11
20
|
import Underpost from '../index.js';
|
|
12
21
|
|
|
@@ -56,7 +65,7 @@ class UnderpostSSH {
|
|
|
56
65
|
* @returns {boolean} True if user exists, false otherwise
|
|
57
66
|
*/
|
|
58
67
|
checkUserExists: (username) => {
|
|
59
|
-
const result = shellExec(`id -u ${username} 2>/dev/null || echo "not_found"`, {
|
|
68
|
+
const result = shellExec(`id -u ${shellArgumentFactory(username)} 2>/dev/null || echo "not_found"`, {
|
|
60
69
|
silent: true,
|
|
61
70
|
stdout: true,
|
|
62
71
|
}).trim();
|
|
@@ -72,7 +81,7 @@ class UnderpostSSH {
|
|
|
72
81
|
* @returns {string} User's home directory path
|
|
73
82
|
*/
|
|
74
83
|
getUserHome: (username) => {
|
|
75
|
-
return shellExec(`getent passwd ${username} | cut -d: -f6`, {
|
|
84
|
+
return shellExec(`getent passwd ${shellArgumentFactory(username)} | cut -d: -f6`, {
|
|
76
85
|
silent: true,
|
|
77
86
|
stdout: true,
|
|
78
87
|
}).trim();
|
|
@@ -89,11 +98,13 @@ class UnderpostSSH {
|
|
|
89
98
|
* @returns {void}
|
|
90
99
|
*/
|
|
91
100
|
createSystemUser: (username, password, groups) => {
|
|
92
|
-
|
|
93
|
-
shellExec(`
|
|
101
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid system username');
|
|
102
|
+
shellExec(`useradd -m -s /bin/bash ${shellArgumentFactory(username)}`);
|
|
103
|
+
shellExec(`printf '%s\n' ${shellArgumentFactory(`${username}:${password}`)} | chpasswd`, { disableLog: true });
|
|
94
104
|
if (groups) {
|
|
95
105
|
for (const group of groups.split(',').map((g) => g.trim())) {
|
|
96
|
-
|
|
106
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(group)) throw new TypeError('Invalid system group');
|
|
107
|
+
shellExec(`usermod -aG ${shellArgumentFactory(group)} ${shellArgumentFactory(username)}`);
|
|
97
108
|
}
|
|
98
109
|
}
|
|
99
110
|
},
|
|
@@ -108,8 +119,8 @@ class UnderpostSSH {
|
|
|
108
119
|
*/
|
|
109
120
|
ensureSSHDirectory: (sshDir) => {
|
|
110
121
|
if (!fs.existsSync(sshDir)) {
|
|
111
|
-
shellExec(`mkdir -p ${sshDir}`);
|
|
112
|
-
shellExec(`chmod 700 ${sshDir}`);
|
|
122
|
+
shellExec(`mkdir -p ${shellArgumentFactory(sshDir)}`);
|
|
123
|
+
shellExec(`chmod 700 ${shellArgumentFactory(sshDir)}`);
|
|
113
124
|
}
|
|
114
125
|
},
|
|
115
126
|
|
|
@@ -125,11 +136,12 @@ class UnderpostSSH {
|
|
|
125
136
|
* @returns {void}
|
|
126
137
|
*/
|
|
127
138
|
setSSHFilePermissions: (sshDir, username, keyPath, pubKeyPath) => {
|
|
128
|
-
shellExec(`chmod 600 ${sshDir}/authorized_keys`);
|
|
129
|
-
shellExec(`chmod 644 ${sshDir}/known_hosts`);
|
|
130
|
-
if (keyPath) shellExec(`chmod 600 ${keyPath}`);
|
|
131
|
-
if (pubKeyPath) shellExec(`chmod 644 ${pubKeyPath}`);
|
|
132
|
-
shellExec(`chown -R ${username}:${username} ${sshDir}`);
|
|
139
|
+
shellExec(`chmod 600 ${shellArgumentFactory(`${sshDir}/authorized_keys`)}`);
|
|
140
|
+
shellExec(`chmod 644 ${shellArgumentFactory(`${sshDir}/known_hosts`)}`);
|
|
141
|
+
if (keyPath) shellExec(`chmod 600 ${shellArgumentFactory(keyPath)}`);
|
|
142
|
+
if (pubKeyPath) shellExec(`chmod 644 ${shellArgumentFactory(pubKeyPath)}`);
|
|
143
|
+
shellExec(`chown -R ${shellArgumentFactory(`${username}:${username}`)} ${shellArgumentFactory(sshDir)}`);
|
|
144
|
+
runSELinuxCommands(selinuxSshContextCommandsFactory({ sshDirectory: sshDir }), { execute: shellExec });
|
|
133
145
|
},
|
|
134
146
|
|
|
135
147
|
/**
|
|
@@ -143,13 +155,12 @@ class UnderpostSSH {
|
|
|
143
155
|
* @returns {void}
|
|
144
156
|
*/
|
|
145
157
|
configureAuthorizedKeys: (sshDir, pubKeyPath, disablePassword) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
158
|
+
const key = fs.readFileSync(pubKeyPath, 'utf8').trim();
|
|
159
|
+
const entry = disablePassword ? `no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${key}` : key;
|
|
160
|
+
const authorizedKeysPath = `${sshDir}/authorized_keys`;
|
|
161
|
+
shellExec(
|
|
162
|
+
`grep -qxF ${shellArgumentFactory(entry)} ${shellArgumentFactory(authorizedKeysPath)} || printf '%s\n' ${shellArgumentFactory(entry)} >> ${shellArgumentFactory(authorizedKeysPath)}`,
|
|
163
|
+
);
|
|
153
164
|
},
|
|
154
165
|
|
|
155
166
|
/**
|
|
@@ -163,9 +174,13 @@ EOF`);
|
|
|
163
174
|
* @returns {void}
|
|
164
175
|
*/
|
|
165
176
|
configureKnownHosts: (sshDir, port, host) => {
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
177
|
+
const sshPort = Number(port);
|
|
178
|
+
if (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65535)
|
|
179
|
+
throw new RangeError('SSH port must be 1-65535');
|
|
180
|
+
const knownHostsPath = shellArgumentFactory(`${sshDir}/known_hosts`);
|
|
181
|
+
shellExec(`ssh-keyscan -p ${sshPort} -H localhost >> ${knownHostsPath}`);
|
|
182
|
+
shellExec(`ssh-keyscan -p ${sshPort} -H 127.0.0.1 >> ${knownHostsPath}`);
|
|
183
|
+
if (host) shellExec(`ssh-keyscan -p ${sshPort} -H ${shellArgumentFactory(host)} >> ${knownHostsPath}`);
|
|
169
184
|
},
|
|
170
185
|
|
|
171
186
|
/**
|
|
@@ -180,10 +195,78 @@ EOF`);
|
|
|
180
195
|
*/
|
|
181
196
|
configureSudoAccess: (username, password, disablePassword) => {
|
|
182
197
|
if (disablePassword) {
|
|
183
|
-
|
|
198
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid sudo username');
|
|
199
|
+
const sudoersPath = `/etc/sudoers.d/90_${username}`;
|
|
200
|
+
const temporaryPath = `/tmp/underpost-sudoers-${process.pid}-${username}`;
|
|
201
|
+
const rule = `${username} ALL=(ALL) NOPASSWD: ALL`;
|
|
202
|
+
try {
|
|
203
|
+
shellExec(`printf '%s\n' ${shellArgumentFactory(rule)} > ${shellArgumentFactory(temporaryPath)}`);
|
|
204
|
+
shellExec(`sudo visudo -cf ${shellArgumentFactory(temporaryPath)}`);
|
|
205
|
+
shellExec(
|
|
206
|
+
`sudo install -m 0440 -o root -g root ${shellArgumentFactory(temporaryPath)} ${shellArgumentFactory(sudoersPath)}`,
|
|
207
|
+
);
|
|
208
|
+
runSELinuxCommands([selinuxRestoreconCommandFactory(sudoersPath, { recursive: false })], {
|
|
209
|
+
execute: shellExec,
|
|
210
|
+
});
|
|
211
|
+
} finally {
|
|
212
|
+
fs.removeSync(temporaryPath);
|
|
213
|
+
}
|
|
184
214
|
} else {
|
|
185
|
-
shellExec(`
|
|
215
|
+
shellExec(`printf '%s\n' ${shellArgumentFactory(`${username}:${password}`)} | sudo chpasswd`, {
|
|
216
|
+
disableLog: true,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Initializes the default SSH files and credentials for a local SSH service.
|
|
223
|
+
* @method
|
|
224
|
+
* @function initializeDefaultSshConfig
|
|
225
|
+
* @memberof UnderpostSSH
|
|
226
|
+
* @param {object} params - SSH configuration values.
|
|
227
|
+
* @param {string} params.userHome - Home directory of the SSH user.
|
|
228
|
+
* @param {string} params.user - SSH user name.
|
|
229
|
+
* @param {string} params.password - SSH key passphrase and user password.
|
|
230
|
+
* @param {string} params.host - Host used in the SSH key comment.
|
|
231
|
+
* @param {number} params.port - SSH port.
|
|
232
|
+
* @param {boolean} params.disablePassword - Whether to configure passwordless sudo.
|
|
233
|
+
* @param {string} [params.controllerPubKeyPath='./engine-private/deploy/id_rsa.pub'] - Preferred controller public key to authorize.
|
|
234
|
+
* @returns {void}
|
|
235
|
+
*/
|
|
236
|
+
initializeDefaultSshConfig: ({
|
|
237
|
+
userHome,
|
|
238
|
+
user,
|
|
239
|
+
password,
|
|
240
|
+
host,
|
|
241
|
+
port,
|
|
242
|
+
disablePassword,
|
|
243
|
+
controllerPubKeyPath = './engine-private/deploy/id_rsa.pub',
|
|
244
|
+
}) => {
|
|
245
|
+
const sshDir = `${userHome}/.ssh`;
|
|
246
|
+
const keyPath = `${sshDir}/id_rsa`;
|
|
247
|
+
const pubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
248
|
+
|
|
249
|
+
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
250
|
+
fs.ensureFileSync(`${sshDir}/authorized_keys`);
|
|
251
|
+
fs.ensureFileSync(`${sshDir}/known_hosts`);
|
|
252
|
+
|
|
253
|
+
if (!fs.existsSync(keyPath)) {
|
|
254
|
+
shellExec(
|
|
255
|
+
`ssh-keygen -t ed25519 -f ${shellArgumentFactory(keyPath)} -N ${shellArgumentFactory(password)} -q -C ${shellArgumentFactory(`${user}@${host}`)}`,
|
|
256
|
+
{ disableLog: true },
|
|
257
|
+
);
|
|
186
258
|
}
|
|
259
|
+
if (!fs.existsSync(pubKeyPath))
|
|
260
|
+
shellExec(
|
|
261
|
+
`ssh-keygen -y -f ${shellArgumentFactory(keyPath)} -P ${shellArgumentFactory(password)} > ${shellArgumentFactory(pubKeyPath)}`,
|
|
262
|
+
{ disableLog: true },
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
const authorizedKeyPath = fs.existsSync(controllerPubKeyPath) ? controllerPubKeyPath : pubKeyPath;
|
|
266
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, authorizedKeyPath, disablePassword);
|
|
267
|
+
Underpost.ssh.configureKnownHosts(sshDir, port, host);
|
|
268
|
+
Underpost.ssh.configureSudoAccess(user, password, disablePassword);
|
|
269
|
+
Underpost.ssh.setSSHFilePermissions(sshDir, user, keyPath, pubKeyPath);
|
|
187
270
|
},
|
|
188
271
|
|
|
189
272
|
/**
|
|
@@ -488,7 +571,44 @@ EOF`);
|
|
|
488
571
|
|
|
489
572
|
// Handle start server
|
|
490
573
|
if (options.start) {
|
|
491
|
-
|
|
574
|
+
if (!options.deployId) {
|
|
575
|
+
Underpost.ssh.initializeDefaultSshConfig({
|
|
576
|
+
userHome,
|
|
577
|
+
user: options.user,
|
|
578
|
+
password: options.password,
|
|
579
|
+
host: options.host,
|
|
580
|
+
port: options.port,
|
|
581
|
+
disablePassword: options.disablePassword,
|
|
582
|
+
});
|
|
583
|
+
} else {
|
|
584
|
+
// Hardening sshd on a host where the account does not exist produces a
|
|
585
|
+
// daemon that starts cleanly and refuses the only user it was meant to
|
|
586
|
+
// admit. Nothing downstream reports that, so the run has to.
|
|
587
|
+
if (!Underpost.ssh.checkUserExists(options.user))
|
|
588
|
+
throw new Error(
|
|
589
|
+
`[ssh] user '${options.user}' does not exist on this host; ` +
|
|
590
|
+
`run: node bin ssh --deploy-id ${options.deployId} --user ${options.user} --user-add`,
|
|
591
|
+
);
|
|
592
|
+
Underpost.ssh.chmod({ user: options.user });
|
|
593
|
+
// The registry records the key pair this deploy authenticates with, so
|
|
594
|
+
// a box whose authorized_keys was lost converges here rather than
|
|
595
|
+
// needing the key re-issued — which would invalidate every other host
|
|
596
|
+
// holding it.
|
|
597
|
+
const recordedPubKey = confNode?.users?.[options.user]?.publicKeyCopyPath;
|
|
598
|
+
if (recordedPubKey && fs.existsSync(recordedPubKey)) {
|
|
599
|
+
const sshDir = `${userHome}/.ssh`;
|
|
600
|
+
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
601
|
+
fs.ensureFileSync(`${sshDir}/authorized_keys`);
|
|
602
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, recordedPubKey, options.disablePassword);
|
|
603
|
+
Underpost.ssh.chmod({ user: options.user });
|
|
604
|
+
logger.info(`Authorized key present for ${options.user}`, { source: recordedPubKey });
|
|
605
|
+
} else if (recordedPubKey) {
|
|
606
|
+
logger.warn('Recorded public key is missing; authorized_keys left untouched', {
|
|
607
|
+
user: options.user,
|
|
608
|
+
expected: recordedPubKey,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
492
612
|
Underpost.ssh.initService({ port: options.port });
|
|
493
613
|
}
|
|
494
614
|
|
|
@@ -606,8 +726,8 @@ EOF
|
|
|
606
726
|
},
|
|
607
727
|
|
|
608
728
|
/**
|
|
609
|
-
* Waits until a TCP SSH port becomes reachable on a host.
|
|
610
|
-
* @
|
|
729
|
+
* Waits until a TCP SSH port becomes reachable on a host. Delegates to
|
|
730
|
+
* {@link ServerConfBuilder.waitForPort}, which owns the probe.
|
|
611
731
|
* @function waitForSshPort
|
|
612
732
|
* @memberof UnderpostSSH
|
|
613
733
|
* @param {object} params
|
|
@@ -617,25 +737,14 @@ EOF
|
|
|
617
737
|
* @param {number} [params.intervalMs=3000] - Poll interval.
|
|
618
738
|
* @returns {Promise<boolean>} True once the port accepts connections, false on timeout.
|
|
619
739
|
*/
|
|
620
|
-
waitForSshPort:
|
|
621
|
-
|
|
622
|
-
while (Date.now() < deadline) {
|
|
623
|
-
const probe = shellExec(
|
|
624
|
-
`timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
|
|
625
|
-
{ silent: true, stdout: true, silentOnError: true, disableLog: true },
|
|
626
|
-
);
|
|
627
|
-
if (`${probe}`.trim() === 'open') return true;
|
|
628
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
629
|
-
}
|
|
630
|
-
logger.warn(`SSH port ${host}:${port} not reachable within timeout`);
|
|
631
|
-
return false;
|
|
632
|
-
},
|
|
740
|
+
waitForSshPort: ({ host, port = 22, timeoutMs = 10 * 60 * 1000, intervalMs = 3000 }) =>
|
|
741
|
+
waitForPort({ host, port, open: true, timeoutMs, intervalMs }),
|
|
633
742
|
|
|
634
743
|
/**
|
|
635
744
|
* Waits until a host's SSH port stops accepting connections (e.g. while it
|
|
636
745
|
* reboots). Used to detect a reboot edge before waiting for the port to come
|
|
637
746
|
* back up, so callers don't latch onto the pre-reboot (ephemeral) sshd.
|
|
638
|
-
* @
|
|
747
|
+
* Delegates to {@link ServerConfBuilder.waitForPort}.
|
|
639
748
|
* @function waitForSshPortClosed
|
|
640
749
|
* @memberof UnderpostSSH
|
|
641
750
|
* @param {object} params
|
|
@@ -645,18 +754,8 @@ EOF
|
|
|
645
754
|
* @param {number} [params.intervalMs=3000] - Poll interval.
|
|
646
755
|
* @returns {Promise<boolean>} True once the port is closed, false on timeout.
|
|
647
756
|
*/
|
|
648
|
-
waitForSshPortClosed:
|
|
649
|
-
|
|
650
|
-
while (Date.now() < deadline) {
|
|
651
|
-
const probe = shellExec(
|
|
652
|
-
`timeout 5 bash -c '</dev/tcp/${host}/${port}' >/dev/null 2>&1 && echo open || echo closed`,
|
|
653
|
-
{ silent: true, stdout: true, silentOnError: true, disableLog: true },
|
|
654
|
-
);
|
|
655
|
-
if (`${probe}`.trim() === 'closed') return true;
|
|
656
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
657
|
-
}
|
|
658
|
-
return false;
|
|
659
|
-
},
|
|
757
|
+
waitForSshPortClosed: ({ host, port = 22, timeoutMs = 3 * 60 * 1000, intervalMs = 3000 }) =>
|
|
758
|
+
waitForPort({ host, port, open: false, timeoutMs, intervalMs }),
|
|
660
759
|
|
|
661
760
|
/**
|
|
662
761
|
* Orchestrates a non-interactive, key-only SSH session against a freshly
|
|
@@ -868,12 +967,26 @@ EOF
|
|
|
868
967
|
* Sets ownership to specified user for ~/.ssh/ and contents.
|
|
869
968
|
*/
|
|
870
969
|
chmod: ({ user }) => {
|
|
871
|
-
|
|
872
|
-
shellExec(`sudo chmod
|
|
873
|
-
shellExec(
|
|
874
|
-
|
|
970
|
+
const sshDirectory = `${Underpost.ssh.getUserHome(user)}/.ssh`;
|
|
971
|
+
shellExec(`sudo chmod 700 ${shellArgumentFactory(sshDirectory)}`);
|
|
972
|
+
shellExec(
|
|
973
|
+
`if [ -f ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)} ]; then sudo chmod 600 ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}; fi`,
|
|
974
|
+
);
|
|
975
|
+
shellExec(
|
|
976
|
+
`if [ -f ${shellArgumentFactory(`${sshDirectory}/known_hosts`)} ]; then sudo chmod 644 ${shellArgumentFactory(`${sshDirectory}/known_hosts`)}; fi`,
|
|
977
|
+
);
|
|
978
|
+
shellExec(
|
|
979
|
+
`if [ -f ${shellArgumentFactory(`${sshDirectory}/id_rsa`)} ]; then sudo chmod 600 ${shellArgumentFactory(`${sshDirectory}/id_rsa`)}; fi`,
|
|
980
|
+
);
|
|
875
981
|
shellExec(`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`);
|
|
876
|
-
shellExec(`chown -R ${user}:${user}
|
|
982
|
+
shellExec(`sudo chown -R ${shellArgumentFactory(`${user}:${user}`)} ${shellArgumentFactory(sshDirectory)}`);
|
|
983
|
+
runSELinuxCommands(
|
|
984
|
+
[
|
|
985
|
+
...selinuxSshContextCommandsFactory({ sshDirectory }),
|
|
986
|
+
selinuxRestoreconCommandFactory('/etc/ssh/ssh_host_ed25519_key', { recursive: false }),
|
|
987
|
+
],
|
|
988
|
+
{ execute: shellExec },
|
|
989
|
+
);
|
|
877
990
|
},
|
|
878
991
|
|
|
879
992
|
/**
|
|
@@ -886,7 +999,7 @@ EOF
|
|
|
886
999
|
* @description
|
|
887
1000
|
* Configures SSH daemon with hardened security settings:
|
|
888
1001
|
* - Disables password authentication (key-only)
|
|
889
|
-
* -
|
|
1002
|
+
* - Allows key-only root login
|
|
890
1003
|
* - Enables ED25519 host key
|
|
891
1004
|
* - Disables X11 forwarding and TCP forwarding
|
|
892
1005
|
* - Sets client alive intervals to prevent ghost connections
|
|
@@ -898,69 +1011,57 @@ EOF
|
|
|
898
1011
|
* - Displays service status with colored output
|
|
899
1012
|
*/
|
|
900
1013
|
initService: ({ port }) => {
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
# ==============================================================
|
|
906
|
-
|
|
907
|
-
# --- Network Settings ---
|
|
908
|
-
Port ${port ? port : '22'}
|
|
909
|
-
# Explicitly listen on all interfaces (IPv4 and IPv6)
|
|
910
|
-
|
|
911
|
-
# --- Host Keys ---
|
|
912
|
-
# ED25519 is the modern standard (Fast & Secure)
|
|
1014
|
+
const sshPort = Number(port || 22);
|
|
1015
|
+
const portCommands = selinuxSshPortCommandsFactory({ port: sshPort });
|
|
1016
|
+
const temporaryPath = `/tmp/underpost-sshd-config-${process.pid}`;
|
|
1017
|
+
const configuration = `Port ${sshPort}
|
|
913
1018
|
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
914
|
-
# RSA is kept for compatibility with older clients (Optional)
|
|
915
|
-
# HostKey /etc/ssh/ssh_host_rsa_key
|
|
916
|
-
|
|
917
|
-
# --- Logging ---
|
|
918
1019
|
SyslogFacility AUTHPRIV
|
|
919
|
-
# VERBOSE logs the key fingerprint used for login (Audit trail)
|
|
920
1020
|
LogLevel VERBOSE
|
|
921
|
-
|
|
922
|
-
# --- Authentication & Security ---
|
|
923
|
-
# STRICTLY KEY-BASED AUTHENTICATION
|
|
924
1021
|
PubkeyAuthentication yes
|
|
925
1022
|
PasswordAuthentication no
|
|
926
|
-
|
|
1023
|
+
KbdInteractiveAuthentication no
|
|
927
1024
|
PermitEmptyPasswords no
|
|
928
|
-
|
|
929
|
-
# PREVENT ROOT LOGIN
|
|
930
|
-
# Administrators should log in as a standard user and use 'sudo'
|
|
931
|
-
PermitRootLogin no
|
|
932
|
-
|
|
933
|
-
# Security checks on ownership of ~/.ssh/ files
|
|
1025
|
+
PermitRootLogin prohibit-password
|
|
934
1026
|
StrictModes yes
|
|
935
1027
|
MaxAuthTries 3
|
|
936
1028
|
LoginGraceTime 60
|
|
937
|
-
|
|
938
|
-
# --- PAM (Pluggable Authentication Modules) ---
|
|
939
|
-
# REQUIRED: Must be 'yes' on RHEL for proper session/SELinux handling.
|
|
940
|
-
# Since PasswordAuthentication is 'no', PAM will not ask for passwords.
|
|
941
1029
|
UsePAM yes
|
|
942
|
-
|
|
943
|
-
# --- Session & Network Health ---
|
|
944
|
-
# Disconnect idle sessions after 5 minutes (300s * 0) to prevent ghost connections
|
|
945
1030
|
ClientAliveInterval 300
|
|
946
1031
|
ClientAliveCountMax 0
|
|
947
|
-
|
|
948
|
-
# --- Feature Restrictions ---
|
|
949
|
-
# Disable GUI forwarding unless explicitly needed
|
|
950
1032
|
X11Forwarding no
|
|
951
|
-
# Disable DNS checks for faster logins (unless you use Host based auth)
|
|
952
1033
|
UseDNS no
|
|
953
|
-
# Disable tunneling unless needed
|
|
954
1034
|
PermitTunnel no
|
|
955
1035
|
AllowTcpForwarding no
|
|
956
|
-
|
|
957
|
-
# --- Subsystem ---
|
|
1036
|
+
Include /etc/ssh/sshd_config.d/*.conf
|
|
958
1037
|
Subsystem sftp /usr/libexec/openssh/sftp-server
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
);
|
|
962
|
-
|
|
963
|
-
|
|
1038
|
+
`;
|
|
1039
|
+
|
|
1040
|
+
fs.writeFileSync(temporaryPath, configuration, { mode: 0o600 });
|
|
1041
|
+
try {
|
|
1042
|
+
if (sshPort !== 22) {
|
|
1043
|
+
shellExec(
|
|
1044
|
+
`if command -v dnf >/dev/null 2>&1 && ! command -v semanage >/dev/null 2>&1; then ${selinuxPackagesCommandFactory()}; fi`,
|
|
1045
|
+
);
|
|
1046
|
+
}
|
|
1047
|
+
runSELinuxCommands(portCommands, { execute: shellExec });
|
|
1048
|
+
shellExec(`sudo ssh-keygen -A`);
|
|
1049
|
+
shellExec(`sudo /usr/sbin/sshd -t -f ${shellArgumentFactory(temporaryPath)}`);
|
|
1050
|
+
shellExec(`sudo install -m 0600 -o root -g root ${shellArgumentFactory(temporaryPath)} /etc/ssh/sshd_config`);
|
|
1051
|
+
runSELinuxCommands(
|
|
1052
|
+
[selinuxRestoreconCommandFactory(['/etc/ssh/sshd_config', '/etc/ssh/ssh_host_ed25519_key'])],
|
|
1053
|
+
{ execute: shellExec },
|
|
1054
|
+
);
|
|
1055
|
+
if (sshPort !== 22) {
|
|
1056
|
+
shellExec(
|
|
1057
|
+
`if command -v firewall-cmd >/dev/null 2>&1 && sudo firewall-cmd --state >/dev/null 2>&1; then sudo firewall-cmd --permanent --add-port=${sshPort}/tcp && sudo firewall-cmd --reload; fi`,
|
|
1058
|
+
);
|
|
1059
|
+
}
|
|
1060
|
+
shellExec(`sudo systemctl enable sshd`);
|
|
1061
|
+
shellExec(`sudo systemctl restart sshd`);
|
|
1062
|
+
} finally {
|
|
1063
|
+
fs.removeSync(temporaryPath);
|
|
1064
|
+
}
|
|
964
1065
|
|
|
965
1066
|
const status = shellExec(`sudo systemctl status sshd`, { silent: true, stdout: true });
|
|
966
1067
|
if (status.match('running')) console.log(status.replaceAll(`running`, `running`.green));
|
package/src/cli/system.js
CHANGED
|
@@ -8,6 +8,12 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import fs from 'fs-extra';
|
|
11
|
+
import {
|
|
12
|
+
selinuxEnforcingCommandsFactory,
|
|
13
|
+
selinuxPackagesCommandFactory,
|
|
14
|
+
selinuxSshContextCommandsFactory,
|
|
15
|
+
shellArgumentFactory,
|
|
16
|
+
} from '../server/selinux.js';
|
|
11
17
|
|
|
12
18
|
/**
|
|
13
19
|
* @class UnderpostSystemProvisionig
|
|
@@ -196,6 +202,8 @@ logdir /var/log/chrony
|
|
|
196
202
|
|
|
197
203
|
// Install essential system tools (avoiding duplicates from container packages)
|
|
198
204
|
`dnf -y install --allowerasing bzip2 openssh-server nano vim-enhanced less openssl-devel git gnupg2 libnsl perl`,
|
|
205
|
+
selinuxPackagesCommandFactory({ sudo: false }),
|
|
206
|
+
...selinuxEnforcingCommandsFactory({ sudo: false }),
|
|
199
207
|
`dnf clean all`,
|
|
200
208
|
|
|
201
209
|
// Install Node.js
|
|
@@ -218,19 +226,24 @@ logdir /var/log/chrony
|
|
|
218
226
|
* @memberof UnderpostSystemProvisionig
|
|
219
227
|
* @returns {string[]} An array of shell commands.
|
|
220
228
|
*/
|
|
221
|
-
user: () =>
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
229
|
+
user: ({ username = 'root', publicKeyPath = '/home/dd/engine/engine-private/deploy/id_rsa.pub' } = {}) => {
|
|
230
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid Rocky Linux username');
|
|
231
|
+
const home = username === 'root' ? '/root' : `/home/${username}`;
|
|
232
|
+
const sshDirectory = `${home}/.ssh`;
|
|
233
|
+
const publicKey = fs.readFileSync(publicKeyPath, 'utf8').trim();
|
|
234
|
+
const userCommand =
|
|
235
|
+
username === 'root'
|
|
236
|
+
? `usermod -s /bin/bash root`
|
|
237
|
+
: `id -u ${shellArgumentFactory(username)} >/dev/null 2>&1 || useradd -m -s /bin/bash -G wheel ${shellArgumentFactory(username)}`;
|
|
238
|
+
return [
|
|
239
|
+
userCommand,
|
|
240
|
+
`install -d -m 0700 -o ${shellArgumentFactory(username)} -g ${shellArgumentFactory(username)} ${shellArgumentFactory(sshDirectory)}`,
|
|
241
|
+
`printf '%s\n' ${shellArgumentFactory(publicKey)} > ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}`,
|
|
242
|
+
`chown ${shellArgumentFactory(`${username}:${username}`)} ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}`,
|
|
243
|
+
`chmod 0600 ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}`,
|
|
244
|
+
...selinuxSshContextCommandsFactory({ sshDirectory, sudo: false }),
|
|
245
|
+
];
|
|
246
|
+
},
|
|
234
247
|
/**
|
|
235
248
|
* @method timezone
|
|
236
249
|
* @description Generates shell commands for configuring the system timezone on Rocky Linux.
|
package/src/cli/test.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import fs from 'fs-extra';
|
|
8
8
|
import { timer } from '../client/components/core/CommonJs.js';
|
|
9
9
|
import { MariaDB } from '../db/mariadb/MariaDB.js';
|
|
10
|
-
import { getNpmRootPath } from '../server/
|
|
10
|
+
import { getNpmRootPath } from '../server/environment.js';
|
|
11
11
|
import { actionInitLog, loggerFactory, setUpInfo } from '../server/logger.js';
|
|
12
12
|
import { pbcopy, shellExec } from '../server/process.js';
|
|
13
13
|
import Underpost from '../index.js';
|