underpost 3.2.0 → 3.2.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.
package/src/cli/deploy.js CHANGED
@@ -132,22 +132,16 @@ class UnderpostDeploy {
132
132
  cmd = [
133
133
  `npm install -g npm@11.2.0`,
134
134
  `npm install -g underpost`,
135
- `underpost secret underpost --create-from-file /etc/config/.env.${env}`,
135
+ `underpost secret underpost --create-from-env`,
136
136
  `underpost start --build --run ${deployId} ${env}`,
137
137
  ];
138
138
  const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
139
- if (!volumes)
140
- volumes = [
141
- {
142
- volumeMountPath: '/etc/config',
143
- volumeName: 'config-volume',
144
- configMap: 'underpost-config',
145
- },
146
- ];
139
+ if (!volumes) volumes = [];
147
140
  const confVolume = fs.existsSync(`./engine-private/conf/${deployId}/conf.volume.json`)
148
141
  ? JSON.parse(fs.readFileSync(`./engine-private/conf/${deployId}/conf.volume.json`, 'utf8'))
149
142
  : [];
150
143
  volumes = volumes.concat(confVolume);
144
+ const containerImage = image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`;
151
145
  return `apiVersion: apps/v1
152
146
  kind: Deployment
153
147
  metadata:
@@ -169,7 +163,10 @@ spec:
169
163
  spec:
170
164
  containers:
171
165
  - name: ${deployId}-${env}-${suffix}
172
- image: ${image ? image : `localhost/rockylinux9-underpost:v${packageJson.version}`}
166
+ image: ${containerImage}
167
+ envFrom:
168
+ - secretRef:
169
+ name: underpost-config
173
170
  ${
174
171
  resources
175
172
  ? ` resources:
@@ -185,13 +182,17 @@ ${
185
182
  - /bin/sh
186
183
  - -c
187
184
  - >
188
- ${cmd.join(` && `)}
185
+ ${cmd.join(' &&\n ')}
189
186
 
190
- ${Underpost.deploy
191
- .volumeFactory(volumes.map((v) => ((v.version = `${deployId}-${env}-${suffix}`), v)))
192
- .render.split(`\n`)
193
- .map((l) => ' ' + l)
194
- .join(`\n`)}
187
+ ${
188
+ volumes.length > 0
189
+ ? Underpost.deploy
190
+ .volumeFactory(volumes.map((v) => ((v.version = `${deployId}-${env}-${suffix}`), v)))
191
+ .render.split(`\n`)
192
+ .map((l) => ' ' + l)
193
+ .join(`\n`)
194
+ : ''
195
+ }
195
196
  ---
196
197
  apiVersion: v1
197
198
  kind: Service
@@ -793,15 +794,16 @@ EOF`);
793
794
  };
794
795
  },
795
796
  /**
796
- * Creates a configmap for a deployment.
797
- * @param {string} env - Environment for which the configmap is being created.
798
- * @param {string} [namespace='default'] - Kubernetes namespace for the configmap.
797
+ * Creates a Kubernetes Secret for a deployment (replaces configMap for secret data).
798
+ * Secrets are mounted as tmpfs (never written to node disk) and support RBAC restrictions.
799
+ * @param {string} env - Environment for which the secret is being created.
800
+ * @param {string} [namespace='default'] - Kubernetes namespace for the secret.
799
801
  * @memberof UnderpostDeploy
800
802
  */
801
803
  configMap(env, namespace = 'default') {
802
- shellExec(`kubectl delete configmap underpost-config -n ${namespace} --ignore-not-found`);
804
+ shellExec(`kubectl delete secret underpost-config -n ${namespace} --ignore-not-found`);
803
805
  shellExec(
804
- `kubectl create configmap underpost-config --from-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
806
+ `kubectl create secret generic underpost-config --from-env-file=/home/dd/engine/engine-private/conf/dd-cron/.env.${env} --dry-run=client -o yaml | kubectl apply -f - -n ${namespace}`,
805
807
  );
806
808
  },
807
809
  /**
@@ -920,6 +922,8 @@ EOF
920
922
  * @param {string} volume.volumeType - Type of the volume (e.g. 'Directory').
921
923
  * @param {string|null} volume.claimName - Name of the persistent volume claim (if applicable).
922
924
  * @param {string|null} volume.configMap - Name of the config map (if applicable).
925
+ * @param {string|null} volume.secret - Name of the Kubernetes Secret (if applicable). Mounts as readOnly.
926
+ * @param {boolean} [volume.emptyDir=false] - If true, uses an emptyDir volume (writable tmpfs).
923
927
  * @returns {object} - Object containing the rendered volume mounts and volumes.
924
928
  * @memberof UnderpostDeploy
925
929
  */
@@ -941,7 +945,17 @@ EOF
941
945
  let _volumes = `
942
946
  volumes:`;
943
947
  volumes.map((volumeData) => {
944
- let { volumeName, volumeMountPath, volumeHostPath, volumeType, claimName, configMap, version } = volumeData;
948
+ let {
949
+ volumeName,
950
+ volumeMountPath,
951
+ volumeHostPath,
952
+ volumeType,
953
+ claimName,
954
+ configMap,
955
+ secret,
956
+ emptyDir,
957
+ version,
958
+ } = volumeData;
945
959
  if (version) {
946
960
  volumeName = `${volumeName}-${version}`;
947
961
  claimName = claimName ? `${claimName}-${version}` : null;
@@ -949,18 +963,23 @@ EOF
949
963
  _volumeMounts += `
950
964
  - name: ${volumeName}
951
965
  mountPath: ${volumeMountPath}
952
- `;
966
+ ${secret ? ` readOnly: true\n` : ''}`;
953
967
 
954
968
  _volumes += `
955
969
  - name: ${volumeName}
956
970
  ${
957
- configMap
958
- ? ` configMap:
971
+ emptyDir
972
+ ? ` emptyDir: {}`
973
+ : secret
974
+ ? ` secret:
975
+ secretName: ${secret}`
976
+ : configMap
977
+ ? ` configMap:
959
978
  name: ${configMap}`
960
- : claimName
961
- ? ` persistentVolumeClaim:
979
+ : claimName
980
+ ? ` persistentVolumeClaim:
962
981
  claimName: ${claimName}`
963
- : ` hostPath:
982
+ : ` hostPath:
964
983
  path: ${volumeHostPath}
965
984
  type: ${volumeType}
966
985
  `
package/src/cli/env.js CHANGED
@@ -12,6 +12,19 @@ import { pbcopy } from '../server/process.js';
12
12
 
13
13
  const logger = loggerFactory(import.meta);
14
14
 
15
+ /**
16
+ * Guards an env file path against stale directory artifacts.
17
+ * Removes the path if it exists as a directory (e.g. `.env/` created by a previous EISDIR bug).
18
+ * @param {string} envPath - The path to the environment file.
19
+ * @memberof UnderpostEnv
20
+ */
21
+ const guardEnvPath = (envPath) => {
22
+ if (fs.existsSync(envPath) && !fs.statSync(envPath).isFile()) {
23
+ logger.warn(`Removing stale directory at env path: ${envPath}`);
24
+ fs.removeSync(envPath);
25
+ }
26
+ };
27
+
15
28
  /**
16
29
  * @class UnderpostRootEnv
17
30
  * @description Manages the environment variables of the underpost root.
@@ -31,6 +44,7 @@ class UnderpostRootEnv {
31
44
  */
32
45
  set(key, value, options = { deployId: '', build: false }) {
33
46
  const _set = (envPath, key, value) => {
47
+ guardEnvPath(envPath);
34
48
  let env = {};
35
49
  if (fs.existsSync(envPath)) env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
36
50
  env[key] = value;
@@ -61,6 +75,7 @@ class UnderpostRootEnv {
61
75
  delete(key) {
62
76
  const exeRootPath = `${getNpmRootPath()}/underpost`;
63
77
  const envPath = `${exeRootPath}/.env`;
78
+ guardEnvPath(envPath);
64
79
  let env = {};
65
80
  if (fs.existsSync(envPath)) env = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
66
81
  delete env[key];
@@ -102,6 +117,7 @@ class UnderpostRootEnv {
102
117
  list(key, value, options = {}) {
103
118
  const exeRootPath = `${getNpmRootPath()}/underpost`;
104
119
  const envPath = `${exeRootPath}/.env`;
120
+ guardEnvPath(envPath);
105
121
  if (!fs.existsSync(envPath)) {
106
122
  logger.warn(`Empty environment variables`);
107
123
  return {};
@@ -137,7 +153,19 @@ class UnderpostRootEnv {
137
153
  const envPath = `${exeRootPath}/.env`;
138
154
  fs.removeSync(envPath);
139
155
  },
156
+ /**
157
+ * @method isInsideContainer
158
+ * @description Detects whether the current process is running inside a container.
159
+ * Checks for Kubernetes service injection or Docker's .dockerenv marker.
160
+ * @returns {boolean} True if running inside a container.
161
+ * @memberof UnderpostEnv
162
+ */
163
+ isInsideContainer() {
164
+ return !!process.env.KUBERNETES_SERVICE_HOST || fs.existsSync('/.dockerenv');
165
+ },
140
166
  };
141
167
  }
142
168
 
143
169
  export default UnderpostRootEnv;
170
+
171
+ export { guardEnvPath };
package/src/cli/fs.js CHANGED
@@ -101,12 +101,14 @@ class UnderpostFileStorage {
101
101
  }
102
102
  }
103
103
  if (options.pull === true) {
104
+ let pullSkipCount = 0;
104
105
  for (const _path of Object.keys(storage)) {
105
106
  if (!fs.existsSync(_path) || options.force === true) {
106
107
  if (options.force === true && fs.existsSync(_path)) fs.removeSync(_path);
107
108
  await Underpost.fs.pull(_path, options);
108
- } else logger.warn(`Pull path already exists`, _path);
109
+ } else pullSkipCount++;
109
110
  }
111
+ if (pullSkipCount > 0) logger.warn(`Pull skipped ${pullSkipCount} files that already exist`);
110
112
  Underpost.repo.initLocalRepo({ path });
111
113
  shellExec(`cd ${path} && git add . && git commit -m "Base pull state"`);
112
114
  } else {
package/src/cli/index.js CHANGED
@@ -337,10 +337,14 @@ program
337
337
  .argument('<platform>', `The secret management platform. Options: ${Object.keys(Underpost.secret).join(', ')}.`)
338
338
  .option('--init', 'Initializes the secrets platform environment.')
339
339
  .option('--create-from-file <path-env-file>', 'Creates secrets from a specified environment file.')
340
+ .option('--create-from-env', 'Creates secrets from container environment variables (envFrom: secretRef).')
341
+ .option('--global-clean', 'Removes all filesystem traces of secrets (engine-private, .env, conf cache).')
340
342
  .option('--list', 'Lists all available secrets for the platform.')
341
343
  .description(`Manages secrets for various platforms.`)
342
344
  .action((...args) => {
345
+ if (args[1].globalClean) return Underpost.secret.globalSecretClean();
343
346
  if (args[1].createFromFile) return Underpost.secret[args[0]].createFromEnvFile(args[1].createFromFile);
347
+ if (args[1].createFromEnv) return Underpost.secret[args[0]].createFromContainerEnv();
344
348
  if (args[1].list) return Underpost.secret[args[0]].list();
345
349
  if (args[1].init) return Underpost.secret[args[0]].init();
346
350
  });
@@ -423,6 +427,7 @@ program
423
427
  .option('--kubeadm', 'Enables the kubeadm context for database operations.')
424
428
  .option('--kind', 'Enables the kind context for database operations.')
425
429
  .option('--k3s', 'Enables the k3s context for database operations.')
430
+ .option('--repo-backup', 'Backs up repositories (git commit+push) inside deployment pods via kubectl exec.')
426
431
  .description(
427
432
  'Manages database operations with support for MariaDB and MongoDB, including import/export, multi-pod targeting, and Git integration.',
428
433
  )
@@ -439,6 +444,7 @@ program
439
444
  .option('--instances', 'Apply to instance data collection')
440
445
  .option('--generate', 'Generate cluster metadata')
441
446
  .option('--itc', 'Apply under container execution context')
447
+ .option('--dev', 'Sets the development cli context')
442
448
  .description('Manages cluster metadata operations, including import and export.')
443
449
  .action(Underpost.db.clusterMetadataBackupCallback);
444
450
 
@@ -468,7 +474,6 @@ program
468
474
  '--create-job-now',
469
475
  'After applying manifests, immediately create a Job from each CronJob (requires --apply).',
470
476
  )
471
- .option('--ssh', 'Execute backup commands via SSH on the remote node instead of locally.')
472
477
  .description('Manages cron jobs: execute jobs directly or generate and apply K8s CronJob manifests.')
473
478
  .action(Underpost.cron.callback);
474
479
 
@@ -1423,6 +1423,153 @@ Prevent build private config repo.`,
1423
1423
  return false;
1424
1424
  }
1425
1425
  },
1426
+
1427
+ /**
1428
+ * Runtime-type → in-pod site-root directory resolver.
1429
+ * Maps each known runtime to the filesystem path where its repository lives inside the container.
1430
+ * @param {string} runtime - The runtime identifier (e.g. 'wp').
1431
+ * @param {string} host - The virtual-host name.
1432
+ * @returns {string|null} Absolute path inside the pod, or null if the runtime has no known mapping.
1433
+ * @memberof UnderpostRepository
1434
+ */
1435
+ runtimeSiteRoot(runtime, host) {
1436
+ const runtimePaths = {
1437
+ wp: `/opt/lampp/htdocs/wp/${host}`,
1438
+ };
1439
+ return runtimePaths[runtime] || null;
1440
+ },
1441
+
1442
+ /**
1443
+ * Backs up all repositories defined in a deployment's conf.server.json by executing
1444
+ * git commit+push inside the running deployment pod via `kubectl exec`.
1445
+ *
1446
+ * Scans every `server[host][path]` entry for a `repository` field. For each match
1447
+ * the runtime-specific site root is resolved and a git backup script is executed
1448
+ * inside the pod. GITHUB_TOKEN and GITHUB_USERNAME are injected as ephemeral
1449
+ * environment variables in the exec command — never persisted to the pod filesystem.
1450
+ *
1451
+ * @param {object} opts
1452
+ * @param {string} opts.deployId - Deployment ID (used to read conf.server.json and find pods).
1453
+ * @param {string} [opts.namespace='default'] - Kubernetes namespace.
1454
+ * @param {string} [opts.env='production'] - Deployment environment.
1455
+ * @returns {void}
1456
+ * @memberof UnderpostRepository
1457
+ */
1458
+ backupPodRepositories({ deployId, namespace = 'default', env = 'production' }) {
1459
+ const confServer = readConfJson(deployId, 'server', { resolve: true });
1460
+ const githubToken = process.env.GITHUB_TOKEN || '';
1461
+ const githubUsername = process.env.GITHUB_USERNAME || 'underpostnet';
1462
+
1463
+ if (!githubToken) {
1464
+ logger.warn('backupPodRepositories: GITHUB_TOKEN not available — git push will fail');
1465
+ }
1466
+
1467
+ // Resolve the active blue/green traffic colour so we target the correct pod
1468
+ const traffic = Underpost.deploy.getCurrentTraffic(deployId, { namespace });
1469
+ if (!traffic) {
1470
+ logger.warn(`backupPodRepositories: could not resolve current traffic for ${deployId} — skipping`);
1471
+ return;
1472
+ }
1473
+
1474
+ // Find a running pod that matches the active traffic colour
1475
+ const pods = Underpost.kubectl.get(`${deployId}-${env}-${traffic}`, 'pods', namespace);
1476
+ const runningPod = pods.find((p) => p.STATUS === 'Running');
1477
+ if (!runningPod) {
1478
+ logger.warn(`backupPodRepositories: no running ${traffic} pod found for ${deployId} in namespace ${namespace}`);
1479
+ return;
1480
+ }
1481
+ const podName = runningPod.NAME;
1482
+
1483
+ for (const host of Object.keys(confServer)) {
1484
+ for (const routePath of Object.keys(confServer[host])) {
1485
+ const entry = confServer[host][routePath];
1486
+ if (!entry.repository) continue;
1487
+
1488
+ const siteRoot = Underpost.repo.runtimeSiteRoot(entry.runtime, host);
1489
+ if (!siteRoot) {
1490
+ logger.warn(`backupPodRepositories: no site-root mapping for runtime '${entry.runtime}' (${host})`);
1491
+ continue;
1492
+ }
1493
+
1494
+ const repoName = entry.repository.split('/').pop().split('.')[0];
1495
+
1496
+ // Build the backup script — secrets are injected as env vars in the exec,
1497
+ // never written to filesystem. The shell process inherits them ephemerally.
1498
+ const backupScript = [
1499
+ `export GITHUB_TOKEN='${githubToken.replace(/'/g, "'\\''")}'`,
1500
+ `export GITHUB_USERNAME='${githubUsername.replace(/'/g, "'\\''")}'`,
1501
+ `git config --global --add safe.directory '${siteRoot}' 2>/dev/null || true`,
1502
+ `cd '${siteRoot}' && git add -A && git commit -m 'backup $(date -u +%Y-%m-%dT%H:%M:%SZ)' || true`,
1503
+ `cd '${siteRoot}' && underpost push . ${githubUsername}/${repoName}`,
1504
+ `cd /home/dd/engine && node bin secret underpost --global-clean`,
1505
+ ].join(' && ');
1506
+
1507
+ try {
1508
+ logger.info(`backupPodRepositories: backing up ${host} (${entry.runtime}) in pod ${podName}`);
1509
+ Underpost.kubectl.exec({ podName, namespace, command: backupScript });
1510
+ logger.info(`backupPodRepositories: git push done for ${host}`);
1511
+ } catch (err) {
1512
+ logger.error(`backupPodRepositories: backup failed for ${host}`, err.message);
1513
+ }
1514
+ }
1515
+ }
1516
+ },
1517
+
1518
+ /**
1519
+ * Clones the deploy-specific private repository into `./engine-private`
1520
+ * when it does not already exist on disk. Returns `{ ephemeral: true }`
1521
+ * If `./engine-private` already exists, the call is a no-op unless
1522
+ * `options.force` is `true`, in which case the directory is removed and
1523
+ * re-cloned.
1524
+ *
1525
+ * @param {string} [deployId] - Deploy ID (e.g. `dd-core`) used to derive
1526
+ * the repo name `engine-{component}-private`. Falls back to
1527
+ * `process.env.DEFAULT_DEPLOY_ID`. When neither is available the
1528
+ * default repo name `engine-private` is used.
1529
+ * @param {object} [options]
1530
+ * @param {boolean} [options.force=false] - Remove existing `engine-private`
1531
+ * and re-clone.
1532
+ * @memberof UnderpostRepository
1533
+ */
1534
+ privateEngineRepoFactory(deployId, options = { force: false }) {
1535
+ if (fs.existsSync('./engine-private') && !options.force) return;
1536
+
1537
+ if (options.force && fs.existsSync('./engine-private')) {
1538
+ fs.removeSync('./engine-private');
1539
+ logger.info('engine-private removed (force re-clone)');
1540
+ }
1541
+
1542
+ const effectiveDeployId = deployId || process.env.DEFAULT_DEPLOY_ID;
1543
+
1544
+ const username = process.env.GITHUB_USERNAME;
1545
+ if (!username) {
1546
+ throw new Error('privateEngineRepoFactory: GITHUB_USERNAME not set');
1547
+ }
1548
+
1549
+ const repoName = effectiveDeployId ? `engine-${effectiveDeployId.split('-')[1]}-private` : 'engine-private';
1550
+ logger.info(`engine-private missing — cloning ${username}/${repoName}`);
1551
+ shellExec(`underpost clone ${username}/${repoName}`);
1552
+ if (!fs.existsSync(`./${repoName}`)) {
1553
+ throw new Error(`privateEngineRepoFactory: clone failed for ${username}/${repoName}`);
1554
+ }
1555
+ if (repoName !== 'engine-private') shellExec(`mv ./${repoName} ./engine-private`);
1556
+ },
1557
+
1558
+ /**
1559
+ * Removes the ephemeral `engine-private/` clone created by
1560
+ * `privateEngineRepoFactory()`. No-op if the directory does not exist.
1561
+ * @memberof UnderpostRepository
1562
+ */
1563
+ cleanupPrivateEngineRepo() {
1564
+ if (fs.existsSync('./engine-private')) {
1565
+ fs.removeSync('./engine-private');
1566
+ logger.info('engine-private ephemeral clone removed');
1567
+ }
1568
+ if (fs.existsSync('/home/dd/engine-private')) {
1569
+ fs.removeSync('/home/dd/engine-private');
1570
+ logger.info('engine-private in /home/dd removed');
1571
+ }
1572
+ },
1426
1573
  };
1427
1574
  }
1428
1575
 
package/src/cli/run.js CHANGED
@@ -1781,7 +1781,7 @@ EOF
1781
1781
  : [
1782
1782
  `npm install -g npm@11.2.0`,
1783
1783
  `npm install -g underpost`,
1784
- `${baseCommand} secret underpost --create-from-file /etc/config/.env.${env}`,
1784
+ `${baseCommand} secret underpost --create-from-env`,
1785
1785
  `${baseCommand} start --build --run ${deployId} ${env}`,
1786
1786
  ];
1787
1787
  shellExec(`node bin run sync${baseClusterCommand} --deploy-id-cron-jobs none dd-test --cmd "${cmd}"`);
@@ -8,6 +8,10 @@ import { shellExec } from '../server/process.js';
8
8
  import fs from 'fs-extra';
9
9
  import dotenv from 'dotenv';
10
10
  import Underpost from '../index.js';
11
+ import { loadConf } from '../server/conf.js';
12
+ import { loggerFactory } from '../server/logger.js';
13
+
14
+ const logger = loggerFactory(import.meta);
11
15
 
12
16
  /**
13
17
  * @class UnderpostSecret
@@ -23,11 +27,80 @@ class UnderpostSecret {
23
27
  */
24
28
  underpost: {
25
29
  createFromEnvFile(envPath) {
30
+ Underpost.env.clean();
26
31
  const envObj = dotenv.parse(fs.readFileSync(envPath, 'utf8'));
27
32
  for (const key of Object.keys(envObj)) {
28
33
  Underpost.env.set(key, envObj[key]);
29
34
  }
30
35
  },
36
+ /** Reads application secrets from process.env (injected via envFrom: secretRef)
37
+ * and writes them to the underpost .env file, filtering out known system and
38
+ * Kubernetes-injected environment variables. Replaces the fragile shell-based
39
+ * `printenv | grep -vE` pattern with a maintainable Node.js blocklist.
40
+ */
41
+ createFromContainerEnv() {
42
+ Underpost.env.clean();
43
+ const systemKeys = new Set([
44
+ 'HOME',
45
+ 'HOSTNAME',
46
+ 'PATH',
47
+ 'TERM',
48
+ 'SHLVL',
49
+ 'PWD',
50
+ '_',
51
+ 'LANG',
52
+ 'LANGUAGE',
53
+ 'LC_ALL',
54
+ 'container',
55
+ 'SHELL',
56
+ 'USER',
57
+ 'LOGNAME',
58
+ 'MAIL',
59
+ 'OLDPWD',
60
+ 'LESSOPEN',
61
+ 'LESSCLOSE',
62
+ 'LS_COLORS',
63
+ 'DISPLAY',
64
+ 'COLORTERM',
65
+ 'EDITOR',
66
+ 'VISUAL',
67
+ 'TERM_PROGRAM',
68
+ 'TERM_PROGRAM_VERSION',
69
+ 'SSH_AUTH_SOCK',
70
+ 'SSH_CLIENT',
71
+ 'SSH_CONNECTION',
72
+ 'SSH_TTY',
73
+ 'XDG_SESSION_ID',
74
+ 'XDG_RUNTIME_DIR',
75
+ 'XDG_DATA_DIRS',
76
+ 'XDG_CONFIG_DIRS',
77
+ 'DBUS_SESSION_BUS_ADDRESS',
78
+ 'GPG_AGENT_INFO',
79
+ 'WINDOWID',
80
+ 'DESKTOP_SESSION',
81
+ 'SESSION_MANAGER',
82
+ 'XAUTHORITY',
83
+ 'WAYLAND_DISPLAY',
84
+ 'which_declare',
85
+ ]);
86
+ const systemKeyPrefixes = ['KUBERNETES_', 'npm_', 'NODE_'];
87
+ for (const [key, value] of Object.entries(process.env)) {
88
+ if (systemKeys.has(key)) continue;
89
+ if (systemKeyPrefixes.some((prefix) => key.startsWith(prefix))) continue;
90
+ Underpost.env.set(key, value);
91
+ }
92
+ },
93
+ },
94
+
95
+ /**
96
+ * Removes all filesystem traces of secrets after deployment startup.
97
+ * Centralizes the defense-in-depth cleanup performed
98
+ * @memberof UnderpostSecret
99
+ */
100
+ globalSecretClean() {
101
+ loadConf('clean');
102
+ Underpost.repo.cleanupPrivateEngineRepo();
103
+ Underpost.env.clean();
31
104
  },
32
105
  };
33
106
  }
@@ -75,14 +75,22 @@ const DropDown = {
75
75
  console.log('DropDown onClick', this.value);
76
76
  if (options && options.resetOnClick) options.resetOnClick();
77
77
  if (options && options.type === 'checkbox') {
78
+ DropDown.Tokens[id].oncheckvalues = {};
79
+ DropDown.Tokens[id].value = [];
80
+ s(`.${id}`).value = [];
81
+ htmls(`.dropdown-current-${id}`, '');
78
82
  if (options.serviceProvider) {
79
- DropDown.Tokens[id].oncheckvalues = {};
80
- DropDown.Tokens[id].value = [];
81
- htmls(`.dropdown-current-${id}`, '');
82
83
  htmls(`.${id}-render-container`, '');
83
84
  } else {
84
- for (const opt of DropDown.Tokens[id].value) {
85
- s(`.dropdown-option-${id}-${opt}`).click();
85
+ for (const optionData of options.data) {
86
+ if (optionData.value !== 'reset' && optionData.value !== 'close' && optionData.checked) {
87
+ optionData.checked = false;
88
+ const vd = optionData.value.trim().replaceAll(' ', '-');
89
+ if (ToggleSwitch.Tokens[`checkbox-role-${vd}`]) {
90
+ const checkbox = s(`.checkbox-role-${vd}-checkbox`);
91
+ if (checkbox && checkbox.checked) ToggleSwitch.Tokens[`checkbox-role-${vd}`].click();
92
+ }
93
+ }
86
94
  }
87
95
  }
88
96
  } else this.Tokens[id].value = undefined;
package/src/index.js CHANGED
@@ -44,7 +44,7 @@ class Underpost {
44
44
  * @type {String}
45
45
  * @memberof Underpost
46
46
  */
47
- static version = 'v3.2.0';
47
+ static version = 'v3.2.3';
48
48
 
49
49
  /**
50
50
  * Required Node.js major version
@@ -30,6 +30,10 @@ RUN dnf clean all
30
30
  RUN node --version
31
31
  RUN npm --version
32
32
 
33
+ # Create non-root user for secure container execution (cron jobs, init containers)
34
+ # Deployment containers override to root via securityContext when npm install -g is needed
35
+ RUN useradd -m -u 1000 -s /bin/bash dd
36
+
33
37
  # Set working directory
34
38
  WORKDIR /home/dd
35
39
 
@@ -47,6 +47,10 @@ RUN dnf clean all
47
47
  RUN node --version
48
48
  RUN npm --version
49
49
 
50
+ # Create non-root user for secure container execution (cron jobs, init containers)
51
+ # Deployment containers override to root via securityContext when npm install -g is needed
52
+ RUN useradd -m -u 1000 -s /bin/bash dd
53
+
50
54
  # Set working directory
51
55
  WORKDIR /home/dd
52
56
 
@@ -239,13 +239,35 @@ Listen ${port}
239
239
  DocumentRoot "${documentRoot}"
240
240
  ServerName ${host}
241
241
  UseCanonicalName Off
242
+ ServerSignature Off
242
243
 
243
244
  <Directory "${documentRoot}">
244
- Options Indexes FollowSymLinks MultiViews
245
+ Options FollowSymLinks MultiViews
245
246
  AllowOverride All
246
247
  Require all granted
247
248
  </Directory>
248
249
 
250
+ # Block access to .git directories and files
251
+ RedirectMatch 404 /\\.git
252
+
253
+ # Block access to hidden dotfiles (.env, .htpasswd, etc.)
254
+ <FilesMatch "^\\.(env|htpasswd|htaccess\\.bak|DS_Store)">
255
+ Require all denied
256
+ </FilesMatch>
257
+
258
+ # Block access to sensitive engine files
259
+ <FilesMatch "(wp-config\\.php\\.bak|wp-config-sample\\.php|\\.sql|\\.sql\\.gz)$">
260
+ Require all denied
261
+ </FilesMatch>
262
+
263
+ # Security headers
264
+ <IfModule mod_headers.c>
265
+ Header always set X-Content-Type-Options "nosniff"
266
+ Header always set X-Frame-Options "SAMEORIGIN"
267
+ Header always set Referrer-Policy "strict-origin-when-cross-origin"
268
+ Header always unset X-Powered-By
269
+ </IfModule>
270
+
249
271
  ${
250
272
  redirect
251
273
  ? `
@@ -53,6 +53,10 @@ RUN dnf clean all
53
53
  RUN node --version
54
54
  RUN npm --version
55
55
 
56
+ # Create non-root user for secure container execution (cron jobs, init containers)
57
+ # Deployment containers override to root via securityContext when npm install -g is needed
58
+ RUN useradd -m -u 1000 -s /bin/bash dd
59
+
56
60
  # Set working directory
57
61
  WORKDIR /home/dd
58
62