mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-09-21 10:40:13 +00:00
fix(agentcompat): secure credentialed agent workspaces
Co-authored-by: naiba/CloudCode <hi+cloudcode@nai.ba>
This commit is contained in:
@@ -4,9 +4,10 @@ package agent
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/nezhahq/nezha/integration/agentcompat/internal/workspace"
|
"github.com/nezhahq/nezha/integration/agentcompat/internal/workspace"
|
||||||
)
|
)
|
||||||
@@ -96,14 +97,36 @@ func (agent *Agent) grantWorkspaceOwnership(config AgentStartConfig) error {
|
|||||||
if config.Credential == nil {
|
if config.Credential == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if err := filepath.WalkDir(agent.workspace.Root(), func(path string, _ os.DirEntry, walkErr error) error {
|
root, err := os.OpenRoot(agent.workspace.Root())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("open agent workspace ownership root: %w", err)
|
||||||
|
}
|
||||||
|
defer root.Close()
|
||||||
|
paths := make([]string, 0)
|
||||||
|
if err := fs.WalkDir(root.FS(), ".", func(path string, entry fs.DirEntry, walkErr error) error {
|
||||||
if walkErr != nil {
|
if walkErr != nil {
|
||||||
return walkErr
|
return walkErr
|
||||||
}
|
}
|
||||||
return os.Chown(path, int(config.Credential.Uid), int(config.Credential.Gid))
|
if entry.Type()&os.ModeSymlink != 0 {
|
||||||
|
return fmt.Errorf("agent workspace symlink is not allowed: %s", path)
|
||||||
|
}
|
||||||
|
paths = append(paths, path)
|
||||||
|
return nil
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("grant agent workspace ownership: %w", err)
|
return fmt.Errorf("grant agent workspace ownership: %w", err)
|
||||||
}
|
}
|
||||||
|
for _, path := range paths {
|
||||||
|
file, err := root.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("grant agent workspace ownership: %w", err)
|
||||||
|
}
|
||||||
|
// Chown the opened descriptor so a concurrent pathname swap cannot retarget ownership.
|
||||||
|
chownErr := file.Chown(int(config.Credential.Uid), int(config.Credential.Gid))
|
||||||
|
closeErr := file.Close()
|
||||||
|
if err := errors.Join(chownErr, closeErr); err != nil {
|
||||||
|
return fmt.Errorf("grant agent workspace ownership: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ func closePreparedWorkspace(workspaceRoot *workspace.Workspace, cause error) err
|
|||||||
|
|
||||||
func exposePreparedBinary(root, binaryPath string) error {
|
func exposePreparedBinary(root, binaryPath string) error {
|
||||||
for _, path := range []string{root, filepath.Dir(binaryPath)} {
|
for _, path := range []string{root, filepath.Dir(binaryPath)} {
|
||||||
if err := os.Chmod(path, 0o755); err != nil {
|
if err := os.Chmod(path, 0o711); err != nil { // #nosec G302 -- Directories need search permission for the credentialed test agent without granting directory reads; payloads and logs remain private.
|
||||||
return fmt.Errorf("make prepared agent binary executable: %w", err)
|
return fmt.Errorf("make prepared agent binary executable: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,8 @@ func TestPreparedBinary_EightIndependentAgentsShareBinaryAndCleanUp(t *testing.T
|
|||||||
preparedRoot := prepared.WorkspaceRoot()
|
preparedRoot := prepared.WorkspaceRoot()
|
||||||
binaryPath := prepared.BinaryPath()
|
binaryPath := prepared.BinaryPath()
|
||||||
require.FileExists(t, binaryPath)
|
require.FileExists(t, binaryPath)
|
||||||
|
requireDirectoryMode(t, preparedRoot, 0o711)
|
||||||
|
requireDirectoryMode(t, filepath.Dir(binaryPath), 0o711)
|
||||||
initialBinaryInfo, err := os.Stat(binaryPath)
|
initialBinaryInfo, err := os.Stat(binaryPath)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
initialBinaryStat, ok := initialBinaryInfo.Sys().(*syscall.Stat_t)
|
initialBinaryStat, ok := initialBinaryInfo.Sys().(*syscall.Stat_t)
|
||||||
@@ -110,6 +112,13 @@ func TestPreparedBinary_EightIndependentAgentsShareBinaryAndCleanUp(t *testing.T
|
|||||||
require.ErrorIs(t, statErr, os.ErrNotExist)
|
require.ErrorIs(t, statErr, os.ErrNotExist)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func requireDirectoryMode(t *testing.T, path string, want os.FileMode) {
|
||||||
|
t.Helper()
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, want, info.Mode().Perm())
|
||||||
|
}
|
||||||
|
|
||||||
func TestAgent_StartBuildsAndOwnsItsBinary(t *testing.T) {
|
func TestAgent_StartBuildsAndOwnsItsBinary(t *testing.T) {
|
||||||
instance, err := Start(t.Context(), AgentStartConfig{
|
instance, err := Start(t.Context(), AgentStartConfig{
|
||||||
SourceDir: testAgentSourceDir(t),
|
SourceDir: testAgentSourceDir(t),
|
||||||
|
|||||||
Reference in New Issue
Block a user