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
55 changes: 36 additions & 19 deletions internal/gcs-sidecar/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,22 +57,16 @@ func (b *Bridge) createContainer(req *request) (err error) {
return errors.Wrap(err, "failed to unmarshal createContainer")
}

// containerConfig can be of type uvnConfig or hcsschema.HostedSystem or guestresource.CWCOWHostedSystem
// containerConfig can be of type uvmConfig or guestresource.CWCOWHostedSystem
var (
uvmConfig prot.UvmConfig
hostedSystemConfig hcsschema.HostedSystem
cwcowHostedSystemConfig guestresource.CWCOWHostedSystem
)
if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &uvmConfig); err == nil &&
uvmConfig.SystemType != "" {
systemType := uvmConfig.SystemType
timeZoneInformation := uvmConfig.TimeZoneInformation
log.G(ctx).Tracef("createContainer: uvmConfig: {systemType: %v, timeZoneInformation: %v}}", systemType, timeZoneInformation)
} else if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &hostedSystemConfig); err == nil &&
hostedSystemConfig.SchemaVersion != nil && hostedSystemConfig.Container != nil {
schemaVersion := hostedSystemConfig.SchemaVersion
container := hostedSystemConfig.Container
log.G(ctx).Tracef("rpcCreate: HostedSystemConfig: {schemaVersion: %v, container: %v}}", schemaVersion, container)
} else if err = commonutils.UnmarshalJSONWithHresult(containerConfig, &cwcowHostedSystemConfig); err == nil &&
cwcowHostedSystemConfig.Spec.Version != "" && cwcowHostedSystemConfig.CWCOWHostedSystem.Container != nil {
cwcowHostedSystem := cwcowHostedSystemConfig.CWCOWHostedSystem
Expand Down Expand Up @@ -551,21 +545,44 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) {
switch settings.RPCType {
case guestrequest.RPCModifyServiceSettings, guestrequest.RPCStartLogForwarding, guestrequest.RPCStopLogForwarding:
log.G(req.ctx).Tracef("%v request received for LogForwardService, proceeding with policy enforcement for log sources", settings.RPCType)
// Enforce the policy for log sources in the request and update the settings with allowed log sources.
// For cwcow, the sidecar-GCS will verify the allowed log sources against policy and append the necessary GUIDs to the ones allowed. Rest are dropped.
// The Enforcer will have to unmarshal the log sources, enforce the policy and then marshal it back to a Base64 encoded JSON string which is what inbox GCS expects.
// It can query etw.GetDefaultLogSources to get the default log sources if the policy allows, and allow providers matching the default list during policy enforcement.
// This is because the log sources can be a combination of default and user specified log sources for which GUIDs need to be appended based on the policy enforcement.
if settings.Settings != "" {
// <EXAMPLE CALL>
// allowedLogSources, err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogForwardServiceSettingsPolicy(req.ctx, settings.LogSources)
// Decode the base64-encoded log sources config
logSources, err := etw.DecodeAndUnmarshalLogSources(settings.Settings)
if err != nil {
return fmt.Errorf("failed to decode log sources: %w", err)
}

// Filter providers against policy — keep only those allowed
var filteredSources []etw.Source
for _, source := range logSources.LogConfig.Sources {
var allowedProviders []etw.EtwProvider
for _, provider := range source.Providers {
if err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogProviderPolicy(
req.ctx, provider.ProviderName); err != nil {
log.G(req.ctx).Tracef("Log provider %q denied by policy", provider.ProviderName)
continue
}
allowedProviders = append(allowedProviders, provider)
}
if len(allowedProviders) > 0 {
filteredSources = append(filteredSources, etw.Source{
Type: source.Type,
Providers: allowedProviders,
})
}
}

filteredLogSources := etw.LogSourcesInfo{
LogConfig: etw.LogConfig{Sources: filteredSources},
}

// For now, we are skipping the policy enforcement and allowing all log sources as the policy enforcer implementation is in progress. We will add the enforcement back once it's implemented.
allowedLogSources := settings.Settings // This is Base64 encoded JSON string of log sources
log.G(req.ctx).Tracef("Allowed log sources after policy enforcement: %v", allowedLogSources)
// Re-encode and apply GUID resolution
encodedFiltered, err := etw.MarshalAndEncodeLogSources(filteredLogSources)
if err != nil {
return fmt.Errorf("failed to encode filtered log sources: %w", err)
}

// Update the allowed log sources in the settings. This will be forwarded to inbox GCS which expects the log sources in a JSON string format with GUIDs for providers included.
allowedLogSources, err := etw.UpdateLogSources(allowedLogSources, false, true)
allowedLogSources, err := etw.UpdateLogSources(encodedFiltered, false, true)
if err != nil {
return fmt.Errorf("failed to update log sources: %w", err)
}
Expand Down
12 changes: 6 additions & 6 deletions internal/vm/vmutils/etw/provider_map.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ func mergeLogSources(resultSources []Source, userSources []Source) []Source {
return resultSources
}

// decodeAndUnmarshalLogSources decodes a base64-encoded JSON string and unmarshals it into a LogSourcesInfo.
func decodeAndUnmarshalLogSources(base64EncodedJSONLogConfig string) (LogSourcesInfo, error) {
// DecodeAndUnmarshalLogSources decodes a base64-encoded JSON string and unmarshals it into a LogSourcesInfo.
func DecodeAndUnmarshalLogSources(base64EncodedJSONLogConfig string) (LogSourcesInfo, error) {
jsonBytes, err := base64.StdEncoding.DecodeString(base64EncodedJSONLogConfig)
if err != nil {
return LogSourcesInfo{}, fmt.Errorf("error decoding base64 log config: %w", err)
Expand Down Expand Up @@ -174,9 +174,9 @@ func applyGUIDPolicy(sources []Source, includeGUIDs bool) ([]Source, error) {
return stripRedundantGUIDs(sources)
}

// marshalAndEncodeLogSources marshals the given LogSourcesInfo to JSON and encodes it as a base64 string.
// MarshalAndEncodeLogSources marshals the given LogSourcesInfo to JSON and encodes it as a base64 string.
// On error, it logs and returns the original fallback string.
func marshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) {
func MarshalAndEncodeLogSources(logCfg LogSourcesInfo) (string, error) {
jsonBytes, err := json.Marshal(logCfg)
if err != nil {
return "", fmt.Errorf("error marshalling log config: %w", err)
Expand All @@ -194,7 +194,7 @@ func UpdateLogSources(base64EncodedJSONLogConfig string, useDefaultLogSources bo
}

if base64EncodedJSONLogConfig != "" {
userLogSources, err := decodeAndUnmarshalLogSources(base64EncodedJSONLogConfig)
userLogSources, err := DecodeAndUnmarshalLogSources(base64EncodedJSONLogConfig)
if err != nil {
return "", fmt.Errorf("failed to decode and unmarshal user log sources: %w", err)
}
Expand All @@ -208,7 +208,7 @@ func UpdateLogSources(base64EncodedJSONLogConfig string, useDefaultLogSources bo
return "", fmt.Errorf("failed to apply GUID policy: %w", err)
}

result, err := marshalAndEncodeLogSources(resultLogCfg)
result, err := MarshalAndEncodeLogSources(resultLogCfg)
if err != nil {
return "", fmt.Errorf("failed to marshal and encode log sources: %w", err)
}
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/api.rego
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,5 @@ 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},
"log_provider": {"introducedVersion": "0.11.0", "default_results": {"allowed": true}, "use_framework": false},
}
13 changes: 13 additions & 0 deletions pkg/securitypolicy/framework.rego
Original file line number Diff line number Diff line change
Expand Up @@ -1299,6 +1299,14 @@ scratch_unmount := {"metadata": [remove_scratch_mount], "allowed": true} {
}
}

# Log provider validation for Windows containers
default log_provider := {"allowed": false}

log_provider := {"allowed": true} {
some allowed_provider in data.policy.allowed_log_providers
lower(input.providerName) == lower(allowed_provider)
}

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

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

errors["log provider not allowed by policy"] {
input.rule == "log_provider"
not log_provider.allowed
}

errors[framework_version_error] {
policy_framework_version == null
framework_version_error := concat(" ", ["framework_version is missing. Current version:", version])
Expand Down
1 change: 1 addition & 0 deletions pkg/securitypolicy/open_door.rego
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ runtime_logging := {"allowed": true}
load_fragment := {"allowed": true}
scratch_mount := {"allowed": true}
scratch_unmount := {"allowed": true}
log_provider := {"allowed": true}
1 change: 1 addition & 0 deletions pkg/securitypolicy/policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ runtime_logging := data.framework.runtime_logging
load_fragment := data.framework.load_fragment
scratch_mount := data.framework.scratch_mount
scratch_unmount := data.framework.scratch_unmount
log_provider := data.framework.log_provider
reason := data.framework.reason
110 changes: 110 additions & 0 deletions pkg/securitypolicy/regopolicy_windows_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1514,3 +1514,113 @@ func substituteUVMPath(sandboxID string, m mountInternal) mountInternal {
_ = sandboxID
return m
}

// Tests for log provider enforcement

func Test_Rego_EnforceLogProviderPolicy_Allowed_Windows(t *testing.T) {
rego := fmt.Sprintf(`package policy
api_version := "%s"
framework_version := "%s"

allowed_log_providers := [
"microsoft.windows.hyperv.compute",
"microsoft-windows-guest-network-service",
]

log_provider := data.framework.log_provider
`, apiVersion, frameworkVersion)

policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType)
if err != nil {
t.Fatalf("failed to create policy: %v", err)
}

ctx := context.Background()
err = policy.EnforceLogProviderPolicy(ctx, "microsoft.windows.hyperv.compute")
if err != nil {
t.Errorf("expected allowed provider to pass: %v", err)
}
}

func Test_Rego_EnforceLogProviderPolicy_Denied_Windows(t *testing.T) {
rego := fmt.Sprintf(`package policy
api_version := "%s"
framework_version := "%s"

allowed_log_providers := [
"microsoft.windows.hyperv.compute",
]

log_provider := data.framework.log_provider
`, apiVersion, frameworkVersion)

policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType)
if err != nil {
t.Fatalf("failed to create policy: %v", err)
}

ctx := context.Background()
err = policy.EnforceLogProviderPolicy(ctx, "some-malicious-provider")
if err == nil {
t.Errorf("expected unknown provider to be denied")
}
}

func Test_Rego_EnforceLogProviderPolicy_CaseInsensitive_Windows(t *testing.T) {
rego := fmt.Sprintf(`package policy
api_version := "%s"
framework_version := "%s"

allowed_log_providers := [
"microsoft.windows.hyperv.compute",
]

log_provider := data.framework.log_provider
`, apiVersion, frameworkVersion)

policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType)
if err != nil {
t.Fatalf("failed to create policy: %v", err)
}

ctx := context.Background()
err = policy.EnforceLogProviderPolicy(ctx, "Microsoft.Windows.Hyperv.Compute")
if err != nil {
t.Errorf("expected case-insensitive match to pass: %v", err)
}
}

func Test_Rego_EnforceLogProviderPolicy_OpenDoor_AllowsAll_Windows(t *testing.T) {
policy, err := newRegoPolicy(openDoorRego, []oci.Mount{}, []oci.Mount{}, testOSType)
if err != nil {
t.Fatalf("failed to create policy: %v", err)
}

ctx := context.Background()
err = policy.EnforceLogProviderPolicy(ctx, "any-provider-at-all")
if err != nil {
t.Errorf("open door should allow any provider: %v", err)
}
}

func Test_Rego_EnforceLogProviderPolicy_EmptyAllowList_DeniesAll_Windows(t *testing.T) {
rego := fmt.Sprintf(`package policy
api_version := "%s"
framework_version := "%s"

allowed_log_providers := []

log_provider := data.framework.log_provider
`, apiVersion, frameworkVersion)

policy, err := newRegoPolicy(rego, []oci.Mount{}, []oci.Mount{}, testOSType)
if err != nil {
t.Fatalf("failed to create policy: %v", err)
}

ctx := context.Background()
err = policy.EnforceLogProviderPolicy(ctx, "microsoft.windows.hyperv.compute")
if err == nil {
t.Errorf("expected empty allow list to deny all providers")
}
}
9 changes: 9 additions & 0 deletions pkg/securitypolicy/securitypolicyenforcer.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ type SecurityPolicyEnforcer interface {
GetUserInfo(spec *oci.Process, rootPath string) (IDName, []IDName, string, error)
EnforceVerifiedCIMsPolicy(ctx context.Context, containerID string, layerHashes []string, mountedCim []string) (err error)
EnforceRegistryChangesPolicy(ctx context.Context, containerID string, registryValues interface{}) error
EnforceLogProviderPolicy(ctx context.Context, providerName string) error
}

//nolint:unused
Expand Down Expand Up @@ -324,6 +325,10 @@ func (OpenDoorSecurityPolicyEnforcer) EnforceRegistryChangesPolicy(ctx context.C
return nil
}

func (OpenDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, string) error {
return nil
}

type ClosedDoorSecurityPolicyEnforcer struct{}

var _ SecurityPolicyEnforcer = (*ClosedDoorSecurityPolicyEnforcer)(nil)
Expand Down Expand Up @@ -452,3 +457,7 @@ func (ClosedDoorSecurityPolicyEnforcer) EnforceVerifiedCIMsPolicy(ctx context.Co
func (ClosedDoorSecurityPolicyEnforcer) EnforceRegistryChangesPolicy(ctx context.Context, containerID string, registryValues interface{}) error {
return errors.New("registry changes are denied by policy")
}

func (ClosedDoorSecurityPolicyEnforcer) EnforceLogProviderPolicy(context.Context, string) error {
return errors.New("log provider is denied by policy")
}
8 changes: 8 additions & 0 deletions pkg/securitypolicy/securitypolicyenforcer_rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -1188,6 +1188,14 @@ func (policy *regoEnforcer) EnforceRegistryChangesPolicy(ctx context.Context, co
return err
}

func (policy *regoEnforcer) EnforceLogProviderPolicy(ctx context.Context, providerName string) error {
input := inputData{
"providerName": providerName,
}
_, err := policy.enforce(ctx, "log_provider", input)
return err
}

func (policy *regoEnforcer) GetUserInfo(process *oci.Process, rootPath string) (IDName, []IDName, string, error) {
return GetAllUserInfo(process, rootPath)
}
Loading