Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 84 additions & 3 deletions internal/gcs-sidecar/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"

Expand Down Expand Up @@ -124,12 +125,17 @@ func (b *Bridge) createContainer(req *request) (err error) {
user := securitypolicy.IDName{
Name: spec.Process.User.Username,
}
_, _, _, err := b.hostState.securityOptions.PolicyEnforcer.EnforceCreateContainerPolicyV2(req.ctx, containerID, spec.Process.Args, spec.Process.Env, spec.Process.Cwd, spec.Mounts, user, nil)
envToKeep, _, allowStdio, err := b.hostState.securityOptions.PolicyEnforcer.EnforceCreateContainerPolicyV2(req.ctx, containerID, spec.Process.Args, spec.Process.Env, spec.Process.Cwd, spec.Mounts, user, nil)

if err != nil {
return fmt.Errorf("CreateContainer operation is denied by policy: %w", err)
}

if envToKeep != nil {
spec.Process.Env = []string(envToKeep)
}
_ = allowStdio // TODO: enforce stdio access for Windows containers

commandLine := len(spec.Process.Args) > 0
c := &Container{
id: containerID,
Expand Down Expand Up @@ -215,6 +221,39 @@ func processParamEnvToOCIEnv(environment map[string]string) []string {
return environmentList
}

// ociEnvToProcessParamEnv is the inverse of processParamEnvToOCIEnv. It converts
// an OCI-style env list (["KEY=VALUE", ...]) back to a ProcessParameters
// Environment map.
func ociEnvToProcessParamEnv(envs []string) map[string]string {
paramEnv := make(map[string]string, len(envs))
for _, env := range envs {
parts := strings.SplitN(env, "=", 2)
if len(parts) == 2 {
paramEnv[parts[0]] = parts[1]
}
}
return paramEnv
}

// rewriteExecRequest re-marshals an execute process request with updated
// ProcessParameters (e.g., after env filtering by policy).
func rewriteExecRequest(req *request, r prot.ContainerExecuteProcess, params hcsschema.ProcessParameters) (*request, error) {
r.Settings.ProcessParameters.Value = &params

buf, err := json.Marshal(r)
if err != nil {
return nil, fmt.Errorf("failed to marshal updated exec request: %w", err)
}

newReq := &request{
ctx: req.ctx,
header: req.header,
message: buf,
}
newReq.header.Size = uint32(len(buf)) + prot.HdrSize
return newReq, nil
}

func (b *Bridge) startContainer(req *request) (err error) {
_, span := oc.StartSpan(req.ctx, "sidecar::startContainer")
defer span.End()
Expand Down Expand Up @@ -283,7 +322,7 @@ func (b *Bridge) executeProcess(req *request) (err error) {

if containerID == UVMContainerID {
log.G(req.ctx).Tracef("Enforcing policy on external exec process")
_, _, err := b.hostState.securityOptions.PolicyEnforcer.EnforceExecExternalProcessPolicy(
envToKeep, _, err := b.hostState.securityOptions.PolicyEnforcer.EnforceExecExternalProcessPolicy(
req.ctx,
commandLine,
processParamEnvToOCIEnv(processParams.Environment),
Expand All @@ -292,6 +331,13 @@ func (b *Bridge) executeProcess(req *request) (err error) {
if err != nil {
return errors.Wrapf(err, "exec is denied due to policy")
}
if envToKeep != nil {
processParams.Environment = ociEnvToProcessParamEnv(envToKeep)
req, err = rewriteExecRequest(req, r, processParams)
if err != nil {
return fmt.Errorf("failed to rewrite exec request with filtered env: %w", err)
}
}
b.forwardRequestToGcs(req)
} else {
// fetch the container command line
Expand All @@ -315,7 +361,7 @@ func (b *Bridge) executeProcess(req *request) (err error) {
Name: processParams.User,
}
log.G(req.ctx).Tracef("Enforcing policy on exec in container")
_, _, _, err = b.hostState.securityOptions.PolicyEnforcer.
envToKeep, _, _, err := b.hostState.securityOptions.PolicyEnforcer.
EnforceExecInContainerPolicyV2(
req.ctx,
containerID,
Expand All @@ -328,6 +374,13 @@ func (b *Bridge) executeProcess(req *request) (err error) {
if err != nil {
return errors.Wrapf(err, "exec in container denied due to policy")
}
if envToKeep != nil {
processParams.Environment = ociEnvToProcessParamEnv(envToKeep)
req, err = rewriteExecRequest(req, r, processParams)
if err != nil {
return fmt.Errorf("failed to rewrite exec request with filtered env: %w", err)
}
}
}
headerID := req.header.ID

Expand Down Expand Up @@ -649,6 +702,13 @@ func (b *Bridge) modifySettings(req *request) (err error) {
case guestresource.ResourceTypeMappedVirtualDisk:
wcowMappedVirtualDisk := modifyGuestSettingsRequest.Settings.(*guestresource.WCOWMappedVirtualDisk)
log.G(ctx).Tracef("wcowMappedVirtualDisk { %v}", wcowMappedVirtualDisk)
if wcowMappedVirtualDisk.ContainerPath != "" {
matched, merr := regexp.MatchString(`(?i)^[Cc]:\\mounts\\scsi\\m[0-9]+$`, wcowMappedVirtualDisk.ContainerPath)
if merr != nil || !matched {
return fmt.Errorf("virtual disk mount path %q does not match expected pattern c:\\mounts\\scsi\\m<N>",
wcowMappedVirtualDisk.ContainerPath)
}
}

case guestresource.ResourceTypeHvSocket:
hvSocketAddress := modifyGuestSettingsRequest.Settings.(*hcsschema.HvSocketAddress)
Expand All @@ -657,6 +717,18 @@ func (b *Bridge) modifySettings(req *request) (err error) {
case guestresource.ResourceTypeMappedDirectory:
settings := modifyGuestSettingsRequest.Settings.(*hcsschema.MappedDirectory)
log.G(ctx).Tracef("hcsschema.MappedDirectory { %v }", settings)
switch modifyGuestSettingsRequest.RequestType {
case guestrequest.RequestTypeAdd:
if err := b.hostState.securityOptions.PolicyEnforcer.EnforceMappedDirectoryMountPolicy(
ctx, settings.ContainerPath, settings.ReadOnly); err != nil {
return fmt.Errorf("mapped directory mount is denied by policy: %w", err)
}
case guestrequest.RequestTypeRemove:
if err := b.hostState.securityOptions.PolicyEnforcer.EnforceMappedDirectoryUnmountPolicy(
ctx, settings.ContainerPath); err != nil {
return fmt.Errorf("mapped directory unmount is denied by policy: %w", err)
}
}

case guestresource.ResourceTypeSecurityPolicy:
securityPolicyRequest := modifyGuestSettingsRequest.Settings.(*guestresource.ConfidentialOptions)
Expand Down Expand Up @@ -815,6 +887,15 @@ func (b *Bridge) modifySettings(req *request) (err error) {
wcowMappedVirtualDisk := modifyGuestSettingsRequest.Settings.(*guestresource.WCOWMappedVirtualDisk)
log.G(ctx).Tracef("ResourceTypeMappedVirtualDiskForContainerScratch: { %v }", wcowMappedVirtualDisk)

// Validate the scratch disk mount path matches the expected pattern
if wcowMappedVirtualDisk.ContainerPath != "" {
matched, merr := regexp.MatchString(`(?i)^[Cc]:\\mounts\\scsi\\m[0-9]+$`, wcowMappedVirtualDisk.ContainerPath)
if merr != nil || !matched {
return fmt.Errorf("scratch disk mount path %q does not match expected pattern c:\\mounts\\scsi\\m<N>",
wcowMappedVirtualDisk.ContainerPath)
}
}

// This will return the volume path of the mounted scratch.
// Scratch disk should be >= 30 GB for refs formatter to work.
// fsFormatter understands only virtualDevObjectPathFormat. Therefore fetch the
Expand Down
80 changes: 80 additions & 0 deletions internal/gcs-sidecar/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -327,3 +327,83 @@ func TestModifySettings_PolicyFragment_TypeAssertionFailure(t *testing.T) {
t.Fatal("expected error for empty fragment, got nil")
}
}

// Tests for environment variable filtering helpers (envlist persistence)

func TestOciEnvToProcessParamEnv_Basic(t *testing.T) {
input := []string{"FOO=bar", "PATH=/usr/bin", "EMPTY="}
result := ociEnvToProcessParamEnv(input)

if result["FOO"] != "bar" {
t.Errorf("FOO = %q, want %q", result["FOO"], "bar")
}
if result["PATH"] != "/usr/bin" {
t.Errorf("PATH = %q, want %q", result["PATH"], "/usr/bin")
}
if result["EMPTY"] != "" {
t.Errorf("EMPTY = %q, want %q", result["EMPTY"], "")
}
if len(result) != 3 {
t.Errorf("len = %d, want 3", len(result))
}
}

func TestOciEnvToProcessParamEnv_ValueWithEquals(t *testing.T) {
input := []string{"CONN=host=db;port=5432"}
result := ociEnvToProcessParamEnv(input)

if result["CONN"] != "host=db;port=5432" {
t.Errorf("CONN = %q, want %q", result["CONN"], "host=db;port=5432")
}
}

func TestOciEnvToProcessParamEnv_MalformedSkipped(t *testing.T) {
input := []string{"GOOD=value", "NOEQUALS", "ALSO_GOOD=yes"}
result := ociEnvToProcessParamEnv(input)

if len(result) != 2 {
t.Errorf("len = %d, want 2 (malformed entry should be skipped)", len(result))
}
if result["GOOD"] != "value" {
t.Errorf("GOOD = %q, want %q", result["GOOD"], "value")
}
if result["ALSO_GOOD"] != "yes" {
t.Errorf("ALSO_GOOD = %q, want %q", result["ALSO_GOOD"], "yes")
}
}

func TestOciEnvToProcessParamEnv_Empty(t *testing.T) {
result := ociEnvToProcessParamEnv([]string{})
if len(result) != 0 {
t.Errorf("len = %d, want 0", len(result))
}
}

func TestOciEnvToProcessParamEnv_Nil(t *testing.T) {
result := ociEnvToProcessParamEnv(nil)
if result == nil {
t.Error("result should be non-nil empty map, got nil")
}
if len(result) != 0 {
t.Errorf("len = %d, want 0", len(result))
}
}

func TestProcessParamEnvToOCIEnv_Roundtrip(t *testing.T) {
original := map[string]string{
"FOO": "bar",
"PATH": "/usr/bin",
}

ociEnv := processParamEnvToOCIEnv(original)
roundtripped := ociEnvToProcessParamEnv(ociEnv)

if len(roundtripped) != len(original) {
t.Fatalf("roundtrip len = %d, want %d", len(roundtripped), len(original))
}
for k, v := range original {
if roundtripped[k] != v {
t.Errorf("roundtrip[%q] = %q, want %q", k, roundtripped[k], v)
}
}
}
2 changes: 2 additions & 0 deletions pkg/securitypolicy/api.rego
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,6 @@ enforcement_points := {
"load_fragment": {"introducedVersion": "0.9.0", "default_results": {"allowed": false, "add_module": false}, "use_framework": false},
"scratch_mount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false},
"scratch_unmount": {"introducedVersion": "0.10.0", "default_results": {"allowed": true}, "use_framework": false},
"mapped_directory_mount": {"introducedVersion": "0.11.0", "default_results": {"allowed": true}, "use_framework": false},
"mapped_directory_unmount": {"introducedVersion": "0.11.0", "default_results": {"allowed": true}, "use_framework": false},
}
44 changes: 44 additions & 0 deletions pkg/securitypolicy/framework.rego
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,35 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} {
}
}

# Mapped directory (VSMB share) validation for Windows containers
default mapped_directory_mount := {"allowed": false}

mapped_directory_mounted(target) {
data.metadata.mapped_directories[target]
}

mapped_directory_mount := {"metadata": [add_mapped_dir], "allowed": true} {
not mapped_directory_mounted(input.containerPath)
input.readOnly
add_mapped_dir := {
"name": "mapped_directories",
"action": "add",
"key": input.containerPath,
"value": {"readOnly": input.readOnly},
}
}

default mapped_directory_unmount := {"allowed": false}

mapped_directory_unmount := {"metadata": [remove_mapped_dir], "allowed": true} {
mapped_directory_mounted(input.unmountTarget)
remove_mapped_dir := {
"name": "mapped_directories",
"action": "remove",
"key": input.unmountTarget,
}
}

# Registry changes validation
default registry_changes := {"allowed": false}

Expand Down Expand Up @@ -1827,6 +1856,21 @@ errors["no scratch at path to unmount"] {
not scratch_mounted(input.unmountTarget)
}

errors["mapped directory already mounted at path"] {
input.rule == "mapped_directory_mount"
mapped_directory_mounted(input.containerPath)
}

errors["writable mapped directory not allowed"] {
input.rule == "mapped_directory_mount"
not input.readOnly
}

errors["no mapped directory at path to unmount"] {
input.rule == "mapped_directory_unmount"
not mapped_directory_mounted(input.unmountTarget)
}

errors[framework_version_error] {
policy_framework_version == null
framework_version_error := concat(" ", ["framework_version is missing. Current version:", version])
Expand Down
2 changes: 2 additions & 0 deletions pkg/securitypolicy/open_door.rego
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ runtime_logging := {"allowed": true}
load_fragment := {"allowed": true}
scratch_mount := {"allowed": true}
scratch_unmount := {"allowed": true}
mapped_directory_mount := {"allowed": true}
mapped_directory_unmount := {"allowed": true}
2 changes: 2 additions & 0 deletions pkg/securitypolicy/policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,6 @@ runtime_logging := data.framework.runtime_logging
load_fragment := data.framework.load_fragment
scratch_mount := data.framework.scratch_mount
scratch_unmount := data.framework.scratch_unmount
mapped_directory_mount := data.framework.mapped_directory_mount
mapped_directory_unmount := data.framework.mapped_directory_unmount
reason := data.framework.reason
Loading
Loading