diff --git a/.gitignore b/.gitignore index 9c35615..bd0eb3b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,6 @@ website/site/ website/docs/*.md install.sh + +# Kaiden workspace configuration +.kaiden/ diff --git a/pkg/runtime/podman/containerfile.go b/pkg/runtime/podman/containerfile.go index 48d2c19..7a87fc0 100644 --- a/pkg/runtime/podman/containerfile.go +++ b/pkg/runtime/podman/containerfile.go @@ -86,7 +86,7 @@ func generateSudoers(sudoBinaries []string) string { // that install scripts can chown files, write to /home/agent, and su to the target user. // _REMOTE_USER and _REMOTE_USER_HOME are exported before the feature block so install // scripts can resolve the target user by name. USER agent:agent is set after all features. -func generateContainerfile(imageConfig *config.ImageConfig, agentConfig *config.AgentConfig, hasAgentSettings bool, featureInfos []featureInstallInfo) string { +func generateContainerfile(imageConfig *config.ImageConfig, agentConfig *config.AgentConfig, hasAgentSettings bool, featureInfos []featureInstallInfo, certsCopied bool) string { if imageConfig == nil { return "" } @@ -107,10 +107,23 @@ func generateContainerfile(imageConfig *config.ImageConfig, agentConfig *config. lines = append(lines, "ARG GID=1000") lines = append(lines, "") + // Copy and install system CA certificates for enterprise proxy support. + // This enables containers to trust self-signed certificates from corporate proxies + // like Netskope during package installation (dnf install, curl, etc.). + // Only add COPY instructions when certificates are actually available in the build context. + if certsCopied { + lines = append(lines, "COPY certs/system-ca.crt /tmp/system-ca.crt") + lines = append(lines, "RUN cp /tmp/system-ca.crt /etc/pki/ca-trust/source/anchors/system-ca.crt && update-ca-trust") + lines = append(lines, "") + } + // Merge packages from image and agent configs allPackages := append([]string{}, imageConfig.Packages...) allPackages = append(allPackages, agentConfig.Packages...) + // Always install nftables for network guard firewall functionality + allPackages = append(allPackages, "nftables") + // Install packages if any if len(allPackages) > 0 { lines = append(lines, fmt.Sprintf("RUN dnf install -y %s", strings.Join(allPackages, " "))) diff --git a/pkg/runtime/podman/containerfile_test.go b/pkg/runtime/podman/containerfile_test.go index 39e4102..cbd1a1b 100644 --- a/pkg/runtime/podman/containerfile_test.go +++ b/pkg/runtime/podman/containerfile_test.go @@ -96,7 +96,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) // Check for FROM line with correct base image expectedFrom := "FROM registry.fedoraproject.org/fedora:latest" @@ -161,7 +161,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) expectedFrom := "FROM registry.fedoraproject.org/fedora:40" if !strings.Contains(result, expectedFrom) { @@ -184,7 +184,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) // Should have all packages in a single RUN command if !strings.Contains(result, "RUN dnf install -y package1 package2 package3 package4") { @@ -207,11 +207,14 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) - // Should not have dnf install line - if strings.Contains(result, "dnf install") { - t.Error("Expected no dnf install line when no packages specified") + // Should have nftables in dnf install line (pre-installed for deny-mode networking) + if !strings.Contains(result, "dnf install") { + t.Error("Expected dnf install line for nftables") + } + if !strings.Contains(result, "nftables") { + t.Error("Expected nftables to be in the package list") } }) @@ -231,7 +234,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) // Should have both RUN commands if !strings.Contains(result, "RUN echo 'image setup'") { @@ -257,7 +260,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, true, nil) + result := generateContainerfile(imageConfig, agentConfig, true, nil, false) expected := "COPY --chown=agent:agent agent-settings/. /home/agent/" if !strings.Contains(result, expected) { @@ -290,7 +293,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) if strings.Contains(result, "agent-settings") { t.Errorf("Expected no agent-settings COPY line, got:\n%s", result) @@ -313,7 +316,7 @@ func TestGenerateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) // Find positions imagePos := strings.Index(result, "RUN echo 'image'") @@ -398,7 +401,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { {dirName: "feature-0", options: nil, envVars: nil}, } - result := generateContainerfile(imageConfig, agentConfig, false, infos) + result := generateContainerfile(imageConfig, agentConfig, false, infos, false) if !strings.Contains(result, "COPY features/feature-0/ /tmp/feature-install/feature-0/") { t.Error("Expected COPY instruction for feature-0") @@ -440,7 +443,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { {dirName: "feature-0", options: map[string]string{"VERSION": "1.21", "INSTALL": "true"}, envVars: nil}, } - result := generateContainerfile(imageConfig, agentConfig, false, infos) + result := generateContainerfile(imageConfig, agentConfig, false, infos, false) expected := `RUN chmod +x /tmp/feature-install/feature-0/install.sh && INSTALL="true" VERSION="1.21" /tmp/feature-install/feature-0/install.sh` if !strings.Contains(result, expected) { @@ -459,7 +462,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { }, } - result := generateContainerfile(imageConfig, agentConfig, false, infos) + result := generateContainerfile(imageConfig, agentConfig, false, infos, false) if !strings.Contains(result, `ENV GOPATH="/home/agent/go"`) { t.Errorf("Expected ENV GOPATH instruction\nGot:\n%s", result) @@ -476,7 +479,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { {dirName: "feature-0", options: nil, envVars: nil}, } - result := generateContainerfile(imageConfig, agentConfig, false, infos) + result := generateContainerfile(imageConfig, agentConfig, false, infos, false) featureCopyPos := strings.Index(result, "COPY features/feature-0/") imageRunPos := strings.Index(result, "RUN echo 'image setup'") @@ -498,7 +501,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { {dirName: "feature-1", options: nil, envVars: nil}, } - result := generateContainerfile(imageConfig, agentConfig, false, infos) + result := generateContainerfile(imageConfig, agentConfig, false, infos, false) feature0Pos := strings.Index(result, "COPY features/feature-0/") feature1Pos := strings.Index(result, "COPY features/feature-1/") @@ -519,7 +522,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { t.Run("no feature instructions when featureInfos is nil", func(t *testing.T) { t.Parallel() - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) if strings.Contains(result, "COPY features/") { t.Errorf("Expected no feature COPY instructions\nGot:\n%s", result) @@ -536,7 +539,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { {dirName: "feature-0", options: nil, envVars: nil}, } - result := generateContainerfile(imageConfig, agentConfig, false, infos) + result := generateContainerfile(imageConfig, agentConfig, false, infos, false) if !strings.Contains(result, `ENV _REMOTE_USER="agent"`) { t.Errorf("Expected ENV _REMOTE_USER=\"agent\"\nGot:\n%s", result) @@ -564,7 +567,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { t.Run("_REMOTE_USER and _REMOTE_USER_HOME are not set when no features", func(t *testing.T) { t.Parallel() - result := generateContainerfile(imageConfig, agentConfig, false, nil) + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) if strings.Contains(result, "_REMOTE_USER") { t.Errorf("Expected no _REMOTE_USER when no features\nGot:\n%s", result) @@ -578,7 +581,7 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { {dirName: "feature-0", options: nil, envVars: nil}, } - result := generateContainerfile(imageConfig, agentConfig, true, infos) + result := generateContainerfile(imageConfig, agentConfig, true, infos, false) featureCopyPos := strings.Index(result, "COPY features/feature-0/") agentSettingsPos := strings.Index(result, "COPY --chown=agent:agent agent-settings/.") @@ -590,4 +593,30 @@ func TestGenerateContainerfile_WithFeatures(t *testing.T) { t.Error("Expected feature COPY to appear before agent-settings COPY") } }) + + t.Run("includes CA certificate COPY when certsCopied is true", func(t *testing.T) { + t.Parallel() + + result := generateContainerfile(imageConfig, agentConfig, false, nil, true) + + if !strings.Contains(result, "COPY certs/system-ca.crt /tmp/system-ca.crt") { + t.Error("Expected CA certificate COPY instruction when certsCopied is true") + } + if !strings.Contains(result, "RUN cp /tmp/system-ca.crt /etc/pki/ca-trust/source/anchors/system-ca.crt && update-ca-trust") { + t.Error("Expected CA certificate installation RUN instruction when certsCopied is true") + } + }) + + t.Run("omits CA certificate COPY when certsCopied is false", func(t *testing.T) { + t.Parallel() + + result := generateContainerfile(imageConfig, agentConfig, false, nil, false) + + if strings.Contains(result, "COPY certs/system-ca.crt") { + t.Error("Expected no CA certificate COPY instruction when certsCopied is false") + } + if strings.Contains(result, "update-ca-trust") { + t.Error("Expected no update-ca-trust when certsCopied is false") + } + }) } diff --git a/pkg/runtime/podman/create.go b/pkg/runtime/podman/create.go index 0c8f45b..3a5a357 100644 --- a/pkg/runtime/podman/create.go +++ b/pkg/runtime/podman/create.go @@ -53,6 +53,7 @@ type podTemplateData struct { AgentUID int BaseImageRegistry string BaseImageVersion string + WorkspaceImage string // The built workspace image with nftables pre-installed SourcePath string ProjectID string Agent string @@ -92,7 +93,8 @@ func (p *podmanRuntime) createInstanceDirectory(name string) (string, error) { // so they can be embedded in the image via a COPY instruction. // If featureInfos is non-empty, the features have already been downloaded to instanceDir/features/ // and the Containerfile will include instructions to install them. -func (p *podmanRuntime) createContainerfile(instanceDir string, imageConfig *config.ImageConfig, agentConfig *config.AgentConfig, settings map[string]agent.SettingsFile, featureInfos []featureInstallInfo) error { +// If certsCopied is true, the Containerfile will include instructions to install system CA certificates. +func (p *podmanRuntime) createContainerfile(instanceDir string, imageConfig *config.ImageConfig, agentConfig *config.AgentConfig, settings map[string]agent.SettingsFile, featureInfos []featureInstallInfo, certsCopied bool) error { // Generate sudoers content sudoersContent := generateSudoers(imageConfig.Sudo) sudoersPath := filepath.Join(instanceDir, "sudoers") @@ -122,7 +124,7 @@ func (p *podmanRuntime) createContainerfile(instanceDir string, imageConfig *con } // Generate Containerfile content - containerfileContent := generateContainerfile(imageConfig, agentConfig, len(settings) > 0, featureInfos) + containerfileContent := generateContainerfile(imageConfig, agentConfig, len(settings) > 0, featureInfos, certsCopied) containerfilePath := filepath.Join(instanceDir, "Containerfile") if err := os.WriteFile(containerfilePath, []byte(containerfileContent), 0644); err != nil { return fmt.Errorf("failed to write Containerfile: %w", err) @@ -131,6 +133,88 @@ func (p *podmanRuntime) createContainerfile(instanceDir string, imageConfig *con return nil } +// findSystemCACertificates attempts to read system CA certificates from common locations. +// Returns the certificate content and true if found, or nil and false otherwise. +func findSystemCACertificates(certPaths []string) ([]byte, bool) { + var certContent []byte + + // Get the system CA certificates from the location pointed by SSL_CERT_FILE if it's set + if caBundlePath := os.Getenv("SSL_CERT_FILE"); caBundlePath != "" { + if abs, err := filepath.Abs(caBundlePath); err == nil { + caBundlePath = abs + } + // Skip directories + if info, err := os.Stat(caBundlePath); err == nil && !info.IsDir() { + if content, err := os.ReadFile(caBundlePath); err == nil && len(content) > 0 { + certContent = content + } + } + } + + if len(certContent) == 0 { + for _, path := range certPaths { + if abs, err := filepath.Abs(path); err == nil { + path = abs + } + // Skip directories to avoid reading random files + if info, err := os.Stat(path); err == nil && info.IsDir() { + continue + } + if content, err := os.ReadFile(path); err == nil && len(content) > 0 { + certContent = content + break + } + } + } + + if len(certContent) == 0 { + return nil, false + } + return certContent, true +} + +// systemCACertPaths lists common system CA bundle locations across Linux distributions. +var systemCACertPaths = []string{ + // Debian / Ubuntu / Gentoo + "/etc/ssl/certs/ca-certificates.crt", + // Fedora / RHEL 6 + "/etc/pki/tls/certs/ca-bundle.crt", + // Fedora / RHEL 7+ / CentOS / Rocky / Alma + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", + // OpenSUSE + "/etc/ssl/ca-bundle.pem", + // SUSE / older distributions + "/etc/ssl/certs/ca-bundle.crt", + // Alpine Linux + "/etc/ssl/cert.pem", + // Arch Linux + "/etc/ca-certificates/extracted/tls-ca-bundle.pem", +} + +// copySystemCACertificates copies system CA certificates to the build context for +// enterprise proxy support (enables trusting self-signed certs during build). +// certPaths is the ordered list of candidate paths; pass systemCACertPaths for production +// or a custom list in tests. Returns true if certificates were copied, false otherwise. +func (p *podmanRuntime) copySystemCACertificates(instanceDir string, certPaths []string) (bool, error) { + certContent, found := findSystemCACertificates(certPaths) + if !found { + return false, nil + } + + // Create certs directory in build context + certsDir := filepath.Join(instanceDir, "certs") + if err := os.MkdirAll(certsDir, 0755); err != nil { + return false, fmt.Errorf("failed to create certs directory: %w", err) + } + + certPath := filepath.Join(certsDir, "system-ca.crt") + if err := os.WriteFile(certPath, certContent, 0644); err != nil { + return false, fmt.Errorf("failed to write system CA certificates: %w", err) + } + + return true, nil +} + // prepareFeatures downloads, orders, and merges options for devcontainer features declared in params. // Feature directories are written to instanceDir/features/{dirName}/. // Returns nil if no features are configured. @@ -474,9 +558,17 @@ func (p *podmanRuntime) Create(ctx context.Context, params runtime.CreateParams) } } + // Copy system CA certificates to build context for enterprise proxy support + // This must happen before Containerfile generation so we know whether to include COPY instructions + certsCopied, err := p.copySystemCACertificates(instanceDir, systemCACertPaths) + if err != nil { + stepLogger.Fail(err) + return runtime.RuntimeInfo{}, err + } + // Create Containerfile stepLogger.Start("Generating Containerfile", "Containerfile generated") - if err := p.createContainerfile(instanceDir, imageConfig, agentConfig, params.AgentSettings, featureInfos); err != nil { + if err := p.createContainerfile(instanceDir, imageConfig, agentConfig, params.AgentSettings, featureInfos, certsCopied); err != nil { stepLogger.Fail(err) return runtime.RuntimeInfo{}, err } @@ -517,6 +609,7 @@ func (p *podmanRuntime) Create(ctx context.Context, params runtime.CreateParams) AgentUID: p.system.Getuid(), BaseImageRegistry: constants.BaseImageRegistry, BaseImageVersion: imageConfig.Version, + WorkspaceImage: imageName, // Use the built workspace image with nftables pre-installed SourcePath: params.SourcePath, ProjectID: params.ProjectID, Agent: params.Agent, diff --git a/pkg/runtime/podman/create_test.go b/pkg/runtime/podman/create_test.go index 6ba3a71..e2d6630 100644 --- a/pkg/runtime/podman/create_test.go +++ b/pkg/runtime/podman/create_test.go @@ -15,6 +15,7 @@ package podman import ( + "bytes" "context" "encoding/json" "errors" @@ -178,6 +179,112 @@ func TestCreateInstanceDirectory(t *testing.T) { }) } +func TestCopySystemCACertificates(t *testing.T) { + t.Parallel() + + t.Run("copies certificates when cert file found", func(t *testing.T) { + t.Parallel() + + instanceDir := t.TempDir() + p := &podmanRuntime{} + + // Create a temporary CA certificate file to inject + tempCertPath := filepath.Join(t.TempDir(), "ca-bundle.crt") + wantContent := []byte("-----BEGIN CERTIFICATE-----\ntest cert content\n-----END CERTIFICATE-----") + if err := os.WriteFile(tempCertPath, wantContent, 0644); err != nil { + t.Fatalf("Failed to create temp cert file: %v", err) + } + + certsCopied, err := p.copySystemCACertificates(instanceDir, []string{tempCertPath}) + if err != nil { + t.Fatalf("copySystemCACertificates() failed: %v", err) + } + if !certsCopied { + t.Error("Expected certsCopied to be true when cert file exists") + } + + // Verify the file was written with the correct content + certPath := filepath.Join(instanceDir, "certs", "system-ca.crt") + gotContent, err := os.ReadFile(certPath) + if err != nil { + t.Fatalf("Failed to read copied cert file: %v", err) + } + if !bytes.Equal(gotContent, wantContent) { + t.Error("Expected written cert content to match original") + } + }) + + t.Run("returns false when no certificates found", func(t *testing.T) { + t.Parallel() + + instanceDir := t.TempDir() + p := &podmanRuntime{} + + certsCopied, err := p.copySystemCACertificates(instanceDir, []string{"/nonexistent/path/cert.crt"}) + if err != nil { + t.Fatalf("copySystemCACertificates() failed: %v", err) + } + if certsCopied { + t.Error("Expected certsCopied to be false when no certs found") + } + // Verify no certs directory was created + if _, err := os.Stat(filepath.Join(instanceDir, "certs")); !os.IsNotExist(err) { + t.Error("Expected certs directory to not exist when no certs were copied") + } + }) + + t.Run("skips directories in cert paths", func(t *testing.T) { + t.Parallel() + + instanceDir := t.TempDir() + p := &podmanRuntime{} + + // Pass a directory as a cert path — should be skipped gracefully + certsCopied, err := p.copySystemCACertificates(instanceDir, []string{t.TempDir()}) + if err != nil { + t.Fatalf("copySystemCACertificates() failed: %v", err) + } + if certsCopied { + t.Error("Expected certsCopied to be false when only directories given") + } + }) +} + +func TestFindSystemCACertificates(t *testing.T) { + // Not parallel: subtests share SSL_CERT_FILE isolation via t.Setenv. + t.Setenv("SSL_CERT_FILE", "") + + t.Run("returns content from first readable file", func(t *testing.T) { + wantContent := []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") + certPath := filepath.Join(t.TempDir(), "ca.crt") + if err := os.WriteFile(certPath, wantContent, 0644); err != nil { + t.Fatalf("Failed to write cert file: %v", err) + } + + got, found := findSystemCACertificates([]string{"/nonexistent", certPath}) + if !found { + t.Error("Expected to find certificate") + } + if !bytes.Equal(got, wantContent) { + t.Error("Expected cert content to match") + } + }) + + t.Run("returns false when all paths missing", func(t *testing.T) { + _, found := findSystemCACertificates([]string{"/nonexistent/a.crt", "/nonexistent/b.pem"}) + if found { + t.Error("Expected to not find certificate") + } + }) + + t.Run("skips directory entries", func(t *testing.T) { + _, found := findSystemCACertificates([]string{t.TempDir()}) + if found { + t.Error("Expected to not find certificate when path is a directory") + } + }) +} + func TestCreateContainerfile(t *testing.T) { t.Parallel() @@ -201,7 +308,7 @@ func TestCreateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - err := p.createContainerfile(instanceDir, imageConfig, agentConfig, nil, nil) + err := p.createContainerfile(instanceDir, imageConfig, agentConfig, nil, nil, false) if err != nil { t.Fatalf("createContainerfile() failed: %v", err) } @@ -252,7 +359,7 @@ func TestCreateContainerfile(t *testing.T) { TerminalCommand: []string{"custom-agent"}, } - err := p.createContainerfile(instanceDir, imageConfig, agentConfig, nil, nil) + err := p.createContainerfile(instanceDir, imageConfig, agentConfig, nil, nil, false) if err != nil { t.Fatalf("createContainerfile() failed: %v", err) } @@ -318,7 +425,7 @@ func TestCreateContainerfile(t *testing.T) { ".gitconfig": {Content: []byte("[user]\n\tname = Agent\n")}, } - err := p.createContainerfile(instanceDir, imageConfig, agentConfig, settings, nil) + err := p.createContainerfile(instanceDir, imageConfig, agentConfig, settings, nil, false) if err != nil { t.Fatalf("createContainerfile() failed: %v", err) } @@ -378,7 +485,7 @@ func TestCreateContainerfile(t *testing.T) { TerminalCommand: []string{"claude"}, } - err := p.createContainerfile(instanceDir, imageConfig, agentConfig, nil, nil) + err := p.createContainerfile(instanceDir, imageConfig, agentConfig, nil, nil, false) if err != nil { t.Fatalf("createContainerfile() failed: %v", err) } @@ -2221,6 +2328,7 @@ func TestRenderPodYAML_Ports(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.example.com/base", BaseImageVersion: "latest", + WorkspaceImage: "kdn-port-workspace", SourcePath: "/workspace/sources", ApprovalHandlerDir: "/tmp/approval", Forwards: []api.WorkspaceForward{ @@ -2262,6 +2370,7 @@ func TestRenderPodYAML_Ports(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.example.com/base", BaseImageVersion: "latest", + WorkspaceImage: "kdn-no-port-workspace", SourcePath: "/workspace/sources", ApprovalHandlerDir: "/tmp/approval", } diff --git a/pkg/runtime/podman/dashboard_test.go b/pkg/runtime/podman/dashboard_test.go index e075b8e..406487a 100644 --- a/pkg/runtime/podman/dashboard_test.go +++ b/pkg/runtime/podman/dashboard_test.go @@ -63,9 +63,10 @@ func TestGetURL_ReturnsCorrectPort(t *testing.T) { const customPort = 31337 data := podTemplateData{ - Name: "port-test", - OnecliWebPort: customPort, - OnecliVersion: defaultOnecliVersion, + Name: "port-test", + OnecliWebPort: customPort, + OnecliVersion: defaultOnecliVersion, + WorkspaceImage: "kdn-port-test", } if err := p.writePodFiles(containerID, data); err != nil { t.Fatalf("writePodFiles() failed: %v", err) diff --git a/pkg/runtime/podman/network.go b/pkg/runtime/podman/network.go index 0320437..6e555df 100644 --- a/pkg/runtime/podman/network.go +++ b/pkg/runtime/podman/network.go @@ -327,7 +327,14 @@ func buildNftScript(agentUID int, hostGW string) string { var parts []string // Ensure nftables is installed before applying rules. - parts = append(parts, "command -v nft >/dev/null 2>&1 || dnf install -y nftables >/dev/null 2>&1") + // Note: nftables and CA certificates are pre-installed in the workspace image during build, + // and the network-guard container uses that image. This dnf install is a defensive fallback + // for edge cases (e.g., manual removal or non-standard images) and should never run during normal operation. + parts = append(parts, "command -v nft >/dev/null 2>&1 || dnf install -y nftables") + + // Verify nft is available after installation attempt + // If this fails, it may indicate enterprise proxy/SSL/certificate issues or network connectivity problems + parts = append(parts, "command -v nft >/dev/null 2>&1 || { echo 'nftables not available - check proxy settings, certificate trust, or network connectivity'; exit 1; }") // IPv4 rules — default accept, block agent UID (except loopback + host gateway) parts = append(parts, diff --git a/pkg/runtime/podman/podman_test.go b/pkg/runtime/podman/podman_test.go index d5229b6..4507b7a 100644 --- a/pkg/runtime/podman/podman_test.go +++ b/pkg/runtime/podman/podman_test.go @@ -175,6 +175,7 @@ func TestWritePodFiles(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "latest", + WorkspaceImage: "kdn-my-project", } err := p.writePodFiles(containerID, data) @@ -218,6 +219,7 @@ func TestWritePodFiles(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "latest", + WorkspaceImage: "kdn-test-ws", } err := p.writePodFiles(containerID, data) @@ -260,6 +262,7 @@ func TestCleanupPodFiles(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "latest", + WorkspaceImage: "kdn-my-ws", } if err := p.writePodFiles(containerID, data); err != nil { @@ -290,6 +293,7 @@ func TestRenderPodYAML(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "latest", + WorkspaceImage: "kdn-my-project", ApprovalHandlerDir: "/tmp/approval-handler/my-project", } @@ -324,8 +328,8 @@ func TestRenderPodYAML(t *testing.T) { if !strings.Contains(yamlStr, "NET_ADMIN") { t.Error("Expected rendered YAML to contain NET_ADMIN capability for network-guard") } - if !strings.Contains(yamlStr, "registry.fedoraproject.org/fedora:latest") { - t.Error("Expected rendered YAML to contain base image for network-guard") + if !strings.Contains(yamlStr, "image: kdn-my-project") { + t.Error("Expected rendered YAML to contain exact workspace image 'kdn-my-project' for network-guard") } // Postgres (5432) and proxy (10255) ports should NOT have hostPort mappings @@ -347,6 +351,7 @@ func TestRenderPodYAML(t *testing.T) { AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "42", + WorkspaceImage: "kdn-test", ApprovalHandlerDir: "/tmp/approval-handler/test", } diff --git a/pkg/runtime/podman/pods/onecli-pod.yaml b/pkg/runtime/podman/pods/onecli-pod.yaml index f8ace98..4823a2b 100644 --- a/pkg/runtime/podman/pods/onecli-pod.yaml +++ b/pkg/runtime/podman/pods/onecli-pod.yaml @@ -62,7 +62,7 @@ spec: mountPath: /app:Z - name: network-guard - image: {{.BaseImageRegistry}}:{{.BaseImageVersion}} + image: {{.WorkspaceImage}} securityContext: capabilities: add: ["NET_ADMIN"] diff --git a/pkg/runtime/podman/start_test.go b/pkg/runtime/podman/start_test.go index f2d9fe8..1c2e0a2 100644 --- a/pkg/runtime/podman/start_test.go +++ b/pkg/runtime/podman/start_test.go @@ -452,6 +452,7 @@ func setupPodFilesWithSource(t *testing.T, p *podmanRuntime, containerID, worksp AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "latest", + WorkspaceImage: "kdn-" + workspaceName, SourcePath: sourceDir, ApprovalHandlerDir: approvalDir, } diff --git a/pkg/runtime/podman/steplogger_test.go b/pkg/runtime/podman/steplogger_test.go index f9c2ea8..24627c2 100644 --- a/pkg/runtime/podman/steplogger_test.go +++ b/pkg/runtime/podman/steplogger_test.go @@ -105,6 +105,7 @@ func setupPodFiles(t *testing.T, p *podmanRuntime, containerID, workspaceName st AgentUID: 1000, BaseImageRegistry: "registry.fedoraproject.org/fedora", BaseImageVersion: "latest", + WorkspaceImage: "kdn-" + workspaceName, ApprovalHandlerDir: approvalDir, } if err := p.writePodFiles(containerID, data); err != nil {