From 4be44733e8ad7d7bcef4d61457490a2769c15a63 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 14 Mar 2026 17:16:54 +0800 Subject: [PATCH] feat: replace cert_dir with support_dir in agent and server configurations - Updated README.md to reflect the new support_dir for auxiliary files. - Refactored agent main.go to use support_dir instead of cert_dir. - Modified config.go to replace cert_dir with support_dir and added legacy support. - Adjusted config tests to validate support_dir usage. - Changed nginx manager to utilize support_dir for file paths. - Updated server configuration to use support_dir for SSL certificates. - Revised documentation to clarify the new configuration parameters. - Enhanced security checks for support file paths to prevent traversal attacks. --- README.md | 1 + atsf_agent/cmd/agent/main.go | 14 +-- atsf_agent/internal/config/config.go | 44 ++++++--- atsf_agent/internal/config/config_test.go | 16 +-- atsf_agent/internal/nginx/manager.go | 74 +++++++------- atsf_agent/internal/nginx/manager_test.go | 114 +++++++++++----------- atsf_server/service/config_version.go | 6 +- atsf_server/service/https_phase1_test.go | 4 +- docs/app-config.md | 17 ++-- docs/deployment.md | 6 +- docs/improve_plan.md | 6 +- 11 files changed, 159 insertions(+), 143 deletions(-) diff --git a/README.md b/README.md index 1473f91f..61e5bdc3 100644 --- a/README.md +++ b/README.md @@ -214,6 +214,7 @@ Docker 镜像工作流仅构建 `atsf_server`,并产出 `linux/amd64` 与 `lin | `agent_token` | 节点专属认证 Token | | `discovery_token` | 首次自动注册使用的全局 Token | | `data_dir` | Agent 托管数据目录 | +| `support_dir` | Agent 存放受管附属文件的目录,当前包含证书与 Lua 观测脚本 | | `openresty_observability_port` | Agent 读取 OpenResty Lua 本地观测指标的 loopback 端口 | | `nginx_path` | 本机 Nginx 路径,设置后走本机模式 | | `nginx_container_name` | Docker 模式下的 Nginx 容器名 | diff --git a/atsf_agent/cmd/agent/main.go b/atsf_agent/cmd/agent/main.go index a3efaa7a..0dd1bec6 100644 --- a/atsf_agent/cmd/agent/main.go +++ b/atsf_agent/cmd/agent/main.go @@ -39,8 +39,8 @@ func main() { Image: cfg.OpenrestyDockerImage, MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, - CertDir: cfg.CertDir, - NginxCertDir: cfg.OpenrestyCertDir, + SupportDir: cfg.SupportDir, + NginxSupportDir: cfg.OpenrestySupportDir, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, }, ) @@ -50,7 +50,7 @@ func main() { "ip", cfg.NodeIP, "heartbeat_interval", cfg.HeartbeatInterval, "route_config", cfg.RouteConfigPath, - "cert_dir", cfg.CertDir, + "support_dir", cfg.SupportDir, ) client := httpclient.New(cfg.ServerURL, cfg.InitialAuthToken(), cfg.RequestTimeout.Duration()) @@ -63,8 +63,8 @@ func main() { MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, RuntimeRouteConfigPath: runtimeRouteConfigPath, - CertDir: cfg.CertDir, - NginxCertDir: cfg.OpenrestyCertDir, + SupportDir: cfg.SupportDir, + NginxSupportDir: cfg.OpenrestySupportDir, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, Executor: nginx.NewExecutor(nginx.ExecutorOptions{ NginxPath: cfg.OpenrestyPath, @@ -73,8 +73,8 @@ func main() { Image: cfg.OpenrestyDockerImage, MainConfigPath: cfg.MainConfigPath, RouteConfigPath: cfg.RouteConfigPath, - CertDir: cfg.CertDir, - NginxCertDir: cfg.OpenrestyCertDir, + SupportDir: cfg.SupportDir, + NginxSupportDir: cfg.OpenrestySupportDir, OpenrestyObservabilityPort: cfg.OpenrestyObservabilityPort, }), } diff --git a/atsf_agent/internal/config/config.go b/atsf_agent/internal/config/config.go index 669fe0fb..8821f550 100644 --- a/atsf_agent/internal/config/config.go +++ b/atsf_agent/internal/config/config.go @@ -14,9 +14,9 @@ import ( const ( defaultDockerMainConfigRelativePath = "etc/nginx/nginx.conf" defaultDockerRouteConfigRelativePath = "etc/nginx/conf.d/atsflare_routes.conf" - defaultCertDirRelativePath = "etc/nginx/certs" + defaultSupportDirRelativePath = "etc/nginx/support" defaultDockerStateRelativePath = "var/lib/atsflare/agent-state.json" - defaultDockerOpenRestyCertDir = "/etc/nginx/atsflare-certs" + defaultDockerOpenRestySupportDir = "/etc/nginx/atsflare-support" defaultOpenRestyObservabilityPort = 18081 ) @@ -35,8 +35,8 @@ type Config struct { DataDir string `json:"data_dir"` MainConfigPath string `json:"main_config_path"` RouteConfigPath string `json:"route_config_path"` - CertDir string `json:"cert_dir"` - OpenrestyCertDir string `json:"openresty_cert_dir"` + SupportDir string `json:"support_dir"` + OpenrestySupportDir string `json:"openresty_support_dir"` OpenrestyObservabilityPort int `json:"openresty_observability_port"` StatePath string `json:"state_path"` HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"` @@ -57,8 +57,10 @@ type configFile struct { DataDir string `json:"data_dir"` MainConfigPath string `json:"main_config_path"` RouteConfigPath string `json:"route_config_path"` - CertDir string `json:"cert_dir"` - OpenrestyCertDir string `json:"openresty_cert_dir"` + SupportDir string `json:"support_dir"` + OpenrestySupportDir string `json:"openresty_support_dir"` + LegacyCertDir string `json:"cert_dir"` + LegacyOpenrestyCertDir string `json:"openresty_cert_dir"` OpenrestyObservabilityPort int `json:"openresty_observability_port"` StatePath string `json:"state_path"` HeartbeatInterval MillisecondDuration `json:"heartbeat_interval"` @@ -87,8 +89,8 @@ func Load(path string) (*Config, error) { DataDir: file.DataDir, MainConfigPath: file.MainConfigPath, RouteConfigPath: file.RouteConfigPath, - CertDir: file.CertDir, - OpenrestyCertDir: file.OpenrestyCertDir, + SupportDir: firstNonEmpty(file.SupportDir, file.LegacyCertDir), + OpenrestySupportDir: firstNonEmpty(file.OpenrestySupportDir, file.LegacyOpenrestyCertDir), OpenrestyObservabilityPort: file.OpenrestyObservabilityPort, StatePath: file.StatePath, HeartbeatInterval: file.HeartbeatInterval, @@ -138,14 +140,14 @@ func applyDefaults(cfg *Config, baseDir string) { cfg.StatePath = joinManagedPath(cfg.DataDir, defaultDockerStateRelativePath) } } - if cfg.CertDir == "" { - cfg.CertDir = joinManagedPath(cfg.DataDir, defaultCertDirRelativePath) + if cfg.SupportDir == "" { + cfg.SupportDir = joinManagedPath(cfg.DataDir, defaultSupportDirRelativePath) } - if cfg.OpenrestyCertDir == "" { + if cfg.OpenrestySupportDir == "" { if cfg.OpenrestyPath != "" { - cfg.OpenrestyCertDir = cfg.CertDir + cfg.OpenrestySupportDir = cfg.SupportDir } else { - cfg.OpenrestyCertDir = defaultDockerOpenRestyCertDir + cfg.OpenrestySupportDir = defaultDockerOpenRestySupportDir } } if cfg.OpenrestyObservabilityPort <= 0 { @@ -173,8 +175,11 @@ func normalizeManagedPaths(cfg *Config) { if usesSlashPath(cfg.RouteConfigPath) { cfg.RouteConfigPath = filepath.ToSlash(cfg.RouteConfigPath) } - if usesSlashPath(cfg.CertDir) { - cfg.CertDir = filepath.ToSlash(cfg.CertDir) + if usesSlashPath(cfg.SupportDir) { + cfg.SupportDir = filepath.ToSlash(cfg.SupportDir) + } + if usesSlashPath(cfg.OpenrestySupportDir) { + cfg.OpenrestySupportDir = filepath.ToSlash(cfg.OpenrestySupportDir) } if usesSlashPath(cfg.StatePath) { cfg.StatePath = filepath.ToSlash(cfg.StatePath) @@ -243,6 +248,15 @@ func detectHostname() string { return strings.TrimSpace(host) } +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" +} + func detectNodeIP() string { interfaces, err := net.Interfaces() if err != nil { diff --git a/atsf_agent/internal/config/config_test.go b/atsf_agent/internal/config/config_test.go index 1fe54864..143b1124 100644 --- a/atsf_agent/internal/config/config_test.go +++ b/atsf_agent/internal/config/config_test.go @@ -39,8 +39,8 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) { if cfg.RouteConfigPath != filepath.Join(dir, "data", defaultDockerRouteConfigRelativePath) { t.Fatalf("unexpected route config path: %s", cfg.RouteConfigPath) } - if cfg.CertDir != filepath.Join(dir, "data", defaultCertDirRelativePath) { - t.Fatalf("unexpected cert dir: %s", cfg.CertDir) + if cfg.SupportDir != filepath.Join(dir, "data", defaultSupportDirRelativePath) { + t.Fatalf("unexpected support dir: %s", cfg.SupportDir) } if cfg.OpenrestyContainerName != "atsflare-openresty" { t.Fatalf("unexpected openresty container name: %s", cfg.OpenrestyContainerName) @@ -48,8 +48,8 @@ func TestLoadDockerModeUsesManagedPaths(t *testing.T) { if cfg.OpenrestyDockerImage != "openresty/openresty:alpine" { t.Fatalf("unexpected openresty image: %s", cfg.OpenrestyDockerImage) } - if cfg.OpenrestyCertDir != defaultDockerOpenRestyCertDir { - t.Fatalf("unexpected openresty cert dir: %s", cfg.OpenrestyCertDir) + if cfg.OpenrestySupportDir != defaultDockerOpenRestySupportDir { + t.Fatalf("unexpected openresty support dir: %s", cfg.OpenrestySupportDir) } if cfg.StatePath != filepath.Join(dir, "data", defaultDockerStateRelativePath) { t.Fatalf("unexpected state path: %s", cfg.StatePath) @@ -94,8 +94,8 @@ func TestLoadPathModeKeepsExplicitPaths(t *testing.T) { if cfg.StatePath != "/tmp/agent-state.json" { t.Fatalf("unexpected state path: %s", cfg.StatePath) } - if cfg.OpenrestyCertDir != cfg.CertDir { - t.Fatalf("expected path mode openresty cert dir to equal cert dir, got %s / %s", cfg.OpenrestyCertDir, cfg.CertDir) + if cfg.OpenrestySupportDir != cfg.SupportDir { + t.Fatalf("expected path mode openresty support dir to equal support dir, got %s / %s", cfg.OpenrestySupportDir, cfg.SupportDir) } if cfg.OpenrestyObservabilityPort != defaultOpenRestyObservabilityPort { t.Fatalf("unexpected path mode openresty observability port: %d", cfg.OpenrestyObservabilityPort) @@ -134,8 +134,8 @@ func TestLoadUsesCustomDataDirForGeneratedFiles(t *testing.T) { if cfg.StatePath != "/srv/atsflare/"+defaultDockerStateRelativePath { t.Fatalf("unexpected state path: %s", cfg.StatePath) } - if cfg.CertDir != "/srv/atsflare/"+defaultCertDirRelativePath { - t.Fatalf("unexpected cert dir: %s", cfg.CertDir) + if cfg.SupportDir != "/srv/atsflare/"+defaultSupportDirRelativePath { + t.Fatalf("unexpected support dir: %s", cfg.SupportDir) } } diff --git a/atsf_agent/internal/nginx/manager.go b/atsf_agent/internal/nginx/manager.go index 74b51280..9ee49a74 100644 --- a/atsf_agent/internal/nginx/manager.go +++ b/atsf_agent/internal/nginx/manager.go @@ -17,7 +17,7 @@ import ( "atsflare-agent/internal/protocol" ) -const CertDirPlaceholder = "__ATSF_CERT_DIR__" +const SupportDirPlaceholder = "__ATSF_SUPPORT_DIR__" const RouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__" const AccessLogPlaceholder = "__ATSF_ACCESS_LOG__" const LuaDirPlaceholder = "__ATSF_LUA_DIR__" @@ -104,8 +104,8 @@ type DockerExecutor struct { Image string MainConfigPath string RouteConfigDir string - CertDir string - NginxCertDir string + SupportDir string + NginxSupportDir string OpenrestyObservabilityPort int Runner CommandRunner } @@ -186,7 +186,7 @@ func (e *DockerExecutor) runContainer(ctx context.Context) error { "-p", fmt.Sprintf("127.0.0.1:%d:%d", e.OpenrestyObservabilityPort, e.OpenrestyObservabilityPort), "-v", fmt.Sprintf("%s:%s", e.MainConfigPath, DockerMainConfigPath), "-v", fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir), - "-v", fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir), + "-v", fmt.Sprintf("%s:%s", e.SupportDir, e.NginxSupportDir), e.Image, } runOutput, runErr := e.Runner.Run(ctx, e.DockerBinary, runArgs...) @@ -201,8 +201,8 @@ type Manager struct { MainConfigPath string RouteConfigPath string RuntimeRouteConfigPath string - CertDir string - NginxCertDir string + SupportDir string + NginxSupportDir string OpenrestyObservabilityPort int Executor Executor } @@ -302,8 +302,8 @@ func (m *Manager) CurrentChecksum() (string, error) { normalizedMain = strings.ReplaceAll(normalizedMain, fmt.Sprintf("%d", m.OpenrestyObservabilityPort), ObservabilityPortPlaceholder) } normalizedRoute := string(data) - if m.NginxCertDir != "" { - normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxCertDir, CertDirPlaceholder) + if m.NginxSupportDir != "" { + normalizedRoute = strings.ReplaceAll(normalizedRoute, m.NginxSupportDir, SupportDirPlaceholder) } files, err := m.readSupportFiles() if err != nil { @@ -321,8 +321,8 @@ type ExecutorOptions struct { Image string MainConfigPath string RouteConfigPath string - CertDir string - NginxCertDir string + SupportDir string + NginxSupportDir string OpenrestyObservabilityPort int } @@ -342,9 +342,9 @@ func NewExecutor(options ExecutorOptions) Executor { if absDir, err := filepath.Abs(routeConfigDir); err == nil { routeConfigDir = absDir } - certDir := options.CertDir - if absDir, err := filepath.Abs(certDir); err == nil { - certDir = absDir + supportDir := options.SupportDir + if absDir, err := filepath.Abs(supportDir); err == nil { + supportDir = absDir } return &DockerExecutor{ DockerBinary: options.DockerBinary, @@ -352,8 +352,8 @@ func NewExecutor(options ExecutorOptions) Executor { Image: options.Image, MainConfigPath: mainConfigPath, RouteConfigDir: routeConfigDir, - CertDir: certDir, - NginxCertDir: options.NginxCertDir, + SupportDir: supportDir, + NginxSupportDir: options.NginxSupportDir, OpenrestyObservabilityPort: options.OpenrestyObservabilityPort, Runner: runner, } @@ -426,7 +426,7 @@ func (e *DockerExecutor) runEphemeralRuntimeCommandWithBinary(ctx context.Contex "-v", fmt.Sprintf("%s:/etc/nginx/conf.d", e.RouteConfigDir), "-v", - fmt.Sprintf("%s:%s", e.CertDir, e.NginxCertDir), + fmt.Sprintf("%s:%s", e.SupportDir, e.NginxSupportDir), e.Image, runtimeBinary, } @@ -459,8 +459,8 @@ func (m *Manager) backup() (*backupState, error) { if err := os.MkdirAll(filepath.Dir(m.RouteConfigPath), 0o755); err != nil { return nil, err } - if m.CertDir != "" { - if err := os.MkdirAll(m.CertDir, 0o755); err != nil { + if m.SupportDir != "" { + if err := os.MkdirAll(m.SupportDir, 0o755); err != nil { return nil, err } } @@ -507,13 +507,13 @@ func (m *Manager) restore(state *backupState) error { } else if err := os.Remove(m.RouteConfigPath); err != nil && !os.IsNotExist(err) { return err } - if m.CertDir == "" { + if m.SupportDir == "" { return nil } - if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) { + if err := os.RemoveAll(m.SupportDir); err != nil && !os.IsNotExist(err) { return err } - if err := os.MkdirAll(m.CertDir, 0o755); err != nil { + if err := os.MkdirAll(m.SupportDir, 0o755); err != nil { return err } for _, file := range state.Files { @@ -532,13 +532,13 @@ func (m *Manager) restore(state *backupState) error { } func (m *Manager) writeSupportFiles(supportFiles []protocol.SupportFile) error { - if m.CertDir == "" { + if m.SupportDir == "" { return nil } - if err := os.RemoveAll(m.CertDir); err != nil && !os.IsNotExist(err) { + if err := os.RemoveAll(m.SupportDir); err != nil && !os.IsNotExist(err) { return err } - if err := os.MkdirAll(m.CertDir, 0o755); err != nil { + if err := os.MkdirAll(m.SupportDir, 0o755); err != nil { return err } for _, file := range supportFiles { @@ -557,17 +557,17 @@ func (m *Manager) writeSupportFiles(supportFiles []protocol.SupportFile) error { } func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) { - if m.CertDir == "" { + if m.SupportDir == "" { return nil, nil } - if _, err := os.Stat(m.CertDir); err != nil { + if _, err := os.Stat(m.SupportDir); err != nil { if os.IsNotExist(err) { return nil, nil } return nil, err } files := make([]protocol.SupportFile, 0) - err := filepath.Walk(m.CertDir, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(m.SupportDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } @@ -578,7 +578,7 @@ func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) { if err != nil { return err } - relativePath, err := filepath.Rel(m.CertDir, path) + relativePath, err := filepath.Rel(m.SupportDir, path) if err != nil { return err } @@ -598,8 +598,8 @@ func (m *Manager) readSupportFiles() ([]protocol.SupportFile, error) { } func (m *Manager) supportFileTargetPath(relativePath string) (string, error) { - if strings.TrimSpace(m.CertDir) == "" { - return "", errors.New("cert dir 不能为空") + if strings.TrimSpace(m.SupportDir) == "" { + return "", errors.New("support dir 不能为空") } candidate := strings.TrimSpace(relativePath) if strings.Contains(candidate, `\`) { @@ -612,22 +612,22 @@ func (m *Manager) supportFileTargetPath(relativePath string) (string, error) { if filepath.IsAbs(normalizedPath) || filepath.VolumeName(normalizedPath) != "" { return "", fmt.Errorf("support file path %q must be relative", relativePath) } - targetPath := filepath.Join(m.CertDir, normalizedPath) - relativeToBase, err := filepath.Rel(m.CertDir, targetPath) + targetPath := filepath.Join(m.SupportDir, normalizedPath) + relativeToBase, err := filepath.Rel(m.SupportDir, targetPath) if err != nil { return "", err } if relativeToBase == ".." || strings.HasPrefix(relativeToBase, ".."+string(os.PathSeparator)) { - return "", fmt.Errorf("support file path %q escapes cert dir", relativePath) + return "", fmt.Errorf("support file path %q escapes support dir", relativePath) } return targetPath, nil } func (m *Manager) renderRouteConfig(content string) string { - if m.NginxCertDir == "" { + if m.NginxSupportDir == "" { return content } - return strings.ReplaceAll(content, CertDirPlaceholder, m.NginxCertDir) + return strings.ReplaceAll(content, SupportDirPlaceholder, m.NginxSupportDir) } func (m *Manager) renderMainConfig(content string) string { @@ -663,10 +663,10 @@ func (m *Manager) accessLogRuntimePath() string { } func (m *Manager) luaRuntimePath() string { - if strings.TrimSpace(m.NginxCertDir) == "" { + if strings.TrimSpace(m.NginxSupportDir) == "" { return "" } - return filepath.ToSlash(m.NginxCertDir) + return filepath.ToSlash(m.NginxSupportDir) } func checksum(content string) string { diff --git a/atsf_agent/internal/nginx/manager_test.go b/atsf_agent/internal/nginx/manager_test.go index ff902e65..f414c3d2 100644 --- a/atsf_agent/internal/nginx/manager_test.go +++ b/atsf_agent/internal/nginx/manager_test.go @@ -117,14 +117,14 @@ func TestDockerExecutorCheckHealthFailsWhenContainerStopped(t *testing.T) { }, } executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "atsflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - CertDir: filepath.Clean("/tmp/certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", - Runner: runner, + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/routes"), + SupportDir: filepath.Clean("/tmp/support"), + NginxSupportDir: "/etc/nginx/atsflare-support", + Runner: runner, } if err := executor.CheckHealth(context.Background()); err == nil { t.Fatal("expected CheckHealth to fail when container is not running") @@ -141,14 +141,14 @@ func TestDockerExecutorStartsContainerWhenMissing(t *testing.T) { }, } executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "atsflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - CertDir: filepath.Clean("/tmp/certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", - Runner: runner, + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/routes"), + SupportDir: filepath.Clean("/tmp/support"), + NginxSupportDir: "/etc/nginx/atsflare-support", + Runner: runner, } if err := executor.Test(context.Background()); err != nil { @@ -176,14 +176,14 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) { }, } executor := &DockerExecutor{ - DockerBinary: "docker", - ContainerName: "atsflare-openresty", - Image: "openresty/openresty:alpine", - MainConfigPath: filepath.Clean("/tmp/nginx.conf"), - RouteConfigDir: filepath.Clean("/tmp/routes"), - CertDir: filepath.Clean("/tmp/certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", - Runner: runner, + DockerBinary: "docker", + ContainerName: "atsflare-openresty", + Image: "openresty/openresty:alpine", + MainConfigPath: filepath.Clean("/tmp/nginx.conf"), + RouteConfigDir: filepath.Clean("/tmp/routes"), + SupportDir: filepath.Clean("/tmp/support"), + NginxSupportDir: "/etc/nginx/atsflare-support", + Runner: runner, } if err := executor.Reload(context.Background()); err != nil { @@ -207,7 +207,7 @@ func TestDockerExecutorStartsStoppedContainer(t *testing.T) { func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { mainConfigPath := filepath.Clean("/tmp/managed/nginx.conf") routeConfigDir := filepath.Clean("/tmp/managed/conf.d") - certDir := filepath.Clean("/tmp/managed/certs") + supportDir := filepath.Clean("/tmp/managed/support") runner := &fakeRunner{} executor := &DockerExecutor{ DockerBinary: "docker", @@ -215,8 +215,8 @@ func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { Image: "openresty/openresty:alpine", MainConfigPath: mainConfigPath, RouteConfigDir: routeConfigDir, - CertDir: certDir, - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: supportDir, + NginxSupportDir: "/etc/nginx/atsflare-support", OpenrestyObservabilityPort: 18081, Runner: runner, } @@ -237,7 +237,7 @@ func TestDockerExecutorRunContainerMountsManagedFiles(t *testing.T) { "-p", "127.0.0.1:18081:18081", "-v", mainConfigPath + ":" + DockerMainConfigPath, "-v", routeConfigDir + ":/etc/nginx/conf.d", - "-v", certDir + ":/etc/nginx/atsflare-certs", + "-v", supportDir + ":/etc/nginx/atsflare-support", "openresty/openresty:alpine", } if !reflect.DeepEqual(runner.calls[0].args, expectedArgs) { @@ -260,8 +260,8 @@ func TestDockerExecutorRecreatesContainerOnStartup(t *testing.T) { Image: "openresty/openresty:alpine", MainConfigPath: filepath.Clean("/tmp/nginx.conf"), RouteConfigDir: filepath.Clean("/tmp/routes"), - CertDir: filepath.Clean("/tmp/certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: filepath.Clean("/tmp/support"), + NginxSupportDir: "/etc/nginx/atsflare-support", OpenrestyObservabilityPort: 18081, Runner: runner, } @@ -287,8 +287,8 @@ func TestNewExecutorUsesAbsoluteDockerMountPath(t *testing.T) { Image: "openresty/openresty:alpine", MainConfigPath: "./data/etc/nginx/nginx.conf", RouteConfigPath: "./data/etc/nginx/conf.d/atsflare_routes.conf", - CertDir: "./data/etc/nginx/certs", - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: "./data/etc/nginx/support", + NginxSupportDir: "/etc/nginx/atsflare-support", OpenrestyObservabilityPort: 18081, }) @@ -330,19 +330,19 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { tempDir := t.TempDir() mainPath := filepath.Join(tempDir, "nginx.conf") routePath := filepath.Join(tempDir, "conf.d", "atsflare_routes.conf") - certDir := filepath.Join(tempDir, "certs") + supportDir := filepath.Join(tempDir, "support") manager := &Manager{ MainConfigPath: mainPath, RouteConfigPath: routePath, - CertDir: certDir, - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: supportDir, + NginxSupportDir: "/etc/nginx/atsflare-support", Executor: &fakeExecutor{}, } err := manager.Apply( context.Background(), "include __ATSF_ROUTE_CONFIG__;\naccess_log __ATSF_ACCESS_LOG__ atsflare_json;\n", - "ssl_certificate __ATSF_CERT_DIR__/1.crt;\n", + "ssl_certificate __ATSF_SUPPORT_DIR__/1.crt;\n", []protocol.SupportFile{{Path: "1.crt", Content: "cert"}}, ) if err != nil { @@ -362,7 +362,7 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { if err != nil { t.Fatalf("failed to read route config: %v", err) } - if string(routeData) != "ssl_certificate /etc/nginx/atsflare-certs/1.crt;\n" { + if string(routeData) != "ssl_certificate /etc/nginx/atsflare-support/1.crt;\n" { t.Fatalf("unexpected route config: %s", string(routeData)) } @@ -372,7 +372,7 @@ func TestManagerApplyAndChecksumIncludeMainConfig(t *testing.T) { } expected := bundleChecksum( "include __ATSF_ROUTE_CONFIG__;\naccess_log __ATSF_ACCESS_LOG__ atsflare_json;\n", - "ssl_certificate __ATSF_CERT_DIR__/1.crt;\n", + "ssl_certificate __ATSF_SUPPORT_DIR__/1.crt;\n", []protocol.SupportFile{{Path: "1.crt", Content: "cert"}}, ) if value != expected { @@ -388,8 +388,8 @@ func TestManagerApplyUsesRuntimeRouteConfigPath(t *testing.T) { MainConfigPath: mainPath, RouteConfigPath: routePath, RuntimeRouteConfigPath: DockerRouteConfigPath, - CertDir: filepath.Join(tempDir, "certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: filepath.Join(tempDir, "support"), + NginxSupportDir: "/etc/nginx/atsflare-support", Executor: &fakeExecutor{}, } @@ -462,12 +462,12 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { manager := &Manager{ MainConfigPath: filepath.Join(tempDir, "nginx.conf"), RouteConfigPath: filepath.Join(tempDir, "routes.conf"), - CertDir: filepath.Join(tempDir, "certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: filepath.Join(tempDir, "support"), + NginxSupportDir: "/etc/nginx/atsflare-support", Executor: &fakeExecutor{}, } - err := manager.Apply(context.Background(), "include __ATSF_ROUTE_CONFIG__;", "ssl_certificate __ATSF_CERT_DIR__/1.crt;", []protocol.SupportFile{ + err := manager.Apply(context.Background(), "include __ATSF_ROUTE_CONFIG__;", "ssl_certificate __ATSF_SUPPORT_DIR__/1.crt;", []protocol.SupportFile{ {Path: "1.crt", Content: "cert-data"}, {Path: "1.key", Content: "key-data"}, }) @@ -479,10 +479,10 @@ func TestManagerApplyWritesSupportFilesAndReplacesPlaceholder(t *testing.T) { if err != nil { t.Fatalf("failed to read route config: %v", err) } - if !strings.Contains(string(routeData), "/etc/nginx/atsflare-certs/1.crt") { + if !strings.Contains(string(routeData), "/etc/nginx/atsflare-support/1.crt") { t.Fatalf("expected placeholder replacement in route config, got %s", string(routeData)) } - certData, err := os.ReadFile(filepath.Join(manager.CertDir, "1.crt")) + certData, err := os.ReadFile(filepath.Join(manager.SupportDir, "1.crt")) if err != nil { t.Fatalf("failed to read cert file: %v", err) } @@ -495,8 +495,8 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { tempDir := t.TempDir() routePath := filepath.Join(tempDir, "routes.conf") mainPath := filepath.Join(tempDir, "nginx.conf") - certDir := filepath.Join(tempDir, "certs") - if err := os.MkdirAll(certDir, 0o755); err != nil { + supportDir := filepath.Join(tempDir, "support") + if err := os.MkdirAll(supportDir, 0o755); err != nil { t.Fatalf("MkdirAll failed: %v", err) } if err := os.WriteFile(mainPath, []byte("old-main"), 0o644); err != nil { @@ -505,14 +505,14 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { if err := os.WriteFile(routePath, []byte("old-route"), 0o644); err != nil { t.Fatalf("WriteFile failed: %v", err) } - if err := os.WriteFile(filepath.Join(certDir, "1.crt"), []byte("old-cert"), 0o600); err != nil { + if err := os.WriteFile(filepath.Join(supportDir, "1.crt"), []byte("old-cert"), 0o600); err != nil { t.Fatalf("WriteFile failed: %v", err) } manager := &Manager{ MainConfigPath: mainPath, RouteConfigPath: routePath, - CertDir: certDir, - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: supportDir, + NginxSupportDir: "/etc/nginx/atsflare-support", Executor: &fakeExecutor{ testErr: errors.New("openresty test failed"), }, @@ -539,7 +539,7 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { if string(routeData) != "old-route" { t.Fatalf("expected route rollback, got %s", string(routeData)) } - certData, err := os.ReadFile(filepath.Join(certDir, "1.crt")) + certData, err := os.ReadFile(filepath.Join(supportDir, "1.crt")) if err != nil { t.Fatalf("failed to read cert file: %v", err) } @@ -549,8 +549,8 @@ func TestManagerRollbackRestoresSupportFiles(t *testing.T) { } func TestManagerSupportFileTargetPathRejectsEscapes(t *testing.T) { - manager := &Manager{CertDir: filepath.Join(t.TempDir(), "certs")} - if err := os.MkdirAll(manager.CertDir, 0o755); err != nil { + manager := &Manager{SupportDir: filepath.Join(t.TempDir(), "support")} + if err := os.MkdirAll(manager.SupportDir, 0o755); err != nil { t.Fatalf("MkdirAll failed: %v", err) } @@ -581,8 +581,8 @@ func TestManagerSupportFileTargetPathRejectsEscapes(t *testing.T) { if err != nil { t.Fatalf("expected path %q to be accepted: %v", testCase.path, err) } - if !strings.HasPrefix(targetPath, manager.CertDir) { - t.Fatalf("expected target path %q to stay under %q", targetPath, manager.CertDir) + if !strings.HasPrefix(targetPath, manager.SupportDir) { + t.Fatalf("expected target path %q to stay under %q", targetPath, manager.SupportDir) } } } @@ -592,8 +592,8 @@ func TestManagerApplyRejectsSupportFilePathTraversal(t *testing.T) { manager := &Manager{ MainConfigPath: filepath.Join(tempDir, "nginx.conf"), RouteConfigPath: filepath.Join(tempDir, "routes.conf"), - CertDir: filepath.Join(tempDir, "certs"), - NginxCertDir: "/etc/nginx/atsflare-certs", + SupportDir: filepath.Join(tempDir, "support"), + NginxSupportDir: "/etc/nginx/atsflare-support", Executor: &fakeExecutor{}, } diff --git a/atsf_server/service/config_version.go b/atsf_server/service/config_version.go index 5da47bfc..a1a14bef 100644 --- a/atsf_server/service/config_version.go +++ b/atsf_server/service/config_version.go @@ -115,7 +115,7 @@ type configBundle struct { } const ( - nginxCertDirPlaceholder = "__ATSF_CERT_DIR__" + nginxSupportDirPlaceholder = "__ATSF_SUPPORT_DIR__" nginxRouteConfigPlaceholder = "__ATSF_ROUTE_CONFIG__" nginxAccessLogPlaceholder = "__ATSF_ACCESS_LOG__" nginxLuaDirPlaceholder = "__ATSF_LUA_DIR__" @@ -751,8 +751,8 @@ func renderHTTPRedirectServer(domain string) string { } func renderHTTPSServer(domain string, originURL string, certificateID uint, customHeaders []ProxyRouteCustomHeaderInput) string { - certPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateCertFileName(certificateID)) - keyPath := fmt.Sprintf("%s/%s", nginxCertDirPlaceholder, certificateKeyFileName(certificateID)) + certPath := fmt.Sprintf("%s/%s", nginxSupportDirPlaceholder, certificateCertFileName(certificateID)) + keyPath := fmt.Sprintf("%s/%s", nginxSupportDirPlaceholder, certificateKeyFileName(certificateID)) return fmt.Sprintf("server {\n listen 443 ssl;\n server_name %s;\n ssl_certificate %s;\n ssl_certificate_key %s;\n\n location / {\n%s proxy_pass %s;\n }\n}\n\n", domain, certPath, keyPath, renderProxyHeaderBlock(customHeaders), originURL) } diff --git a/atsf_server/service/https_phase1_test.go b/atsf_server/service/https_phase1_test.go index 0cbebacc..8c377350 100644 --- a/atsf_server/service/https_phase1_test.go +++ b/atsf_server/service/https_phase1_test.go @@ -69,8 +69,8 @@ func TestCreateTLSCertificateAndRenderHTTPSConfig(t *testing.T) { if !strings.Contains(result.Version.RenderedConfig, "return 301 https://$host$request_uri;") { t.Fatal("expected rendered config to include http redirect") } - if !strings.Contains(result.Version.RenderedConfig, "__ATSF_CERT_DIR__/") { - t.Fatal("expected rendered config to keep certificate dir placeholder") + if !strings.Contains(result.Version.RenderedConfig, "__ATSF_SUPPORT_DIR__/") { + t.Fatal("expected rendered config to keep support dir placeholder for certificates") } if !strings.Contains(result.Version.SupportFilesJSON, ".crt") || !strings.Contains(result.Version.SupportFilesJSON, ".key") { t.Fatal("expected support files to contain certificate and key") diff --git a/docs/app-config.md b/docs/app-config.md index e54c0525..3c975328 100644 --- a/docs/app-config.md +++ b/docs/app-config.md @@ -256,8 +256,8 @@ go run ./cmd/agent -config ./agent.json "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", - "cert_dir": "/usr/local/openresty/nginx/conf/certs", - "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs", + "support_dir": "/usr/local/openresty/nginx/conf/support", + "openresty_support_dir": "/usr/local/openresty/nginx/conf/support", "openresty_observability_port": 18081, "state_path": "./data/agent-state.json", "heartbeat_interval": 10000, @@ -282,8 +282,8 @@ go run ./cmd/agent -config ./agent.json | `data_dir` | Agent 数据目录,用于存储托管配置、证书和状态文件 | 否 | 配置文件所在目录下的 `data` 子目录 | `./data` | | `main_config_path` | 第五版主配置接管时 OpenResty 主配置文件写入路径 | 第五版本机模式建议必填 | Docker 模式可使用受管默认路径;本机模式建议显式设置 | `/usr/local/openresty/nginx/conf/nginx.conf` | | `route_config_path` | 路由配置文件写入路径 | 否 | 默认为 `data_dir` 下托管路径 | `/etc/nginx/conf.d/atsflare_routes.conf` | -| `cert_dir` | Agent 在本机写入证书文件的目录 | 否 | 默认为 `data_dir` 下托管证书目录 | `./data/etc/nginx/certs` | -| `openresty_cert_dir` | OpenResty 实际读取证书的目录 | 否 | 本机模式默认等于 `cert_dir`;Docker 模式默认 `/etc/nginx/atsflare-certs` | `/usr/local/openresty/nginx/conf/certs` | +| `support_dir` | Agent 在本机写入受管附属文件的目录,当前包含证书与 Lua 观测脚本 | 否 | 默认为 `data_dir` 下托管 support 目录 | `./data/etc/nginx/support` | +| `openresty_support_dir` | OpenResty 实际读取受管附属文件的目录 | 否 | 本机模式默认等于 `support_dir`;Docker 模式默认 `/etc/nginx/atsflare-support` | `/usr/local/openresty/nginx/conf/support` | | `state_path` | Agent 本地状态文件路径 | 否 | 默认为 `data_dir` 下托管状态文件 | `./data/agent-state.json` | | `heartbeat_interval` | 心跳间隔 | 否 | `10000` 毫秒 | `10000` | | `request_timeout` | HTTP 请求超时时间 | 否 | `10000` 毫秒 | `10000` | @@ -297,6 +297,7 @@ go run ./cmd/agent -config ./agent.json * `node_name` 与 `node_ip` 未填写时会自动探测;若自动探测失败,配置校验会报错 * 未配置 `openresty_path` 时,默认为 Docker OpenResty 模式 * `openresty_observability_port` 默认仅绑定本地回环地址;若节点本机已有端口冲突,可改为其他未占用端口 +* 为兼容旧节点,Agent 仍可读取历史字段 `cert_dir` / `openresty_cert_dir`,但保存配置时会统一写回 `support_dir` / `openresty_support_dir` * 配置保存时,`agent_version`、`nginx_version` 由程序运行时维护,不需要写入 JSON * 第五版主配置接管完成后,本机模式下应优先通过 `main_config_path` 由 Agent 写入受管主配置,而不是依赖节点手工维护 include 规则 @@ -308,14 +309,14 @@ go run ./cmd/agent -config ./agent.json | --- | --- | | `main_config_path` | 第五版 Docker 模式默认可落在 `data_dir/etc/nginx/nginx.conf`;本机模式建议显式配置 | | `route_config_path` | `data_dir/etc/nginx/conf.d/atsflare_routes.conf` | -| `cert_dir` | `data_dir/etc/nginx/certs` | +| `support_dir` | `data_dir/etc/nginx/support` | | `state_path` | `data_dir/var/lib/atsflare/agent-state.json` | Docker OpenResty 模式下: | 字段 | 默认值 | | --- | --- | -| `openresty_cert_dir` | `/etc/nginx/atsflare-certs` | +| `openresty_support_dir` | `/etc/nginx/atsflare-support` | 补充说明: @@ -347,8 +348,8 @@ Docker OpenResty 模式下: "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", - "cert_dir": "/usr/local/openresty/nginx/conf/certs", - "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs" + "support_dir": "/usr/local/openresty/nginx/conf/support", + "openresty_support_dir": "/usr/local/openresty/nginx/conf/support" } ``` diff --git a/docs/deployment.md b/docs/deployment.md index 58bbd739..ac532b62 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -263,7 +263,7 @@ export LOG_LEVEL='info' 验证点: -1. 首次启动后确认 `data/etc/nginx/nginx.conf`、`data/etc/nginx/conf.d/atsflare_routes.conf` 与 `data/etc/nginx/certs` 已由 Agent 创建 +1. 首次启动后确认 `data/etc/nginx/nginx.conf`、`data/etc/nginx/conf.d/atsflare_routes.conf` 与 `data/etc/nginx/support` 已由 Agent 创建 2. 确认容器实际挂载了主配置、路由目录和证书目录 3. 确认宿主机本地可访问 `http://127.0.0.1:18081/atsflare/observability` 与 `http://127.0.0.1:18081/atsflare/stub_status` 3. 在管理端发布一次新版本后,确认节点 `current_version` 追平激活版本 @@ -293,8 +293,8 @@ docker exec atsflare-openresty openresty -t "openresty_path": "/usr/local/openresty/nginx/sbin/openresty", "main_config_path": "/usr/local/openresty/nginx/conf/nginx.conf", "route_config_path": "/usr/local/openresty/nginx/conf/conf.d/atsflare_routes.conf", - "cert_dir": "/usr/local/openresty/nginx/conf/certs", - "openresty_cert_dir": "/usr/local/openresty/nginx/conf/certs", + "support_dir": "/usr/local/openresty/nginx/conf/support", + "openresty_support_dir": "/usr/local/openresty/nginx/conf/support", "openresty_observability_port": 18081 } ``` diff --git a/docs/improve_plan.md b/docs/improve_plan.md index 8a62e272..908c63f6 100644 --- a/docs/improve_plan.md +++ b/docs/improve_plan.md @@ -59,7 +59,7 @@ 维护期内优先处理以下高风险或高敏感问题: -* Agent 写入 `support_files` 时缺少对目标路径必须位于 `cert_dir` 内的强约束,存在路径穿越风险 +* Agent 写入 `support_files` 时缺少对目标路径必须位于 `support_dir` 内的强约束,存在路径穿越风险 * 手动上传 Server 二进制后会执行 `--version` 检测,属于高敏感执行链路,必须进一步加固 --- @@ -106,7 +106,7 @@ * 为 Agent 支持文件写入增加安全路径校验 * 拒绝绝对路径 * 拒绝 `..` 跳目录 - * 通过 `filepath.Rel` 或安全辅助函数确认最终路径仍位于 `cert_dir` 内 + * 通过 `filepath.Rel` 或安全辅助函数确认最终路径仍位于 `support_dir` 内 * `writeSupportFiles`、`restore`、未来新增的写文件入口全部复用同一套安全函数 * 收紧手动上传升级链路 * 明确只允许 root 用户 @@ -214,4 +214,4 @@ * 评估配置接口压缩与更细粒度 manifest 同步 * 评估手动上传升级链路的更安全替代实现 -* 根据实际运行数据决定是否继续做更细的持久化优化 \ No newline at end of file +* 根据实际运行数据决定是否继续做更细的持久化优化