backend/docs/docker.md
The Docker client package (backend/pkg/docker) provides a secure and isolated containerized environment for PentAGI's AI agents to execute penetration testing operations. This package serves as a wrapper around the official Docker SDK, offering specialized functionality for managing containers that AI agents use to perform security testing tasks.
The Docker client is a critical component that enables PentAGI's core promise of secure, isolated penetration testing. It provides the foundation for:
The Docker client package consists of several key components:
backend/pkg/docker/
├── client.go # Main Docker client implementation
└── (future files) # Additional Docker utilities
const WorkFolderPathInContainer = "/work" // Standard working directory in containers
const BaseContainerPortsNumber = 28000 // Starting port number for dynamic allocation
const defaultImage = "debian:latest" // Fallback image if custom image fails
const containerPortsNumber = 2 // Number of ports allocated per container
const limitContainerPortsNumber = 2000 // Maximum port range for allocation
PentAGI uses a deterministic port allocation algorithm to ensure each flow gets unique, predictable ports:
func GetPrimaryContainerPorts(flowID int64) []int {
ports := make([]int, containerPortsNumber)
for i := 0; i < containerPortsNumber; i++ {
delta := (int(flowID)*containerPortsNumber + i) % limitContainerPortsNumber
ports[i] = BaseContainerPortsNumber + delta
}
return ports
}
This ensures that:
The Docker client is configured through several environment variables defined in the main configuration:
| Variable | Default | Description |
|---|---|---|
DOCKER_HOST | unix:///var/run/docker.sock | Docker daemon connection |
DOCKER_INSIDE | false | Whether PentAGI communicates with host Docker daemon from containers |
DOCKER_NET_ADMIN | false | Whether PentAGI grants the primary container NET_ADMIN capability for advanced networking. |
DOCKER_SOCKET | /var/run/docker.sock | Path to Docker socket on host |
DOCKER_NETWORK | Docker network for container communication (bridge mode) or host for host network mode | |
DOCKER_PUBLIC_IP | 0.0.0.0 | Public IP for port binding (bridge mode only) |
DOCKER_WORK_DIR | Custom work directory path on host | |
DOCKER_DEFAULT_IMAGE | debian:latest | Fallback image if AI-selected image fails |
DOCKER_DEFAULT_IMAGE_FOR_PENTEST | vxcontrol/kali-linux | Default Docker image for penetration testing tasks |
DATA_DIR | ./data | Local data directory for file operations |
type Config struct {
// Docker (terminal) settings
DockerInside bool `env:"DOCKER_INSIDE" envDefault:"false"`
DockerNetAdmin bool `env:"DOCKER_NET_ADMIN" envDefault:"false"`
DockerSocket string `env:"DOCKER_SOCKET"`
DockerNetwork string `env:"DOCKER_NETWORK"`
DockerPublicIP string `env:"DOCKER_PUBLIC_IP" envDefault:"0.0.0.0"`
DockerWorkDir string `env:"DOCKER_WORK_DIR"`
DockerDefaultImage string `env:"DOCKER_DEFAULT_IMAGE" envDefault:"debian:latest"`
DockerDefaultImageForPentest string `env:"DOCKER_DEFAULT_IMAGE_FOR_PENTEST" envDefault:"vxcontrol/kali-linux"`
DataDir string `env:"DATA_DIR" envDefault:"./data"`
}
The DOCKER_NET_ADMIN option controls whether PentAGI containers are granted the NET_ADMIN Linux capability, which provides advanced networking permissions essential for many penetration testing operations.
When DOCKER_NET_ADMIN=true, containers receive the following networking capabilities:
Enabling NET_ADMIN (DOCKER_NET_ADMIN=true):
Disabling NET_ADMIN (DOCKER_NET_ADMIN=false):
The NET_ADMIN capability is applied differently based on container type and configuration:
// Primary containers (when DOCKER_NET_ADMIN=true)
hostConfig := &container.HostConfig{
CapAdd: []string{"NET_RAW", "NET_ADMIN"}, // Full networking capabilities
// ... other configurations
}
// Primary containers (when DOCKER_NET_ADMIN=false)
hostConfig := &container.HostConfig{
CapAdd: []string{"NET_RAW"}, // Basic raw socket access only
// ... other configurations
}
PentAGI supports running inside Docker containers while still managing other containers. This is controlled by the DOCKER_INSIDE setting:
DOCKER_INSIDE=false: PentAGI runs on host, manages containers directlyDOCKER_INSIDE=true: PentAGI runs in container, mounts Docker socket to manage sibling containersPentAGI supports two network modes for container isolation:
When DOCKER_NETWORK is set to a custom network name (e.g., pentagi-network), containers are connected to an isolated bridge network:
When DOCKER_NETWORK is set to the special value host, containers use the host's network stack directly:
Security Consideration: Host network mode reduces isolation. Use only when necessary for penetration testing tasks requiring direct host network access.
The main interface defines all Docker operations available to PentAGI components:
type DockerClient interface {
// Container lifecycle management
RunContainer(ctx context.Context, containerName string, containerType database.ContainerType,
flowID int64, config *container.Config, hostConfig *container.HostConfig) (database.Container, error)
StopContainer(ctx context.Context, containerID string, dbID int64) error
RemoveContainer(ctx context.Context, containerID string, dbID int64) error
IsContainerRunning(ctx context.Context, containerID string) (bool, error)
// Command execution
ContainerExecCreate(ctx context.Context, container string, config container.ExecOptions) (container.ExecCreateResponse, error)
ContainerExecAttach(ctx context.Context, execID string, config container.ExecAttachOptions) (types.HijackedResponse, error)
ContainerExecInspect(ctx context.Context, execID string) (container.ExecInspect, error)
// File operations
CopyToContainer(ctx context.Context, containerID string, dstPath string, content io.Reader, options container.CopyToContainerOptions) error
CopyFromContainer(ctx context.Context, containerID string, srcPath string) (io.ReadCloser, container.PathStat, error)
// Utility methods
Cleanup(ctx context.Context) error
GetDefaultImage() string
}
type dockerClient struct {
db database.Querier // Database for container state management
logger *logrus.Logger // Structured logging
dataDir string // Local data directory
hostDir string // Host-mapped data directory
client *client.Client // Docker SDK client
inside bool // Running inside Docker
defImage string // Default fallback image
socket string // Docker socket path
network string // Docker network name
publicIP string // Public IP for port binding
}
The RunContainer method handles the complete container creation workflow:
Preparation:
Image Management:
Container Configuration:
/workunless-stopped)Storage Setup:
/work in containerNetwork and Ports:
DOCKER_NETWORK=host): Uses host network stack, skips port bindingsContainer Startup:
containerConfig := &container.Config{
Image: "kali:latest", // AI-selected or default image
Hostname: "a1b2c3d4", // Generated from container name
WorkingDir: "/work", // Standard working directory
Entrypoint: []string{"tail", "-f", "/dev/null"}, // Keep container running
ExposedPorts: nat.PortSet{
"28000/tcp": {}, // Flow-specific ports
"28001/tcp": {},
},
}
hostConfig := &container.HostConfig{
CapAdd: []string{"NET_RAW"}, // Required capabilities for network tools
RestartPolicy: container.RestartPolicy{
Name: "unless-stopped", // Auto-restart unless explicitly stopped
},
Binds: []string{
"/host/data/flow-123:/work", // Work directory mount
"/var/run/docker.sock:/var/run/docker.sock", // Docker socket (if inside Docker)
},
PortBindings: nat.PortMap{
"28000/tcp": []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: "28000"}},
"28001/tcp": []nat.PortBinding{{HostIP: "0.0.0.0", HostPort: "28001"}},
},
}
PentAGI tracks container states in the database:
Starting: Container creation in progressRunning: Container is active and availableStopped: Container has been stopped but not removedFailed: Container creation or startup failedDeleted: Container has been removedContainers follow a specific naming pattern for easy identification:
func PrimaryTerminalName(flowID int64) string {
return fmt.Sprintf("pentagi-terminal-%d", flowID)
}
This creates names like pentagi-terminal-123 for flow ID 123, making it easy to:
The Cleanup method performs comprehensive cleanup:
Flow State Assessment:
Container Cleanup:
Parallel Processing:
PentAGI implements a multi-layered security approach for container isolation:
hostConfig := &container.HostConfig{
CapAdd: []string{"NET_RAW"}, // Required for network scanning tools
// Other dangerous capabilities are not granted
}
The Docker client integrates with PentAGI's tool system to provide terminal access:
type terminal struct {
flowID int64
containerID int64
containerLID string
dockerClient docker.DockerClient
tlp TermLogProvider
}
The terminal tool uses the Docker client for:
The provider system uses Docker client for environment preparation:
// In providers.go
type flowProvider struct {
// ... other fields
docker docker.DockerClient
publicIP string
}
Providers use the Docker client to:
Container states are persisted in the PostgreSQL database:
-- Container state tracking
CREATE TABLE containers (
id SERIAL PRIMARY KEY,
flow_id INTEGER REFERENCES flows(id),
name VARCHAR NOT NULL,
image VARCHAR NOT NULL,
status container_status NOT NULL,
local_id VARCHAR,
local_dir VARCHAR,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
All Docker operations are instrumented with:
// Initialize Docker client
dockerClient, err := docker.NewDockerClient(ctx, db, cfg)
if err != nil {
return fmt.Errorf("failed to create docker client: %w", err)
}
// Create container for a flow
containerName := docker.PrimaryTerminalName(flowID)
container, err := dockerClient.RunContainer(
ctx,
containerName,
database.ContainerTypePrimary,
flowID,
&container.Config{
Image: "kali:latest",
Entrypoint: []string{"tail", "-f", "/dev/null"},
},
&container.HostConfig{
CapAdd: []string{"NET_RAW", "NET_ADMIN"},
},
)
// Execute command in container
createResp, err := dockerClient.ContainerExecCreate(ctx, containerName, container.ExecOptions{
Cmd: []string{"sh", "-c", "nmap -sS 192.168.1.1"},
AttachStdout: true,
AttachStderr: true,
WorkingDir: "/work",
Tty: true,
})
// Attach to execution
resp, err := dockerClient.ContainerExecAttach(ctx, createResp.ID, container.ExecAttachOptions{
Tty: true,
})
// Read output
output, err := io.ReadAll(resp.Reader)
// Write file to container
content := "#!/bin/bash\necho 'Hello from container'"
archive := createTarArchive("script.sh", content)
err := dockerClient.CopyToContainer(ctx, containerID, "/work", archive, container.CopyToContainerOptions{})
// Read file from container
reader, stats, err := dockerClient.CopyFromContainer(ctx, containerID, "/work/results.txt")
defer reader.Close()
// Extract content from tar
content := extractFromTar(reader)
// Check if container is running
isRunning, err := dockerClient.IsContainerRunning(ctx, containerID)
// Stop container
err = dockerClient.StopContainer(ctx, containerID, dbID)
// Remove container and volumes
err = dockerClient.RemoveContainer(ctx, containerID, dbID)
// Global cleanup (usually called on startup)
err = dockerClient.Cleanup(ctx)
// The client implements comprehensive error handling
container, err := dockerClient.RunContainer(ctx, name, containerType, flowID, config, hostConfig)
if err != nil {
// Errors include:
// - Image pull failures (handled with fallback)
// - Container creation failures
// - Network configuration issues
// - Database update failures
// The client automatically:
// - Updates database with failure status
// - Cleans up partially created resources
// - Logs detailed error information
return fmt.Errorf("container creation failed: %w", err)
}
The Docker client handles several categories of errors:
Docker Daemon Errors:
Image-Related Errors:
Container Runtime Errors:
Network and Storage Errors:
Image Fallback:
if err := dc.pullImage(ctx, config.Image); err != nil {
logger.WithError(err).Warnf("failed to pull image '%s', using default", config.Image)
config.Image = dc.defImage
// Retry with default image
}
Container Cleanup:
if containerCreationFails {
defer updateContainerInfo(database.ContainerStatusFailed, containerID)
// Clean up any partially created resources
}
State Synchronization:
Cleanup() method on application startup