限制Linux管理服务回环监听并补齐安全回归
This commit is contained in:
parent
0b74886c45
commit
1150c3904e
@ -54,6 +54,9 @@ func New(ctx context.Context, cfg config.Config) (*App, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string) (*App, error) {
|
func NewWithConfigPath(ctx context.Context, cfg config.Config, configPath string) (*App, error) {
|
||||||
|
if err := cfg.ValidateServerListener(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if !cfg.SetupReady {
|
if !cfg.SetupReady {
|
||||||
return nil, fmt.Errorf("系统尚未初始化,请先启动 scert serve 完成首次配置")
|
return nil, fmt.Errorf("系统尚未初始化,请先启动 scert serve 完成首次配置")
|
||||||
}
|
}
|
||||||
|
|||||||
19
backend/internal/app/listener_linux_test.go
Normal file
19
backend/internal/app/listener_linux_test.go
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewRejectsNonLoopbackBeforeCreatingResources(t *testing.T) {
|
||||||
|
cfg := config.Default(t.TempDir())
|
||||||
|
cfg.SetupReady = true
|
||||||
|
cfg.Server.Host = "::"
|
||||||
|
application, err := NewWithConfigPath(context.Background(), cfg, "")
|
||||||
|
if application != nil || err == nil || !strings.Contains(err.Error(), "只允许监听回环") {
|
||||||
|
t.Fatalf("正式服务入口未在创建资源前拒绝非回环监听:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,6 +40,9 @@ func Run(ctx context.Context, options ServerOptions) (bool, error) {
|
|||||||
if options.Config.SetupReady {
|
if options.Config.SetupReady {
|
||||||
return false, errors.New("已经初始化,不能启动临时初始化服务")
|
return false, errors.New("已经初始化,不能启动临时初始化服务")
|
||||||
}
|
}
|
||||||
|
if err := options.Config.ValidateServerListener(); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
firewallManager, err := firewall.NewManagerWithMode(options.FirewallMode)
|
firewallManager, err := firewall.NewManagerWithMode(options.FirewallMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|||||||
18
backend/internal/bootstrap/server_listener_linux_test.go
Normal file
18
backend/internal/bootstrap/server_listener_linux_test.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package bootstrap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRunRejectsNonLoopbackBeforeInitialization(t *testing.T) {
|
||||||
|
cfg := config.Default(t.TempDir())
|
||||||
|
cfg.Server.Host = "0.0.0.0"
|
||||||
|
completed, err := Run(context.Background(), ServerOptions{Config: cfg})
|
||||||
|
if completed || err == nil || !strings.Contains(err.Error(), "只允许监听回环") {
|
||||||
|
t.Fatalf("初始化入口未在创建资源前拒绝非回环监听:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,7 +24,14 @@ func loadServeConfig(path string, defaults config.Config) (config.Config, error)
|
|||||||
if !info.Mode().IsRegular() {
|
if !info.Mode().IsRegular() {
|
||||||
return config.Config{}, errors.New("服务配置必须是普通文件")
|
return config.Config{}, errors.New("服务配置必须是普通文件")
|
||||||
}
|
}
|
||||||
return config.LoadWithEnv(path)
|
cfg, err := config.LoadWithEnv(path)
|
||||||
|
if err != nil {
|
||||||
|
return config.Config{}, err
|
||||||
|
}
|
||||||
|
if err := cfg.ValidateServerListener(); err != nil {
|
||||||
|
return config.Config{}, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
}
|
}
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
return config.Config{}, err
|
return config.Config{}, err
|
||||||
@ -36,6 +43,9 @@ func loadServeConfig(path string, defaults config.Config) (config.Config, error)
|
|||||||
if err := defaults.Validate(); err != nil {
|
if err := defaults.Validate(); err != nil {
|
||||||
return config.Config{}, err
|
return config.Config{}, err
|
||||||
}
|
}
|
||||||
|
if err := defaults.ValidateServerListener(); err != nil {
|
||||||
|
return config.Config{}, err
|
||||||
|
}
|
||||||
return defaults, nil
|
return defaults, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
62
backend/internal/cli/serve_listener_linux_test.go
Normal file
62
backend/internal/cli/serve_listener_linux_test.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.pcux.cn/xuans/Scert/backend/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadServeConfigWithoutListenerOverride(t *testing.T) {
|
||||||
|
t.Setenv("SCERT_SERVER_HOST", "")
|
||||||
|
if err := os.Unsetenv("SCERT_SERVER_HOST"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
root := t.TempDir()
|
||||||
|
path := filepath.Join(root, "scert.json")
|
||||||
|
cfg := config.Default(root)
|
||||||
|
loaded, err := loadServeConfig(path, cfg)
|
||||||
|
if err != nil || loaded.Server.Host != "127.0.0.1" {
|
||||||
|
t.Fatalf("未配置环境变量时默认监听不安全:%v", err)
|
||||||
|
}
|
||||||
|
cfg.Server.Host = "0.0.0.0"
|
||||||
|
if err := cfg.Save(path); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, err := loadServeConfig(path, cfg); err == nil {
|
||||||
|
t.Fatal("没有安全环境覆盖的历史通配监听未被拒绝")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadServeConfigEnforcesEffectiveLoopback(t *testing.T) {
|
||||||
|
for _, existing := range []bool{false, true} {
|
||||||
|
for _, host := range []string{"127.0.0.1", "::1", "0.0.0.0", "::", "192.0.2.1"} {
|
||||||
|
t.Run(host+map[bool]string{false: "/new", true: "/existing"}[existing], func(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
cfg := config.Default(root)
|
||||||
|
cfg.Server.Host = "0.0.0.0"
|
||||||
|
path := filepath.Join(root, "scert.json")
|
||||||
|
if existing {
|
||||||
|
data, err := json.Marshal(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(path, data, 0o600); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Setenv("SCERT_SERVER_HOST", host)
|
||||||
|
loaded, err := loadServeConfig(path, cfg)
|
||||||
|
allowed := host == "127.0.0.1" || host == "::1"
|
||||||
|
if allowed && (err != nil || loaded.Server.Host != host) {
|
||||||
|
t.Fatalf("回环环境覆盖未生效:%v", err)
|
||||||
|
}
|
||||||
|
if !allowed && err == nil {
|
||||||
|
t.Fatal("环境覆盖后的非回环地址未被拒绝")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -131,7 +131,7 @@ func Default(baseDir string) Config {
|
|||||||
Version: CurrentVersion,
|
Version: CurrentVersion,
|
||||||
SetupReady: false,
|
SetupReady: false,
|
||||||
Admin: AdminConfig{},
|
Admin: AdminConfig{},
|
||||||
Server: ServerConfig{Host: "0.0.0.0", Port: 8080, HTTPSMode: "direct"},
|
Server: ServerConfig{Host: defaultServerHost(), Port: 8080, HTTPSMode: "direct"},
|
||||||
Web: WebConfig{Enabled: true},
|
Web: WebConfig{Enabled: true},
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
Driver: "sqlite", DSN: filepath.Join(paths.Data, "scert.db"),
|
Driver: "sqlite", DSN: filepath.Join(paths.Data, "scert.db"),
|
||||||
@ -330,7 +330,7 @@ func (c *Config) Normalize() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.Server.Host == "" {
|
if c.Server.Host == "" {
|
||||||
c.Server.Host = "0.0.0.0"
|
c.Server.Host = defaultServerHost()
|
||||||
}
|
}
|
||||||
if c.Server.HTTPSMode == "" {
|
if c.Server.HTTPSMode == "" {
|
||||||
c.Server.HTTPSMode = "direct"
|
c.Server.HTTPSMode = "direct"
|
||||||
|
|||||||
26
backend/internal/config/listener.go
Normal file
26
backend/internal/config/listener.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"runtime"
|
||||||
|
)
|
||||||
|
|
||||||
|
func defaultServerHost() string {
|
||||||
|
if runtime.GOOS == "linux" {
|
||||||
|
return "127.0.0.1"
|
||||||
|
}
|
||||||
|
return "0.0.0.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateServerListener 校验环境覆盖后的实际监听配置,不通过域名解析判断回环地址。
|
||||||
|
func (c Config) ValidateServerListener() error {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
address := net.ParseIP(c.Server.Host)
|
||||||
|
if address == nil || !address.IsLoopback() {
|
||||||
|
return errors.New("Linux 管理服务只允许监听回环 IP 地址,请使用 127.0.0.1 或 ::1,并通过 Nginx 的 80/443 端口访问")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
36
backend/internal/config/listener_linux_test.go
Normal file
36
backend/internal/config/listener_linux_test.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLinuxServerDefaultsToLoopback(t *testing.T) {
|
||||||
|
cfg := Default(t.TempDir())
|
||||||
|
if cfg.Server.Host != "127.0.0.1" {
|
||||||
|
t.Fatal("Linux 默认监听地址不是回环地址")
|
||||||
|
}
|
||||||
|
cfg.Server.Host = ""
|
||||||
|
cfg.Normalize()
|
||||||
|
if cfg.Server.Host != "127.0.0.1" {
|
||||||
|
t.Fatal("空监听地址未回退到回环地址")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinuxServerListenerRejectsNonLoopback(t *testing.T) {
|
||||||
|
for _, host := range []string{"", "0.0.0.0", "::", "192.0.2.1", "10.0.0.1", "2001:db8::1", "localhost", "127.0.0.1.example.test", " 127.0.0.1", "127.0.0.1:8443"} {
|
||||||
|
t.Run(host, func(t *testing.T) {
|
||||||
|
cfg := Default(t.TempDir())
|
||||||
|
cfg.Server.Host = host
|
||||||
|
if err := cfg.ValidateServerListener(); err == nil {
|
||||||
|
t.Fatal("非回环或模糊监听配置未被拒绝")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, host := range []string{"127.0.0.1", "::1", "::ffff:127.0.0.1"} {
|
||||||
|
t.Run(host, func(t *testing.T) {
|
||||||
|
cfg := Default(t.TempDir())
|
||||||
|
cfg.Server.Host = host
|
||||||
|
if err := cfg.ValidateServerListener(); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -20,7 +20,7 @@ func TestProxyHTTPSPortValidationAndPersistence(t *testing.T) {
|
|||||||
if err := cfg.Validate(); err != nil {
|
if err := cfg.Validate(); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
path := filepath.Join(cfg.Paths.Config, "scert.json")
|
path := filepath.Join(t.TempDir(), "scert.json")
|
||||||
if err := cfg.Save(path); err != nil {
|
if err := cfg.Save(path); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,13 @@
|
|||||||
# Linux 生产流程测试记录
|
# Linux 生产流程测试记录
|
||||||
|
|
||||||
|
## 2026年10月2日12:42 Linux 回环监听回归
|
||||||
|
|
||||||
|
- [x] 第二台隔离源码目录以 nobody 用户、Go 1.26.6 执行 config、bootstrap、cli、app、serviceready、servicehost 六包完整回归,实际退出零;监听相关新用例重复 20 次通过,六包 go vet 退出零。
|
||||||
|
- [x] 覆盖默认与空值回环、IPv4/IPv6 通配地址拒绝、非回环 IP 和域名拒绝、环境覆盖后校验、无环境覆盖时历史通配配置拒绝、初始化及正式入口在建立资源前拒绝。
|
||||||
|
- [x] 首轮失败原因分别为旧配置测试误写 /scert/conf 以及隔离源码未复制 deploy 目录;修正测试临时文件路径并补齐原有部署文件后完整重跑。没有提权绕过权限,没有触碰生产配置或数据库。
|
||||||
|
- [x] 路径扫描命中的 install-linux.sh 与源码 SHA256 相同;命中内容为已有的 /tmp/ 禁止目录规则。下一次构建配置专属 TMPDIR,扫描仍覆盖实际临时目录,不删除检查项。
|
||||||
|
- [ ] 正式包构建、安装及运行态复核待进行,本节不计作清库首次生产验收。
|
||||||
|
|
||||||
## 2026年10月2日12:19 启动通知原生回归记录
|
## 2026年10月2日12:19 启动通知原生回归记录
|
||||||
|
|
||||||
范围为开发回归及旧线上候选只读检查,不是清库后的首次生产验收。未创建辅助验收脚本,未额外备份或删除现有文件,未开放端口,未部署新候选。
|
范围为开发回归及旧线上候选只读检查,不是清库后的首次生产验收。未创建辅助验收脚本,未额外备份或删除现有文件,未开放端口,未部署新候选。
|
||||||
@ -1894,3 +1902,13 @@ SQLite 新测试程序 SHA-256:4c81656fae63a2eebad999d58e6a79e63e6f441810e02ce
|
|||||||
- [~] 生产整体验收仍受第二台正式域名证书、短暂启动 502、三数据库 Web 首次流程、Smail 发信和密码重置闭环阻塞。
|
- [~] 生产整体验收仍受第二台正式域名证书、短暂启动 502、三数据库 Web 首次流程、Smail 发信和密码重置闭环阻塞。
|
||||||
|
|
||||||
当前总体状态保持 **P0-03:完成,等待复审**。
|
当前总体状态保持 **P0-03:完成,等待复审**。
|
||||||
|
# 2026年10月2日12:35 回环监听安全复核
|
||||||
|
|
||||||
|
本节为只读现场核查及代码整改记录,不是清库首次生产验收;没有重启线上服务、修改 Nginx、开放端口或触碰其他业务。
|
||||||
|
|
||||||
|
- [x] 第一台与第二台实际监听均为 Scert 127.0.0.1:8443、MySQL 127.0.0.1:3306 和 127.0.0.1:33060;第一台到第二台上述三个端口的四秒限时 TCP 探测均未连接成功。
|
||||||
|
- [x] 第二台 Nginx 配置检查成功,管理上游为回环 HTTPS,开启 proxy_ssl_verify、SNI 和系统 CA 校验。
|
||||||
|
- [x] 发现两台磁盘配置仍保存通配地址,但 systemd 的 SCERT_SERVER_HOST 将实际监听覆盖为回环地址。新增校验必须针对环境覆盖后的有效配置,不误伤当前安全运行方式。
|
||||||
|
- [~] 补齐程序独立启动防护:Linux 默认及空值回退改为回环地址,CLI 配置入口、初始化入口、正式应用入口在建立资源前拒绝非回环监听;新增 Linux 回归用例待执行。
|
||||||
|
- [!] 12:26 正式构建因 GOFLAGS 覆盖被拒绝;12:30 重试移除覆盖后,12:31 被 install-linux.sh 的发布路径扫描拦截。两次均判失败,不以进程启动时的 Result=success 冒充构建成功,不绕过路径检查。
|
||||||
|
- [ ] 最新代码正式候选包、双机升级及严格 HTTPS 复测待完成。
|
||||||
|
|||||||
@ -1,5 +1,26 @@
|
|||||||
# Scert 整改任务执行清单
|
# Scert 整改任务执行清单
|
||||||
|
|
||||||
|
## 2026年10月2日12:42 当前安全整改入口
|
||||||
|
|
||||||
|
- [x] Linux 默认及空值回退使用回环地址;CLI、初始化及正式应用入口拒绝实际非回环监听,保留旧配置经安全环境覆盖启动的兼容性。
|
||||||
|
- [x] Linux 非特权用户执行 config、bootstrap、cli、app、serviceready、servicehost 六包完整回归通过;新增监听用例重复 20 次通过;六包 go vet 通过。
|
||||||
|
- [x] 修正旧代理端口持久化测试写入正式配置目录的问题,明确写入测试临时目录;隔离源码补齐 deploy 目录后重跑,首轮失败不计通过。
|
||||||
|
- [~] 准备从干净提交重新正式构建;使用专属临时目录,避免通用 /tmp/ 路径与安装器禁止目录规则误匹配,仍检查真实构建路径。
|
||||||
|
- [ ] 新版配套部署、systemd 通知、严格 HTTPS 及跨机端口复核待进行;线上仍为此前候选。
|
||||||
|
- [!] 生产门禁未放行,P0-10、邮件实发与三数据库完整首次流程保持待办。
|
||||||
|
|
||||||
|
## 2026年10月2日12:35 公网监听风险收口
|
||||||
|
|
||||||
|
需求确认:Linux 管理服务不能直接暴露公网,仅允许回环监听并经 Nginx 的 80/443 访问;不能仅依赖防火墙阻挡错误监听。本节作为新增安全整改入口,不覆盖历史未完成门禁。
|
||||||
|
|
||||||
|
- [x] 双机只读检查:Scert 均监听 127.0.0.1:8443,MySQL 均为回环监听;第一台探测第二台 8443、3306、33060 未连接成功,仅作为该来源的 IPv4 证据。
|
||||||
|
- [x] 第二台 Nginx 上游为回环 HTTPS,保留证书及域名校验。
|
||||||
|
- [x] Linux 默认及空值回退改为回环地址;配置加载后的有效监听地址、初始化入口及正式应用入口均增加非回环拒绝检查,等待原生回归。
|
||||||
|
- [~] 原生 Linux 正负向监听用例及关联包回归;历史配置可由既有 systemd 环境覆盖为回环地址,不能因校验时机错误导致安全的旧部署无法升级。
|
||||||
|
- [!] 正式构建两次失败:首次为外加 GOFLAGS 被禁止,移除后第二次被发布路径检查拦截。尚未产生可部署正式包,未改线上程序,不跳过检查。
|
||||||
|
- [ ] 修复构建环境后重新生成干净提交的候选包、配套升级并复核监听与 HTTPS。
|
||||||
|
- [!] P0-10、系统通知实发、三数据库清库完整 Web 流程及总体生产验收仍未通过。
|
||||||
|
|
||||||
## 2026年10月2日12:19 当前复核入口
|
## 2026年10月2日12:19 当前复核入口
|
||||||
|
|
||||||
本节覆盖下方启动通知整改的早期执行态记录;未验证项目不升级为复审通过。
|
本节覆盖下方启动通知整改的早期执行态记录;未验证项目不升级为复审通过。
|
||||||
|
|||||||
@ -1,5 +1,12 @@
|
|||||||
# Scert 整改、合并与优化清单
|
# Scert 整改、合并与优化清单
|
||||||
|
|
||||||
|
## 2026年10月2日12:42 最新进度入口
|
||||||
|
|
||||||
|
- [x] 已补齐 Linux 独立启动时的回环监听限制;实际非回环 IP、通配地址及域名均被启动入口拒绝。
|
||||||
|
- [x] 六个关联包原生 Linux 回归及 go vet 通过,新增监听正负向用例重复 20 次通过。
|
||||||
|
- [~] 正式候选包重新构建与双机升级待执行,不把源码回归或旧实例健康当作新版部署完成。
|
||||||
|
- [!] 总体仍未通过生产验收;P0-10、系统通知、三数据库清库 Web 流程和最终复审继续保留。
|
||||||
|
|
||||||
## 2026年10月2日12:19 最新进度入口
|
## 2026年10月2日12:19 最新进度入口
|
||||||
|
|
||||||
以下为当前状态,下方同日早期记录保留作为过程证据。
|
以下为当前状态,下方同日早期记录保留作为过程证据。
|
||||||
@ -2629,3 +2636,9 @@ P0-11 生产验收门禁
|
|||||||
- [~] 需要将最新候选包按正式安装流程部署到双机,并复核页面和 API 后再进入最终生产验收。
|
- [~] 需要将最新候选包按正式安装流程部署到双机,并复核页面和 API 后再进入最终生产验收。
|
||||||
|
|
||||||
本轮不删除历史数据库迁移文件和禁用兼容测试;它们只用于既有数据库升级和安全回归,不会启用 SSH 密钥能力。总体生产验收仍未通过,既有证书 SAN、启动早期 502、Smail 系统账户发信和三数据库完整 Web 流程仍是门禁项。
|
本轮不删除历史数据库迁移文件和禁用兼容测试;它们只用于既有数据库升级和安全回归,不会启用 SSH 密钥能力。总体生产验收仍未通过,既有证书 SAN、启动早期 502、Smail 系统账户发信和三数据库完整 Web 流程仍是门禁项。
|
||||||
|
# 2026年10月2日12:35 新增安全整改进度
|
||||||
|
|
||||||
|
- [x] 已核实两台 Scert 实际均为回环监听,第二台 Nginx 上游严格 TLS 校验保持开启。
|
||||||
|
- [~] 补齐 Linux 非 systemd 启动场景:默认监听及空值回退使用回环地址,实际非回环配置拒绝启动;关联回归及正式包部署尚未完成。
|
||||||
|
- [!] 原生正式构建被安全检查拦截,未部署未完成校验的产物;最新详细状态见《Scert整改任务执行清单》同日入口。
|
||||||
|
- [!] 维持生产未放行状态,不把本次端口检查或启动通知整改计为 P0-10、邮件及三数据库首次流程通过。
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user