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.
This commit is contained in:
ryan
2026-03-14 17:16:54 +08:00
parent bdfa80f214
commit 4be44733e8
11 changed files with 159 additions and 143 deletions
+37 -37
View File
@@ -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 {
+57 -57
View File
@@ -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{},
}