@underpostnet/cyberia 3.2.90 → 3.3.73
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/.dockerignore +1 -0
- package/.env.example +16 -0
- package/.github/workflows/coverall.cyberia.ci.yml +107 -0
- package/.github/workflows/cyberia-client.cd.yml +6 -18
- package/.github/workflows/cyberia-server.cd.yml +6 -18
- package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
- package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
- package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
- package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
- package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
- package/.github/workflows/engine-cyberia.cd.yml +6 -78
- package/.github/workflows/engine-cyberia.ci.yml +4 -5
- package/.github/workflows/ghpkg.ci.yml +42 -21
- package/.github/workflows/hardhat.ci.yml +12 -11
- package/.github/workflows/npmpkg.ci.yml +17 -15
- package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
- package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
- package/.github/workflows/release.cd.yml +2 -9
- package/.prettierignore +1 -0
- package/AGENTS.md +47 -0
- package/CHANGELOG.md +581 -2417
- package/CLI-HELP.md +416 -127
- package/Dockerfile +25 -9
- package/Dockerfile.dev +25 -9
- package/Dockerfile.test +4 -4
- package/README.md +21 -1
- package/bin/build.js +70 -43
- package/bin/build.template.js +8 -3
- package/bin/cyberia.js +372 -135
- package/bin/deploy.js +18 -104
- package/bin/index.js +372 -135
- package/compose.env +16 -0
- package/conf.js +93 -4
- package/deploy/cyberia-client/deploy.sh +47 -0
- package/deploy/cyberia-client/package.sh +18 -0
- package/deploy/cyberia-client/state.sh +14 -0
- package/deploy/cyberia-server/deploy.sh +47 -0
- package/deploy/cyberia-server/package.sh +18 -0
- package/deploy/cyberia-server/state.sh +14 -0
- package/deploy/dd-cyberia/deploy.sh +22 -0
- package/deploy/dd-cyberia/init.sh +55 -0
- package/deploy/dd-cyberia/package.sh +17 -0
- package/deploy/dd-cyberia/state.sh +14 -0
- package/deploy/dd-cyberia/sync-deploy.sh +116 -0
- package/deploy/lib/github-actions-logging.sh +594 -0
- package/deploy/lib/host.sh +185 -0
- package/deploy/lib/state.sh +62 -0
- package/deploy/pwa-microservices-template/deploy.sh +45 -0
- package/deploy/release/deploy.sh +32 -0
- package/deployment.yaml +7 -2
- package/docker-compose.yml +31 -37
- package/examples/static-page/README.md +19 -19
- package/examples/static-page/ssr-components/CustomPage.js +2 -2
- package/hardhat/hardhat.config.js +1 -6
- package/hardhat/package-lock.json +104 -43
- package/hardhat/package.json +3 -3
- package/manifests/alertmanager/deployment.yaml +81 -0
- package/manifests/blackbox-exporter/deployment.yaml +75 -0
- package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
- package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
- package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
- package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
- package/manifests/grafana/deployment.yaml +26 -7
- package/manifests/grafana/kustomization.yaml +1 -0
- package/manifests/grafana/service.yaml +6 -1
- package/manifests/mariadb/pv.yaml +1 -0
- package/manifests/mongodb/statefulset.yaml +9 -2
- package/manifests/mysql/pv-pvc.yaml +14 -1
- package/manifests/postgresql/pv.yaml +12 -1
- package/manifests/postgresql/pvc.yaml +1 -0
- package/manifests/prometheus/deployment.yaml +18 -22
- package/package.json +83 -10
- package/scripts/audit-selinux.sh +64 -0
- package/scripts/event-monitor.sh +56 -0
- package/scripts/gpu-diag.sh +0 -0
- package/scripts/ip-info.sh +0 -0
- package/scripts/k3s-node-setup.sh +30 -14
- package/scripts/kubeadm-node-setup.sh +40 -31
- package/scripts/lxd-vm-setup.sh +0 -0
- package/scripts/maas-nat-firewalld.sh +0 -0
- package/scripts/maas-setup.sh +8 -8
- package/scripts/nat-iptables.sh +2 -0
- package/scripts/rhel-grpc-setup.sh +0 -0
- package/scripts/rocky-kickstart.sh +25 -9
- package/scripts/selinux-normalize.sh +907 -0
- package/scripts/shutdown-machine.sh +843 -0
- package/scripts/test-monitor.sh +3 -3
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
- package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
- package/src/api/core/core.controller.js +1 -1
- package/src/api/core/core.router.js +2 -2
- package/src/api/core/core.service.js +2 -2
- package/src/api/crypto/crypto.controller.js +1 -1
- package/src/api/crypto/crypto.service.js +1 -1
- package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
- package/src/api/cyberia-action/cyberia-action.router.js +1 -1
- package/src/api/cyberia-action/cyberia-action.service.js +2 -2
- package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
- package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
- package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
- package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
- package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
- package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
- package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
- package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
- package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
- package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
- package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
- package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
- package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
- package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
- package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
- package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
- package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
- package/src/api/cyberia-map/cyberia-map.router.js +1 -1
- package/src/api/cyberia-map/cyberia-map.service.js +2 -2
- package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
- package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
- package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
- package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
- package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
- package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
- package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
- package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
- package/src/api/default/default.controller.js +1 -1
- package/src/api/default/default.router.js +1 -1
- package/src/api/default/default.service.js +2 -2
- package/src/api/document/document.controller.js +1 -1
- package/src/api/document/document.router.js +1 -1
- package/src/api/document/document.service.js +2 -2
- package/src/api/file/file.controller.js +1 -1
- package/src/api/file/file.router.js +1 -1
- package/src/api/file/file.service.js +2 -2
- package/src/api/instance/instance.controller.js +1 -1
- package/src/api/instance/instance.router.js +1 -1
- package/src/api/instance/instance.service.js +2 -2
- package/src/api/ipfs/ipfs.controller.js +1 -1
- package/src/api/ipfs/ipfs.router.js +2 -2
- package/src/api/ipfs/ipfs.service.js +2 -2
- package/src/api/object-layer/object-layer.controller.js +1 -1
- package/src/api/object-layer/object-layer.model.js +204 -3
- package/src/api/object-layer/object-layer.router.js +2 -2
- package/src/api/object-layer/object-layer.service.js +4 -17
- package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
- package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
- package/src/api/test/test.controller.js +1 -1
- package/src/api/test/test.service.js +1 -1
- package/src/api/user/guest.service.js +3 -3
- package/src/api/user/user.controller.js +1 -1
- package/src/api/user/user.router.js +2 -2
- package/src/api/user/user.service.js +4 -4
- package/src/api.js +5 -9
- package/src/cli/app.js +391 -0
- package/src/cli/baremetal.js +30 -36
- package/src/cli/client.js +306 -0
- package/src/cli/cloud-init.js +3 -3
- package/src/cli/cluster.js +242 -135
- package/src/cli/db.js +126 -67
- package/src/cli/deploy.js +158 -60
- package/src/cli/docker-compose.js +5 -5
- package/src/cli/domains.js +184 -0
- package/src/cli/dotenv-store.js +143 -0
- package/src/cli/event.js +2281 -0
- package/src/cli/fs.js +3 -3
- package/src/cli/host.js +672 -0
- package/src/cli/image.js +4 -7
- package/src/cli/index.js +426 -134
- package/src/cli/ipfs.js +34 -30
- package/src/cli/kickstart.js +1 -1
- package/src/cli/kubectl.js +218 -41
- package/src/cli/lxd.js +3 -3
- package/src/cli/monitor.js +1359 -26
- package/src/cli/package.js +90 -0
- package/src/cli/release.js +73 -34
- package/src/cli/repository.js +564 -367
- package/src/cli/run.js +362 -654
- package/src/cli/secrets.js +2158 -964
- package/src/cli/ssh.js +651 -331
- package/src/cli/state.js +394 -0
- package/src/cli/static.js +2 -2
- package/src/cli/system.js +26 -13
- package/src/cli/test.js +199 -125
- package/src/cli/vultr.js +623 -0
- package/src/cli/wireguard.js +2930 -0
- package/src/client/components/core/CalendarCore.js +53 -96
- package/src/client/components/core/Docs.js +258 -211
- package/src/client/components/core/FileExplorer.js +0 -21
- package/src/client/components/core/PanelForm.js +4 -4
- package/src/client/components/core/Repository.js +56 -0
- package/src/client/components/core/Translate.js +7 -0
- package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
- package/src/client/components/core/Wallet.js +0 -11
- package/src/client/components/core/Worker.js +7 -1
- package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
- package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
- package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
- package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
- package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
- package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
- package/src/client/components/default/AppShellDefault.js +1 -1
- package/src/client/components/default/SettingsDefault.js +2 -2
- package/src/client/components/itemledger/SettingsItemledger.js +2 -2
- package/src/client/components/underpost/AppShellUnderpost.js +3 -2
- package/src/client/components/underpost/SettingsUnderpost.js +2 -2
- package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
- package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
- package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
- package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
- package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
- package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
- package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
- package/src/client/public/default/android-chrome-144x144.png +0 -0
- package/src/client/public/default/android-chrome-192x192.png +0 -0
- package/src/client/public/default/android-chrome-256x256.png +0 -0
- package/src/client/public/default/android-chrome-36x36.png +0 -0
- package/src/client/public/default/android-chrome-384x384.png +0 -0
- package/src/client/public/default/android-chrome-48x48.png +0 -0
- package/src/client/public/default/android-chrome-512x512.png +0 -0
- package/src/client/public/default/android-chrome-72x72.png +0 -0
- package/src/client/public/default/android-chrome-96x96.png +0 -0
- package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
- package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
- package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
- package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
- package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
- package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
- package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
- package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
- package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
- package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
- package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
- package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
- package/src/client/public/default/apple-touch-icon.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
- package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
- package/src/client/public/default/assets/background/white.jpg +0 -0
- package/src/client/public/default/browserconfig.xml +1 -1
- package/src/client/public/default/favicon-16x16.png +0 -0
- package/src/client/public/default/favicon-32x32.png +0 -0
- package/src/client/public/default/favicon-48x48.png +0 -0
- package/src/client/public/default/favicon.ico +0 -0
- package/src/client/public/default/manifest.webmanifest +5 -5
- package/src/client/public/default/mstile-144x144.png +0 -0
- package/src/client/public/default/mstile-150x150.png +0 -0
- package/src/client/public/default/mstile-310x150.png +0 -0
- package/src/client/public/default/mstile-310x310.png +0 -0
- package/src/client/public/default/mstile-70x70.png +0 -0
- package/src/client/public/default/yandex-browser-50x50.png +0 -0
- package/src/client/public/default/yandex-browser-manifest.json +1 -1
- package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
- package/src/client/services/object-layer/object-layer.management.js +4 -4
- package/src/client/ssr/RootDocument.js +1 -1
- package/src/client/ssr/body/404.js +1 -1
- package/src/client/ssr/body/500.js +1 -1
- package/src/client/ssr/body/CacheControl.js +1 -1
- package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
- package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
- package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
- package/src/client/ssr/head/CryptokoynScripts.js +1 -1
- package/src/client/ssr/head/Css.js +1 -1
- package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
- package/src/client/ssr/head/DefaultScripts.js +1 -1
- package/src/client/ssr/head/ItemledgerScripts.js +1 -1
- package/src/client/ssr/head/Microdata.js +1 -1
- package/src/client/ssr/head/Production.js +1 -1
- package/src/client/ssr/head/Pwa.js +1 -1
- package/src/client/ssr/head/PwaItemledger.js +1 -1
- package/src/client/ssr/head/Seo.js +1 -1
- package/src/client/ssr/head/UnderpostScripts.js +1 -1
- package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
- package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
- package/src/client/ssr/views/Cyberia404.js +7 -5
- package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
- package/src/client/ssr/views/Maintenance.js +1 -1
- package/src/client/ssr/views/NoNetworkConnection.js +1 -1
- package/src/client/ssr/views/Test.js +1 -1
- package/src/client/sw/core.sw.js +20 -14
- package/src/client-builder/client-build-docs.js +61 -46
- package/src/client-builder/client-build-live.js +2 -2
- package/src/client-builder/client-build.js +58 -44
- package/src/client-builder/client-dev-server.js +3 -3
- package/src/client-builder/client-icons.js +2 -2
- package/src/client-builder/ssr.js +3 -3
- package/src/client.build.js +3 -6
- package/src/client.dev.js +3 -6
- package/src/db/DataBaseProvider.js +186 -3
- package/src/db/mariadb/MariaDB.js +3 -2
- package/src/db/mongo/MongoBootstrap.js +273 -101
- package/src/db/mongo/MongooseDB.js +11 -8
- package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
- package/src/grpc/cyberia/grpc-server.js +1 -1
- package/src/index.js +103 -23
- package/src/mailer/EmailRender.js +2 -2
- package/src/mailer/MailerInterceptor.js +87 -0
- package/src/mailer/MailerProvider.js +50 -6
- package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
- package/src/projects/cyberia/besu-genesis-generator.js +3 -3
- package/src/projects/cyberia/catalog-cyberia.js +54 -6
- package/src/projects/cyberia/gemini-client.js +1 -1
- package/src/projects/cyberia/generate-saga.js +15 -24
- package/src/projects/cyberia/hot-reload-trigger.js +1 -1
- package/src/projects/cyberia/instance-data.js +22 -3
- package/src/projects/cyberia/ipfs-client.js +4 -4
- package/src/projects/cyberia/map-preview-generator.js +1 -1
- package/src/projects/cyberia/object-layer.js +12 -22
- package/src/projects/cyberia/semantic-layer-generator.js +1 -1
- package/src/projects/underpost/catalog-underpost.js +13 -2
- package/src/proxy.js +4 -7
- package/src/runtime/cyberia-client/Dockerfile +9 -4
- package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
- package/src/runtime/cyberia-server/Dockerfile +9 -4
- package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
- package/src/runtime/engine-cyberia/Dockerfile +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
- package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
- package/src/runtime/engine-cyberia/compose.env +16 -0
- package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
- package/src/runtime/express/Express.js +32 -27
- package/src/runtime/lampp/Lampp.js +22 -70
- package/src/runtime/nginx/Nginx.js +1 -1
- package/src/runtime/wp/Wp.js +16 -48
- package/src/server/{catalog.js → build/catalog.js} +15 -5
- package/src/server/build/coverage.js +117 -0
- package/src/server/build/execution.js +402 -0
- package/src/server/build/package.js +465 -0
- package/src/server/build/testing.js +528 -0
- package/src/server/{dns.js → network/dns.js} +241 -14
- package/src/server/network/forward-proxy.js +541 -0
- package/src/server/{middlewares.js → network/middlewares.js} +59 -4
- package/src/server/network/node-capability.js +98 -0
- package/src/server/{peer.js → network/peer.js} +3 -3
- package/src/server/{proxy.js → network/proxy.js} +7 -13
- package/src/server/network/router.js +356 -0
- package/src/server/{tls.js → network/tls.js} +2 -2
- package/src/server/network/underpost-compression.js +186 -0
- package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
- package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
- package/src/server/{cri.js → ops/cri.js} +2 -2
- package/src/server/ops/cron.js +949 -0
- package/src/server/ops/event-notification.js +284 -0
- package/src/server/{logger.js → ops/logger.js} +108 -23
- package/src/server/ops/monitoring.js +1724 -0
- package/src/server/ops/systemd.js +259 -0
- package/src/server/{conf.js → runtime/conf.js} +521 -520
- package/src/server/runtime/config-scope.js +215 -0
- package/src/server/runtime/environment.js +149 -0
- package/src/server/{process.js → runtime/process.js} +153 -13
- package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
- package/src/server/{runtime.js → runtime/runtime.js} +12 -8
- package/src/server/{start.js → runtime/start.js} +128 -39
- package/src/server/{auth.js → security/auth.js} +7 -8
- package/src/server/security/container-storage.js +382 -0
- package/src/server/{crypto.js → security/crypto.js} +1 -1
- package/src/server/security/selinux.js +185 -0
- package/src/server/{backup.js → storage/backup.js} +17 -8
- package/src/server/{data-query.js → storage/data-query.js} +1 -1
- package/src/server/{downloader.js → storage/downloader.js} +2 -2
- package/src/server/storage/repository.js +68 -0
- package/src/server.js +4 -7
- package/src/ws/IoInterface.js +1 -1
- package/src/ws/IoServer.js +1 -1
- package/src/ws/core/core.ws.connection.js +1 -1
- package/src/ws/core/core.ws.emit.js +1 -1
- package/src/ws/core/core.ws.server.js +1 -1
- package/src/ws/default/default.ws.connection.js +1 -1
- package/src/ws/default/default.ws.emit.js +1 -1
- package/src/ws/default/default.ws.server.js +1 -1
- package/test/e2e/event-e2e-public-ingress-down.js +45 -0
- package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
- package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
- package/test/{api.test.js → integration/app/api.test.js} +33 -14
- package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
- package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
- package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
- package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
- package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
- package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
- package/test/integration/infra/1-security/config-scope.test.js +305 -0
- package/test/integration/infra/1-security/container-storage.test.js +356 -0
- package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
- package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
- package/test/integration/infra/1-security/selinux.test.js +69 -0
- package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
- package/test/integration/infra/1-security/systemd-service.test.js +70 -0
- package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
- package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
- package/test/integration/infra/2-network/node-capability.test.js +182 -0
- package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
- package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
- package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
- package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
- package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
- package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
- package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
- package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
- package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
- package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
- package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
- package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
- package/test/integration/infra/5-observability/event-notification.test.js +197 -0
- package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
- package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
- package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
- package/test/support/shell-harness.js +73 -0
- package/test/unit/build-template.test.js +97 -0
- package/test/unit/build-workflow-offline.test.js +132 -0
- package/test/unit/catalog.test.js +71 -0
- package/test/unit/client-build-docs.test.js +334 -0
- package/test/unit/conf-loading.test.js +303 -0
- package/test/unit/conf-resolution.test.js +979 -0
- package/test/unit/coverage-artifact.test.js +130 -0
- package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
- package/test/unit/deploy-env-resolution.test.js +38 -0
- package/test/unit/deploy-log-table.test.js +447 -0
- package/test/unit/execution-profiles.test.js +219 -0
- package/test/unit/fleet-sync-source.test.js +149 -0
- package/test/unit/logger-redaction.test.js +112 -0
- package/test/unit/package.test.js +403 -0
- package/test/unit/prepare-host.test.js +299 -0
- package/test/unit/process-environment.test.js +18 -0
- package/test/unit/propagation-message.test.js +78 -0
- package/test/unit/redeploy-plan.test.js +24 -0
- package/test/unit/release-bump.test.js +474 -0
- package/test/unit/ssh-output-redaction.test.js +28 -0
- package/test/unit/start-options.test.js +46 -0
- package/test/unit/test-tiers.test.js +193 -0
- package/vitest.config.js +58 -0
- package/.nycrc +0 -9
- package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
- package/manifests/mongodb/pv-pvc.yaml +0 -59
- package/scripts/link-local-underpost-cli.sh +0 -6
- package/src/api/user/user.build.js +0 -16
- package/src/cli/env.js +0 -177
- package/src/client/components/core/Blockchain.js +0 -41
- package/src/client/components/core/Blog.js +0 -9
- package/src/client/components/core/KeyboardAvoidance.js +0 -145
- package/src/client/public/default/assets/background/dark.jpg +0 -0
- package/src/client/public/default/assets/background/white0-min.jpg +0 -0
- package/src/client/public/default/assets/background/white0.jpg +0 -0
- package/src/client/public/doc/favicon.ico +0 -0
- package/src/client/public/doc/sitemap +0 -148
- package/src/server/cron.js +0 -476
package/src/cli/ssh.js
CHANGED
|
@@ -5,14 +5,146 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { generateRandomPasswordSelection } from '../client/components/core/CommonJs.js';
|
|
8
|
-
import {
|
|
9
|
-
import { loggerFactory } from '../server/logger.js';
|
|
10
|
-
import { waitForPort } from '../server/conf.js';
|
|
8
|
+
import { shellExec } from '../server/runtime/process.js';
|
|
9
|
+
import { loggerFactory, redactSensitiveText } from '../server/ops/logger.js';
|
|
10
|
+
import { waitForPort } from '../server/runtime/conf.js';
|
|
11
|
+
import {
|
|
12
|
+
runSELinuxCommands,
|
|
13
|
+
selinuxPackagesCommandFactory,
|
|
14
|
+
selinuxRestoreconCommandFactory,
|
|
15
|
+
selinuxSshContextCommandsFactory,
|
|
16
|
+
selinuxSshPortCommandsFactory,
|
|
17
|
+
shellArgumentFactory,
|
|
18
|
+
} from '../server/security/selinux.js';
|
|
19
|
+
import ContainerStorageService from '../server/security/container-storage.js';
|
|
11
20
|
import fs from 'fs-extra';
|
|
12
21
|
import Underpost from '../index.js';
|
|
13
22
|
|
|
14
23
|
const logger = loggerFactory(import.meta);
|
|
15
24
|
|
|
25
|
+
const shellExecRedacted = (command, options = {}) => {
|
|
26
|
+
const silent = options.silent === true;
|
|
27
|
+
try {
|
|
28
|
+
const result = shellExec(command, { ...options, silent: true });
|
|
29
|
+
if (!silent) {
|
|
30
|
+
if (result.stdout) process.stdout.write(redactSensitiveText(result.stdout));
|
|
31
|
+
if (result.stderr) process.stderr.write(redactSensitiveText(result.stderr));
|
|
32
|
+
}
|
|
33
|
+
return result.stdout;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
error.stdout = redactSensitiveText(error.stdout || '');
|
|
36
|
+
error.stderr = redactSensitiveText(error.stderr || '');
|
|
37
|
+
error.message = redactSensitiveText(error.message || error);
|
|
38
|
+
if (!silent) {
|
|
39
|
+
if (error.stdout) process.stdout.write(error.stdout);
|
|
40
|
+
if (error.stderr) process.stderr.write(error.stderr);
|
|
41
|
+
}
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* SSH users are cluster scoped: one registry and one key store serve every
|
|
48
|
+
* deploy id running on the cluster, so an account is provisioned once instead
|
|
49
|
+
* of once per app.
|
|
50
|
+
*
|
|
51
|
+
* A record is `{ user, password, groups, keyPath, pubKeyPath, hosts }`, where an
|
|
52
|
+
* empty password means key-only and `hosts` carries one connection per host:
|
|
53
|
+
*
|
|
54
|
+
* hosts: [{ host: '10.0.0.2', port: 22 }, { host: '10.0.0.3', port: 22 }]
|
|
55
|
+
*
|
|
56
|
+
* One account reaches many hosts — the same operator account on every WireGuard
|
|
57
|
+
* spoke is the normal case — so the host cannot be a field of the record. It was,
|
|
58
|
+
* and registering that account for a second host silently replaced the first,
|
|
59
|
+
* leaving remediation unable to resolve a spoke it had just been given.
|
|
60
|
+
*
|
|
61
|
+
* The port belongs to the connection for the same reason: two hosts of one
|
|
62
|
+
* account can listen on different ports, and a single record-level port could
|
|
63
|
+
* only describe one of them.
|
|
64
|
+
*/
|
|
65
|
+
const USERS_CONF_PATH = './engine-private/deploy/conf.users.json';
|
|
66
|
+
const USERS_KEYS_PATH = './engine-private/deploy/users';
|
|
67
|
+
const DEFAULT_SSH_PORT = 22;
|
|
68
|
+
/** The checkout's own key, and where a workload receives it instead: a projected Secret volume. */
|
|
69
|
+
const HOST_KEY_PATH = './engine-private/deploy/id_rsa';
|
|
70
|
+
const SECRET_KEY_PATH = '/etc/underpost/secrets/ssh/id_rsa';
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The private key this process authenticates with.
|
|
74
|
+
*
|
|
75
|
+
* One resolver for every SSH caller, because the answer differs by where the process runs and
|
|
76
|
+
* nothing else should have to know that. A pod has the key as a projected Secret and no checkout
|
|
77
|
+
* to read it from; the host CLI has the checkout and no mount. The mount is preferred over the
|
|
78
|
+
* checkout default but never over a caller's own path, because on a host the key is per target —
|
|
79
|
+
* the hub answers to a different one than the LAN nodes — and only the caller knows which.
|
|
80
|
+
* @param {string} [keyPath] - Explicit path, which always wins.
|
|
81
|
+
* @returns {string} Path to the private key.
|
|
82
|
+
* @memberof UnderpostSSH
|
|
83
|
+
*/
|
|
84
|
+
const sshKeyPathFactory = (keyPath = '') =>
|
|
85
|
+
`${keyPath || ''}`.trim() || (fs.existsSync(SECRET_KEY_PATH) ? SECRET_KEY_PATH : '') || HOST_KEY_PATH;
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* @method hostNameFactory
|
|
89
|
+
* @description A host as the registry compares it: the bare address, so a value
|
|
90
|
+
* written with a prefix (`10.0.0.2/32`) matches one without.
|
|
91
|
+
* @param {string} host - Host or IP, with or without a CIDR suffix.
|
|
92
|
+
* @returns {string} Normalized host.
|
|
93
|
+
* @memberof UnderpostSSH
|
|
94
|
+
*/
|
|
95
|
+
const hostNameFactory = (host) => `${host || ''}`.trim().split('/')[0];
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @method connectionFactory
|
|
99
|
+
* @description Normalizes one connection entry.
|
|
100
|
+
* @param {object} [connection] - Raw connection entry.
|
|
101
|
+
* @returns {?{host: string, port: number}} Normalized connection, or null without a host.
|
|
102
|
+
* @memberof UnderpostSSH
|
|
103
|
+
*/
|
|
104
|
+
const connectionFactory = (connection = {}) => {
|
|
105
|
+
const host = hostNameFactory(connection.host);
|
|
106
|
+
return host ? { host, port: Number(connection.port) || DEFAULT_SSH_PORT } : null;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* @method upsertConnection
|
|
111
|
+
* @description Adds a connection to a host list, replacing the entry for that
|
|
112
|
+
* host when one is already present.
|
|
113
|
+
*
|
|
114
|
+
* The one write path for `hosts`, so re-registering an account for a host it
|
|
115
|
+
* already reaches updates that connection instead of appending a duplicate the
|
|
116
|
+
* reverse lookup would then have to choose between.
|
|
117
|
+
* @param {Array<{host: string, port: number}>} [hosts] - Existing connections.
|
|
118
|
+
* @param {object} connection - Connection to add.
|
|
119
|
+
* @returns {Array<{host: string, port: number}>} Updated connections.
|
|
120
|
+
* @memberof UnderpostSSH
|
|
121
|
+
*/
|
|
122
|
+
const upsertConnection = (hosts = [], connection) => {
|
|
123
|
+
const entry = connectionFactory(connection);
|
|
124
|
+
if (!entry) return hosts;
|
|
125
|
+
return [...hosts.filter((existing) => existing.host !== entry.host), entry];
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* @method userRecordFactory
|
|
130
|
+
* @description Normalizes one registry entry, so every consumer reads one shape
|
|
131
|
+
* whether the file was written by hand, partially, or not at all.
|
|
132
|
+
* @param {object} [entry] - Raw registry entry.
|
|
133
|
+
* @returns {object} Normalized record.
|
|
134
|
+
* @memberof UnderpostSSH
|
|
135
|
+
*/
|
|
136
|
+
const userRecordFactory = (entry = {}) => ({
|
|
137
|
+
user: `${entry.user || ''}`.trim(),
|
|
138
|
+
password: `${entry.password ?? ''}`,
|
|
139
|
+
groups: `${entry.groups || ''}`,
|
|
140
|
+
keyPath: `${entry.keyPath || ''}`,
|
|
141
|
+
pubKeyPath: `${entry.pubKeyPath || ''}`,
|
|
142
|
+
hosts: (Array.isArray(entry.hosts) ? entry.hosts : []).reduce(
|
|
143
|
+
(hosts, connection) => upsertConnection(hosts, connection),
|
|
144
|
+
[],
|
|
145
|
+
),
|
|
146
|
+
});
|
|
147
|
+
|
|
16
148
|
/**
|
|
17
149
|
* @class UnderpostSSH
|
|
18
150
|
* @description Manages SSH key generation and connection setup.
|
|
@@ -21,33 +153,103 @@ const logger = loggerFactory(import.meta);
|
|
|
21
153
|
class UnderpostSSH {
|
|
22
154
|
static API = {
|
|
23
155
|
/**
|
|
24
|
-
*
|
|
156
|
+
* Path of the cluster users registry.
|
|
25
157
|
* @method
|
|
26
|
-
* @function
|
|
158
|
+
* @function usersConfPath
|
|
27
159
|
* @memberof UnderpostSSH
|
|
28
|
-
* @
|
|
29
|
-
* @returns {{confNode: Object, confNodePath: string}} Configuration node and its file path
|
|
30
|
-
* @description Loads or creates a config node with users object structure
|
|
160
|
+
* @returns {string} Path to the users configuration file
|
|
31
161
|
*/
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
162
|
+
usersConfPath: () => USERS_CONF_PATH,
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Key store directory for a user.
|
|
166
|
+
* @method
|
|
167
|
+
* @function userKeyDir
|
|
168
|
+
* @memberof UnderpostSSH
|
|
169
|
+
* @param {string} user - SSH user name
|
|
170
|
+
* @returns {string} Directory holding `<dir>/id_rsa` and `<dir>/id_rsa.pub`
|
|
171
|
+
*/
|
|
172
|
+
userKeyDir: (user) => `${USERS_KEYS_PATH}/${user}`,
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Loads the cluster users registry from disk.
|
|
176
|
+
* @method
|
|
177
|
+
* @function loadUsers
|
|
178
|
+
* @memberof UnderpostSSH
|
|
179
|
+
* @returns {Array<Object>} Registered user records, empty when unregistered
|
|
180
|
+
*/
|
|
181
|
+
loadUsers: () =>
|
|
182
|
+
(fs.existsSync(USERS_CONF_PATH) ? JSON.parse(fs.readFileSync(USERS_CONF_PATH, 'utf8')) : [])
|
|
183
|
+
.map(userRecordFactory)
|
|
184
|
+
.filter((entry) => entry.user),
|
|
37
185
|
|
|
38
186
|
/**
|
|
39
|
-
* Saves
|
|
187
|
+
* Saves the cluster users registry to disk.
|
|
40
188
|
* @method
|
|
41
|
-
* @function
|
|
189
|
+
* @function saveUsers
|
|
42
190
|
* @memberof UnderpostSSH
|
|
43
|
-
* @param {
|
|
44
|
-
* @param {Object} confNode - Configuration object to save
|
|
191
|
+
* @param {Array<Object>} users - User records to persist
|
|
45
192
|
* @returns {void}
|
|
46
193
|
*/
|
|
47
|
-
|
|
48
|
-
fs.outputFileSync(
|
|
194
|
+
saveUsers: (users) => {
|
|
195
|
+
fs.outputFileSync(USERS_CONF_PATH, JSON.stringify(users.map(userRecordFactory), null, 2), 'utf8');
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Looks up a registered user record.
|
|
200
|
+
* @method
|
|
201
|
+
* @function findUser
|
|
202
|
+
* @memberof UnderpostSSH
|
|
203
|
+
* @param {string} user - SSH user name
|
|
204
|
+
* @returns {Object|undefined} The record, or undefined when unregistered
|
|
205
|
+
*/
|
|
206
|
+
findUser: (user) => Underpost.ssh.loadUsers().find((entry) => entry.user === user),
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Resolves the connection an account uses to reach one host.
|
|
210
|
+
* @method
|
|
211
|
+
* @function resolveConnection
|
|
212
|
+
* @memberof UnderpostSSH
|
|
213
|
+
* @param {object} params
|
|
214
|
+
* @param {string} params.host - Host or IP the account was registered against.
|
|
215
|
+
* @param {string} [params.user] - Restrict the lookup to this account.
|
|
216
|
+
* @returns {Object|undefined} Flat connection `{ user, host, port, password, keyPath, pubKeyPath }`, or undefined
|
|
217
|
+
* @description
|
|
218
|
+
* The reverse of {@link UnderpostSSH.findUser}, for callers that know where
|
|
219
|
+
* they must act but not who acts there — remediation resolving a WireGuard
|
|
220
|
+
* spoke's tunnel address to the account that can repair it. Returns the flat
|
|
221
|
+
* view every SSH caller needs, so no consumer has to know the registry is
|
|
222
|
+
* keyed by host.
|
|
223
|
+
*/
|
|
224
|
+
resolveConnection: ({ host, user = '' } = {}) => {
|
|
225
|
+
const target = hostNameFactory(host);
|
|
226
|
+
if (!target) return undefined;
|
|
227
|
+
for (const record of Underpost.ssh.loadUsers()) {
|
|
228
|
+
if (user && record.user !== user) continue;
|
|
229
|
+
const connection = record.hosts.find((entry) => entry.host === target);
|
|
230
|
+
if (!connection) continue;
|
|
231
|
+
return {
|
|
232
|
+
user: record.user,
|
|
233
|
+
host: connection.host,
|
|
234
|
+
port: connection.port,
|
|
235
|
+
password: record.password,
|
|
236
|
+
keyPath: record.keyPath,
|
|
237
|
+
pubKeyPath: record.pubKeyPath,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return undefined;
|
|
49
241
|
},
|
|
50
242
|
|
|
243
|
+
/**
|
|
244
|
+
* Lists every host an account is registered for.
|
|
245
|
+
* @method
|
|
246
|
+
* @function userHosts
|
|
247
|
+
* @memberof UnderpostSSH
|
|
248
|
+
* @param {string} user - SSH user name
|
|
249
|
+
* @returns {Array<{host: string, port: number}>} Connection metadata per host
|
|
250
|
+
*/
|
|
251
|
+
userHosts: (user) => Underpost.ssh.findUser(user)?.hosts || [],
|
|
252
|
+
|
|
51
253
|
/**
|
|
52
254
|
* Checks if a system user exists.
|
|
53
255
|
* @method
|
|
@@ -57,7 +259,7 @@ class UnderpostSSH {
|
|
|
57
259
|
* @returns {boolean} True if user exists, false otherwise
|
|
58
260
|
*/
|
|
59
261
|
checkUserExists: (username) => {
|
|
60
|
-
const result = shellExec(`id -u ${username} 2>/dev/null || echo "not_found"`, {
|
|
262
|
+
const result = shellExec(`id -u ${shellArgumentFactory(username)} 2>/dev/null || echo "not_found"`, {
|
|
61
263
|
silent: true,
|
|
62
264
|
stdout: true,
|
|
63
265
|
}).trim();
|
|
@@ -73,7 +275,7 @@ class UnderpostSSH {
|
|
|
73
275
|
* @returns {string} User's home directory path
|
|
74
276
|
*/
|
|
75
277
|
getUserHome: (username) => {
|
|
76
|
-
return shellExec(`getent passwd ${username} | cut -d: -f6`, {
|
|
278
|
+
return shellExec(`getent passwd ${shellArgumentFactory(username)} | cut -d: -f6`, {
|
|
77
279
|
silent: true,
|
|
78
280
|
stdout: true,
|
|
79
281
|
}).trim();
|
|
@@ -90,11 +292,13 @@ class UnderpostSSH {
|
|
|
90
292
|
* @returns {void}
|
|
91
293
|
*/
|
|
92
294
|
createSystemUser: (username, password, groups) => {
|
|
93
|
-
|
|
94
|
-
shellExec(`
|
|
295
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid system username');
|
|
296
|
+
shellExec(`useradd -m -s /bin/bash ${shellArgumentFactory(username)}`);
|
|
297
|
+
shellExec(`printf '%s\n' ${shellArgumentFactory(`${username}:${password}`)} | chpasswd`, { disableLog: true });
|
|
95
298
|
if (groups) {
|
|
96
299
|
for (const group of groups.split(',').map((g) => g.trim())) {
|
|
97
|
-
|
|
300
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(group)) throw new TypeError('Invalid system group');
|
|
301
|
+
shellExec(`usermod -aG ${shellArgumentFactory(group)} ${shellArgumentFactory(username)}`);
|
|
98
302
|
}
|
|
99
303
|
}
|
|
100
304
|
},
|
|
@@ -109,8 +313,8 @@ class UnderpostSSH {
|
|
|
109
313
|
*/
|
|
110
314
|
ensureSSHDirectory: (sshDir) => {
|
|
111
315
|
if (!fs.existsSync(sshDir)) {
|
|
112
|
-
shellExec(`mkdir -p ${sshDir}`);
|
|
113
|
-
shellExec(`chmod 700 ${sshDir}`);
|
|
316
|
+
shellExec(`mkdir -p ${shellArgumentFactory(sshDir)}`);
|
|
317
|
+
shellExec(`chmod 700 ${shellArgumentFactory(sshDir)}`);
|
|
114
318
|
}
|
|
115
319
|
},
|
|
116
320
|
|
|
@@ -126,11 +330,12 @@ class UnderpostSSH {
|
|
|
126
330
|
* @returns {void}
|
|
127
331
|
*/
|
|
128
332
|
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}`);
|
|
333
|
+
shellExec(`chmod 600 ${shellArgumentFactory(`${sshDir}/authorized_keys`)}`);
|
|
334
|
+
shellExec(`chmod 644 ${shellArgumentFactory(`${sshDir}/known_hosts`)}`);
|
|
335
|
+
if (keyPath) shellExec(`chmod 600 ${shellArgumentFactory(keyPath)}`);
|
|
336
|
+
if (pubKeyPath) shellExec(`chmod 644 ${shellArgumentFactory(pubKeyPath)}`);
|
|
337
|
+
shellExec(`chown -R ${shellArgumentFactory(`${username}:${username}`)} ${shellArgumentFactory(sshDir)}`);
|
|
338
|
+
runSELinuxCommands(selinuxSshContextCommandsFactory({ sshDirectory: sshDir }), { execute: shellExec });
|
|
134
339
|
},
|
|
135
340
|
|
|
136
341
|
/**
|
|
@@ -140,17 +345,16 @@ class UnderpostSSH {
|
|
|
140
345
|
* @memberof UnderpostSSH
|
|
141
346
|
* @param {string} sshDir - SSH directory path
|
|
142
347
|
* @param {string} pubKeyPath - Public key file path
|
|
143
|
-
* @param {boolean}
|
|
348
|
+
* @param {boolean} restrictForwarding - Whether to add no-forwarding restrictions
|
|
144
349
|
* @returns {void}
|
|
145
350
|
*/
|
|
146
|
-
configureAuthorizedKeys: (sshDir, pubKeyPath,
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
351
|
+
configureAuthorizedKeys: (sshDir, pubKeyPath, restrictForwarding) => {
|
|
352
|
+
const key = fs.readFileSync(pubKeyPath, 'utf8').trim();
|
|
353
|
+
const entry = restrictForwarding ? `no-port-forwarding,no-X11-forwarding,no-agent-forwarding ${key}` : key;
|
|
354
|
+
const authorizedKeysPath = `${sshDir}/authorized_keys`;
|
|
355
|
+
shellExec(
|
|
356
|
+
`grep -qxF ${shellArgumentFactory(entry)} ${shellArgumentFactory(authorizedKeysPath)} || printf '%s\n' ${shellArgumentFactory(entry)} >> ${shellArgumentFactory(authorizedKeysPath)}`,
|
|
357
|
+
);
|
|
154
358
|
},
|
|
155
359
|
|
|
156
360
|
/**
|
|
@@ -164,27 +368,94 @@ EOF`);
|
|
|
164
368
|
* @returns {void}
|
|
165
369
|
*/
|
|
166
370
|
configureKnownHosts: (sshDir, port, host) => {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
371
|
+
const sshPort = Number(port);
|
|
372
|
+
if (!Number.isInteger(sshPort) || sshPort < 1 || sshPort > 65535)
|
|
373
|
+
throw new RangeError('SSH port must be 1-65535');
|
|
374
|
+
const knownHostsPath = shellArgumentFactory(`${sshDir}/known_hosts`);
|
|
375
|
+
shellExec(`ssh-keyscan -p ${sshPort} -H localhost >> ${knownHostsPath}`);
|
|
376
|
+
shellExec(`ssh-keyscan -p ${sshPort} -H 127.0.0.1 >> ${knownHostsPath}`);
|
|
377
|
+
if (host) shellExec(`ssh-keyscan -p ${sshPort} -H ${shellArgumentFactory(host)} >> ${knownHostsPath}`);
|
|
170
378
|
},
|
|
171
379
|
|
|
172
380
|
/**
|
|
173
|
-
*
|
|
381
|
+
* Grants passwordless sudo to a key-only account, or sets the user password.
|
|
174
382
|
* @method
|
|
175
383
|
* @function configureSudoAccess
|
|
176
384
|
* @memberof UnderpostSSH
|
|
177
385
|
* @param {string} username - Username to configure
|
|
178
|
-
* @param {string} password - User password
|
|
179
|
-
* @param {boolean} disablePassword - Whether to enable passwordless sudo
|
|
386
|
+
* @param {string} password - User password; empty means key-only
|
|
180
387
|
* @returns {void}
|
|
181
388
|
*/
|
|
182
|
-
configureSudoAccess: (username, password
|
|
183
|
-
if (
|
|
184
|
-
|
|
389
|
+
configureSudoAccess: (username, password) => {
|
|
390
|
+
if (!password) {
|
|
391
|
+
if (!/^[a-z_][a-z0-9_-]*[$]?$/i.test(username)) throw new TypeError('Invalid sudo username');
|
|
392
|
+
const sudoersPath = `/etc/sudoers.d/90_${username}`;
|
|
393
|
+
const temporaryPath = `/tmp/underpost-sudoers-${process.pid}-${username}`;
|
|
394
|
+
const rule = `${username} ALL=(ALL) NOPASSWD: ALL`;
|
|
395
|
+
try {
|
|
396
|
+
shellExec(`printf '%s\n' ${shellArgumentFactory(rule)} > ${shellArgumentFactory(temporaryPath)}`);
|
|
397
|
+
shellExec(`sudo visudo -cf ${shellArgumentFactory(temporaryPath)}`);
|
|
398
|
+
shellExec(
|
|
399
|
+
`sudo install -m 0440 -o root -g root ${shellArgumentFactory(temporaryPath)} ${shellArgumentFactory(sudoersPath)}`,
|
|
400
|
+
);
|
|
401
|
+
runSELinuxCommands([selinuxRestoreconCommandFactory(sudoersPath, { recursive: false })], {
|
|
402
|
+
execute: shellExec,
|
|
403
|
+
});
|
|
404
|
+
} finally {
|
|
405
|
+
fs.removeSync(temporaryPath);
|
|
406
|
+
}
|
|
185
407
|
} else {
|
|
186
|
-
shellExec(`
|
|
408
|
+
shellExec(`printf '%s\n' ${shellArgumentFactory(`${username}:${password}`)} | sudo chpasswd`, {
|
|
409
|
+
disableLog: true,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* Initializes the default SSH files and credentials for a local SSH service.
|
|
416
|
+
* @method
|
|
417
|
+
* @function initializeDefaultSshConfig
|
|
418
|
+
* @memberof UnderpostSSH
|
|
419
|
+
* @param {object} params - SSH configuration values.
|
|
420
|
+
* @param {string} params.user - SSH user name.
|
|
421
|
+
* @param {string} params.password - SSH key passphrase and user password; empty means key-only.
|
|
422
|
+
* @param {string} params.host - Host used in the SSH key comment.
|
|
423
|
+
* @param {number} params.port - SSH port.
|
|
424
|
+
* @param {string} [params.controllerPubKeyPath='./engine-private/deploy/id_rsa.pub'] - Preferred controller public key to authorize.
|
|
425
|
+
* @returns {void}
|
|
426
|
+
*/
|
|
427
|
+
initializeDefaultSshConfig: ({
|
|
428
|
+
user,
|
|
429
|
+
password,
|
|
430
|
+
host,
|
|
431
|
+
port,
|
|
432
|
+
controllerPubKeyPath = './engine-private/deploy/id_rsa.pub',
|
|
433
|
+
}) => {
|
|
434
|
+
const sshDir = `${Underpost.ssh.getUserHome(user)}/.ssh`;
|
|
435
|
+
const keyPath = `${sshDir}/id_rsa`;
|
|
436
|
+
const pubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
437
|
+
|
|
438
|
+
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
439
|
+
fs.ensureFileSync(`${sshDir}/authorized_keys`);
|
|
440
|
+
fs.ensureFileSync(`${sshDir}/known_hosts`);
|
|
441
|
+
|
|
442
|
+
if (!fs.existsSync(keyPath)) {
|
|
443
|
+
shellExec(
|
|
444
|
+
`ssh-keygen -t ed25519 -f ${shellArgumentFactory(keyPath)} -N ${shellArgumentFactory(password)} -q -C ${shellArgumentFactory(`${user}@${host}`)}`,
|
|
445
|
+
{ disableLog: true },
|
|
446
|
+
);
|
|
187
447
|
}
|
|
448
|
+
if (!fs.existsSync(pubKeyPath))
|
|
449
|
+
shellExec(
|
|
450
|
+
`ssh-keygen -y -f ${shellArgumentFactory(keyPath)} -P ${shellArgumentFactory(password)} > ${shellArgumentFactory(pubKeyPath)}`,
|
|
451
|
+
{ disableLog: true },
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
const authorizedKeyPath = fs.existsSync(controllerPubKeyPath) ? controllerPubKeyPath : pubKeyPath;
|
|
455
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, authorizedKeyPath, !password);
|
|
456
|
+
Underpost.ssh.configureKnownHosts(sshDir, port, host);
|
|
457
|
+
Underpost.ssh.configureSudoAccess(user, password);
|
|
458
|
+
Underpost.ssh.setSSHFilePermissions(sshDir, user, keyPath, pubKeyPath);
|
|
188
459
|
},
|
|
189
460
|
|
|
190
461
|
/**
|
|
@@ -193,11 +464,10 @@ EOF`);
|
|
|
193
464
|
* @function callback
|
|
194
465
|
* @memberof UnderpostSSH
|
|
195
466
|
* @param {Object} options - Configuration options for SSH operations
|
|
196
|
-
* @param {string} [options.deployId=''] - Deployment ID context for SSH operations
|
|
197
467
|
* @param {boolean} [options.generate=false] - Generate new SSH credentials
|
|
198
468
|
* @param {string} [options.user=''] - SSH user name (defaults to 'root')
|
|
199
|
-
* @param {string} [options.password=''] - SSH user password (
|
|
200
|
-
* @param {string} [options.host=''] - SSH host address (
|
|
469
|
+
* @param {string} [options.password=''] - SSH user password (taken from the registry, else generated unless --disable-password)
|
|
470
|
+
* @param {string} [options.host=''] - SSH host address (taken from the registry, else the public IP)
|
|
201
471
|
* @param {string} [options.filter=''] - Filter for user/group listings
|
|
202
472
|
* @param {string} [options.groups=''] - Comma-separated list of groups for the user (defaults to 'wheel')
|
|
203
473
|
* @param {number} [options.port=22] - SSH port number
|
|
@@ -208,24 +478,23 @@ EOF`);
|
|
|
208
478
|
* @param {boolean} [options.reset=false] - Reset SSH configuration (clear authorized_keys and known_hosts)
|
|
209
479
|
* @param {boolean} [options.keysList=false] - List authorized SSH keys
|
|
210
480
|
* @param {boolean} [options.hostsList=false] - List known SSH hosts
|
|
211
|
-
* @param {boolean} [options.disablePassword=false] -
|
|
481
|
+
* @param {boolean} [options.disablePassword=false] - Create the account key-only instead of generating a password
|
|
212
482
|
* @param {boolean} [options.keyTest=false] - Test SSH key generation
|
|
213
483
|
* @param {boolean} [options.stop=false] - Stop SSH service
|
|
214
484
|
* @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
|
|
217
485
|
* @returns {Promise<void>}
|
|
218
486
|
* @description
|
|
219
|
-
* Handles
|
|
487
|
+
* Handles SSH operations against the cluster users registry
|
|
488
|
+
* (`engine-private/deploy/conf.users.json`) and key store
|
|
489
|
+
* (`engine-private/deploy/users/<user>`):
|
|
220
490
|
* - User creation with automatic key generation and backup
|
|
221
491
|
* - User removal with key cleanup
|
|
222
|
-
* - Key import/export between SSH directory and
|
|
492
|
+
* - Key import/export between SSH directory and the key store
|
|
223
493
|
* - SSH service initialization and hardening
|
|
224
494
|
* - User and group listing with optional filtering
|
|
225
495
|
*/
|
|
226
496
|
callback: async (
|
|
227
497
|
options = {
|
|
228
|
-
deployId: '',
|
|
229
498
|
generate: false,
|
|
230
499
|
user: '',
|
|
231
500
|
password: '',
|
|
@@ -244,62 +513,49 @@ EOF`);
|
|
|
244
513
|
keyTest: false,
|
|
245
514
|
stop: false,
|
|
246
515
|
status: false,
|
|
247
|
-
connectUri: false,
|
|
248
|
-
copy: false,
|
|
249
516
|
},
|
|
250
517
|
) => {
|
|
251
|
-
let confNode, confNodePath;
|
|
252
|
-
|
|
253
|
-
// Set defaults
|
|
254
518
|
if (!options.user) options.user = 'root';
|
|
255
|
-
if (!options.host) options.host = await Underpost.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 = Underpost.ssh.getUserHome(options.user);
|
|
261
|
-
options.userHome = userHome;
|
|
262
519
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
520
|
+
const users = Underpost.ssh.loadUsers();
|
|
521
|
+
const confUserIndex = users.findIndex((entry) => entry.user === options.user);
|
|
522
|
+
const confUser = users[confUserIndex];
|
|
523
|
+
const registeredHosts = confUser?.hosts || [];
|
|
524
|
+
|
|
525
|
+
// Explicit flags win; the registry fills in what was omitted, but only for
|
|
526
|
+
// the operations that act on one host. A single registered host is
|
|
527
|
+
// unambiguous and several are not — an account on many spokes must be told
|
|
528
|
+
// which one is meant rather than acting on whichever the registry lists
|
|
529
|
+
// first. Listing and removal are account-scoped and need no host at all.
|
|
530
|
+
const hostScoped = options.userAdd || options.start || options.keyTest || options.hostsList;
|
|
531
|
+
if (!options.host && hostScoped) {
|
|
532
|
+
if (registeredHosts.length > 1)
|
|
533
|
+
throw new Error(
|
|
534
|
+
`[ssh] user '${options.user}' is registered for several hosts ` +
|
|
535
|
+
`(${registeredHosts.map((entry) => entry.host).join(', ')}); pass --host to select one`,
|
|
536
|
+
);
|
|
537
|
+
options.host = registeredHosts[0]?.host || (await Underpost.dns.getPublicIp());
|
|
538
|
+
}
|
|
539
|
+
if (!options.port)
|
|
540
|
+
options.port =
|
|
541
|
+
registeredHosts.find((entry) => entry.host === hostNameFactory(options.host))?.port || DEFAULT_SSH_PORT;
|
|
542
|
+
if (!options.groups) options.groups = 'wheel';
|
|
543
|
+
if (!options.password) {
|
|
544
|
+
const generatePassword = !confUser && !options.disablePassword;
|
|
545
|
+
options.password = generatePassword ? generateRandomPasswordSelection(16) : confUser?.password || '';
|
|
285
546
|
}
|
|
286
547
|
|
|
287
|
-
|
|
548
|
+
const keyDir = Underpost.ssh.userKeyDir(options.user);
|
|
549
|
+
const keyPath = confUser?.keyPath || `${keyDir}/id_rsa`;
|
|
550
|
+
const pubKeyPath = confUser?.pubKeyPath || `${keyDir}/id_rsa.pub`;
|
|
551
|
+
const sshDirFactory = () => `${Underpost.ssh.getUserHome(options.user)}/.ssh`;
|
|
288
552
|
|
|
289
|
-
|
|
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
|
-
}
|
|
553
|
+
logger.info('options', options);
|
|
298
554
|
|
|
299
|
-
// Handle reset operation
|
|
300
555
|
if (options.reset) {
|
|
301
|
-
|
|
302
|
-
shellExec(`> ${
|
|
556
|
+
const sshDir = sshDirFactory();
|
|
557
|
+
shellExec(`> ${sshDir}/authorized_keys`);
|
|
558
|
+
shellExec(`> ${sshDir}/known_hosts`);
|
|
303
559
|
}
|
|
304
560
|
|
|
305
561
|
if (options.userLs) {
|
|
@@ -318,36 +574,30 @@ EOF`);
|
|
|
318
574
|
console.log('Users'.bold.blue);
|
|
319
575
|
console.log(`user : x : UID : GID : GECOS : home_dir : shell`.blue);
|
|
320
576
|
console.log(filter ? usersOut.replaceAll(filter, filter.red) : usersOut);
|
|
577
|
+
console.log('Registered cluster users'.bold.blue);
|
|
578
|
+
users.forEach((entry) =>
|
|
579
|
+
entry.hosts.forEach((connection) => console.log(`${entry.user}@${connection.host}:${connection.port}`)),
|
|
580
|
+
);
|
|
321
581
|
}
|
|
322
582
|
|
|
323
|
-
// Handle user removal (works with or without deployId)
|
|
324
583
|
if (options.userRemove) {
|
|
325
584
|
const groups = shellExec(`id -Gn ${options.user}`, { silent: true, stdout: true }).trim().replace(/ /g, ', ');
|
|
326
585
|
shellExec(`userdel -r ${options.user}`);
|
|
327
586
|
|
|
328
|
-
// Remove sudoers file if it exists
|
|
329
587
|
const sudoersFile = `/etc/sudoers.d/90_${options.user}`;
|
|
330
588
|
if (fs.existsSync(sudoersFile)) {
|
|
331
589
|
shellExec(`sudo rm -f ${sudoersFile}`);
|
|
332
590
|
logger.info(`Sudoers file removed: ${sudoersFile}`);
|
|
333
591
|
}
|
|
334
592
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
}
|
|
593
|
+
if (fs.existsSync(keyDir)) {
|
|
594
|
+
fs.removeSync(keyDir);
|
|
595
|
+
logger.info(`Key store removed from ${keyDir}`);
|
|
596
|
+
}
|
|
348
597
|
|
|
349
|
-
|
|
350
|
-
|
|
598
|
+
if (confUser) {
|
|
599
|
+
users.splice(confUserIndex, 1);
|
|
600
|
+
Underpost.ssh.saveUsers(users);
|
|
351
601
|
}
|
|
352
602
|
|
|
353
603
|
logger.info(`User removed`);
|
|
@@ -355,145 +605,114 @@ EOF`);
|
|
|
355
605
|
return;
|
|
356
606
|
}
|
|
357
607
|
|
|
358
|
-
// Handle user addition (works with or without deployId)
|
|
359
608
|
if (options.userAdd) {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
// If deployId is provided, check for existing config and backup keys
|
|
363
|
-
if (options.deployId) {
|
|
364
|
-
if (!confNode) {
|
|
365
|
-
const config = Underpost.ssh.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 = Underpost.ssh.checkUserExists(options.user);
|
|
382
|
-
if (!userExists) {
|
|
383
|
-
Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const userHome = Underpost.ssh.getUserHome(options.user);
|
|
387
|
-
const sshDir = `${userHome}/.ssh`;
|
|
388
|
-
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
609
|
+
if (!Underpost.ssh.checkUserExists(options.user))
|
|
610
|
+
Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
|
|
389
611
|
|
|
390
|
-
|
|
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
|
-
Underpost.ssh.configureAuthorizedKeys(sshDir, userPubKeyPath, options.disablePassword);
|
|
398
|
-
Underpost.ssh.configureSudoAccess(options.user, options.password, options.disablePassword);
|
|
399
|
-
Underpost.ssh.configureKnownHosts(sshDir, options.port, options.host);
|
|
400
|
-
Underpost.ssh.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
|
-
Underpost.ssh.createSystemUser(options.user, options.password, options.groups);
|
|
410
|
-
|
|
411
|
-
const userHome = Underpost.ssh.getUserHome(options.user);
|
|
412
|
-
const sshDir = `${userHome}/.ssh`;
|
|
612
|
+
const sshDir = sshDirFactory();
|
|
413
613
|
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
614
|
+
const hostKeyPath = `${sshDir}/id_rsa`;
|
|
615
|
+
const hostPubKeyPath = `${sshDir}/id_rsa.pub`;
|
|
616
|
+
|
|
617
|
+
// A key already trusted cluster wide is reinstalled, never re-issued:
|
|
618
|
+
// regenerating it would invalidate every host that already holds it.
|
|
619
|
+
const storedKeys = fs.existsSync(keyPath) && fs.existsSync(pubKeyPath);
|
|
620
|
+
if (storedKeys) {
|
|
621
|
+
fs.copyFileSync(keyPath, hostKeyPath);
|
|
622
|
+
fs.copyFileSync(pubKeyPath, hostPubKeyPath);
|
|
623
|
+
logger.info(`Keys imported from ${keyDir} to ${sshDir}`);
|
|
624
|
+
} else if (!fs.existsSync(hostKeyPath)) {
|
|
419
625
|
shellExec(
|
|
420
|
-
`ssh-keygen -t ed25519 -f ${
|
|
626
|
+
`ssh-keygen -t ed25519 -f ${hostKeyPath} -N "${options.password}" -q -C "${options.user}@${options.host}"`,
|
|
627
|
+
{ disableLog: true },
|
|
421
628
|
);
|
|
629
|
+
} else if (!fs.existsSync(hostPubKeyPath)) {
|
|
630
|
+
shellExec(`ssh-keygen -y -f ${hostKeyPath} -P "${options.password}" > ${hostPubKeyPath}`, {
|
|
631
|
+
disableLog: true,
|
|
632
|
+
});
|
|
422
633
|
}
|
|
423
634
|
|
|
424
|
-
Underpost.ssh.configureAuthorizedKeys(sshDir,
|
|
425
|
-
Underpost.ssh.configureSudoAccess(options.user, options.password
|
|
635
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, hostPubKeyPath, !options.password);
|
|
636
|
+
Underpost.ssh.configureSudoAccess(options.user, options.password);
|
|
426
637
|
Underpost.ssh.configureKnownHosts(sshDir, options.port, options.host);
|
|
427
|
-
Underpost.ssh.setSSHFilePermissions(sshDir, options.user,
|
|
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
|
-
Underpost.ssh.saveConfigNode(confNodePath, confNode);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
logger.info(`User added`);
|
|
454
|
-
return;
|
|
455
|
-
}
|
|
638
|
+
Underpost.ssh.setSSHFilePermissions(sshDir, options.user, hostKeyPath, hostPubKeyPath);
|
|
456
639
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
confNodePath = config.confNodePath;
|
|
640
|
+
if (!storedKeys) {
|
|
641
|
+
fs.ensureDirSync(keyDir);
|
|
642
|
+
fs.copyFileSync(hostKeyPath, keyPath);
|
|
643
|
+
fs.copyFileSync(hostPubKeyPath, pubKeyPath);
|
|
644
|
+
logger.info(`Keys backed up to ${keyDir}`);
|
|
463
645
|
}
|
|
464
646
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
647
|
+
// Additive: registering the account for another host adds that host,
|
|
648
|
+
// it does not replace the record. One operator account across several
|
|
649
|
+
// spokes is the normal case, and replacing here is what previously left
|
|
650
|
+
// remediation unable to resolve a spoke that had just been registered.
|
|
651
|
+
const record = {
|
|
652
|
+
...(confUser || {}),
|
|
653
|
+
user: options.user,
|
|
654
|
+
password: options.password,
|
|
655
|
+
groups: options.groups,
|
|
656
|
+
keyPath,
|
|
657
|
+
pubKeyPath,
|
|
658
|
+
hosts: upsertConnection(registeredHosts, { host: options.host, port: options.port }),
|
|
659
|
+
};
|
|
660
|
+
if (confUser) users[confUserIndex] = record;
|
|
661
|
+
else users.push(record);
|
|
662
|
+
Underpost.ssh.saveUsers(users);
|
|
663
|
+
|
|
664
|
+
logger.info(`User added`, { user: options.user, hosts: record.hosts });
|
|
665
|
+
return;
|
|
471
666
|
}
|
|
472
667
|
|
|
473
|
-
// Handle generate root keys
|
|
474
668
|
if (options.generate)
|
|
475
669
|
Underpost.ssh.generateKeys({ user: options.user, password: options.password, host: options.host });
|
|
476
670
|
|
|
477
|
-
|
|
478
|
-
if (options.
|
|
479
|
-
if (options.hostsList) shellExec(`cat ${userHome}/.ssh/known_hosts`);
|
|
671
|
+
if (options.keysList) shellExec(`cat ${sshDirFactory()}/authorized_keys`);
|
|
672
|
+
if (options.hostsList) shellExec(`cat ${sshDirFactory()}/known_hosts`);
|
|
480
673
|
|
|
481
|
-
|
|
482
|
-
if (options.keyTest) {
|
|
483
|
-
const keyPath = `${userHome}/.ssh/id_rsa`;
|
|
484
|
-
shellExec(`ssh-keygen -y -f ${keyPath} -P "${options.password}"`);
|
|
485
|
-
}
|
|
674
|
+
if (options.keyTest) shellExec(`ssh-keygen -y -f ${keyPath} -P "${options.password}"`);
|
|
486
675
|
|
|
487
|
-
// Handle stop server
|
|
488
676
|
if (options.stop) shellExec('service sshd stop');
|
|
489
677
|
|
|
490
|
-
// Handle start server
|
|
491
678
|
if (options.start) {
|
|
492
|
-
|
|
679
|
+
if (!confUser) {
|
|
680
|
+
Underpost.ssh.initializeDefaultSshConfig({
|
|
681
|
+
user: options.user,
|
|
682
|
+
password: options.password,
|
|
683
|
+
host: options.host,
|
|
684
|
+
port: options.port,
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
// Hardening sshd on a host where the account does not exist produces a
|
|
688
|
+
// daemon that starts cleanly and refuses the only user it was meant to
|
|
689
|
+
// admit. Nothing downstream reports that, so the run has to.
|
|
690
|
+
if (!Underpost.ssh.checkUserExists(options.user))
|
|
691
|
+
throw new Error(
|
|
692
|
+
`[ssh] user '${options.user}' does not exist on this host; ` +
|
|
693
|
+
`run: node bin ssh --user ${options.user} --user-add`,
|
|
694
|
+
);
|
|
695
|
+
// The registry records the key pair this cluster authenticates with, so
|
|
696
|
+
// a box whose authorized_keys was lost converges here rather than
|
|
697
|
+
// needing the key re-issued — which would invalidate every other host
|
|
698
|
+
// holding it.
|
|
699
|
+
if (fs.existsSync(pubKeyPath)) {
|
|
700
|
+
const sshDir = sshDirFactory();
|
|
701
|
+
Underpost.ssh.ensureSSHDirectory(sshDir);
|
|
702
|
+
fs.ensureFileSync(`${sshDir}/authorized_keys`);
|
|
703
|
+
Underpost.ssh.configureAuthorizedKeys(sshDir, pubKeyPath, !options.password);
|
|
704
|
+
logger.info(`Authorized key present for ${options.user}`, { source: pubKeyPath });
|
|
705
|
+
} else {
|
|
706
|
+
logger.warn('Registered public key is missing; authorized_keys left untouched', {
|
|
707
|
+
user: options.user,
|
|
708
|
+
expected: pubKeyPath,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
Underpost.ssh.chmod({ user: options.user });
|
|
712
|
+
}
|
|
493
713
|
Underpost.ssh.initService({ port: options.port });
|
|
494
714
|
}
|
|
495
715
|
|
|
496
|
-
// Handle status server
|
|
497
716
|
if (options.status) shellExec('service sshd status');
|
|
498
717
|
},
|
|
499
718
|
|
|
@@ -515,9 +734,13 @@ EOF`);
|
|
|
515
734
|
* @param {string} params.remoteDir - Destination directory on the node.
|
|
516
735
|
* @param {number} [params.port=22] - SSH port.
|
|
517
736
|
* @param {string} [params.user='root'] - SSH user (key-only).
|
|
518
|
-
* @param {string} [params.keyPath
|
|
737
|
+
* @param {string} [params.keyPath] - Private key path; resolved by {@link sshKeyPathFactory} when empty.
|
|
519
738
|
* @param {string} [params.owner='1000:1000'] - chown target on the node (empty to skip).
|
|
520
739
|
* @param {string} [params.mode='755'] - chmod mode on the node (empty to skip).
|
|
740
|
+
* @param {boolean} [params.containerStorage=false] - Give the destination tree the persistent
|
|
741
|
+
* `container_file_t` mapping on the node. Required for a hostPath volume: the label has to
|
|
742
|
+
* exist on the node that actually mounts it, and a directory created here by `mkdir -p`
|
|
743
|
+
* otherwise inherits whatever the parent carries — which no unprivileged pod can read.
|
|
521
744
|
* @returns {void}
|
|
522
745
|
*/
|
|
523
746
|
copyDirToNode: ({
|
|
@@ -526,16 +749,18 @@ EOF`);
|
|
|
526
749
|
remoteDir,
|
|
527
750
|
port = 22,
|
|
528
751
|
user = 'root',
|
|
529
|
-
keyPath = '
|
|
752
|
+
keyPath = '',
|
|
530
753
|
owner = '1000:1000',
|
|
531
754
|
mode = '755',
|
|
755
|
+
containerStorage = false,
|
|
532
756
|
}) => {
|
|
533
757
|
if (!host) throw new Error('copyDirToNode requires a host');
|
|
534
758
|
if (!localDir || !fs.existsSync(localDir)) throw new Error(`copyDirToNode: local dir not found: ${localDir}`);
|
|
535
759
|
if (!remoteDir) throw new Error('copyDirToNode requires a remoteDir');
|
|
760
|
+
const key = sshKeyPathFactory(keyPath);
|
|
536
761
|
try {
|
|
537
|
-
shellExec(`chmod 600 ${
|
|
538
|
-
const sshOpts = `-i ${
|
|
762
|
+
shellExec(`chmod 600 ${key}`, { silent: true, silentOnError: true, disableLog: true });
|
|
763
|
+
const sshOpts = `-i ${key} -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p ${port}`;
|
|
539
764
|
shellExec(`ssh ${sshOpts} ${user}@${host} 'mkdir -p ${remoteDir}'`, {
|
|
540
765
|
silent: true,
|
|
541
766
|
disableLog: true,
|
|
@@ -551,12 +776,32 @@ EOF`);
|
|
|
551
776
|
silent: true,
|
|
552
777
|
disableLog: true,
|
|
553
778
|
});
|
|
779
|
+
// After the fixups, so the restorecon pass covers the files tar just extracted. A no-op
|
|
780
|
+
// on a node without SELinux, and it never runs unless the caller asked for it.
|
|
781
|
+
if (containerStorage)
|
|
782
|
+
shellExec(
|
|
783
|
+
`ssh ${sshOpts} ${user}@${host} ${shellArgumentFactory(
|
|
784
|
+
ContainerStorageService.containerStorageSubtreeCommandsFactory(remoteDir, {
|
|
785
|
+
sudo: user !== 'root',
|
|
786
|
+
}).join('; '),
|
|
787
|
+
)}`,
|
|
788
|
+
{ silent: true, disableLog: true },
|
|
789
|
+
);
|
|
554
790
|
} catch (err) {
|
|
555
791
|
logger.error(`copyDirToNode failed`);
|
|
556
792
|
process.exit(1);
|
|
557
793
|
}
|
|
558
794
|
},
|
|
559
795
|
|
|
796
|
+
/**
|
|
797
|
+
* The private key this process authenticates with; see {@link sshKeyPathFactory}.
|
|
798
|
+
* @function keyPathFactory
|
|
799
|
+
* @param {string} [keyPath] - Explicit path, which always wins.
|
|
800
|
+
* @returns {string} Path to the private key.
|
|
801
|
+
* @memberof UnderpostSSH
|
|
802
|
+
*/
|
|
803
|
+
keyPathFactory: sshKeyPathFactory,
|
|
804
|
+
|
|
560
805
|
/**
|
|
561
806
|
* Generic SSH remote command runner that SSH execution logic.
|
|
562
807
|
* Executes arbitrary shell commands on a remote server via SSH with proper credential handling.
|
|
@@ -564,46 +809,68 @@ EOF`);
|
|
|
564
809
|
* @function sshRemoteRunner
|
|
565
810
|
* @param {string} remoteCommand - The command to execute on the remote server
|
|
566
811
|
* @param {Object} options - Configuration options for SSH execution
|
|
567
|
-
* @param {string} [options.
|
|
568
|
-
* @param {string} [options.
|
|
812
|
+
* @param {string} [options.user] - SSH user for cluster credential lookup
|
|
813
|
+
* @param {string} [options.host] - Host to reach; selects the connection when the user has several
|
|
569
814
|
* @param {boolean} [options.dev=false] - Development mode flag
|
|
570
815
|
* @param {string} [options.cd='/home/dd/engine'] - Working directory on remote server
|
|
571
816
|
* @param {boolean} [options.useSudo=true] - Whether to use sudo for command execution
|
|
572
|
-
* @param {boolean} [options.remote=true] - Whether to execute as remote command (if false, runs locally)
|
|
817
|
+
* @param {boolean} [options.remote=true] - Whether to execute as remote command (if false, runs locally in `cd`)
|
|
818
|
+
* @param {boolean} [options.silent=false] - Suppress the command's output; it is still returned to the caller
|
|
573
819
|
* @returns {Promise<string>} Output from the shell execution
|
|
574
820
|
* @memberof UnderpostSSH
|
|
575
821
|
*/
|
|
576
822
|
sshRemoteRunner: async (remoteCommand, options = {}) => {
|
|
577
|
-
const {
|
|
578
|
-
|
|
579
|
-
|
|
823
|
+
const {
|
|
824
|
+
user = '',
|
|
825
|
+
host = '',
|
|
826
|
+
dev = false,
|
|
827
|
+
cd = '/home/dd/engine',
|
|
828
|
+
useSudo = true,
|
|
829
|
+
remote = true,
|
|
830
|
+
silent = false,
|
|
831
|
+
} = options;
|
|
832
|
+
|
|
833
|
+
// Local execution still honours `cd`: the underpost CLI resolves its deploy
|
|
834
|
+
// configuration relative to the working directory, so running it from
|
|
835
|
+
// wherever the caller happened to start would act on a different cluster's
|
|
836
|
+
// config, or none. It honours it only when that directory exists — a checkout
|
|
837
|
+
// that lives elsewhere (a CI container, a developer tree) has no deploy path to
|
|
838
|
+
// enter, and chdir failing there turns a runnable local command into an error
|
|
839
|
+
// reported as a failed remediation.
|
|
840
|
+
// The generated wrapper is transport, not information: echoing it buries
|
|
841
|
+
// whatever the command actually said under a page of boilerplate.
|
|
580
842
|
if (!remote) {
|
|
581
|
-
|
|
843
|
+
const cwd = cd && fs.existsSync(cd) ? cd : '';
|
|
844
|
+
return shellExecRedacted(remoteCommand, { ...(cwd ? { cwd } : {}), silent, disableLog: true });
|
|
582
845
|
}
|
|
583
846
|
|
|
584
|
-
// Set up SSH credentials from config
|
|
585
|
-
if (
|
|
586
|
-
|
|
587
|
-
|
|
847
|
+
// Set up SSH credentials from the cluster config
|
|
848
|
+
if (user) await Underpost.ssh.setDefautlSshCredentials({ user, host });
|
|
849
|
+
|
|
850
|
+
// The store holds the key `setDefautlSshCredentials` just resolved for this target, which is
|
|
851
|
+
// the only per-target answer; a pod has no such key on disk and takes its mount instead.
|
|
852
|
+
const sshKey = fs.existsSync(SECRET_KEY_PATH)
|
|
853
|
+
? shellArgumentFactory(SECRET_KEY_PATH)
|
|
854
|
+
: '$(node bin host get --plain DEFAULT_SSH_KEY_PATH)';
|
|
588
855
|
|
|
589
|
-
// Build the complete SSH command
|
|
590
856
|
const sshScript = `#!/usr/bin/env bash
|
|
591
857
|
set -euo pipefail
|
|
592
858
|
|
|
593
|
-
REMOTE_USER=$(node bin
|
|
594
|
-
REMOTE_HOST=$(node bin
|
|
595
|
-
REMOTE_PORT=$(node bin
|
|
596
|
-
SSH_KEY=$
|
|
859
|
+
REMOTE_USER=$(node bin host get --plain DEFAULT_SSH_USER)
|
|
860
|
+
REMOTE_HOST=$(node bin host get --plain DEFAULT_SSH_HOST)
|
|
861
|
+
REMOTE_PORT=$(node bin host get --plain DEFAULT_SSH_PORT)
|
|
862
|
+
SSH_KEY=${sshKey}
|
|
597
863
|
|
|
598
|
-
|
|
864
|
+
# A projected Secret is read-only, and its mode is already restrictive enough for ssh.
|
|
865
|
+
chmod 600 "$SSH_KEY" 2>/dev/null || true
|
|
599
866
|
|
|
600
|
-
ssh -i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$REMOTE_USER@$REMOTE_HOST" -p $REMOTE_PORT sh <<EOF
|
|
867
|
+
ssh -i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR "$REMOTE_USER@$REMOTE_HOST" -p $REMOTE_PORT sh <<EOF
|
|
601
868
|
${cd ? `cd ${cd}` : ''}
|
|
602
869
|
${useSudo ? `sudo -n -- /bin/bash -lc "${remoteCommand}"` : remoteCommand}
|
|
603
870
|
EOF
|
|
604
871
|
`;
|
|
605
872
|
|
|
606
|
-
return
|
|
873
|
+
return shellExecRedacted(sshScript, { silent, disableLog: true });
|
|
607
874
|
},
|
|
608
875
|
|
|
609
876
|
/**
|
|
@@ -651,7 +918,7 @@ EOF
|
|
|
651
918
|
* @param {string} params.command - Remote command batch to execute.
|
|
652
919
|
* @param {number} [params.port=22] - SSH port.
|
|
653
920
|
* @param {string} [params.user='root'] - SSH user (key-only).
|
|
654
|
-
* @param {string} [params.keyPath] - Private key path
|
|
921
|
+
* @param {string} [params.keyPath] - Private key path; resolved by {@link sshKeyPathFactory} when empty.
|
|
655
922
|
* @param {number} [params.connectTimeoutSec=15] - Per-attempt SSH connect timeout.
|
|
656
923
|
* @param {number} [params.retries=3] - Auth/exec retry attempts.
|
|
657
924
|
* @param {number} [params.retryDelayMs=5000] - Base backoff between retries.
|
|
@@ -663,7 +930,7 @@ EOF
|
|
|
663
930
|
command,
|
|
664
931
|
port = 22,
|
|
665
932
|
user = 'root',
|
|
666
|
-
keyPath = '
|
|
933
|
+
keyPath = '',
|
|
667
934
|
connectTimeoutSec = 15,
|
|
668
935
|
retries = 3,
|
|
669
936
|
retryDelayMs = 5000,
|
|
@@ -677,10 +944,11 @@ EOF
|
|
|
677
944
|
if (!reachable) return { ok: false, code: 255, stdout: '', stderr: 'ssh port unreachable', attempts: 0 };
|
|
678
945
|
}
|
|
679
946
|
|
|
680
|
-
|
|
947
|
+
const key = sshKeyPathFactory(keyPath);
|
|
948
|
+
shellExec(`chmod 600 ${key}`, { silent: true, silentOnError: true, disableLog: true });
|
|
681
949
|
|
|
682
950
|
const sshOpts = [
|
|
683
|
-
`-i ${
|
|
951
|
+
`-i ${key}`,
|
|
684
952
|
`-o BatchMode=yes`,
|
|
685
953
|
`-o PreferredAuthentications=publickey`,
|
|
686
954
|
`-o PubkeyAuthentication=yes`,
|
|
@@ -756,7 +1024,7 @@ EOF
|
|
|
756
1024
|
remotePath = '/tmp/underpost-remote-script.sh',
|
|
757
1025
|
port = 22,
|
|
758
1026
|
user = 'root',
|
|
759
|
-
keyPath = '
|
|
1027
|
+
keyPath = '',
|
|
760
1028
|
retries = 3,
|
|
761
1029
|
waitForPortMs = 0,
|
|
762
1030
|
}) => {
|
|
@@ -779,25 +1047,73 @@ EOF
|
|
|
779
1047
|
},
|
|
780
1048
|
|
|
781
1049
|
/**
|
|
782
|
-
* Loads
|
|
1050
|
+
* Loads a user's SSH credentials for one host and sets them in the host
|
|
1051
|
+
* configuration store.
|
|
1052
|
+
*
|
|
1053
|
+
* `host` is what selects the connection: an account registered for several
|
|
1054
|
+
* hosts has several, and picking one without being told would send a repair
|
|
1055
|
+
* to the wrong machine. Omitting it is only unambiguous for an account with
|
|
1056
|
+
* exactly one registered host.
|
|
1057
|
+
*
|
|
1058
|
+
* A named host is resolved against the registry and nowhere else. The root
|
|
1059
|
+
* env store is ambient — whatever the previous command left there — so
|
|
1060
|
+
* substituting it for a host the registry does not know would silently point
|
|
1061
|
+
* an operation at a different machine, which for remediation means repairing
|
|
1062
|
+
* one host because another was unreachable. Its `DEFAULT_SSH_*` values are
|
|
1063
|
+
* promoted only when no host was named, for a controller that dispatches
|
|
1064
|
+
* work but was never itself provisioned by `--user-add`.
|
|
783
1065
|
* @async
|
|
784
1066
|
* @function setDefautlSshCredentials
|
|
785
1067
|
* @memberof UnderpostSSH
|
|
786
1068
|
* @param {Object} options - Options for setting default SSH credentials
|
|
787
|
-
* @param {string} options.
|
|
788
|
-
* @param {string} options.
|
|
1069
|
+
* @param {string} options.user - SSH user name registered at cluster scope
|
|
1070
|
+
* @param {string} [options.host] - Host to connect to; required when the account has several
|
|
789
1071
|
* @returns {Promise<void>}
|
|
1072
|
+
* @throws {Error} When a named host has no connection registered for that account.
|
|
790
1073
|
*/
|
|
791
|
-
setDefautlSshCredentials: async (options = {
|
|
792
|
-
const
|
|
793
|
-
if (
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
1074
|
+
setDefautlSshCredentials: async (options = { user: '', host: '' }) => {
|
|
1075
|
+
const hosts = Underpost.ssh.userHosts(options.user);
|
|
1076
|
+
if (!options.host && hosts.length > 1)
|
|
1077
|
+
throw new Error(
|
|
1078
|
+
`[ssh] user '${options.user}' is registered for several hosts ` +
|
|
1079
|
+
`(${hosts.map((entry) => entry.host).join(', ')}); the caller must name one`,
|
|
1080
|
+
);
|
|
1081
|
+
|
|
1082
|
+
const registered = Underpost.ssh.resolveConnection({
|
|
1083
|
+
user: options.user,
|
|
1084
|
+
host: options.host || hosts[0]?.host,
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
if (!registered && options.host)
|
|
1088
|
+
throw new Error(
|
|
1089
|
+
`[ssh] no connection is registered for '${options.user}@${options.host}' in ${USERS_CONF_PATH}; ` +
|
|
1090
|
+
`run: node bin ssh --user ${options.user} --host ${options.host} --user-add`,
|
|
1091
|
+
);
|
|
1092
|
+
|
|
1093
|
+
const connection =
|
|
1094
|
+
registered ||
|
|
1095
|
+
(process.env.DEFAULT_SSH_USER && process.env.DEFAULT_SSH_HOST && process.env.DEFAULT_SSH_KEY_PATH
|
|
1096
|
+
? {
|
|
1097
|
+
user: process.env.DEFAULT_SSH_USER,
|
|
1098
|
+
host: process.env.DEFAULT_SSH_HOST,
|
|
1099
|
+
keyPath: process.env.DEFAULT_SSH_KEY_PATH,
|
|
1100
|
+
port: process.env.DEFAULT_SSH_PORT || DEFAULT_SSH_PORT,
|
|
1101
|
+
}
|
|
1102
|
+
: undefined);
|
|
1103
|
+
|
|
1104
|
+
if (!connection) {
|
|
1105
|
+
logger.warn(`No SSH credentials for '${options.user}'`, {
|
|
1106
|
+
host: options.host || '(unspecified)',
|
|
1107
|
+
registry: USERS_CONF_PATH,
|
|
1108
|
+
});
|
|
1109
|
+
return;
|
|
1110
|
+
}
|
|
1111
|
+
if (!registered) logger.warn(`Using the deploy environment DEFAULT_SSH_* values`, { requested: options.user });
|
|
1112
|
+
|
|
1113
|
+
Underpost.host.store.set('DEFAULT_SSH_USER', connection.user);
|
|
1114
|
+
Underpost.host.store.set('DEFAULT_SSH_HOST', connection.host);
|
|
1115
|
+
Underpost.host.store.set('DEFAULT_SSH_KEY_PATH', connection.keyPath);
|
|
1116
|
+
Underpost.host.store.set('DEFAULT_SSH_PORT', connection.port);
|
|
801
1117
|
},
|
|
802
1118
|
|
|
803
1119
|
/**
|
|
@@ -848,12 +1164,26 @@ EOF
|
|
|
848
1164
|
* Sets ownership to specified user for ~/.ssh/ and contents.
|
|
849
1165
|
*/
|
|
850
1166
|
chmod: ({ user }) => {
|
|
851
|
-
|
|
852
|
-
shellExec(`sudo chmod
|
|
853
|
-
shellExec(
|
|
854
|
-
|
|
1167
|
+
const sshDirectory = `${Underpost.ssh.getUserHome(user)}/.ssh`;
|
|
1168
|
+
shellExec(`sudo chmod 700 ${shellArgumentFactory(sshDirectory)}`);
|
|
1169
|
+
shellExec(
|
|
1170
|
+
`if [ -f ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)} ]; then sudo chmod 600 ${shellArgumentFactory(`${sshDirectory}/authorized_keys`)}; fi`,
|
|
1171
|
+
);
|
|
1172
|
+
shellExec(
|
|
1173
|
+
`if [ -f ${shellArgumentFactory(`${sshDirectory}/known_hosts`)} ]; then sudo chmod 644 ${shellArgumentFactory(`${sshDirectory}/known_hosts`)}; fi`,
|
|
1174
|
+
);
|
|
1175
|
+
shellExec(
|
|
1176
|
+
`if [ -f ${shellArgumentFactory(`${sshDirectory}/id_rsa`)} ]; then sudo chmod 600 ${shellArgumentFactory(`${sshDirectory}/id_rsa`)}; fi`,
|
|
1177
|
+
);
|
|
855
1178
|
shellExec(`sudo chmod 600 /etc/ssh/ssh_host_ed25519_key`);
|
|
856
|
-
shellExec(`chown -R ${user}:${user}
|
|
1179
|
+
shellExec(`sudo chown -R ${shellArgumentFactory(`${user}:${user}`)} ${shellArgumentFactory(sshDirectory)}`);
|
|
1180
|
+
runSELinuxCommands(
|
|
1181
|
+
[
|
|
1182
|
+
...selinuxSshContextCommandsFactory({ sshDirectory }),
|
|
1183
|
+
selinuxRestoreconCommandFactory('/etc/ssh/ssh_host_ed25519_key', { recursive: false }),
|
|
1184
|
+
],
|
|
1185
|
+
{ execute: shellExec },
|
|
1186
|
+
);
|
|
857
1187
|
},
|
|
858
1188
|
|
|
859
1189
|
/**
|
|
@@ -866,7 +1196,7 @@ EOF
|
|
|
866
1196
|
* @description
|
|
867
1197
|
* Configures SSH daemon with hardened security settings:
|
|
868
1198
|
* - Disables password authentication (key-only)
|
|
869
|
-
* -
|
|
1199
|
+
* - Allows key-only root login
|
|
870
1200
|
* - Enables ED25519 host key
|
|
871
1201
|
* - Disables X11 forwarding and TCP forwarding
|
|
872
1202
|
* - Sets client alive intervals to prevent ghost connections
|
|
@@ -878,69 +1208,57 @@ EOF
|
|
|
878
1208
|
* - Displays service status with colored output
|
|
879
1209
|
*/
|
|
880
1210
|
initService: ({ port }) => {
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
# ==============================================================
|
|
886
|
-
|
|
887
|
-
# --- Network Settings ---
|
|
888
|
-
Port ${port ? port : '22'}
|
|
889
|
-
# Explicitly listen on all interfaces (IPv4 and IPv6)
|
|
890
|
-
|
|
891
|
-
# --- Host Keys ---
|
|
892
|
-
# ED25519 is the modern standard (Fast & Secure)
|
|
1211
|
+
const sshPort = Number(port || 22);
|
|
1212
|
+
const portCommands = selinuxSshPortCommandsFactory({ port: sshPort });
|
|
1213
|
+
const temporaryPath = `/tmp/underpost-sshd-config-${process.pid}`;
|
|
1214
|
+
const configuration = `Port ${sshPort}
|
|
893
1215
|
HostKey /etc/ssh/ssh_host_ed25519_key
|
|
894
|
-
# RSA is kept for compatibility with older clients (Optional)
|
|
895
|
-
# HostKey /etc/ssh/ssh_host_rsa_key
|
|
896
|
-
|
|
897
|
-
# --- Logging ---
|
|
898
1216
|
SyslogFacility AUTHPRIV
|
|
899
|
-
# VERBOSE logs the key fingerprint used for login (Audit trail)
|
|
900
1217
|
LogLevel VERBOSE
|
|
901
|
-
|
|
902
|
-
# --- Authentication & Security ---
|
|
903
|
-
# STRICTLY KEY-BASED AUTHENTICATION
|
|
904
1218
|
PubkeyAuthentication yes
|
|
905
1219
|
PasswordAuthentication no
|
|
906
|
-
|
|
1220
|
+
KbdInteractiveAuthentication no
|
|
907
1221
|
PermitEmptyPasswords no
|
|
908
|
-
|
|
909
|
-
# PREVENT ROOT LOGIN
|
|
910
|
-
# Administrators should log in as a standard user and use 'sudo'
|
|
911
|
-
PermitRootLogin no
|
|
912
|
-
|
|
913
|
-
# Security checks on ownership of ~/.ssh/ files
|
|
1222
|
+
PermitRootLogin prohibit-password
|
|
914
1223
|
StrictModes yes
|
|
915
1224
|
MaxAuthTries 3
|
|
916
1225
|
LoginGraceTime 60
|
|
917
|
-
|
|
918
|
-
# --- PAM (Pluggable Authentication Modules) ---
|
|
919
|
-
# REQUIRED: Must be 'yes' on RHEL for proper session/SELinux handling.
|
|
920
|
-
# Since PasswordAuthentication is 'no', PAM will not ask for passwords.
|
|
921
1226
|
UsePAM yes
|
|
922
|
-
|
|
923
|
-
# --- Session & Network Health ---
|
|
924
|
-
# Disconnect idle sessions after 5 minutes (300s * 0) to prevent ghost connections
|
|
925
1227
|
ClientAliveInterval 300
|
|
926
1228
|
ClientAliveCountMax 0
|
|
927
|
-
|
|
928
|
-
# --- Feature Restrictions ---
|
|
929
|
-
# Disable GUI forwarding unless explicitly needed
|
|
930
1229
|
X11Forwarding no
|
|
931
|
-
# Disable DNS checks for faster logins (unless you use Host based auth)
|
|
932
1230
|
UseDNS no
|
|
933
|
-
# Disable tunneling unless needed
|
|
934
1231
|
PermitTunnel no
|
|
935
1232
|
AllowTcpForwarding no
|
|
936
|
-
|
|
937
|
-
# --- Subsystem ---
|
|
1233
|
+
Include /etc/ssh/sshd_config.d/*.conf
|
|
938
1234
|
Subsystem sftp /usr/libexec/openssh/sftp-server
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
);
|
|
942
|
-
|
|
943
|
-
|
|
1235
|
+
`;
|
|
1236
|
+
|
|
1237
|
+
fs.writeFileSync(temporaryPath, configuration, { mode: 0o600 });
|
|
1238
|
+
try {
|
|
1239
|
+
if (sshPort !== 22) {
|
|
1240
|
+
shellExec(
|
|
1241
|
+
`if command -v dnf >/dev/null 2>&1 && ! command -v semanage >/dev/null 2>&1; then ${selinuxPackagesCommandFactory()}; fi`,
|
|
1242
|
+
);
|
|
1243
|
+
}
|
|
1244
|
+
runSELinuxCommands(portCommands, { execute: shellExec });
|
|
1245
|
+
shellExec(`sudo ssh-keygen -A`);
|
|
1246
|
+
shellExec(`sudo /usr/sbin/sshd -t -f ${shellArgumentFactory(temporaryPath)}`);
|
|
1247
|
+
shellExec(`sudo install -m 0600 -o root -g root ${shellArgumentFactory(temporaryPath)} /etc/ssh/sshd_config`);
|
|
1248
|
+
runSELinuxCommands(
|
|
1249
|
+
[selinuxRestoreconCommandFactory(['/etc/ssh/sshd_config', '/etc/ssh/ssh_host_ed25519_key'])],
|
|
1250
|
+
{ execute: shellExec },
|
|
1251
|
+
);
|
|
1252
|
+
if (sshPort !== 22) {
|
|
1253
|
+
shellExec(
|
|
1254
|
+
`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`,
|
|
1255
|
+
);
|
|
1256
|
+
}
|
|
1257
|
+
shellExec(`sudo systemctl enable sshd`);
|
|
1258
|
+
shellExec(`sudo systemctl restart sshd`);
|
|
1259
|
+
} finally {
|
|
1260
|
+
fs.removeSync(temporaryPath);
|
|
1261
|
+
}
|
|
944
1262
|
|
|
945
1263
|
const status = shellExec(`sudo systemctl status sshd`, { silent: true, stdout: true });
|
|
946
1264
|
if (status.match('running')) console.log(status.replaceAll(`running`, `running`.green));
|
|
@@ -952,4 +1270,6 @@ EOF`,
|
|
|
952
1270
|
};
|
|
953
1271
|
}
|
|
954
1272
|
|
|
1273
|
+
export { connectionFactory, hostNameFactory, upsertConnection, userRecordFactory };
|
|
1274
|
+
|
|
955
1275
|
export default UnderpostSSH;
|