From f60d3467f2fac8e66861667e9a58fe376662d7c3 Mon Sep 17 00:00:00 2001 From: Melorise <0d00@0721.hk> Date: Wed, 29 Jul 2026 12:24:38 +0800 Subject: [PATCH] fix: make panel access settings declarative --- README.md | 13 ++- core/app/api/v2/setting.go | 10 ++ core/init/hook/hook.go | 19 ++++ docs/nPanel-nixos-design.md | 13 ++- frontend/src/routers/modules/container.ts | 12 --- frontend/src/routers/modules/setting.ts | 12 --- .../src/views/app-store/installed/index.vue | 17 +--- .../src/views/container/dashboard/index.vue | 35 +------ .../views/container/docker-status/index.vue | 6 -- frontend/src/views/container/index.vue | 4 - frontend/src/views/home/index.vue | 9 -- frontend/src/views/setting/index.vue | 4 - frontend/src/views/setting/panel/index.vue | 49 +--------- nix/module.nix | 98 ++++++++++--------- 14 files changed, 102 insertions(+), 199 deletions(-) diff --git a/README.md b/README.md index 4e5acf1..7542023 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,10 @@ nPanel 不编辑、生成、扫描或接管用户的 Flake、`hardware-configura imports = [ inputs.npanel.nixosModules.default ]; virtualisation.docker.enable = true; - services.nPanel.enable = true; + services.nPanel = { + enable = true; + securityEntrance = "npanelAccess"; + }; } ``` @@ -28,6 +31,8 @@ nPanel 不编辑、生成、扫描或接管用户的 Flake、`hardware-configura services.nPanel = { stateDir = "/srv/npanel"; runtimeDir = "/run/npanel"; + port = 9999; + securityEntrance = "npanelAccess"; initialPasswordFile = "/run/secrets/npanel-initial-password"; }; } @@ -37,9 +42,9 @@ nPanel 不编辑、生成、扫描或接管用户的 Flake、`hardware-configura 权限写入 `stateDir/initial-password`。账户及密码属于 nPanel 自身的可变状态, 不写入 Nix 配置或 Nix store。 -首次启动会额外生成 16 位随机安全入口并以 `0600` 权限写入 -`stateDir/initial-entrance`。使用 `http://127.0.0.1:/<该文件内容>` 首次访问; -登录后可在面板安全设置中修改或关闭该入口。 +使用 `http://127.0.0.1:/` 访问面板。`port` 与 +`securityEntrance` 是 NixOS 声明配置,在每次 nPanel 启动时同步到内部数据库;面板 +不会提供修改它们的入口。 ## 状态与权限 diff --git a/core/app/api/v2/setting.go b/core/app/api/v2/setting.go index f36f239..55dd4f7 100644 --- a/core/app/api/v2/setting.go +++ b/core/app/api/v2/setting.go @@ -18,6 +18,7 @@ import ( "github.com/1Panel-dev/1Panel/core/constant" "github.com/1Panel-dev/1Panel/core/global" "github.com/1Panel-dev/1Panel/core/utils/common" + "github.com/1Panel-dev/1Panel/core/utils/ctl_conf" "github.com/gin-gonic/gin" ) @@ -91,6 +92,10 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) { return } if req.Key == "SecurityEntrance" { + if ctl_conf.LoadWithoutPanic("MANAGED_ENTRANCE") != "" { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrInvalidParams", errors.New("SecurityEntrance is managed by NixOS")) + return + } if !checkEntrancePattern(req.Value) { helper.ErrorWithDetail(c, http.StatusBadRequest, "ErrInvalidParams", buserr.WithName("ErrEntranceFormat", req.Value)) return @@ -330,6 +335,11 @@ func (b *BaseApi) UpdateBindInfo(c *gin.Context) { // @Router /core/settings/port/update [post] // @x-panel-log {"bodyKeys":["serverPort"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统端口 => [serverPort]","formatEN":"update system port => [serverPort]"} func (b *BaseApi) UpdatePort(c *gin.Context) { + if ctl_conf.LoadWithoutPanic("MANAGED_PORT") != "" { + helper.ErrorWithDetail(c, http.StatusForbidden, "ErrInvalidParams", errors.New("ServerPort is managed by NixOS")) + return + } + var req dto.PortUpdate if err := helper.CheckBindAndValidate(&req, c); err != nil { return diff --git a/core/init/hook/hook.go b/core/init/hook/hook.go index 51ddff4..77b2d9a 100644 --- a/core/init/hook/hook.go +++ b/core/init/hook/hook.go @@ -27,11 +27,30 @@ func Init() { } handleUserInfo(global.CONF.Base.ChangeUserInfo, settingRepo) + applyManagedSettings(settingRepo) generateKey() initDockerConf() } +// applyManagedSettings makes values declared by the NixOS module authoritative +// over the panel's mutable settings database on every core startup. +func applyManagedSettings(settingRepo repo.ISettingRepo) { + if port := ctl_conf.LoadWithoutPanic("MANAGED_PORT"); port != "" { + if err := settingRepo.Update("ServerPort", port); err != nil { + global.LOG.Fatalf("apply managed panel port failed, err: %v", err) + } + global.CONF.Conn.Port = port + } + + if entrance := ctl_conf.LoadWithoutPanic("MANAGED_ENTRANCE"); entrance != "" { + if err := settingRepo.Update("SecurityEntrance", entrance); err != nil { + global.LOG.Fatalf("apply managed security entrance failed, err: %v", err) + } + global.CONF.Conn.Entrance = entrance + } +} + func handleUserInfo(tags string, settingRepo repo.ISettingRepo) { if len(tags) == 0 { return diff --git a/docs/nPanel-nixos-design.md b/docs/nPanel-nixos-design.md index 3eed67e..42e8a65 100644 --- a/docs/nPanel-nixos-design.md +++ b/docs/nPanel-nixos-design.md @@ -18,11 +18,16 @@ nPanel 是独立维护的、源自社区版 1Panel 的 NixOS 单机 Docker 服 imports = [ inputs.npanel.nixosModules.default ]; virtualisation.docker.enable = true; - services.nPanel.enable = true; + services.nPanel = { + enable = true; + securityEntrance = "npanelAccess"; + }; } ``` -具体选项名和默认状态目录将在打包阶段确定。以上示例不意味着用户需要按任何特定方式重组其配置文件。 +`services.nPanel.port` 与 `services.nPanel.securityEntrance` 是服务访问面的声明式 +配置。它们在每次 core 启动时覆盖 nPanel 内部数据库,面板不提供修改入口。以上示例 +不意味着用户需要按任何特定方式重组其配置文件。 ## 所有权边界 @@ -49,7 +54,7 @@ Nix 配置不能替代日常运维。下列功能保留在 nPanel 范围内: - Docker、Compose、容器、镜像、网络、卷、镜像仓库、模板与日志; - 应用商店、已安装应用、网站、运行时、反向代理、数据库、SSL 与 AI; - 仪表盘、主机监控、资源历史、进程查看与进程终止; -- 用于运维数据与故障处理的 root 级文件操作,并准确将不可变的 NixOS 路径标记为只读; +- 用于运维数据与故障处理的 root 级文件操作。Nix store 与 `/etc` 生成链接等路径由系统权限拒绝写入;在未知用户 Flake 位置的前提下,面板不能自动识别和保护任意用户配置文件; - 本地与远程 SSH 终端、容器终端与数据库终端; - systemd 单元状态、journal 查看,以及运行时的启动、停止、重启与重载操作; - 备份、恢复、告警与 1Panel 领域内的计划任务。 @@ -195,4 +200,4 @@ NixOS 模块从本项目源码打包 core、agent 和前端,并提供: - 独立 Flake、可配置状态目录、运行时目录和 systemd core/agent 服务已实现。 - 前端与后端已移除持久化系统策略、工具箱、脚本库、快照、多节点和商业入口的可达路由。 - 保留 Docker、Compose、应用、网站、数据库、备份、文件、终端、监控、进程和日志的运行时运维能力。 -- 核心、agent、前端和 Flake 求值必须在发布前持续执行构建与边界审计;NixOS VM 冒烟测试是发布门槛。 +- 核心、agent、前端和 Flake 求值必须在发布前持续执行构建与边界审计;NixOS VM 冒烟测试是发布门槛,本轮受限修正不执行 VM 测试。 diff --git a/frontend/src/routers/modules/container.ts b/frontend/src/routers/modules/container.ts index 6512043..3425542 100644 --- a/frontend/src/routers/modules/container.ts +++ b/frontend/src/routers/modules/container.ts @@ -129,18 +129,6 @@ const containerRouter = { permission: 'container_view', }, }, - { - path: 'setting', - name: 'ContainerSetting', - component: () => import('@/views/container/setting/index.vue'), - hidden: true, - meta: { - activeMenu: '/containers', - parent: 'menu.container', - title: 'container.setting', - permission: 'container_view', - }, - }, ], }, ], diff --git a/frontend/src/routers/modules/setting.ts b/frontend/src/routers/modules/setting.ts index acfaaac..8e38ed9 100644 --- a/frontend/src/routers/modules/setting.ts +++ b/frontend/src/routers/modules/setting.ts @@ -86,18 +86,6 @@ const settingRouter = { adminOnly: true, }, }, - { - path: 'safe', - name: 'Safe', - component: () => import('@/views/setting/safe/index.vue'), - hidden: true, - meta: { - parent: 'menu.settings', - title: 'setting.safe', - activeMenu: '/settings', - adminOnly: true, - }, - }, ], }, ], diff --git a/frontend/src/views/app-store/installed/index.vue b/frontend/src/views/app-store/installed/index.vue index 9ccee92..f14b9b5 100644 --- a/frontend/src/views/app-store/installed/index.vue +++ b/frontend/src/views/app-store/installed/index.vue @@ -32,21 +32,6 @@ - - - diff --git a/frontend/src/views/home/index.vue b/frontend/src/views/home/index.vue index 4f8ef5c..ea970c5 100644 --- a/frontend/src/views/home/index.vue +++ b/frontend/src/views/home/index.vue @@ -13,15 +13,6 @@ diff --git a/frontend/src/views/setting/index.vue b/frontend/src/views/setting/index.vue index 53836ba..86c3a65 100644 --- a/frontend/src/views/setting/index.vue +++ b/frontend/src/views/setting/index.vue @@ -21,10 +21,6 @@ const buttons = computed(() => { label: i18n.global.t('setting.panel'), path: '/settings/panel', }, - { - label: i18n.global.t('setting.safe'), - path: '/settings/safe', - }, ] : []), ...(globalStore.hasPermission('alert_view') diff --git a/frontend/src/views/setting/panel/index.vue b/frontend/src/views/setting/panel/index.vue index 60d3372..16969e6 100644 --- a/frontend/src/views/setting/panel/index.vue +++ b/frontend/src/views/setting/panel/index.vue @@ -107,16 +107,6 @@ {{ $t('setting.systemIPHelper') }} - - - - - - - @@ -171,9 +160,7 @@ import { MsgSuccess } from '@/utils/message'; import Timeout from '@/views/setting/panel/timeout/index.vue'; import PanelName from '@/views/setting/panel/name/index.vue'; import SystemIP from '@/views/setting/panel/systemip/index.vue'; -import Proxy from '@/views/setting/panel/proxy/index.vue'; import HideMenu from '@/views/setting/panel/hidemenu/index.vue'; -import { getXpackProxyDocker } from '@/extensions/xpack'; import { getXpackSetting, updateXpackSettingByKey } from '@/utils/xpack'; import { setPrimaryColor } from '@/utils/theme'; import { codeEditorThemeStorageKey } from '@/utils/code-editor-theme'; @@ -220,15 +207,6 @@ const form = reactive({ developerMode: '', systemIP: '', - proxyShow: '', - proxyUrl: '', - proxyType: '', - proxyPort: '', - proxyUser: '', - proxyPasswd: '', - proxyPasswdKeep: '', - proxyDocker: '', - hideMenu: '', }); @@ -236,7 +214,6 @@ const show = ref(); const panelNameRef = ref(); const systemIPRef = ref(); -const proxyRef = ref(); const timeoutRef = ref(); const hideMenuRef = ref(); const watermarkRef = ref(); @@ -278,22 +255,11 @@ const search = async () => { form.docSource = res.data.docSource || 'withByRegion'; form.edition = res.data.edition; - form.proxyUrl = res.data.proxyUrl; - form.proxyType = res.data.proxyType; - form.proxyPort = res.data.proxyPort; - form.proxyShow = form.proxyUrl ? form.proxyUrl + ':' + form.proxyPort : unset.value; - form.proxyUser = res.data.proxyUser; - form.proxyPasswd = res.data.proxyPasswd; - form.proxyPasswdKeep = res.data.proxyPasswdKeep; - form.developerMode = res.data.developerMode; form.hideMenu = res.data.hideMenu; if (isXpackOrEE.value) { - const [xpackRes, proxyDockerRes] = await Promise.all([ - getXpackSetting(), - getXpackProxyDocker().catch(() => null), - ]); + const xpackRes = await getXpackSetting(); if (xpackRes) { form.theme = xpackRes.data.theme || themeConfig.value.theme || 'light'; form.themeColor = JSON.parse(xpackRes.data.themeColor || '{"light":"#005eeb","dark":"#F0BE96"}'); @@ -309,7 +275,6 @@ const search = async () => { watermark.value = null; } } - form.proxyDocker = proxyDockerRes?.data?.proxyDocker || ''; } else { themeConfig.value.theme = form.theme; } @@ -324,18 +289,6 @@ const onChangeTimeout = () => { const onChangeSystemIP = () => { systemIPRef.value.acceptParams({ systemIP: form.systemIP }); }; -const onChangeProxy = () => { - proxyRef.value.acceptParams({ - url: form.proxyUrl, - type: form.proxyType, - port: form.proxyPort, - user: form.proxyUser, - passwd: form.proxyPasswd, - passwdKeep: form.proxyPasswdKeep, - proxyDocker: form.proxyDocker, - }); -}; - const onChangeHideMenus = () => { hideMenuRef.value.acceptParams({ hideMenu: form.hideMenu, diff --git a/nix/module.nix b/nix/module.nix index eb2e2ba..f8dad86 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -34,62 +34,58 @@ let password_file=${lib.escapeShellArg (if cfg.initialPasswordFile == null then "" else cfg.initialPasswordFile)} install -d -m 0750 "$state_dir" - if [ -s "$control_file" ]; then - # Older nPanel generations created this file without the mandatory - # upstream security-entrance setting. Repair that state in place while - # keeping all user-owned panel data and credentials untouched. - if ! grep -q '^ORIGINAL_ENTRANCE=' "$control_file"; then - entrance="$(set +o pipefail; tr -dc 'A-Za-z0-9' &2 + if [ ! -s "$control_file" ]; then + if [ -n "$password_file" ]; then + password="$(cat "$password_file")" + if [ -z "$password" ]; then + echo "nPanel: initialPasswordFile is empty" >&2 exit 1 fi - printf '\nORIGINAL_ENTRANCE=%s\n' "$entrance" >> "$control_file" - chmod 0600 "$control_file" - (umask 077; printf '%s\n' "$entrance" > "$state_dir/initial-entrance") - echo "nPanel created a one-time security entrance in $state_dir/initial-entrance" >&2 + else + # `head` deliberately closes the random stream after 32 bytes. Do not + # let that expected SIGPIPE be promoted by the script-wide pipefail. + password="$(set +o pipefail; tr -dc 'A-Za-z0-9' &2 + exit 1 + fi + (umask 077; printf '%s\n' "$password" > "$state_dir/initial-password") + echo "nPanel created a one-time bootstrap password in $state_dir/initial-password" >&2 fi - exit 0 - fi - if [ -n "$password_file" ]; then - password="$(cat "$password_file")" - if [ -z "$password" ]; then - echo "nPanel: initialPasswordFile is empty" >&2 - exit 1 - fi - else - # `head` deliberately closes the random stream after 32 bytes. Do not - # let that expected SIGPIPE be promoted by the script-wide pipefail. - password="$(set +o pipefail; tr -dc 'A-Za-z0-9' &2 - exit 1 - fi - (umask 077; printf '%s\n' "$password" > "$state_dir/initial-password") - echo "nPanel created a one-time bootstrap password in $state_dir/initial-password" >&2 - fi - - entrance="$(set +o pipefail; tr -dc 'A-Za-z0-9' &2 - exit 1 - fi - (umask 077; printf '%s\n' "$entrance" > "$state_dir/initial-entrance") - echo "nPanel created a one-time security entrance in $state_dir/initial-entrance" >&2 - - umask 077 - cat > "$control_file" < "$control_file" < "$tmp" + else + cat "$control_file" > "$tmp" + printf '%s=%s\n' "$key" "$value" >> "$tmp" + fi + chmod 0600 "$tmp" + mv "$tmp" "$control_file" + } + + update_control_value ORIGINAL_PORT ${lib.escapeShellArg (toString cfg.port)} + update_control_value ORIGINAL_ENTRANCE ${lib.escapeShellArg cfg.securityEntrance} + update_control_value MANAGED_PORT ${lib.escapeShellArg (toString cfg.port)} + update_control_value MANAGED_ENTRANCE ${lib.escapeShellArg cfg.securityEntrance} ''; commonServiceConfig = { @@ -126,7 +122,7 @@ in package = mkOption { type = types.package; default = packageForSystem; - defaultText = lib.literalExpression "inputs.npanel.packages.\${pkgs.system}.default"; + defaultText = lib.literalExpression "self.packages.\${pkgs.system}.default"; description = "nPanel core 软件包。agent 与前端从该软件包的 passthru 属性取得。"; }; @@ -145,7 +141,13 @@ in port = mkOption { type = types.port; default = 9999; - description = "首次初始化时写入 nPanel 数据库的监听端口;之后请在 nPanel 自身设置中调整。"; + description = "nPanel Web 服务监听端口。此值由 NixOS 持续接管,不能在面板中修改。"; + }; + + securityEntrance = mkOption { + type = types.strMatching "[A-Za-z0-9]{5,116}"; + example = "npanelAccess"; + description = "nPanel 安全入口路径后缀(不含斜杠)。此值由 NixOS 持续接管,不能在面板中修改。"; }; initialUser = mkOption { @@ -178,6 +180,10 @@ in assertion = cfg.stateDir != cfg.runtimeDir; message = "services.nPanel.stateDir 与 runtimeDir 必须是不同目录。"; } + { + assertion = cfg.securityEntrance != ""; + message = "services.nPanel.securityEntrance 是必填项,且必须为 5 到 116 位字母或数字。"; + } ]; systemd.tmpfiles.rules = [