@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.
Files changed (490) hide show
  1. package/.dockerignore +1 -0
  2. package/.env.example +16 -0
  3. package/.github/workflows/coverall.cyberia.ci.yml +107 -0
  4. package/.github/workflows/cyberia-client.cd.yml +6 -18
  5. package/.github/workflows/cyberia-server.cd.yml +6 -18
  6. package/.github/workflows/docker-image.cyberia-client.ci.yml +8 -5
  7. package/.github/workflows/docker-image.cyberia-client.dev.ci.yml +8 -3
  8. package/.github/workflows/docker-image.cyberia-server.ci.yml +8 -5
  9. package/.github/workflows/docker-image.cyberia-server.dev.ci.yml +8 -5
  10. package/.github/workflows/docker-image.engine-cyberia.ci.yml +15 -1
  11. package/.github/workflows/docker-image.engine-cyberia.dev.ci.yml +15 -1
  12. package/.github/workflows/engine-cyberia.cd.yml +6 -78
  13. package/.github/workflows/engine-cyberia.ci.yml +4 -5
  14. package/.github/workflows/ghpkg.ci.yml +42 -21
  15. package/.github/workflows/hardhat.ci.yml +12 -11
  16. package/.github/workflows/npmpkg.ci.yml +17 -15
  17. package/.github/workflows/pwa-microservices-template-page.cd.yml +1 -16
  18. package/.github/workflows/pwa-microservices-template-test.ci.yml +2 -2
  19. package/.github/workflows/release.cd.yml +2 -9
  20. package/.prettierignore +1 -0
  21. package/AGENTS.md +47 -0
  22. package/CHANGELOG.md +581 -2417
  23. package/CLI-HELP.md +416 -127
  24. package/Dockerfile +25 -9
  25. package/Dockerfile.dev +25 -9
  26. package/Dockerfile.test +4 -4
  27. package/README.md +21 -1
  28. package/bin/build.js +70 -43
  29. package/bin/build.template.js +8 -3
  30. package/bin/cyberia.js +372 -135
  31. package/bin/deploy.js +18 -104
  32. package/bin/index.js +372 -135
  33. package/compose.env +16 -0
  34. package/conf.js +93 -4
  35. package/deploy/cyberia-client/deploy.sh +47 -0
  36. package/deploy/cyberia-client/package.sh +18 -0
  37. package/deploy/cyberia-client/state.sh +14 -0
  38. package/deploy/cyberia-server/deploy.sh +47 -0
  39. package/deploy/cyberia-server/package.sh +18 -0
  40. package/deploy/cyberia-server/state.sh +14 -0
  41. package/deploy/dd-cyberia/deploy.sh +22 -0
  42. package/deploy/dd-cyberia/init.sh +55 -0
  43. package/deploy/dd-cyberia/package.sh +17 -0
  44. package/deploy/dd-cyberia/state.sh +14 -0
  45. package/deploy/dd-cyberia/sync-deploy.sh +116 -0
  46. package/deploy/lib/github-actions-logging.sh +594 -0
  47. package/deploy/lib/host.sh +185 -0
  48. package/deploy/lib/state.sh +62 -0
  49. package/deploy/pwa-microservices-template/deploy.sh +45 -0
  50. package/deploy/release/deploy.sh +32 -0
  51. package/deployment.yaml +7 -2
  52. package/docker-compose.yml +31 -37
  53. package/examples/static-page/README.md +19 -19
  54. package/examples/static-page/ssr-components/CustomPage.js +2 -2
  55. package/hardhat/hardhat.config.js +1 -6
  56. package/hardhat/package-lock.json +104 -43
  57. package/hardhat/package.json +3 -3
  58. package/manifests/alertmanager/deployment.yaml +81 -0
  59. package/manifests/blackbox-exporter/deployment.yaml +75 -0
  60. package/manifests/cronjobs/dd-cron/dd-cron-backup.yaml +36 -7
  61. package/manifests/cronjobs/dd-cron/dd-cron-vultr.yaml +77 -0
  62. package/manifests/deployment/dd-cyberia-development/deployment.yaml +7 -2
  63. package/manifests/deployment/dd-default-development/deployment.yaml +2 -2
  64. package/manifests/grafana/deployment.yaml +26 -7
  65. package/manifests/grafana/kustomization.yaml +1 -0
  66. package/manifests/grafana/service.yaml +6 -1
  67. package/manifests/mariadb/pv.yaml +1 -0
  68. package/manifests/mongodb/statefulset.yaml +9 -2
  69. package/manifests/mysql/pv-pvc.yaml +14 -1
  70. package/manifests/postgresql/pv.yaml +12 -1
  71. package/manifests/postgresql/pvc.yaml +1 -0
  72. package/manifests/prometheus/deployment.yaml +18 -22
  73. package/package.json +83 -10
  74. package/scripts/audit-selinux.sh +64 -0
  75. package/scripts/event-monitor.sh +56 -0
  76. package/scripts/gpu-diag.sh +0 -0
  77. package/scripts/ip-info.sh +0 -0
  78. package/scripts/k3s-node-setup.sh +30 -14
  79. package/scripts/kubeadm-node-setup.sh +40 -31
  80. package/scripts/lxd-vm-setup.sh +0 -0
  81. package/scripts/maas-nat-firewalld.sh +0 -0
  82. package/scripts/maas-setup.sh +8 -8
  83. package/scripts/nat-iptables.sh +2 -0
  84. package/scripts/rhel-grpc-setup.sh +0 -0
  85. package/scripts/rocky-kickstart.sh +25 -9
  86. package/scripts/selinux-normalize.sh +907 -0
  87. package/scripts/shutdown-machine.sh +843 -0
  88. package/scripts/test-monitor.sh +3 -3
  89. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.controller.js +1 -1
  90. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.router.js +2 -2
  91. package/src/api/atlas-sprite-sheet/atlas-sprite-sheet.service.js +2 -2
  92. package/src/api/core/core.controller.js +1 -1
  93. package/src/api/core/core.router.js +2 -2
  94. package/src/api/core/core.service.js +2 -2
  95. package/src/api/crypto/crypto.controller.js +1 -1
  96. package/src/api/crypto/crypto.service.js +1 -1
  97. package/src/api/cyberia-action/cyberia-action.controller.js +1 -1
  98. package/src/api/cyberia-action/cyberia-action.router.js +1 -1
  99. package/src/api/cyberia-action/cyberia-action.service.js +2 -2
  100. package/src/api/cyberia-client-hints/cyberia-client-hints.controller.js +1 -1
  101. package/src/api/cyberia-client-hints/cyberia-client-hints.router.js +1 -1
  102. package/src/api/cyberia-client-hints/cyberia-client-hints.service.js +2 -2
  103. package/src/api/cyberia-dialogue/cyberia-dialogue.controller.js +1 -1
  104. package/src/api/cyberia-dialogue/cyberia-dialogue.router.js +1 -1
  105. package/src/api/cyberia-dialogue/cyberia-dialogue.service.js +2 -2
  106. package/src/api/cyberia-entity/cyberia-entity.controller.js +1 -1
  107. package/src/api/cyberia-entity/cyberia-entity.router.js +1 -1
  108. package/src/api/cyberia-entity/cyberia-entity.service.js +2 -2
  109. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.controller.js +1 -1
  110. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.router.js +1 -1
  111. package/src/api/cyberia-entity-type-default/cyberia-entity-type-default.service.js +2 -2
  112. package/src/api/cyberia-instance/cyberia-fallback-capture.js +466 -0
  113. package/src/api/cyberia-instance/cyberia-fallback-default-items.js +63 -0
  114. package/src/api/cyberia-instance/cyberia-fallback-world.js +16 -1
  115. package/src/api/cyberia-instance/cyberia-instance-map.service.js +1 -1
  116. package/src/api/cyberia-instance/cyberia-instance.controller.js +4 -1
  117. package/src/api/cyberia-instance/cyberia-instance.router.js +15 -2
  118. package/src/api/cyberia-instance/cyberia-instance.service.js +45 -2
  119. package/src/api/cyberia-instance-conf/cyberia-instance-conf.controller.js +1 -1
  120. package/src/api/cyberia-instance-conf/cyberia-instance-conf.model.js +1 -0
  121. package/src/api/cyberia-instance-conf/cyberia-instance-conf.router.js +1 -1
  122. package/src/api/cyberia-instance-conf/cyberia-instance-conf.service.js +2 -2
  123. package/src/api/cyberia-map/cyberia-map.controller.js +1 -1
  124. package/src/api/cyberia-map/cyberia-map.router.js +1 -1
  125. package/src/api/cyberia-map/cyberia-map.service.js +2 -2
  126. package/src/api/cyberia-quest/cyberia-quest.controller.js +1 -1
  127. package/src/api/cyberia-quest/cyberia-quest.router.js +1 -1
  128. package/src/api/cyberia-quest/cyberia-quest.service.js +2 -2
  129. package/src/api/cyberia-quest-progress/cyberia-quest-progress.controller.js +1 -1
  130. package/src/api/cyberia-quest-progress/cyberia-quest-progress.router.js +1 -1
  131. package/src/api/cyberia-quest-progress/cyberia-quest-progress.service.js +2 -2
  132. package/src/api/cyberia-saga/cyberia-saga.controller.js +1 -1
  133. package/src/api/cyberia-saga/cyberia-saga.router.js +1 -1
  134. package/src/api/cyberia-saga/cyberia-saga.service.js +2 -2
  135. package/src/api/cyberia-server-defaults/cyberia-server-defaults.js +16 -29
  136. package/src/api/cyberia-skill/cyberia-skill.controller.js +1 -1
  137. package/src/api/cyberia-skill/cyberia-skill.router.js +1 -1
  138. package/src/api/cyberia-skill/cyberia-skill.service.js +2 -2
  139. package/src/api/default/default.controller.js +1 -1
  140. package/src/api/default/default.router.js +1 -1
  141. package/src/api/default/default.service.js +2 -2
  142. package/src/api/document/document.controller.js +1 -1
  143. package/src/api/document/document.router.js +1 -1
  144. package/src/api/document/document.service.js +2 -2
  145. package/src/api/file/file.controller.js +1 -1
  146. package/src/api/file/file.router.js +1 -1
  147. package/src/api/file/file.service.js +2 -2
  148. package/src/api/instance/instance.controller.js +1 -1
  149. package/src/api/instance/instance.router.js +1 -1
  150. package/src/api/instance/instance.service.js +2 -2
  151. package/src/api/ipfs/ipfs.controller.js +1 -1
  152. package/src/api/ipfs/ipfs.router.js +2 -2
  153. package/src/api/ipfs/ipfs.service.js +2 -2
  154. package/src/api/object-layer/object-layer.controller.js +1 -1
  155. package/src/api/object-layer/object-layer.model.js +204 -3
  156. package/src/api/object-layer/object-layer.router.js +2 -2
  157. package/src/api/object-layer/object-layer.service.js +4 -17
  158. package/src/api/object-layer-render-frames/object-layer-render-frames.controller.js +1 -1
  159. package/src/api/object-layer-render-frames/object-layer-render-frames.router.js +1 -1
  160. package/src/api/object-layer-render-frames/object-layer-render-frames.service.js +2 -2
  161. package/src/api/test/test.controller.js +1 -1
  162. package/src/api/test/test.service.js +1 -1
  163. package/src/api/user/guest.service.js +3 -3
  164. package/src/api/user/user.controller.js +1 -1
  165. package/src/api/user/user.router.js +2 -2
  166. package/src/api/user/user.service.js +4 -4
  167. package/src/api.js +5 -9
  168. package/src/cli/app.js +391 -0
  169. package/src/cli/baremetal.js +30 -36
  170. package/src/cli/client.js +306 -0
  171. package/src/cli/cloud-init.js +3 -3
  172. package/src/cli/cluster.js +242 -135
  173. package/src/cli/db.js +126 -67
  174. package/src/cli/deploy.js +158 -60
  175. package/src/cli/docker-compose.js +5 -5
  176. package/src/cli/domains.js +184 -0
  177. package/src/cli/dotenv-store.js +143 -0
  178. package/src/cli/event.js +2281 -0
  179. package/src/cli/fs.js +3 -3
  180. package/src/cli/host.js +672 -0
  181. package/src/cli/image.js +4 -7
  182. package/src/cli/index.js +426 -134
  183. package/src/cli/ipfs.js +34 -30
  184. package/src/cli/kickstart.js +1 -1
  185. package/src/cli/kubectl.js +218 -41
  186. package/src/cli/lxd.js +3 -3
  187. package/src/cli/monitor.js +1359 -26
  188. package/src/cli/package.js +90 -0
  189. package/src/cli/release.js +73 -34
  190. package/src/cli/repository.js +564 -367
  191. package/src/cli/run.js +362 -654
  192. package/src/cli/secrets.js +2158 -964
  193. package/src/cli/ssh.js +651 -331
  194. package/src/cli/state.js +394 -0
  195. package/src/cli/static.js +2 -2
  196. package/src/cli/system.js +26 -13
  197. package/src/cli/test.js +199 -125
  198. package/src/cli/vultr.js +623 -0
  199. package/src/cli/wireguard.js +2930 -0
  200. package/src/client/components/core/CalendarCore.js +53 -96
  201. package/src/client/components/core/Docs.js +258 -211
  202. package/src/client/components/core/FileExplorer.js +0 -21
  203. package/src/client/components/core/PanelForm.js +4 -4
  204. package/src/client/components/core/Repository.js +56 -0
  205. package/src/client/components/core/Translate.js +7 -0
  206. package/src/client/components/core/{FullScreen.js → ViewModeController.js} +42 -39
  207. package/src/client/components/core/Wallet.js +0 -11
  208. package/src/client/components/core/Worker.js +7 -1
  209. package/src/client/components/cryptokoyn/SettingsCryptokoyn.js +2 -2
  210. package/src/client/components/cyberia/FallbackWorldEngineCyberia.js +368 -0
  211. package/src/client/components/cyberia-portal/AppShellCyberiaPortal.js +35 -1
  212. package/src/client/components/cyberia-portal/RouterCyberiaPortal.js +4 -0
  213. package/src/client/components/cyberia-portal/SettingsCyberiaPortal.js +2 -2
  214. package/src/client/components/cyberia-portal/TranslateCyberiaPortal.js +4 -0
  215. package/src/client/components/default/AppShellDefault.js +1 -1
  216. package/src/client/components/default/SettingsDefault.js +2 -2
  217. package/src/client/components/itemledger/SettingsItemledger.js +2 -2
  218. package/src/client/components/underpost/AppShellUnderpost.js +3 -2
  219. package/src/client/components/underpost/SettingsUnderpost.js +2 -2
  220. package/src/client/public/cyberia-docs/ARCHITECTURE.md +17 -3
  221. package/src/client/public/cyberia-docs/CYBERIA-CLI.md +32 -5
  222. package/src/client/public/cyberia-docs/CYBERIA-SERVER.md +1 -1
  223. package/src/client/public/cyberia-docs/CYBERIA.md +21 -1
  224. package/src/client/public/cyberia-docs/HARDHAT-MODULE.md +23 -6
  225. package/src/client/public/cyberia-docs/ROADMAP.md +1 -1
  226. package/src/client/public/cyberia-docs/WHITE-PAPER.md +1 -1
  227. package/src/client/public/default/android-chrome-144x144.png +0 -0
  228. package/src/client/public/default/android-chrome-192x192.png +0 -0
  229. package/src/client/public/default/android-chrome-256x256.png +0 -0
  230. package/src/client/public/default/android-chrome-36x36.png +0 -0
  231. package/src/client/public/default/android-chrome-384x384.png +0 -0
  232. package/src/client/public/default/android-chrome-48x48.png +0 -0
  233. package/src/client/public/default/android-chrome-512x512.png +0 -0
  234. package/src/client/public/default/android-chrome-72x72.png +0 -0
  235. package/src/client/public/default/android-chrome-96x96.png +0 -0
  236. package/src/client/public/default/apple-touch-icon-1024x1024.png +0 -0
  237. package/src/client/public/default/apple-touch-icon-114x114.png +0 -0
  238. package/src/client/public/default/apple-touch-icon-120x120.png +0 -0
  239. package/src/client/public/default/apple-touch-icon-144x144.png +0 -0
  240. package/src/client/public/default/apple-touch-icon-152x152.png +0 -0
  241. package/src/client/public/default/apple-touch-icon-167x167.png +0 -0
  242. package/src/client/public/default/apple-touch-icon-180x180.png +0 -0
  243. package/src/client/public/default/apple-touch-icon-57x57.png +0 -0
  244. package/src/client/public/default/apple-touch-icon-60x60.png +0 -0
  245. package/src/client/public/default/apple-touch-icon-72x72.png +0 -0
  246. package/src/client/public/default/apple-touch-icon-76x76.png +0 -0
  247. package/src/client/public/default/apple-touch-icon-precomposed.png +0 -0
  248. package/src/client/public/default/apple-touch-icon.png +0 -0
  249. package/src/client/public/default/apple-touch-startup-image-1125x2436.png +0 -0
  250. package/src/client/public/default/apple-touch-startup-image-1136x640.png +0 -0
  251. package/src/client/public/default/apple-touch-startup-image-1170x2532.png +0 -0
  252. package/src/client/public/default/apple-touch-startup-image-1179x2556.png +0 -0
  253. package/src/client/public/default/apple-touch-startup-image-1242x2208.png +0 -0
  254. package/src/client/public/default/apple-touch-startup-image-1242x2688.png +0 -0
  255. package/src/client/public/default/apple-touch-startup-image-1284x2778.png +0 -0
  256. package/src/client/public/default/apple-touch-startup-image-1290x2796.png +0 -0
  257. package/src/client/public/default/apple-touch-startup-image-1334x750.png +0 -0
  258. package/src/client/public/default/apple-touch-startup-image-1488x2266.png +0 -0
  259. package/src/client/public/default/apple-touch-startup-image-1536x2048.png +0 -0
  260. package/src/client/public/default/apple-touch-startup-image-1620x2160.png +0 -0
  261. package/src/client/public/default/apple-touch-startup-image-1640x2160.png +0 -0
  262. package/src/client/public/default/apple-touch-startup-image-1668x2224.png +0 -0
  263. package/src/client/public/default/apple-touch-startup-image-1668x2388.png +0 -0
  264. package/src/client/public/default/apple-touch-startup-image-1792x828.png +0 -0
  265. package/src/client/public/default/apple-touch-startup-image-2048x1536.png +0 -0
  266. package/src/client/public/default/apple-touch-startup-image-2048x2732.png +0 -0
  267. package/src/client/public/default/apple-touch-startup-image-2160x1620.png +0 -0
  268. package/src/client/public/default/apple-touch-startup-image-2160x1640.png +0 -0
  269. package/src/client/public/default/apple-touch-startup-image-2208x1242.png +0 -0
  270. package/src/client/public/default/apple-touch-startup-image-2224x1668.png +0 -0
  271. package/src/client/public/default/apple-touch-startup-image-2266x1488.png +0 -0
  272. package/src/client/public/default/apple-touch-startup-image-2388x1668.png +0 -0
  273. package/src/client/public/default/apple-touch-startup-image-2436x1125.png +0 -0
  274. package/src/client/public/default/apple-touch-startup-image-2532x1170.png +0 -0
  275. package/src/client/public/default/apple-touch-startup-image-2556x1179.png +0 -0
  276. package/src/client/public/default/apple-touch-startup-image-2688x1242.png +0 -0
  277. package/src/client/public/default/apple-touch-startup-image-2732x2048.png +0 -0
  278. package/src/client/public/default/apple-touch-startup-image-2778x1284.png +0 -0
  279. package/src/client/public/default/apple-touch-startup-image-2796x1290.png +0 -0
  280. package/src/client/public/default/apple-touch-startup-image-640x1136.png +0 -0
  281. package/src/client/public/default/apple-touch-startup-image-750x1334.png +0 -0
  282. package/src/client/public/default/apple-touch-startup-image-828x1792.png +0 -0
  283. package/src/client/public/default/assets/background/white.jpg +0 -0
  284. package/src/client/public/default/browserconfig.xml +1 -1
  285. package/src/client/public/default/favicon-16x16.png +0 -0
  286. package/src/client/public/default/favicon-32x32.png +0 -0
  287. package/src/client/public/default/favicon-48x48.png +0 -0
  288. package/src/client/public/default/favicon.ico +0 -0
  289. package/src/client/public/default/manifest.webmanifest +5 -5
  290. package/src/client/public/default/mstile-144x144.png +0 -0
  291. package/src/client/public/default/mstile-150x150.png +0 -0
  292. package/src/client/public/default/mstile-310x150.png +0 -0
  293. package/src/client/public/default/mstile-310x310.png +0 -0
  294. package/src/client/public/default/mstile-70x70.png +0 -0
  295. package/src/client/public/default/yandex-browser-50x50.png +0 -0
  296. package/src/client/public/default/yandex-browser-manifest.json +1 -1
  297. package/src/client/services/cyberia-instance/cyberia-instance.service.js +40 -0
  298. package/src/client/services/object-layer/object-layer.management.js +4 -4
  299. package/src/client/ssr/RootDocument.js +1 -1
  300. package/src/client/ssr/body/404.js +1 -1
  301. package/src/client/ssr/body/500.js +1 -1
  302. package/src/client/ssr/body/CacheControl.js +1 -1
  303. package/src/client/ssr/body/CyberiaDefaultSplashScreen.js +1 -1
  304. package/src/client/ssr/body/DefaultSplashScreen.js +1 -1
  305. package/src/client/ssr/body/SwaggerDarkMode.js +1 -1
  306. package/src/client/ssr/body/UnderpostDefaultSplashScreen.js +1 -1
  307. package/src/client/ssr/head/CryptokoynScripts.js +1 -1
  308. package/src/client/ssr/head/Css.js +1 -1
  309. package/src/client/ssr/head/CyberiaPortalScripts.js +1 -1
  310. package/src/client/ssr/head/DefaultScripts.js +1 -1
  311. package/src/client/ssr/head/ItemledgerScripts.js +1 -1
  312. package/src/client/ssr/head/Microdata.js +1 -1
  313. package/src/client/ssr/head/Production.js +1 -1
  314. package/src/client/ssr/head/Pwa.js +1 -1
  315. package/src/client/ssr/head/PwaItemledger.js +1 -1
  316. package/src/client/ssr/head/Seo.js +1 -1
  317. package/src/client/ssr/head/UnderpostScripts.js +1 -1
  318. package/src/client/ssr/mailer/DefaultRecoverEmail.js +1 -1
  319. package/src/client/ssr/mailer/DefaultVerifyEmail.js +1 -1
  320. package/src/client/ssr/views/Cyberia404.js +7 -5
  321. package/src/client/ssr/views/CyberiaServerMetrics.js +1 -1
  322. package/src/client/ssr/views/Maintenance.js +1 -1
  323. package/src/client/ssr/views/NoNetworkConnection.js +1 -1
  324. package/src/client/ssr/views/Test.js +1 -1
  325. package/src/client/sw/core.sw.js +20 -14
  326. package/src/client-builder/client-build-docs.js +61 -46
  327. package/src/client-builder/client-build-live.js +2 -2
  328. package/src/client-builder/client-build.js +58 -44
  329. package/src/client-builder/client-dev-server.js +3 -3
  330. package/src/client-builder/client-icons.js +2 -2
  331. package/src/client-builder/ssr.js +3 -3
  332. package/src/client.build.js +3 -6
  333. package/src/client.dev.js +3 -6
  334. package/src/db/DataBaseProvider.js +186 -3
  335. package/src/db/mariadb/MariaDB.js +3 -2
  336. package/src/db/mongo/MongoBootstrap.js +273 -101
  337. package/src/db/mongo/MongooseDB.js +11 -8
  338. package/src/{server/valkey.js → db/valkey/Valkey.js} +5 -4
  339. package/src/grpc/cyberia/grpc-server.js +1 -1
  340. package/src/index.js +103 -23
  341. package/src/mailer/EmailRender.js +2 -2
  342. package/src/mailer/MailerInterceptor.js +87 -0
  343. package/src/mailer/MailerProvider.js +50 -6
  344. package/src/projects/cyberia/atlas-sprite-sheet-generator.js +1 -1
  345. package/src/projects/cyberia/besu-genesis-generator.js +3 -3
  346. package/src/projects/cyberia/catalog-cyberia.js +54 -6
  347. package/src/projects/cyberia/gemini-client.js +1 -1
  348. package/src/projects/cyberia/generate-saga.js +15 -24
  349. package/src/projects/cyberia/hot-reload-trigger.js +1 -1
  350. package/src/projects/cyberia/instance-data.js +22 -3
  351. package/src/projects/cyberia/ipfs-client.js +4 -4
  352. package/src/projects/cyberia/map-preview-generator.js +1 -1
  353. package/src/projects/cyberia/object-layer.js +12 -22
  354. package/src/projects/cyberia/semantic-layer-generator.js +1 -1
  355. package/src/projects/underpost/catalog-underpost.js +13 -2
  356. package/src/proxy.js +4 -7
  357. package/src/runtime/cyberia-client/Dockerfile +9 -4
  358. package/src/runtime/cyberia-client/Dockerfile.dev +8 -2
  359. package/src/runtime/cyberia-server/Dockerfile +9 -4
  360. package/src/runtime/cyberia-server/Dockerfile.dev +8 -2
  361. package/src/runtime/engine-cyberia/Dockerfile +25 -9
  362. package/src/runtime/engine-cyberia/Dockerfile.dev +25 -9
  363. package/src/runtime/engine-cyberia/Dockerfile.test +4 -4
  364. package/src/runtime/engine-cyberia/compose.env +16 -0
  365. package/src/runtime/engine-cyberia/docker-compose.yml +31 -37
  366. package/src/runtime/express/Express.js +32 -27
  367. package/src/runtime/lampp/Lampp.js +22 -70
  368. package/src/runtime/nginx/Nginx.js +1 -1
  369. package/src/runtime/wp/Wp.js +16 -48
  370. package/src/server/{catalog.js → build/catalog.js} +15 -5
  371. package/src/server/build/coverage.js +117 -0
  372. package/src/server/build/execution.js +402 -0
  373. package/src/server/build/package.js +465 -0
  374. package/src/server/build/testing.js +528 -0
  375. package/src/server/{dns.js → network/dns.js} +241 -14
  376. package/src/server/network/forward-proxy.js +541 -0
  377. package/src/server/{middlewares.js → network/middlewares.js} +59 -4
  378. package/src/server/network/node-capability.js +98 -0
  379. package/src/server/{peer.js → network/peer.js} +3 -3
  380. package/src/server/{proxy.js → network/proxy.js} +7 -13
  381. package/src/server/network/router.js +356 -0
  382. package/src/server/{tls.js → network/tls.js} +2 -2
  383. package/src/server/network/underpost-compression.js +186 -0
  384. package/src/server/{underpost-gateway.js → network/underpost-gateway.js} +26 -16
  385. package/src/server/{underpost-ingress.js → network/underpost-ingress.js} +19 -3
  386. package/src/server/{cri.js → ops/cri.js} +2 -2
  387. package/src/server/ops/cron.js +949 -0
  388. package/src/server/ops/event-notification.js +284 -0
  389. package/src/server/{logger.js → ops/logger.js} +108 -23
  390. package/src/server/ops/monitoring.js +1724 -0
  391. package/src/server/ops/systemd.js +259 -0
  392. package/src/server/{conf.js → runtime/conf.js} +521 -520
  393. package/src/server/runtime/config-scope.js +215 -0
  394. package/src/server/runtime/environment.js +149 -0
  395. package/src/server/{process.js → runtime/process.js} +153 -13
  396. package/src/server/{runtime-status.js → runtime/runtime-status.js} +155 -16
  397. package/src/server/{runtime.js → runtime/runtime.js} +12 -8
  398. package/src/server/{start.js → runtime/start.js} +128 -39
  399. package/src/server/{auth.js → security/auth.js} +7 -8
  400. package/src/server/security/container-storage.js +382 -0
  401. package/src/server/{crypto.js → security/crypto.js} +1 -1
  402. package/src/server/security/selinux.js +185 -0
  403. package/src/server/{backup.js → storage/backup.js} +17 -8
  404. package/src/server/{data-query.js → storage/data-query.js} +1 -1
  405. package/src/server/{downloader.js → storage/downloader.js} +2 -2
  406. package/src/server/storage/repository.js +68 -0
  407. package/src/server.js +4 -7
  408. package/src/ws/IoInterface.js +1 -1
  409. package/src/ws/IoServer.js +1 -1
  410. package/src/ws/core/core.ws.connection.js +1 -1
  411. package/src/ws/core/core.ws.emit.js +1 -1
  412. package/src/ws/core/core.ws.server.js +1 -1
  413. package/src/ws/default/default.ws.connection.js +1 -1
  414. package/src/ws/default/default.ws.emit.js +1 -1
  415. package/src/ws/default/default.ws.server.js +1 -1
  416. package/test/e2e/event-e2e-public-ingress-down.js +45 -0
  417. package/test/e2e/event-e2e-wireguard-server-down.js +38 -0
  418. package/test/e2e/event-e2e-wireguard-spoke-down.js +40 -0
  419. package/test/{api.test.js → integration/app/api.test.js} +33 -14
  420. package/test/integration/app/cyberia/cyberia-cli-plain-reads.test.js +30 -0
  421. package/test/{cyberia-instance-conf-defaults.test.js → integration/app/cyberia/cyberia-instance-conf-defaults.test.js} +2 -1
  422. package/test/integration/app/cyberia/cyberia-load.test.js +467 -0
  423. package/test/integration/app/cyberia/fallback-world-capture.test.js +253 -0
  424. package/test/integration/app/cyberia/object-layer-item-id.test.js +327 -0
  425. package/test/{shape-generator.test.js → integration/app/cyberia/shape-generator.test.js} +1 -1
  426. package/test/integration/infra/1-security/config-scope.test.js +305 -0
  427. package/test/integration/infra/1-security/container-storage.test.js +356 -0
  428. package/test/integration/infra/1-security/in-pod-cli-surface.test.js +86 -0
  429. package/test/integration/infra/1-security/secret-onboarding.test.js +930 -0
  430. package/test/integration/infra/1-security/selinux.test.js +69 -0
  431. package/test/{sops-secret-store.test.js → integration/infra/1-security/sops-secret-store.test.js} +415 -61
  432. package/test/integration/infra/1-security/systemd-service.test.js +70 -0
  433. package/test/integration/infra/1-security/underpost-config-secret.test.js +367 -0
  434. package/test/integration/infra/2-network/dns-firewall.test.js +566 -0
  435. package/test/integration/infra/2-network/node-capability.test.js +182 -0
  436. package/test/integration/infra/2-network/wireguard-cli.test.js +1163 -0
  437. package/test/integration/infra/2-network/wireguard-edge.test.js +1731 -0
  438. package/test/{cluster-instances.test.js → integration/infra/3-cluster/cluster-instances.test.js} +11 -15
  439. package/test/{deploy-node-placement.test.js → integration/infra/3-cluster/deploy-node-placement.test.js} +3 -3
  440. package/test/integration/infra/3-cluster/docker-compose-stack.test.js +313 -0
  441. package/test/integration/infra/4-ingress/deploy-routes.test.js +379 -0
  442. package/test/integration/infra/4-ingress/gateway-static-assets.test.js +453 -0
  443. package/test/{instance-traffic-plan.test.js → integration/infra/4-ingress/instance-traffic-plan.test.js} +10 -6
  444. package/test/{underpost-gateway.test.js → integration/infra/4-ingress/underpost-gateway.test.js} +47 -6
  445. package/test/{underpost-ingress.test.js → integration/infra/4-ingress/underpost-ingress.test.js} +56 -4
  446. package/test/integration/infra/5-observability/cron-jobs.test.js +652 -0
  447. package/test/{deploy-monitor.test.js → integration/infra/5-observability/deploy-monitor.test.js} +29 -31
  448. package/test/integration/infra/5-observability/event-notification.test.js +197 -0
  449. package/test/integration/infra/5-observability/event-remediation.test.js +502 -0
  450. package/test/integration/infra/5-observability/event-targets.test.js +1174 -0
  451. package/test/integration/infra/5-observability/monitoring-stack.test.js +902 -0
  452. package/test/support/shell-harness.js +73 -0
  453. package/test/unit/build-template.test.js +97 -0
  454. package/test/unit/build-workflow-offline.test.js +132 -0
  455. package/test/unit/catalog.test.js +71 -0
  456. package/test/unit/client-build-docs.test.js +334 -0
  457. package/test/unit/conf-loading.test.js +303 -0
  458. package/test/unit/conf-resolution.test.js +979 -0
  459. package/test/unit/coverage-artifact.test.js +130 -0
  460. package/test/{crypto.test.js → unit/crypto.test.js} +1 -1
  461. package/test/unit/deploy-env-resolution.test.js +38 -0
  462. package/test/unit/deploy-log-table.test.js +447 -0
  463. package/test/unit/execution-profiles.test.js +219 -0
  464. package/test/unit/fleet-sync-source.test.js +149 -0
  465. package/test/unit/logger-redaction.test.js +112 -0
  466. package/test/unit/package.test.js +403 -0
  467. package/test/unit/prepare-host.test.js +299 -0
  468. package/test/unit/process-environment.test.js +18 -0
  469. package/test/unit/propagation-message.test.js +78 -0
  470. package/test/unit/redeploy-plan.test.js +24 -0
  471. package/test/unit/release-bump.test.js +474 -0
  472. package/test/unit/ssh-output-redaction.test.js +28 -0
  473. package/test/unit/start-options.test.js +46 -0
  474. package/test/unit/test-tiers.test.js +193 -0
  475. package/vitest.config.js +58 -0
  476. package/.nycrc +0 -9
  477. package/manifests/cronjobs/dd-cron/dd-cron-dns.yaml +0 -48
  478. package/manifests/mongodb/pv-pvc.yaml +0 -59
  479. package/scripts/link-local-underpost-cli.sh +0 -6
  480. package/src/api/user/user.build.js +0 -16
  481. package/src/cli/env.js +0 -177
  482. package/src/client/components/core/Blockchain.js +0 -41
  483. package/src/client/components/core/Blog.js +0 -9
  484. package/src/client/components/core/KeyboardAvoidance.js +0 -145
  485. package/src/client/public/default/assets/background/dark.jpg +0 -0
  486. package/src/client/public/default/assets/background/white0-min.jpg +0 -0
  487. package/src/client/public/default/assets/background/white0.jpg +0 -0
  488. package/src/client/public/doc/favicon.ico +0 -0
  489. package/src/client/public/doc/sitemap +0 -148
  490. package/src/server/cron.js +0 -476
@@ -0,0 +1,907 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Staged SELinux recovery and normalization for a live Underpost / Kubernetes host.
4
+ #
5
+ # inspect -> validate -> normalize -> verify -> enforce
6
+ #
7
+ # Never reboots, never restarts Kubernetes, kubelet, the container runtime, the CNI or any
8
+ # workload, never touches PVs/PVCs or the contents of a data directory, and never disables
9
+ # SELinux. The only mutations it can make are:
10
+ #
11
+ # * creating a node storage directory that a PersistentVolume references and that is missing;
12
+ # * adding a `semanage fcontext` mapping for a discovered storage root;
13
+ # * running `restorecon` over those roots;
14
+ # * flipping SELinux to Enforcing, and only in the `enforce` stage, and only after `verify`
15
+ # passes on this same run.
16
+ #
17
+ # Every mutating stage requires --apply. Without it the script reports what it would do.
18
+ #
19
+ # Rocky Linux 9 / RHEL 9.
20
+ set -euo pipefail
21
+
22
+ SHARED_TYPE='container_file_t'
23
+ # Overridable so the rollback round-trip can be exercised without writing under /var/lib.
24
+ STATE_DIR="${UNDERPOST_SELINUX_STATE_DIR:-/var/lib/underpost/selinux-normalize}"
25
+ KUBECTL_TIMEOUT='20s'
26
+ STAGE='inspect'
27
+ APPLY=0
28
+ ASSUME_YES=0
29
+ AVC_SINCE=''
30
+ AVC_SINCE_EXPLICIT=0
31
+ ALLOW_UNLABELED_MOUNTS=0
32
+ declare -a EXTRA_PATHS=()
33
+
34
+ RED=''; YELLOW=''; GREEN=''; BOLD=''; RESET=''
35
+ if [ -t 1 ]; then RED=$'\033[31m'; YELLOW=$'\033[33m'; GREEN=$'\033[32m'; BOLD=$'\033[1m'; RESET=$'\033[0m'; fi
36
+
37
+ say() { printf '%s\n' "$*"; }
38
+ head1() { printf '\n%s==> %s%s\n' "$BOLD" "$*" "$RESET"; }
39
+ ok() { printf ' %s[ ok ]%s %s\n' "$GREEN" "$RESET" "$*"; }
40
+ warn() { printf ' %s[warn]%s %s\n' "$YELLOW" "$RESET" "$*"; }
41
+ bad() { printf ' %s[fail]%s %s\n' "$RED" "$RESET" "$*"; }
42
+ info() { printf ' %s\n' "$*"; }
43
+
44
+ usage() {
45
+ cat <<'USAGE'
46
+ Usage: selinux-normalize.sh [STAGE] [options]
47
+
48
+ Stages (each one re-runs the ones before it that are read-only):
49
+ inspect Report SELinux state, Kubernetes state, discovered storage and current labels.
50
+ validate inspect + assert every precondition for a safe normalization. Read-only.
51
+ normalize Register the persistent container_file_t mappings and restore the discovered
52
+ storage roots. Requires --apply to mutate.
53
+ verify Re-check labels, workload health and fresh AVC denials since the last normalize.
54
+ enforce Transition Permissive -> Enforcing, only if verify passes. Requires --apply.
55
+ rollback Undo the fcontext mappings this script added, and restore the affected trees.
56
+ Requires --apply.
57
+ mark Move the denial window to now, without touching labels or SELinux mode. Use after
58
+ suspending or fixing a workload, so verify measures the state you just created
59
+ rather than the denials that led you to change it. Requires --apply.
60
+
61
+ Options:
62
+ --apply Perform mutations. Without it, mutating stages only report.
63
+ --yes Do not prompt before the Enforcing transition.
64
+ --allow-unlabeled-mounts
65
+ Let verify pass with workload hostPath mounts still unresolved. Those
66
+ mounts deny under Enforcing; only pass this once you have decided each
67
+ one is acceptable to break.
68
+ --path PATH Additional node storage root to include (repeatable).
69
+ --since WHEN ausearch window for the AVC scan (default: recent).
70
+ -h, --help This text.
71
+
72
+ Exit codes: 0 success, 1 stage failed / unsafe to proceed, 2 usage or missing tooling.
73
+ USAGE
74
+ }
75
+
76
+ while [ "$#" -gt 0 ]; do
77
+ case "$1" in
78
+ inspect|validate|normalize|verify|enforce|rollback|mark) STAGE="$1"; shift ;;
79
+ --apply) APPLY=1; shift ;;
80
+ --yes) ASSUME_YES=1; shift ;;
81
+ --allow-unlabeled-mounts) ALLOW_UNLABELED_MOUNTS=1; shift ;;
82
+ --path) [ "$#" -ge 2 ] || { echo "--path requires a value" >&2; exit 2; }; EXTRA_PATHS+=("$2"); shift 2 ;;
83
+ --since) [ "$#" -ge 2 ] || { echo "--since requires a value" >&2; exit 2; }; AVC_SINCE="$2"; AVC_SINCE_EXPLICIT=1; shift 2 ;;
84
+ -h|--help) usage; exit 0 ;;
85
+ *) echo "Unknown argument: $1" >&2; usage >&2; exit 2 ;;
86
+ esac
87
+ done
88
+
89
+ SUDO=''
90
+ SUDO_RO=''
91
+ if [ "$(id -u)" -ne 0 ]; then
92
+ SUDO='sudo'
93
+ # Read-only probes never prompt: an inspect/validate/verify run must not block on a password,
94
+ # and a probe that cannot elevate degrades to "unknown" rather than hanging the operator.
95
+ SUDO_RO='sudo -n'
96
+ fi
97
+
98
+ privileged_read() {
99
+ if [ -z "$SUDO_RO" ]; then "$@"; else $SUDO_RO "$@"; fi
100
+ }
101
+
102
+ run() {
103
+ if [ "$APPLY" -eq 1 ]; then
104
+ info "+ $*"
105
+ eval "$@"
106
+ else
107
+ info "would run: $*"
108
+ fi
109
+ }
110
+
111
+ # ---------------------------------------------------------------------------- inspect: SELinux
112
+
113
+ SELINUX_ENABLED=0
114
+ SELINUX_MODE='unknown'
115
+ SELINUX_CONFIG_MODE='unknown'
116
+ SELINUX_POLICY='unknown'
117
+ HAVE_SEMANAGE=0
118
+ HAVE_RESTORECON=0
119
+ HAVE_AUSEARCH=0
120
+
121
+ inspect_selinux() {
122
+ head1 'SELinux state'
123
+ if ! command -v getenforce >/dev/null 2>&1; then
124
+ bad 'getenforce is missing: SELinux userspace is not installed.'
125
+ info 'Remediation: dnf install -y policycoreutils policycoreutils-python-utils selinux-policy-targeted audit'
126
+ return
127
+ fi
128
+ SELINUX_MODE="$(getenforce)"
129
+ command -v selinuxenabled >/dev/null 2>&1 && selinuxenabled && SELINUX_ENABLED=1
130
+ [ -r /etc/selinux/config ] && SELINUX_CONFIG_MODE="$(awk -F= '/^SELINUX=/{print $2}' /etc/selinux/config)"
131
+ [ -r /etc/selinux/config ] && SELINUX_POLICY="$(awk -F= '/^SELINUXTYPE=/{print $2}' /etc/selinux/config)"
132
+ command -v semanage >/dev/null 2>&1 && HAVE_SEMANAGE=1
133
+ command -v restorecon >/dev/null 2>&1 && HAVE_RESTORECON=1
134
+ command -v ausearch >/dev/null 2>&1 && HAVE_AUSEARCH=1
135
+
136
+ info "runtime mode : $SELINUX_MODE"
137
+ info "config mode : $SELINUX_CONFIG_MODE"
138
+ info "policy : $SELINUX_POLICY"
139
+ info "semanage present : $([ "$HAVE_SEMANAGE" -eq 1 ] && echo yes || echo 'no <-- policycoreutils-python-utils')"
140
+ info "restorecon present: $([ "$HAVE_RESTORECON" -eq 1 ] && echo yes || echo 'no <-- policycoreutils')"
141
+ info "ausearch present : $([ "$HAVE_AUSEARCH" -eq 1 ] && echo yes || echo 'no <-- audit')"
142
+ [ -e /.autorelabel ] && warn '/.autorelabel is present: the next boot will relabel the whole filesystem.'
143
+ return 0
144
+ }
145
+
146
+ # ------------------------------------------------------------------------- inspect: Kubernetes
147
+
148
+ KUBE_AVAILABLE=0
149
+ NODES_TOTAL=0
150
+ NODES_READY=0
151
+ declare -a STORAGE_PATHS=()
152
+ declare -a MANAGED_ROOTS=()
153
+ declare -a PV_SUBTREES=()
154
+ declare -a ROOT_REASON=()
155
+ declare -a REFUSED=()
156
+ declare -a UNCOVERED=()
157
+ declare -a POD_HOSTPATHS=()
158
+ declare -a RESIDUAL=()
159
+ declare -a AUDIT_SINCE_ARGS=(-ts recent)
160
+
161
+ kubectl_q() { kubectl --request-timeout="$KUBECTL_TIMEOUT" "$@" 2>/dev/null; }
162
+
163
+ inspect_kubernetes() {
164
+ head1 'Kubernetes state'
165
+ if ! command -v kubectl >/dev/null 2>&1; then
166
+ warn 'kubectl is not on PATH; storage discovery falls back to the declared platform roots.'
167
+ return 0
168
+ fi
169
+ if ! kubectl_q version -o json >/dev/null; then
170
+ warn 'No reachable API server; storage discovery falls back to the declared platform roots.'
171
+ return 0
172
+ fi
173
+ KUBE_AVAILABLE=1
174
+ local nodes
175
+ nodes="$(kubectl_q get nodes --no-headers || true)"
176
+ NODES_TOTAL="$(printf '%s' "$nodes" | grep -c . || true)"
177
+ NODES_READY="$(printf '%s' "$nodes" | awk '$2 ~ /(^|,)Ready(,|$)/' | grep -c . || true)"
178
+ info "nodes ready : ${NODES_READY}/${NODES_TOTAL}"
179
+ info "this host : $(hostname)"
180
+ return 0
181
+ }
182
+
183
+ # Paths that must never receive the shared container label, whatever discovery turns up.
184
+ #
185
+ # A pod hostPath is not evidence that a directory is workload data: the CNI plugin directory, the
186
+ # container runtime socket, /proc and the kubelet tree are all mounted into pods and every one of
187
+ # them has a policy type that exists for a reason. Relabeling /opt/cni/bin would strip `bin_t`
188
+ # from the calico executables the kubelet runs.
189
+ PROTECTED_PATHS_REGEX='^(/|/usr|/etc|/bin|/sbin|/lib|/lib64|/boot|/proc|/sys|/dev|/run|/var|/var/lib|/var/run|/var/log|/var/lib/kubelet|/var/lib/containerd|/var/lib/docker|/var/lib/etcd|/var/lib/calico|/var/lib/cni|/opt|/opt/cni|/opt/cni/bin|/home|/root|/srv|/mnt|/tmp)$'
190
+
191
+ # Types whose whole purpose is something other than container data. `usr_t` is deliberately absent:
192
+ # it is the policy default under /opt and is exactly what the local-path provisioner root wrongly
193
+ # carries, so refusing it would refuse the fix.
194
+ PROTECTED_TYPES='bin_t lib_t shell_exec_t etc_t kubernetes_file_t container_runtime_exec_t container_var_lib_t cni_var_lib_t systemd_unit_file_t passwd_file_t shadow_t admin_home_t'
195
+
196
+ # The node directory backing every hostPath PersistentVolume the deploy flow materializes. Read
197
+ # from the source of truth the CLI itself uses, so the script and the code can never disagree.
198
+ resolve_host_volume_root() {
199
+ local repo_env script_dir
200
+ script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
201
+ repo_env="$script_dir/../src/server/runtime/environment.js"
202
+ if [ -r "$repo_env" ]; then
203
+ local value
204
+ value="$(sed -nE "s/^const HOST_VOLUME_ROOT = '([^']+)';.*/\1/p" "$repo_env" | head -1)"
205
+ [ -n "$value" ] && { printf '%s' "$value"; return 0; }
206
+ fi
207
+ printf '%s' "${UNDERPOST_HOST_VOLUME_ROOT:-/home/dd/engine/volume}"
208
+ }
209
+
210
+ is_protected_path() {
211
+ printf '%s' "$1" | grep -qE "$PROTECTED_PATHS_REGEX"
212
+ }
213
+
214
+ is_protected_type() {
215
+ local type; type="$(path_type "$1")"
216
+ [ -n "$type" ] || return 1
217
+ printf '%s' " $PROTECTED_TYPES " | grep -q " $type "
218
+ }
219
+
220
+ # Classifies a pod hostPath mount so the report separates the mounts that need an operator
221
+ # decision from the ones whose current type is already correct.
222
+ #
223
+ # system the path is inside a protected tree, or already carries a type the container policy
224
+ # defines for that role (`container_var_lib_t`, `cert_t`, `proc_t`, `kubernetes_file_t`
225
+ # …). These are mounted by privileged system DaemonSets and are not workload data.
226
+ # ok already container-accessible.
227
+ # review neither — the only class worth acting on.
228
+ classify_pod_mount() {
229
+ local path="$1" type="$2"
230
+ [ -e "$path" ] || { printf 'absent'; return 0; }
231
+ [ "$type" = "$SHARED_TYPE" ] && { printf 'ok'; return 0; }
232
+ is_protected_path "$path" && { printf 'system'; return 0; }
233
+ case "$type" in
234
+ container_*|proc_t|sysfs_t|cert_t|kubernetes_file_t|modules_object_t|iptables_var_run_t|var_run_t|var_lib_t|root_t|etc_t|bin_t|lib_t|shell_exec_t)
235
+ printf 'system'; return 0 ;;
236
+ esac
237
+ printf 'review'
238
+ }
239
+
240
+ # Storage discovery, in three tiers with different authority:
241
+ #
242
+ # managed roots mapped with `semanage fcontext` and relabeled recursively. Only directories
243
+ # this platform owns end up here: its declared data roots, the local-path
244
+ # provisioner's configured node roots, and anything the operator named.
245
+ # PV subtrees node-local paths a PersistentVolume actually references. Used to prove the
246
+ # managed roots cover the live storage; one that is not covered is reported
247
+ # and skipped, never silently relabeled or promoted to a root of its own.
248
+ # pod hostPaths reported only. A pod mounting a host directory says nothing about whether
249
+ # that directory is workload data.
250
+ discover_pv_paths() {
251
+ head1 'PersistentVolume / PersistentVolumeClaim storage discovery'
252
+ local host_volume_root; host_volume_root="$(resolve_host_volume_root)"
253
+
254
+ if [ "$KUBE_AVAILABLE" -eq 1 ]; then
255
+ local rows
256
+ rows="$(kubectl_q get pv -o custom-columns=\
257
+ NAME:.metadata.name,CLASS:.spec.storageClassName,PHASE:.status.phase,\
258
+ CLAIM:.spec.claimRef.name,HOSTPATH:.spec.hostPath.path,LOCALPATH:.spec.local.path || true)"
259
+ if [ -n "$rows" ]; then
260
+ printf '%s\n' "$rows" | sed 's/^/ /'
261
+ local name class phase claim hostpath localpath path
262
+ while read -r name class phase claim hostpath localpath; do
263
+ [ -n "$name" ] && [ "$name" != 'NAME' ] || continue
264
+ path=''
265
+ [ "$hostpath" != '<none>' ] && path="$hostpath"
266
+ [ -z "$path" ] && [ "$localpath" != '<none>' ] && path="$localpath"
267
+ [ -n "$path" ] && PV_SUBTREES+=("$path") || true
268
+ done <<< "$rows"
269
+ else
270
+ info 'no PersistentVolumes found'
271
+ fi
272
+
273
+ # Dynamically provisioned claims have no path of their own: the data is a subdirectory of
274
+ # the provisioner's node root, so the root is what must carry the label. Read it from the
275
+ # live ConfigMap rather than assuming the upstream default.
276
+ local ns cfg roots root
277
+ for ns in local-path-storage kube-system; do
278
+ cfg="$(kubectl_q get configmap local-path-config -n "$ns" -o jsonpath='{.data.config\.json}' || true)"
279
+ [ -n "$cfg" ] || continue
280
+ roots="$(printf '%s' "$cfg" | tr -d ' \n' | grep -oE '"paths":\[[^]]*\]|"sharedFileSystemPath":"[^"]*"' | grep -oE '/[^",]+' || true)"
281
+ if [ -n "$roots" ]; then
282
+ say ''
283
+ info "local-path provisioner (ns/$ns) node roots:"
284
+ while read -r root; do
285
+ [ -n "$root" ] || continue
286
+ info " $root"
287
+ MANAGED_ROOTS+=("$root")
288
+ ROOT_REASON+=("$root|local-path provisioner node root (ns/$ns)")
289
+ done <<< "$roots"
290
+ fi
291
+ break
292
+ done
293
+
294
+ # Reported, never labeled. A host directory being mounted into a pod says nothing about
295
+ # whether it holds workload data, so the decision is left to the operator — but each mount
296
+ # is classified so the handful that actually need a decision are not buried under the
297
+ # couple of dozen host-integration mounts every CNI and kubelet DaemonSet carries.
298
+ #
299
+ # `go-template` rather than `jsonpath`: inside a nested range, jsonpath's `$` resolves to
300
+ # the document root, not the enclosing item, so the pod's own namespace and name came back
301
+ # empty and every row read as a bare "/".
302
+ local pod_rows p pods current class width
303
+ #
304
+ # Three sources, not one. A pod only exists while it runs, so reading pods alone reports a
305
+ # clean host for a CronJob that is merely scheduled — its mount reappears at the next fire.
306
+ # The workload templates are the durable declaration and are what must be audited.
307
+ # Terminal pods are excluded for the mirror-image reason: a finished Job cannot produce
308
+ # another denial, and its own template already speaks for the next run.
309
+ pod_rows="$(
310
+ {
311
+ kubectl_q get pods -A --field-selector=status.phase!=Succeeded,status.phase!=Failed \
312
+ -o go-template='{{range .items}}{{$ns := .metadata.namespace}}{{$n := .metadata.name}}{{range .spec.volumes}}{{if .hostPath}}{{.hostPath.path}}{{"\t"}}{{$ns}}/{{$n}}{{"\n"}}{{end}}{{end}}{{end}}' || true
313
+ kubectl_q get deployments,statefulsets,daemonsets,jobs,replicasets -A \
314
+ -o go-template='{{range .items}}{{$ns := .metadata.namespace}}{{$k := .kind}}{{$n := .metadata.name}}{{range .spec.template.spec.volumes}}{{if .hostPath}}{{.hostPath.path}}{{"\t"}}{{$ns}}/{{$k}}/{{$n}}{{"\n"}}{{end}}{{end}}{{end}}' || true
315
+ kubectl_q get cronjobs -A \
316
+ -o go-template='{{range .items}}{{$ns := .metadata.namespace}}{{$n := .metadata.name}}{{if not .spec.suspend}}{{range .spec.jobTemplate.spec.template.spec.volumes}}{{if .hostPath}}{{.hostPath.path}}{{"\t"}}{{$ns}}/CronJob/{{$n}}{{"\n"}}{{end}}{{end}}{{end}}{{end}}' || true
317
+ } | grep -E '^/' | sort -u || true
318
+ )"
319
+ if [ -n "$pod_rows" ]; then
320
+ say ''
321
+ info 'hostPath mounts declared by live pods and workload templates (never relabeled here):'
322
+ width="$(printf '%s\n' "$pod_rows" | cut -f1 | awk '{ if (length($0) > m) m = length($0) } END { print (m > 58 ? 58 : (m < 24 ? 24 : m)) }')"
323
+ printf " %-${width}s %-22s %-8s %s\n" PATH 'CURRENT TYPE' CLASS 'MOUNTED BY'
324
+ while read -r p; do
325
+ [ -n "$p" ] || continue
326
+ pods="$(printf '%s\n' "$pod_rows" | awk -F'\t' -v k="$p" '$1 == k {print $2}' | paste -sd, - | cut -c1-56)"
327
+ current="$(path_type "$p")"
328
+ class="$(classify_pod_mount "$p" "$current")"
329
+ printf " %-${width}s %-22s %-8s %s\n" "$p" "${current:-absent}" "$class" "$pods"
330
+ [ "$class" = 'review' ] && POD_HOSTPATHS+=("$p") || true
331
+ done <<< "$(printf '%s\n' "$pod_rows" | cut -f1 | sort -u)"
332
+ info 'system = host-integration mount (CNI, kubelet, runtime, /proc); its type is correct as-is.'
333
+ info 'ok = already container-accessible.'
334
+ info 'absent = not present on this node (a mount belonging to a pod scheduled elsewhere).'
335
+ info 'review = none of the above, and outside every managed root — the rows needing a decision.'
336
+ fi
337
+ fi
338
+
339
+ # Declared platform roots. Kept even when the API is reachable: a root can exist on this node
340
+ # with no object currently referencing it (a Retained volume between deploys).
341
+ MANAGED_ROOTS+=('/data')
342
+ ROOT_REASON+=('/data|declared platform data root')
343
+ MANAGED_ROOTS+=("$host_volume_root")
344
+ ROOT_REASON+=("$host_volume_root|declared hostPath volume root (HOST_VOLUME_ROOT)")
345
+ if [ "$KUBE_AVAILABLE" -ne 1 ]; then
346
+ MANAGED_ROOTS+=('/opt/local-path-provisioner')
347
+ ROOT_REASON+=('/opt/local-path-provisioner|local-path provisioner default root (no API to query)')
348
+ fi
349
+ local extra
350
+ for extra in ${EXTRA_PATHS[@]+"${EXTRA_PATHS[@]}"}; do
351
+ MANAGED_ROOTS+=("$extra")
352
+ ROOT_REASON+=("$extra|named by the operator with --path")
353
+ done
354
+
355
+ # Collapse to non-overlapping roots: an fcontext expression is `<root>(/.*)?`, so a nested
356
+ # entry is a redundant rule and a second relabel pass over the same tree. Only managed roots
357
+ # take part — a discovered subtree can never absorb or widen one.
358
+ local sorted candidate covered root
359
+ sorted="$(printf '%s\n' "${MANAGED_ROOTS[@]}" | sed 's:/*$::' | grep -E '^/.+' | sort -u)"
360
+ MANAGED_ROOTS=()
361
+ while read -r candidate; do
362
+ [ -n "$candidate" ] || continue
363
+ covered=0
364
+ while read -r root; do
365
+ { [ -n "$root" ] && [ "$root" != "$candidate" ] && case "$candidate" in "$root"/*) covered=1 ;; esac; } || true
366
+ done <<< "$sorted"
367
+ [ "$covered" -eq 0 ] && MANAGED_ROOTS+=("$candidate") || true
368
+ done <<< "$sorted"
369
+
370
+ # Refuse anything whose type or path says it is not workload data, before it can be reported
371
+ # as an intended change. A refusal is fatal in `validate`, so it can never reach `--apply`.
372
+ STORAGE_PATHS=()
373
+ for candidate in "${MANAGED_ROOTS[@]}"; do
374
+ if is_protected_path "$candidate"; then
375
+ REFUSED+=("$candidate|system directory; relabeling it would change the policy type of host-owned files")
376
+ continue
377
+ fi
378
+ if is_protected_type "$candidate"; then
379
+ REFUSED+=("$candidate|carries the protected type '$(path_type "$candidate")'; that type is not container data")
380
+ continue
381
+ fi
382
+ STORAGE_PATHS+=("$candidate")
383
+ done
384
+
385
+ head1 'Storage roots selected for labeling'
386
+ if [ "${#STORAGE_PATHS[@]}" -eq 0 ]; then
387
+ warn 'none'
388
+ else
389
+ local reason entry
390
+ for candidate in "${STORAGE_PATHS[@]}"; do
391
+ reason=''
392
+ for entry in ${ROOT_REASON[@]+"${ROOT_REASON[@]}"}; do
393
+ [ "${entry%%|*}" = "$candidate" ] && reason="${entry#*|}"
394
+ done
395
+ printf ' %-42s %s\n' "$candidate" "${reason:-discovered}"
396
+ done
397
+ fi
398
+
399
+ if [ "${#REFUSED[@]}" -gt 0 ]; then
400
+ head1 'Refused (never relabeled)'
401
+ for candidate in "${REFUSED[@]}"; do
402
+ printf ' %-42s %s\n' "${candidate%%|*}" "${candidate#*|}"
403
+ done
404
+ fi
405
+
406
+ # Every live PersistentVolume path must fall inside a managed root, otherwise its data is not
407
+ # actually being fixed by this run and the operator has to say so explicitly.
408
+ UNCOVERED=()
409
+ local subtree
410
+ for subtree in ${PV_SUBTREES[@]+"${PV_SUBTREES[@]}"}; do
411
+ covered=0
412
+ for root in ${STORAGE_PATHS[@]+"${STORAGE_PATHS[@]}"}; do
413
+ { [ "$subtree" = "$root" ] || case "$subtree" in "$root"/*) covered=1 ;; esac; } || true
414
+ [ "$subtree" = "$root" ] && covered=1 || true
415
+ done
416
+ [ "$covered" -eq 0 ] && UNCOVERED+=("$subtree") || true
417
+ done
418
+ if [ "${#UNCOVERED[@]}" -gt 0 ]; then
419
+ head1 'PersistentVolume paths NOT covered by any managed root'
420
+ printf '%s\n' "${UNCOVERED[@]}" | sort -u | sed 's/^/ /'
421
+ warn 'These are not labeled by this run. Add one with --path if it is genuinely workload data.'
422
+ fi
423
+
424
+ # POD_HOSTPATHS now holds only the mounts classified `review`: not a system path, not already
425
+ # container-accessible. Those still inside a managed root are fine — the relabel covers them.
426
+ # Whatever is left is the complete set of places a denial can still come from after this run.
427
+ local mount current
428
+ RESIDUAL=()
429
+ for mount in ${POD_HOSTPATHS[@]+"${POD_HOSTPATHS[@]}"}; do
430
+ [ -e "$mount" ] || continue
431
+ covered=0
432
+ for root in ${STORAGE_PATHS[@]+"${STORAGE_PATHS[@]}"}; do
433
+ { [ "$mount" = "$root" ] && covered=1; } || case "$mount" in "$root"/*) covered=1 ;; esac
434
+ done
435
+ [ "$covered" -eq 0 ] && RESIDUAL+=("$mount|$(path_type "$mount")") || true
436
+ done
437
+ if [ "${#RESIDUAL[@]}" -gt 0 ]; then
438
+ head1 'Workload hostPath mounts this run does not fix'
439
+ for mount in "${RESIDUAL[@]}"; do
440
+ printf ' %-52s %s\n' "${mount%%|*}" "${mount#*|}"
441
+ done
442
+ info 'Deliberate exclusions, and the only remaining source of a container denial. If a'
443
+ info ' workload genuinely needs to read or write one, move that data under a storage root,'
444
+ info ' or name the specific subdirectory with --path — never the whole tree.'
445
+ fi
446
+ return 0
447
+ }
448
+
449
+ # Prints the SELinux type of a path, or nothing when it cannot be read.
450
+ #
451
+ # Never fails. `set -o pipefail` makes `ls -Zd <missing> | awk ...` a failing pipeline, and a
452
+ # `current="$(path_type "$p")"` assignment takes that status — which under `set -e` aborted the
453
+ # whole run on the first path that does not exist on this node (a mount belonging to a pod on
454
+ # another node, or a PV pinned elsewhere).
455
+ path_type() {
456
+ [ -e "$1" ] || return 0
457
+ { ls -Zd "$1" 2>/dev/null || true; } | awk '{print $1}' | awk -F: '{print $3}' || true
458
+ }
459
+
460
+ report_labels() {
461
+ head1 'Current labels vs intended'
462
+ printf ' %-46s %-22s %-22s %s\n' PATH CURRENT INTENDED ACTION
463
+ local path current mismatched=0 missing=0
464
+ for path in "${STORAGE_PATHS[@]}"; do
465
+ if [ ! -e "$path" ]; then
466
+ printf ' %-46s %-22s %-22s %s\n' "$path" '(absent)' "$SHARED_TYPE" 'create + label'
467
+ missing=$((missing + 1))
468
+ continue
469
+ fi
470
+ current="$(path_type "$path")"
471
+ if [ "$current" = "$SHARED_TYPE" ]; then
472
+ printf ' %-46s %-22s %-22s %s\n' "$path" "$current" "$SHARED_TYPE" 'none'
473
+ else
474
+ printf ' %-46s %-22s %-22s %s\n' "$path" "${current:-unknown}" "$SHARED_TYPE" 'relabel'
475
+ mismatched=$((mismatched + 1))
476
+ fi
477
+ done
478
+ # Nested entries carrying a stale label under an already-correct root: `restorecon -R` fixes
479
+ # them, and this is what proves the recursion is actually needed.
480
+ local stale
481
+ for path in "${STORAGE_PATHS[@]}"; do
482
+ [ -d "$path" ] || continue
483
+ stale="$(privileged_read find "$path" -mindepth 1 -maxdepth 3 -exec ls -Zd {} + 2>/dev/null \
484
+ | awk -F: -v t="$SHARED_TYPE" '$3 != t' | head -5 || true)"
485
+ [ -n "$stale" ] && { warn "entries under $path carrying a non-$SHARED_TYPE type:"; printf '%s\n' "$stale" | sed 's/^/ /'; }
486
+ done
487
+ MISMATCHED_COUNT="$mismatched"
488
+ MISSING_COUNT="$missing"
489
+ return 0
490
+ }
491
+
492
+ # Timestamp of the last applied normalization, in ausearch's own format.
493
+ normalize_marker_file() { printf '%s/last-normalize.txt' "$STATE_DIR"; }
494
+
495
+ # Resolves the audit window into ausearch arguments.
496
+ #
497
+ # Defaults to the moment the last `normalize --apply` finished rather than to `recent`: the
498
+ # denials this run just fixed are still inside a ten-minute `recent` window, so verifying against
499
+ # it would count the very records the relabel eliminated and never report a clean host. An
500
+ # explicit `--since` always wins.
501
+ resolve_avc_window() {
502
+ AUDIT_SINCE_ARGS=()
503
+ local marker; marker="$(normalize_marker_file)"
504
+ if [ "$AVC_SINCE_EXPLICIT" -eq 0 ] && [ -r "$marker" ]; then
505
+ local stamp; stamp="$(head -1 "$marker")"
506
+ if [ -n "$stamp" ]; then
507
+ # shellcheck disable=SC2206 -- deliberate split: ausearch takes date and time as two args.
508
+ AUDIT_SINCE_ARGS=(-ts $stamp)
509
+ AVC_SINCE="$stamp (last applied normalization)"
510
+ return 0
511
+ fi
512
+ fi
513
+ [ -n "$AVC_SINCE" ] || AVC_SINCE='recent'
514
+ AUDIT_SINCE_ARGS=(-ts "$AVC_SINCE")
515
+ return 0
516
+ }
517
+
518
+ report_avc() {
519
+ resolve_avc_window
520
+ head1 "AVC denials since: $AVC_SINCE"
521
+ if [ "$HAVE_AUSEARCH" -ne 1 ]; then
522
+ warn 'ausearch is unavailable; install the audit package to see denials.'
523
+ AVC_COUNT=-1
524
+ return 0
525
+ fi
526
+ local raw
527
+ if ! privileged_read ausearch -m AVC,USER_AVC,SELINUX_ERR "${AUDIT_SINCE_ARGS[@]}" --raw >/dev/null 2>&1; then
528
+ if [ "$(id -u)" -ne 0 ] && ! sudo -n true 2>/dev/null; then
529
+ warn 'the audit log needs root and passwordless sudo is unavailable; re-run this stage as root.'
530
+ AVC_COUNT=-1
531
+ return 0
532
+ fi
533
+ fi
534
+ raw="$(privileged_read ausearch -m AVC,USER_AVC,SELINUX_ERR "${AUDIT_SINCE_ARGS[@]}" --raw 2>/dev/null || true)"
535
+ AVC_COUNT="$(printf '%s' "$raw" | grep -c 'avc: denied' || true)"
536
+ if [ "$AVC_COUNT" -eq 0 ]; then
537
+ ok 'no denials in the window'
538
+ return 0
539
+ fi
540
+ warn "$AVC_COUNT denial record(s); grouped by source domain, target type and target class:"
541
+ local denials
542
+ denials="$(printf '%s' "$raw" | grep 'avc: denied' || true)"
543
+ printf '%s' "$denials" \
544
+ | sed -E 's/.*comm="?([^" ]+)"?.*scontext=([^ ]+).*tcontext=([^ ]+).*tclass=([^ ]+).*/\1 \2 \3 \4/' \
545
+ | awk '{ sub(/:c[0-9,c]+$/, "", $2); print }' \
546
+ | sort | uniq -c | sort -rn | head -20 | sed 's/^/ /'
547
+
548
+ # Not every denial is a labeling problem, and telling an operator to relabel something that
549
+ # carries no file label at all sends them after the wrong fix. A denial whose target class is
550
+ # a filesystem object is answered by `normalize`; one whose target is another domain — MCS
551
+ # category separation on a keyring, a socket, a process — is not, and stays after any relabel.
552
+ AVC_FILE_COUNT="$(printf '%s' "$denials" | grep -cE 'tclass=(file|dir|lnk_file|chr_file|blk_file|fifo_file|sock_file)' || true)"
553
+ AVC_OTHER_COUNT=$((AVC_COUNT - AVC_FILE_COUNT))
554
+ say ''
555
+ info "filesystem-object denials : $AVC_FILE_COUNT (resolved by labeling — see the mount report above)"
556
+ info "other-class denials : $AVC_OTHER_COUNT (NOT resolved by labeling)"
557
+ if [ "$AVC_OTHER_COUNT" -gt 0 ]; then
558
+ printf '%s' "$denials" \
559
+ | grep -vE 'tclass=(file|dir|lnk_file|chr_file|blk_file|fifo_file|sock_file)' \
560
+ | sed -E 's/.*comm="?([^" ]+)"?.*tclass=([^ ]+).*/\1 tclass=\2/' \
561
+ | sort | uniq -c | sort -rn | head -10 | sed 's/^/ /'
562
+ info 'Relabeling will not clear these. Confirm each is tolerated by the workload before'
563
+ info ' enforcing — a keyring or socket probe an application already ignores is harmless,'
564
+ info ' a denial it depends on is not.'
565
+ fi
566
+ return 0
567
+ }
568
+
569
+ # ---------------------------------------------------------------------------------- validate
570
+
571
+ validate() {
572
+ head1 'Preconditions'
573
+ local failures=0
574
+
575
+ if [ "$SELINUX_ENABLED" -ne 1 ]; then
576
+ bad 'SELinux is disabled. A relabel is meaningless until it is enabled and the filesystem'
577
+ info ' has been relabeled at boot. Set SELINUX=permissive in /etc/selinux/config, touch'
578
+ info ' /.autorelabel, and reboot during a maintenance window before running this script.'
579
+ failures=$((failures + 1))
580
+ else
581
+ ok "SELinux enabled, mode $SELINUX_MODE, policy $SELINUX_POLICY"
582
+ fi
583
+
584
+ if [ "$SELINUX_POLICY" != 'targeted' ]; then
585
+ bad "policy is '$SELINUX_POLICY'; this script only reasons about the targeted policy."
586
+ failures=$((failures + 1))
587
+ fi
588
+
589
+ if [ "$HAVE_SEMANAGE" -ne 1 ]; then
590
+ bad 'semanage is missing. Persistent mappings cannot be registered, and chcon alone would'
591
+ info ' be undone by the next relabel. Remediation:'
592
+ info " $SUDO dnf install -y policycoreutils-python-utils"
593
+ failures=$((failures + 1))
594
+ else
595
+ ok 'semanage available'
596
+ fi
597
+
598
+ if [ "$HAVE_RESTORECON" -ne 1 ]; then
599
+ bad "restorecon is missing. Remediation: $SUDO dnf install -y policycoreutils"
600
+ failures=$((failures + 1))
601
+ else
602
+ ok 'restorecon available'
603
+ fi
604
+
605
+ if [ "$KUBE_AVAILABLE" -eq 1 ]; then
606
+ if [ "$NODES_TOTAL" -gt 0 ] && [ "$NODES_READY" -eq "$NODES_TOTAL" ]; then
607
+ ok "all $NODES_TOTAL node(s) Ready"
608
+ else
609
+ warn "$NODES_READY/$NODES_TOTAL nodes Ready — normalization is still safe (it touches no"
610
+ info ' workload), but do not proceed to the Enforcing stage on a degraded cluster.'
611
+ fi
612
+ local notrunning
613
+ notrunning="$(kubectl_q get pods -A --no-headers | awk '$4 !~ /^(Running|Completed|Succeeded)$/' | grep -c . || true)"
614
+ [ "$notrunning" -eq 0 ] && ok 'every pod is Running/Completed' \
615
+ || warn "$notrunning pod(s) are neither Running nor Completed"
616
+ else
617
+ warn 'Kubernetes is unreachable; discovery used the declared platform roots only.'
618
+ fi
619
+
620
+ if [ "${#STORAGE_PATHS[@]}" -eq 0 ]; then
621
+ bad 'discovery selected no storage root; there is nothing this run could normalize.'
622
+ failures=$((failures + 1))
623
+ fi
624
+
625
+ # Belt and braces: the same guard discovery already applied, re-checked here so a future
626
+ # change to discovery cannot route around it on the way to `--apply`.
627
+ local path
628
+ for path in ${STORAGE_PATHS[@]+"${STORAGE_PATHS[@]}"}; do
629
+ if is_protected_path "$path"; then
630
+ bad "refusing to operate on system directory '$path'"
631
+ failures=$((failures + 1))
632
+ elif is_protected_type "$path"; then
633
+ bad "refusing to relabel '$path': it carries the protected type '$(path_type "$path")'"
634
+ failures=$((failures + 1))
635
+ fi
636
+ done
637
+
638
+ if [ "${#REFUSED[@]}" -gt 0 ]; then
639
+ ok "${#REFUSED[@]} discovered path(s) excluded by the protection rules (listed above)"
640
+ fi
641
+ if [ "${#UNCOVERED[@]}" -gt 0 ]; then
642
+ warn "${#UNCOVERED[@]} PersistentVolume path(s) fall outside every managed root and will not be"
643
+ info ' labeled by this run. That is correct for a volume pinned to another node; for one on'
644
+ info ' this node it means the fix is incomplete — add it with --path.'
645
+ fi
646
+
647
+ if [ "$failures" -eq 0 ]; then
648
+ ok 'all preconditions met'
649
+ return 0
650
+ fi
651
+ bad "$failures precondition(s) failed; no mutation will be attempted."
652
+ return 1
653
+ }
654
+
655
+ # --------------------------------------------------------------------------------- normalize
656
+
657
+ state_file() { printf '%s/fcontext-added.txt' "$STATE_DIR"; }
658
+
659
+ record_state() {
660
+ [ "$APPLY" -eq 1 ] || return 0
661
+ $SUDO install -d -m 0750 "$STATE_DIR"
662
+ printf '%s\n' "$1" | $SUDO tee -a "$(state_file)" >/dev/null
663
+ }
664
+
665
+ fcontext_registered() {
666
+ privileged_read semanage fcontext -l 2>/dev/null | grep -qE "^$(printf '%s' "$1" | sed 's/[][\\.^$*+?(){}|]/\\&/g')\(/\.\*\)\? " || return 1
667
+ }
668
+
669
+ normalize() {
670
+ head1 'Normalizing labels'
671
+ [ "$APPLY" -eq 1 ] || warn 'dry run: pass --apply to perform these changes'
672
+ local path expr
673
+ for path in "${STORAGE_PATHS[@]}"; do
674
+ expr="${path}(/.*)?"
675
+ if [ ! -e "$path" ]; then
676
+ # Only ever created, never re-moded: an existing tree keeps the ownership and mode the
677
+ # workload's own init container set.
678
+ run "$SUDO install -d -m 0755 '$path'"
679
+ fi
680
+ if fcontext_registered "$path"; then
681
+ ok "mapping already registered: $expr -> $SHARED_TYPE"
682
+ else
683
+ # `-a` falls back to `-m`: some semanage versions treat an entry that already exists
684
+ # as an error rather than a warning, and under `set -e` that would abort the run
685
+ # partway through, leaving some roots relabeled and others not.
686
+ run "$SUDO semanage fcontext -a -t $SHARED_TYPE '$expr' 2>/dev/null || $SUDO semanage fcontext -m -t $SHARED_TYPE '$expr'"
687
+ # Recorded only when the mapping was absent beforehand, so a rollback never deletes an
688
+ # entry that a previous `underpost cluster` run legitimately owns.
689
+ record_state "$path"
690
+ fi
691
+ done
692
+ for path in "${STORAGE_PATHS[@]}"; do
693
+ [ -e "$path" ] || continue
694
+ # -R descends into the volume trees; -F resets the SELinux user as well, because entries
695
+ # created by the CLI carry unconfined_u where the runtime's carry system_u. Contents,
696
+ # ownership, mode, ACLs and other xattrs are untouched.
697
+ #
698
+ # `|| true` on the pipeline: restorecon exits non-zero when it cannot relabel a single
699
+ # entry (a file deleted mid-walk by a running database is routine), and one such entry
700
+ # must not abandon the remaining roots. A leftover mislabel is caught by `verify`.
701
+ run "{ $SUDO restorecon -RFv '$path' || true; } | tail -40"
702
+ done
703
+ if [ "$APPLY" -eq 1 ]; then
704
+ # Written last: `verify` measures denials from here, so it must mark the point at which
705
+ # every label was already correct.
706
+ $SUDO install -d -m 0750 "$STATE_DIR"
707
+ date '+%m/%d/%Y %H:%M:%S' | $SUDO tee "$(normalize_marker_file)" >/dev/null
708
+ ok 'normalization applied'
709
+ info "verify will count denials from $(cat "$(normalize_marker_file)" 2>/dev/null || echo now)"
710
+ else
711
+ warn 'nothing was changed'
712
+ fi
713
+ return 0
714
+ }
715
+
716
+ # ------------------------------------------------------------------------------------ verify
717
+
718
+ verify() {
719
+ head1 'Verification'
720
+ local failures=0
721
+
722
+ report_labels
723
+ if [ "${MISMATCHED_COUNT:-0}" -eq 0 ] && [ "${MISSING_COUNT:-0}" -eq 0 ]; then
724
+ ok 'every discovered storage root carries the shared container type'
725
+ else
726
+ bad "${MISMATCHED_COUNT:-0} root(s) mislabeled, ${MISSING_COUNT:-0} absent"
727
+ failures=$((failures + 1))
728
+ fi
729
+
730
+ if [ "$KUBE_AVAILABLE" -eq 1 ]; then
731
+ head1 'Workload health'
732
+ local bound unbound
733
+ bound="$(kubectl_q get pvc -A --no-headers | awk '$3 == "Bound"' | grep -c . || true)"
734
+ unbound="$(kubectl_q get pvc -A --no-headers | awk '$3 != "Bound"' | grep -c . || true)"
735
+ info "PVCs bound: $bound, not bound: $unbound"
736
+ [ "$unbound" -eq 0 ] || { bad 'some PVCs are not Bound'; failures=$((failures + 1)); }
737
+
738
+ local unhealthy
739
+ unhealthy="$(kubectl_q get pods -A --no-headers | awk '$4 !~ /^(Running|Completed|Succeeded)$/' || true)"
740
+ if [ -n "$unhealthy" ]; then
741
+ bad 'pods not in a healthy phase:'
742
+ printf '%s\n' "$unhealthy" | sed 's/^/ /'
743
+ failures=$((failures + 1))
744
+ else
745
+ ok 'every pod is Running/Completed'
746
+ fi
747
+
748
+ local restarts
749
+ restarts="$(kubectl_q get pods -A --no-headers | awk '$5 > 0 {print " " $0}' | head -10 || true)"
750
+ [ -n "$restarts" ] && { warn 'pods with restarts (check whether they predate this run):'; printf '%s\n' "$restarts"; }
751
+ else
752
+ warn 'Kubernetes unreachable; workload health could not be verified.'
753
+ failures=$((failures + 1))
754
+ fi
755
+
756
+ # A scheduled workload declaring a mount no container can use is a denial that has not
757
+ # happened yet, not an absent one — and it is invisible in the audit log until that workload
758
+ # next runs. Failing here is what keeps `verify` from blessing a host whose nightly CronJob
759
+ # breaks the moment Enforcing is active.
760
+ if [ "${#RESIDUAL[@]}" -gt 0 ]; then
761
+ if [ "$ALLOW_UNLABELED_MOUNTS" -eq 1 ]; then
762
+ warn "${#RESIDUAL[@]} unresolved workload hostPath mount(s), accepted via --allow-unlabeled-mounts"
763
+ else
764
+ bad "${#RESIDUAL[@]} workload hostPath mount(s) are still unresolved (listed above). Each one"
765
+ info ' denies under Enforcing the next time its workload runs, whether or not a pod exists now.'
766
+ info ' Resolve them, or re-run with --allow-unlabeled-mounts to accept the breakage knowingly.'
767
+ failures=$((failures + 1))
768
+ fi
769
+ fi
770
+
771
+ local uncovered
772
+ for uncovered in ${UNCOVERED[@]+"${UNCOVERED[@]}"}; do
773
+ [ -e "$uncovered" ] || continue
774
+ if [ "$(path_type "$uncovered")" != "$SHARED_TYPE" ]; then
775
+ bad "$uncovered exists on this host, is not covered by any managed root, and carries"
776
+ info " '$(path_type "$uncovered")'. Re-run with --path '$uncovered' if it is workload data."
777
+ failures=$((failures + 1))
778
+ fi
779
+ done
780
+
781
+ report_avc
782
+ if [ "${AVC_COUNT:-0}" -gt 0 ]; then
783
+ bad "denials are still being recorded; do not transition to Enforcing yet."
784
+ failures=$((failures + 1))
785
+ elif [ "${AVC_COUNT:-0}" -lt 0 ]; then
786
+ warn 'AVC state unknown (no ausearch); the Enforcing stage will refuse to run.'
787
+ failures=$((failures + 1))
788
+ fi
789
+
790
+ head1 'Verdict'
791
+ if [ "$failures" -eq 0 ]; then
792
+ ok 'safe to proceed toward Enforcing'
793
+ return 0
794
+ fi
795
+ bad "$failures check(s) failed; NOT safe to proceed toward Enforcing"
796
+ return 1
797
+ }
798
+
799
+ # ----------------------------------------------------------------------------------- enforce
800
+
801
+ enforce() {
802
+ head1 'Controlled Enforcing transition'
803
+ if [ "$SELINUX_MODE" = 'Enforcing' ]; then
804
+ ok 'already Enforcing; only the persisted config is checked'
805
+ fi
806
+ if [ "$APPLY" -ne 1 ]; then
807
+ warn 'dry run: pass --apply to perform the transition'
808
+ elif [ "$ASSUME_YES" -ne 1 ] && [ "$SELINUX_MODE" != 'Enforcing' ]; then
809
+ printf ' Transition this host to Enforcing now? [y/N] '
810
+ local answer; read -r answer
811
+ case "$answer" in [yY]|[yY][eE][sS]) ;; *) warn 'aborted by operator'; return 1 ;; esac
812
+ fi
813
+ # setenforce first: it is instantly reversible with `setenforce 0` and needs no reboot. The
814
+ # config file is only persisted after the runtime switch has been taken and re-verified, so a
815
+ # host that misbehaves under Enforcing never reboots into it.
816
+ run "$SUDO setenforce 1"
817
+ if [ "$APPLY" -eq 1 ]; then
818
+ say ''
819
+ info 'Runtime mode is now Enforcing. Re-verifying before persisting the config...'
820
+ AVC_SINCE="$(date '+%m/%d/%Y %H:%M:%S')"
821
+ AVC_SINCE_EXPLICIT=1
822
+ if ! verify; then
823
+ bad 'post-transition verification failed. Reverting the runtime mode; config untouched.'
824
+ $SUDO setenforce 0
825
+ return 1
826
+ fi
827
+ run "$SUDO sed -i -E 's/^SELINUX=.*/SELINUX=enforcing/' /etc/selinux/config"
828
+ ok 'Enforcing is now both active and persisted.'
829
+ info "Revert at any time with: $SUDO setenforce 0"
830
+ info "Revert persistently with: $SUDO sed -i -E 's/^SELINUX=.*/SELINUX=permissive/' /etc/selinux/config"
831
+ fi
832
+ return 0
833
+ }
834
+
835
+ # ---------------------------------------------------------------------------------- rollback
836
+
837
+ rollback() {
838
+ head1 'Rollback'
839
+ local file; file="$(state_file)"
840
+ if [ ! -r "$file" ]; then
841
+ warn "no recorded changes at $file; nothing to roll back"
842
+ return 0
843
+ fi
844
+ [ "$APPLY" -eq 1 ] || warn 'dry run: pass --apply to perform the rollback'
845
+ local path
846
+ while read -r path; do
847
+ [ -n "$path" ] || continue
848
+ run "$SUDO semanage fcontext -d '${path}(/.*)?' || true"
849
+ [ -e "$path" ] && run "$SUDO restorecon -RF '$path'" || true
850
+ done < "$file"
851
+ # Directories this script created and files it wrote are left in place: removing a node
852
+ # storage root would destroy live PersistentVolume data.
853
+ run "$SUDO rm -f '$file'"
854
+ ok 'mappings this script added have been removed; storage contents were not touched.'
855
+ return 0
856
+ }
857
+
858
+ # -------------------------------------------------------------------------------------- main
859
+
860
+ inspect_selinux
861
+ inspect_kubernetes
862
+ discover_pv_paths
863
+
864
+ case "$STAGE" in
865
+ inspect)
866
+ report_labels
867
+ report_avc
868
+ say ''
869
+ info "Next: $0 validate"
870
+ ;;
871
+ validate)
872
+ report_labels
873
+ report_avc
874
+ validate
875
+ ;;
876
+ normalize)
877
+ validate || exit 1
878
+ report_labels
879
+ normalize
880
+ say ''
881
+ info "Next: $0 verify --since recent"
882
+ ;;
883
+ verify)
884
+ verify
885
+ ;;
886
+ enforce)
887
+ validate || exit 1
888
+ verify || { bad 'refusing to transition to Enforcing while verification fails'; exit 1; }
889
+ enforce
890
+ ;;
891
+ mark)
892
+ head1 'Denial window'
893
+ if [ "$APPLY" -eq 1 ]; then
894
+ $SUDO install -d -m 0750 "$STATE_DIR"
895
+ date '+%m/%d/%Y %H:%M:%S' | $SUDO tee "$(normalize_marker_file)" >/dev/null
896
+ ok "verify will now count denials from $(cat "$(normalize_marker_file)")"
897
+ info 'Labels and SELinux mode were not touched.'
898
+ else
899
+ warn 'dry run: pass --apply to move the window to now'
900
+ fi
901
+ ;;
902
+ rollback)
903
+ rollback
904
+ ;;
905
+ *)
906
+ usage >&2; exit 2 ;;
907
+ esac