完善部署现场只读核查并补充Linux安全回归和进度
This commit is contained in:
parent
80b6c0c707
commit
44c88fe9b3
@ -44,6 +44,8 @@ type RecoveryLockObservation struct {
|
||||
// InspectPending 只读取待恢复记录与远端文件,绝不运行命令、建立锁或修改事务。
|
||||
// 缺失的历史证据保持缺失,当前观测摘要不能补写为当时的恢复证据。
|
||||
func (s *Service) InspectPending(ctx context.Context, id string, resolve func(context.Context, string) (Server, error)) (RecoveryInspection, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||
defer cancel()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return RecoveryInspection{}, err
|
||||
}
|
||||
@ -55,7 +57,7 @@ func (s *Service) InspectPending(ctx context.Context, id string, resolve func(co
|
||||
}
|
||||
records, err := s.journal.ListPendingDeploymentTransactions(ctx)
|
||||
if err != nil {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionUnavailable
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionUnavailable)
|
||||
}
|
||||
var record *TransactionRecord
|
||||
for i := range records {
|
||||
@ -74,7 +76,7 @@ func (s *Service) InspectPending(ctx context.Context, id string, resolve func(co
|
||||
}
|
||||
server, err := resolve(ctx, record.ServerID)
|
||||
if err != nil {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionUnavailable
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionUnavailable)
|
||||
}
|
||||
server = server.Normalize()
|
||||
if server.ID != record.ServerID || server.Validate() != nil {
|
||||
@ -88,7 +90,7 @@ func (s *Service) InspectPending(ctx context.Context, id string, resolve func(co
|
||||
}
|
||||
client, err := s.factory.Connect(ctx, server)
|
||||
if err != nil {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionUnavailable
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionUnavailable)
|
||||
}
|
||||
defer client.Close()
|
||||
reader, ok := client.(EvidenceClient)
|
||||
@ -121,13 +123,13 @@ func (s *Service) InspectPending(ctx context.Context, id string, resolve func(co
|
||||
}
|
||||
exists, err := client.Exists(ctx, item.path)
|
||||
if err != nil {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionRead
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionRead)
|
||||
}
|
||||
observation := RecoveryFileObservation{Path: item.path, Kind: item.kind, Exists: exists, Comparison: "missing"}
|
||||
if exists {
|
||||
digest, err := reader.FileDigest(ctx, item.path)
|
||||
if err != nil || !validDigest(digest) {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionRead
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionRead)
|
||||
}
|
||||
observation.Digest, observation.Comparison = digest, "unrecorded"
|
||||
if validDigest(item.expected) {
|
||||
@ -142,12 +144,12 @@ func (s *Service) InspectPending(ctx context.Context, id string, resolve func(co
|
||||
lock := RecoveryLockObservation{Path: target + ".scert-lock", Ownership: "missing"}
|
||||
lock.Exists, err = client.Exists(ctx, lock.Path)
|
||||
if err != nil {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionRead
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionRead)
|
||||
}
|
||||
if lock.Exists {
|
||||
owner, err := reader.ReadLockOwner(ctx, lock.Path)
|
||||
if err != nil {
|
||||
return RecoveryInspection{}, ErrRecoveryInspectionRead
|
||||
return RecoveryInspection{}, inspectionError(ctx, ErrRecoveryInspectionRead)
|
||||
}
|
||||
lock.Ownership = "unproven"
|
||||
if record.Evidence != nil && validDigest(owner) {
|
||||
@ -166,6 +168,13 @@ func (s *Service) InspectPending(ctx context.Context, id string, resolve func(co
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func inspectionError(ctx context.Context, fallback error) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func validInspectionID(id string) bool {
|
||||
if len(id) == 0 || len(id) > 160 {
|
||||
return false
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type inspectionCancelClient struct {
|
||||
*inspectionClient
|
||||
cancel context.CancelFunc
|
||||
stage string
|
||||
}
|
||||
|
||||
func (c *inspectionCancelClient) Exists(ctx context.Context, path string) (bool, error) {
|
||||
if c.stage == "exists" || (c.stage == "lock-exists" && strings.HasSuffix(path, ".scert-lock")) {
|
||||
c.cancel()
|
||||
return false, errors.New("sensitive-read-error")
|
||||
}
|
||||
return c.memoryClient.Exists(ctx, path)
|
||||
}
|
||||
|
||||
func (c *inspectionCancelClient) FileDigest(ctx context.Context, path string) (string, error) {
|
||||
if c.stage == "digest" {
|
||||
c.cancel()
|
||||
return "", errors.New("sensitive-read-error")
|
||||
}
|
||||
return c.memoryClient.FileDigest(ctx, path)
|
||||
}
|
||||
|
||||
func (c *inspectionCancelClient) ReadLockOwner(ctx context.Context, path string) (string, error) {
|
||||
if c.stage == "owner" {
|
||||
c.cancel()
|
||||
return "", errors.New("sensitive-read-error")
|
||||
}
|
||||
return c.memoryClient.ReadLockOwner(ctx, path)
|
||||
}
|
||||
|
||||
func TestRecoveryInspectionCancellationBoundaries(t *testing.T) {
|
||||
for _, stage := range []string{"resolve", "exists", "digest", "lock-exists", "owner"} {
|
||||
t.Run(stage, func(t *testing.T) {
|
||||
server, memory, record := recoveryPreflightFixture()
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
client := &inspectionCancelClient{inspectionClient: &inspectionClient{memoryClient: memory}, cancel: cancel, stage: stage}
|
||||
journal := &memoryTransactionJournal{pending: []TransactionRecord{record}}
|
||||
factory := &inspectionFactory{client: client}
|
||||
service := NewServiceWithJournal(factory, journal)
|
||||
result, err := service.InspectPending(ctx, record.ID, func(ctx context.Context, id string) (Server, error) {
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok || time.Until(deadline) > 30*time.Second {
|
||||
t.Fatal("inspection lacks bounded deadline")
|
||||
}
|
||||
if stage == "resolve" {
|
||||
cancel()
|
||||
return Server{}, errors.New("sensitive-resolve-error")
|
||||
}
|
||||
return server, nil
|
||||
})
|
||||
if !errors.Is(err, context.Canceled) || !reflect.DeepEqual(result, RecoveryInspection{}) {
|
||||
t.Fatalf("cancellation lost: %+v %v", result, err)
|
||||
}
|
||||
if client.closed != factory.calls || client.mutations != 0 || len(journal.updated) != 0 || len(journal.finished) != 0 {
|
||||
t.Fatal("cancellation changed state or leaked client")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveryInspectionPreservesEarlierDeadline(t *testing.T) {
|
||||
server, memory, record := recoveryPreflightFixture()
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Second)
|
||||
defer cancel()
|
||||
deadline, _ := ctx.Deadline()
|
||||
client := &inspectionClient{memoryClient: memory}
|
||||
service := NewServiceWithJournal(fixedClientFactory{client}, &memoryTransactionJournal{pending: []TransactionRecord{record}})
|
||||
_, err := service.InspectPending(ctx, record.ID, func(ctx context.Context, id string) (Server, error) {
|
||||
actual, ok := ctx.Deadline()
|
||||
if !ok || !actual.Equal(deadline) {
|
||||
t.Fatal("caller deadline extended")
|
||||
}
|
||||
return server, nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
154
backend/internal/deployment/recovery_inspection_sftp_test.go
Normal file
154
backend/internal/deployment/recovery_inspection_sftp_test.go
Normal file
@ -0,0 +1,154 @@
|
||||
//go:build linux
|
||||
|
||||
package deployment
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
func TestRecoveryInspectionOverPasswordSFTP(t *testing.T) {
|
||||
signer := newTestECDSAHostSigner(t)
|
||||
endpoint := startTestPasswordSSHServerWithHandler(t, func(password string) error {
|
||||
if password != "test-password" {
|
||||
return errors.New("invalid test password")
|
||||
}
|
||||
return nil
|
||||
}, serveTestSFTP, signer)
|
||||
for _, scenario := range []string{"trusted", "legacy", "target-symlink", "target-directory", "target-unreadable", "target-oversized", "owner-oversized", "owner-symlink", "lock-symlink"} {
|
||||
t.Run(scenario, func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
server, memory, record := recoveryPreflightFixture(root)
|
||||
model := testSSHServerModel(endpoint, "secret://ssh/inspection", ssh.FingerprintSHA256(signer.PublicKey()))
|
||||
server.Address, server.Port, server.Username = model.Address, model.Port, model.Username
|
||||
server.AuthMethod, server.PasswordRef, server.HostKeyFingerprint = model.AuthMethod, model.PasswordRef, model.HostKeyFingerprint
|
||||
record.Evidence.Address, record.Evidence.Port, record.Evidence.HostKeyFingerprint = server.Address, server.Port, server.HostKeyFingerprint
|
||||
for path, file := range memory.files {
|
||||
if strings.HasSuffix(path, ".scert-lock") {
|
||||
if err := os.MkdirAll(path, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(path, file.data, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
target := record.Paths[0]
|
||||
lock := record.LockPaths[0]
|
||||
owner := lock + "/owner"
|
||||
switch scenario {
|
||||
case "legacy":
|
||||
record.Evidence = nil
|
||||
case "target-symlink", "owner-symlink", "lock-symlink":
|
||||
name := target
|
||||
if scenario == "owner-symlink" {
|
||||
name = owner
|
||||
}
|
||||
if scenario == "lock-symlink" {
|
||||
name = lock
|
||||
}
|
||||
if err := os.Rename(name, name+".original"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Symlink(name+".original", name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "target-directory":
|
||||
if err := os.Rename(target, target+".original"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(target, 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "target-unreadable":
|
||||
if os.Geteuid() == 0 {
|
||||
t.Skip("requires unprivileged Linux test user")
|
||||
}
|
||||
if err := os.Chmod(target, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "target-oversized":
|
||||
if err := os.Truncate(target, 16*1024*1024+1); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
case "owner-oversized":
|
||||
if err := os.Truncate(owner, 129); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
before := snapshotInspectionFiles(t, root)
|
||||
journal := &memoryTransactionJournal{pending: []TransactionRecord{record}}
|
||||
service := NewServiceWithJournal(SSHFactory{
|
||||
Secrets: staticSecretReader{server.PasswordRef: {"password": "test-password"}},
|
||||
Timeout: 5 * time.Second,
|
||||
}, journal)
|
||||
result, err := service.InspectPending(t.Context(), record.ID, func(context.Context, string) (Server, error) { return server, nil })
|
||||
if scenario == "trusted" || scenario == "legacy" {
|
||||
if err != nil || len(result.Files) != 9 || len(result.Locks) != 3 || result.HistoricalEvidence != (scenario == "trusted") {
|
||||
t.Fatalf("inspection failed: %+v %v", result, err)
|
||||
}
|
||||
for _, file := range result.Files {
|
||||
want := "match"
|
||||
if scenario == "legacy" {
|
||||
want = "unrecorded"
|
||||
}
|
||||
if file.Comparison != want {
|
||||
t.Fatalf("unexpected observation: %+v", file)
|
||||
}
|
||||
}
|
||||
} else if !errors.Is(err, ErrRecoveryInspectionRead) || !reflect.DeepEqual(result, RecoveryInspection{}) {
|
||||
t.Fatalf("unsafe or unreadable file was accepted: %+v %v", result, err)
|
||||
}
|
||||
if !reflect.DeepEqual(before, snapshotInspectionFiles(t, root)) || len(journal.updated) != 0 || len(journal.finished) != 0 {
|
||||
t.Fatal("read-only inspection changed files or journal")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func snapshotInspectionFiles(t *testing.T, root string) map[string]string {
|
||||
t.Helper()
|
||||
result := make(map[string]string)
|
||||
err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value := info.Mode().String()
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
target, err := os.Readlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value += target
|
||||
} else if info.Mode().IsRegular() && info.Mode().Perm() != 0 {
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
value += contentDigest(data)
|
||||
}
|
||||
result[path] = value
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@ -300,6 +300,26 @@ func (c *sshClientAdapter) CreateOwnedLock(ctx context.Context, path, token stri
|
||||
|
||||
func (c *sshClientAdapter) ReadLockOwner(ctx context.Context, path string) (string, error) {
|
||||
return sshOperation(ctx, c, func() (string, error) {
|
||||
info, err := c.sftp.Lstat(path)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return "", ErrRecoveryInspectionRead
|
||||
}
|
||||
info, err = c.sftp.Lstat(path + "/owner")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.Mode().IsRegular() || info.Size() > 128 {
|
||||
return "", ErrRecoveryInspectionRead
|
||||
}
|
||||
file, err := c.sftp.Open(path + "/owner")
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return "", nil
|
||||
@ -308,7 +328,10 @@ func (c *sshClientAdapter) ReadLockOwner(ctx context.Context, path string) (stri
|
||||
return "", err
|
||||
}
|
||||
defer file.Close()
|
||||
data, err := io.ReadAll(file)
|
||||
data, err := io.ReadAll(io.LimitReader(file, 129))
|
||||
if len(data) > 128 {
|
||||
return "", ErrRecoveryInspectionRead
|
||||
}
|
||||
return strings.TrimSpace(string(data)), err
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Linux 生产流程测试记录
|
||||
|
||||
> 本轮最新证据见文末「2026年10月3日17:22 只读恢复核查原生 Linux 回归」,不代表正式部署验收通过。
|
||||
|
||||
## 2026年10月2日21:30 手动全局续期后台化回归(未部署)
|
||||
|
||||
- [x] 修改前基线 4ab3f93 已提交推送。本批继续只用密码 SSH/SCP 和严格主机密钥校验;第一台隔离目录 /var/tmp/scert-report-race-20261002-1801/renewal-source,以 nobody 运行原生测试,不创建辅助验收脚本。
|
||||
@ -2169,3 +2171,24 @@ SQLite 新测试程序 SHA-256:4c81656fae63a2eebad999d58e6a79e63e6f441810e02ce
|
||||
- [~] 补齐程序独立启动防护:Linux 默认及空值回退改为回环地址,CLI 配置入口、初始化入口、正式应用入口在建立资源前拒绝非回环监听;新增 Linux 回归用例待执行。
|
||||
- [!] 12:26 正式构建因 GOFLAGS 覆盖被拒绝;12:30 重试移除覆盖后,12:31 被 install-linux.sh 的发布路径扫描拦截。两次均判失败,不以进程启动时的 Result=success 冒充构建成功,不绕过路径检查。
|
||||
- [ ] 最新代码正式候选包、双机升级及严格 HTTPS 复测待完成。
|
||||
## 2026年10月3日17:22 只读恢复核查原生 Linux 回归(未部署)
|
||||
|
||||
- [x] 修改前检查点 `80b6c0c` 已推送。第一台密码 SSH 使用既有固定主机密钥严格校验,未启用密钥登录,未绕过 TLS。
|
||||
- [x] 基线源码归档 `scert-inspection-80b6c0c.tar` 两端 SHA256 一致:`0032a7f3bbfc4d6de0258dddff50679490fd7cc11b5603c956f851407a476cf0`。归档仅包含已跟踪后端源码,不包含运行时数据库或秘密。
|
||||
- [x] 测试目录为 `/var/tmp/scert-inspection-20261003-1714/backend`;Go 为既有 1.26.6 linux/amd64;以 nobody 身份运行隔离文件测试,生产数据库未清理或参与测试。
|
||||
- [x] 基线 `TestRecoveryInspection` 通过。后续上传四份源文件修复和新增测试后,专项命令 `go test -count=1 -timeout=180s -v ./internal/deployment -run 'TestRecoveryInspection|TestRecoveryPreflightOverPasswordSFTP'` 退出码 0。
|
||||
- [x] 真实密码 SSH/SFTP 的 9 个核查场景全部通过,包括可信、无历史证据、目标符号链接、目录、不可读、超大文件、超大归属文件、归属文件符号链接和锁目录符号链接。
|
||||
- [x] `go test -race -count=1 -timeout=240s ./internal/deployment` 退出码 0,`go vet ./internal/deployment` 退出码 0。
|
||||
- [~] `go test -p 2 -count=1 -timeout=8m ./...` 已启动,另行记录终态;不得将上述专项扩称为后端全量通过。
|
||||
|
||||
日志在测试目录上一级:`inspection.log`、`deployment-race.log`、`deployment-vet.log`、`backend-full.log`。本轮没有创建辅助验收脚本,没有远端备份,没有修改业务事务终态,也未部署新增功能。只读采样不构成原子快照、Nginx/HTTPS 或历史提交/回滚证据。
|
||||
|
||||
### 17:35 全量测试环境修复与后续执行
|
||||
|
||||
- [x] 外部依赖代理连通性探测失败,curl 退出码 28;原联网下载进程已中止,并确认相关 PID 消失,未将该次测试记为通过。
|
||||
- [x] 复用第一台既有离线代理,保留项目 go.sum 校验;首次离线全量编译期间 SSH 响应变慢且新连接握手超时,已中止该次全量执行并确认测试 PID 消失,不能据此认定是代码错误或 OOM。
|
||||
- [x] 17:33 再次核实 Scert 与 nginx 均为 active;本机严格 TLS 校验的管理域名 `/readyz` 返回 200。此为服务器回环路径检查,不代表公共入口验收。核查时约 889 MiB 可用内存,1 GiB swap 未使用,指定时段内未检出内核 OOM 记录。
|
||||
- [x] 四份增量 Go 文件的服务器 SHA256 与本地逐一一致。
|
||||
- [~] 改为受限单元 `scert-inspection-full-20261003-1735.service` 执行同一份源码的后端全量测试:nobody、单编译并发、CPU 50%、MemoryHigh 500 MiB、MemoryMax 650 MiB、20 分钟运行上限,且 Go 内存软限制 350 MiB。日志为 `backend-full-bounded.log`,终态尚待获取。
|
||||
|
||||
这只是隔离测试进程的资源限制,没有调整生产服务配置、重启生产服务或清理业务数据。
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Scert 开发任务清单
|
||||
|
||||
> 当前进度见文末「2026年10月3日17:22 恢复核查开发状态补充」,总体开发和生产验收仍未完成。
|
||||
|
||||
## 2026年10月3日 当前开发与验收任务入口
|
||||
|
||||
本轮只处理 Linux,目标是在不放开额外公网端口、不降低 TLS 校验、不使用辅助验收脚本的前提下完成上线。
|
||||
@ -3872,3 +3874,14 @@ Linux Ubuntu 22.04 上的 `Scert-admin` 手动启停、无开机自启动、无
|
||||
- [~] P0-10 其他任务、跨数据库实机矩阵和统一复审继续进行中。
|
||||
- [ ] 真实生产数据库、密码 SSH、远端部署、HTTPS/DNS、ACME、Linux/Windows 服务和最终生产门禁仍未完成。
|
||||
- [!] 不新增 `[✓]`,不以本地结果代替生产验收。
|
||||
## 2026年10月3日17:22 恢复核查开发状态补充
|
||||
|
||||
- [x] 只读核查底层实现完成并通过 Linux 专项,等待复审:9 个文件观察和 3 个锁观察,缺历史证据保持缺失,不回传敏感内容,不运行远端命令或更新事务。
|
||||
- [x] 新增整体 30 秒超时和调用者更短超时保留;取消发生在解析服务器、存在性检查、摘要和锁读取时均不返回半份结果。
|
||||
- [x] 锁目录和归属文件拒绝符号链接,归属文件限制为 128 字节;真实 SFTP 包含不可读、目录、超大文件等拒绝场景。
|
||||
- [x] 部署包竞态测试、静态检查通过;原生 Linux 项目内测试,不属于正式产品入口验收。
|
||||
- [ ] 本机 IPC/终端、人工处置记录和审计、并发确认与双机正式安装仍未完成,不宣布开发全部完成。
|
||||
|
||||
### 17:35 全量回归状态
|
||||
|
||||
后端全量因测试环境问题两次中止,尚未通过。当前使用离线依赖缓存和内存/CPU 受限 Linux 单元继续验证;没有因此缩减开发范围,也没有把 IPC、人工处置或正式部署标为完成。
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Scert 整改任务执行清单
|
||||
|
||||
> 当前进度见文末「2026年10月3日17:22 当前执行进度补充」,历史章节保留供追溯。
|
||||
|
||||
## 2026年10月3日 当前任务清单:Linux 双服务器上线冲刺
|
||||
|
||||
### 当前需求与验收任务
|
||||
@ -2322,3 +2324,17 @@ P0-05 从 `[!]` 调整为 `[~]`,并未标记完成;P0-04 剩余门禁和全
|
||||
### 当前结论
|
||||
|
||||
本批已经从“候选包未完成”推进到“最新候选包双机运行并完成基础安全复核”,但仍不能标记最终生产验收通过。当前状态保持 **P0-03:完成,等待复审**;下一阶段优先处理第一台管理证书 SAN、历史恢复现场核查和清库后的浏览器业务验收。
|
||||
## 2026年10月3日17:22 当前执行进度补充
|
||||
|
||||
- [x] 本轮修改前检查点 `80b6c0c` 已提交并推送;历史检查点 `ba57490` 也已成功推送。
|
||||
- [x] 第一台原生 Linux Go 1.26.6、nobody 隔离测试用户已核验;复用既有工具链,不新增公网端口,不创建辅助测试脚本。
|
||||
- [x] 部署只读核查增加整体超时、取消语义和锁归属读取限制;专项、部署包全量竞态与静态检查均通过。
|
||||
- [~] 后端全量回归已启动,结果未出之前不标通过。
|
||||
- [ ] 后续顺序:本机 IPC 和终端入口、人工处置审计和并发幂等、Linux 正式构建、双机及数据库/邮件/DNS/HTTPS/回滚完整验收。
|
||||
|
||||
生产服务和历史恢复现场未因隔离测试改变;总体上线门禁仍未通过。具体专项证据见《Linux生产流程测试记录》和《部署事务人工核查任务清单》。
|
||||
|
||||
### 17:35 验证执行状态
|
||||
|
||||
- [x] 已确认中止的联网及首次离线全量测试进程退出;复核生产服务和本机严格 TLS 就绪状态。
|
||||
- [~] 后端全量改用 `scert-inspection-full-20261003-1735.service` 受限运行,结果待获取;不得按前面的专项结果替代。
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# Scert 整改、合并与优化清单
|
||||
|
||||
> 当前进度以文末「2026年10月3日17:22 最新执行状态」为准:恢复核查底层专项已通过,本机入口和生产门禁仍未完成。
|
||||
|
||||
## 2026年10月3日 Linux上线冲刺任务清单(当前执行入口)
|
||||
|
||||
本轮只处理 Linux,不处理 Windows;目标是完成明日上线前的可验收闭环。生产结论必须来自两台真实服务器、真实 HTTPS、浏览器、数据库、邮件和回滚证据,不能用构建或单元测试替代。
|
||||
@ -2865,3 +2867,18 @@ P0-11 生产验收门禁
|
||||
### 当前结论
|
||||
|
||||
本批已经从“候选包未完成”推进到“最新候选包双机运行并完成基础安全复核”,但仍不能标记最终生产验收通过。当前状态保持 **P0-03:完成,等待复审**;下一阶段优先处理第一台管理证书 SAN、历史恢复现场核查和清库后的浏览器业务验收。
|
||||
## 2026年10月3日17:22 最新执行状态:恢复核查底层已通过 Linux 专项
|
||||
|
||||
本节是当前进度补充,后续历史章节不能覆盖本节。范围仍为 Linux 双机上线,Windows 不在本轮范围。
|
||||
|
||||
- [x] 第二台磁盘清理后于 17:08 实测:29 GB 总量、3.2 GB 已用、26 GB 可用、使用率 12%,inode 使用率 3%;Scert、nginx、mysql、scert-nginx、pmail 均为 active。
|
||||
- [x] P0-10 恢复核查子项 R1 已完成编码并通过原生 Linux 专项、部署包全量竞态测试和静态检查,等待完整流程复审。详见《部署事务人工核查任务清单》。
|
||||
- [ ] 恢复核查本机 IPC 和终端入口、独立人工处置及审计、幂等和并发门禁尚未完成;现有无历史证据事务仍不得自动转为成功。
|
||||
- [ ] 本轮新代码尚未打包上线;三数据库首次 Web、真实邮件与重置、双机正式域名正负向、DNS/ACME 和回滚验收继续保留。
|
||||
- [~] 后端全量回归正在执行,专项测试不能代替全量结果或生产验收。
|
||||
|
||||
历史章节中的第一台管理证书 SAN 结论本轮未重新验证,不能作为当前证书状态。总体生产验收仍未通过,不因磁盘空间恢复或专项测试通过而关闭 P0 门禁。
|
||||
|
||||
### 17:35 后端全量回归进度
|
||||
|
||||
前两次全量尝试分别因依赖代理不可达及 SSH 响应异常而中止,没有通过结论。服务 active 和本机严格 TLS 就绪检查已复核,当前以限制内存和 CPU 的 Linux 单元重新执行全量回归,等待终态。未修改生产配置或历史恢复记录,未关闭任何生产门禁。
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# 部署事务人工核查任务清单
|
||||
|
||||
> 当前状态以文末 2026年10月3日17:22 补充为准:R1 已完成等待复审,R2 至 R5 未完成,R6 持续同步。
|
||||
|
||||
## 2026年10月3日 需求确认与执行顺序
|
||||
|
||||
依据现有整改清单的 P0-10 恢复门禁继续开发,仅处理 Linux。人工核查不能用当前正常状态补造历史部署证据;不能直接修改生产数据库终态。
|
||||
@ -17,3 +19,23 @@
|
||||
- 服务器身份或历史路径无效时拒绝读取;证据缺失时明确报告,不自动生成恢复证据、不删锁、不清理现场。
|
||||
- 保留现有密码重置通道的身份校验及邮箱验证码要求,不因接入核查而降低密码重置安全边界。
|
||||
- Git 修改前检查点为 ba57490;推送收到 Authentication failed,本地继续,远端同步仍待解决。
|
||||
## 2026年10月3日17:22 当前进度补充
|
||||
|
||||
本节优先于下方历史状态;保留历史原文,不替换中文字符。
|
||||
|
||||
- [x] R1:只读核查底层已实现,待产品入口和完整流程复审。整体操作限制为 30 秒,尊重调用者更短的超时;读取失败或取消时返回空结果,不把部分观察当成成功。
|
||||
- [x] 锁归属读取增加类型和大小检查:拒绝符号链接锁目录、符号链接归属文件及超过 128 字节的归属文件,限制实际读取长度。
|
||||
- [x] 原生 Linux 专项通过:5 个取消边界、较早超时保留、9 个真实密码 SSH/SFTP 场景、原有只读核查和恢复预检用例。不可读文件场景以 nobody 用户运行并通过,没有因 root 权限跳过。
|
||||
- [x] 部署包全量竞态测试通过:`go test -race -count=1 -timeout=240s ./internal/deployment`,退出码 0;`go vet ./internal/deployment` 退出码 0。
|
||||
- [ ] R2:本机 IPC、终端列表及核查菜单尚未接入,不能将底层方法描述为用户已经可以使用的功能。
|
||||
- [ ] R3、R4:人工处置、确认幂等、并发互斥和独立审计尚未完成;本轮取消测试不代表人工处置流程全部完成。
|
||||
- [ ] R5:此次新代码尚未进入双机正式候选包,不改变线上历史事务状态。
|
||||
- [~] R6:同步总清单、执行清单、开发清单和 Linux 测试记录;总体生产验收保持未通过。
|
||||
|
||||
修正历史 Git 状态:`ba57490` 后续已经推送成功;本轮修改前检查点 `80b6c0c` 也已推送成功,不再存在该检查点认证阻塞。
|
||||
|
||||
证据目录:第一台 `/var/tmp/scert-inspection-20261003-1714/`,专项 `inspection.log`、竞态 `deployment-race.log`、静态检查 `deployment-vet.log`。这里运行的是项目内 Go 测试,不是辅助验收脚本;未清理业务数据库、证书或恢复现场。后端全量测试已启动,结果另行记录。
|
||||
|
||||
### 17:35 全量验证状态跟进
|
||||
|
||||
联网全量运行因代理不可达中止;首次离线全量运行出现 SSH 响应异常后主动中止,均未记为通过。生产 Scert/nginx 已复核 active,本机严格 TLS 的 `/readyz` 返回 200。当前以受限 Linux 单元运行后端全量测试,内存上限 650 MiB、CPU 50%,结果仍待获取。R1 的专项、竞态和静态检查结论不变,R2 至 R5 仍未完成。
|
||||
|
||||
Loading…
Reference in New Issue
Block a user