Skip to content
Open
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
166 changes: 144 additions & 22 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
)
Comment thread
takuro-sato marked this conversation as resolved.
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 @@ -536,7 +530,6 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) {
defer span.End()
defer func() { oc.SetSpanStatus(span, err) }()

// Todo: Add policy enforcement for modifying service settings
modifyRequest, err := unmarshalModifyServiceSettings(req)
if err != nil {
return fmt.Errorf("failed to unmarshal modifyServiceSettings request: %w", err)
Expand All @@ -551,28 +544,110 @@ 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 != "" {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we know security policy exists at this point?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From guest point of view, no we don't know. Host might not set security policy before setting log provider. In that case the default closed door policy is used and any log provider is denied

// <EXAMPLE CALL>
// allowedLogSources, err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogForwardServiceSettingsPolicy(req.ctx, settings.LogSources)
// Decode the base64-encoded log sources config so we can
// enforce policy on the requested provider list.
logSources, err := etw.DecodeAndUnmarshalLogSources(settings.Settings)
if err != nil {
return fmt.Errorf("failed to decode log sources: %w", err)
}
Comment thread
takuro-sato marked this conversation as resolved.

// Validate host-supplied (Name, GUID) pairs before
// name-based policy enforcement.
if err := validateLogProviders(logSources.LogConfig.Sources); err != nil {
return fmt.Errorf("log providers rejected: %w", err)
}

// 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)
// Collect every requested provider name and ask the
// enforcer to validate them as a batch. The enforcer's
// behaviour depends on allow_log_provider_dropping in the
// active policy:
// - false (default, fail-close): any disallowed provider
// causes the call to be denied.
// - true: disallowed providers are silently dropped and
// the kept subset is returned for forwarding.
var requestedNames []string
for _, source := range logSources.LogConfig.Sources {
for _, provider := range source.Providers {
requestedNames = append(requestedNames, provider.ProviderName)
}
}

// 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)
keptNames, err := b.hostState.securityOptions.PolicyEnforcer.EnforceLogProviderPolicy(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like all the kept/dropped names and the logging of them should be encapsulated by the policy enforcement and the handlers should just forward the "final log sources request" to inbox GCS?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It follows the existing function getEnvsToKeep for env var. I think the dropping should be consistent with env var and they can stay like this at least for now. What do you think?

req.ctx, requestedNames)
if err != nil {
return fmt.Errorf("log providers denied by policy: %w", err)
}

// Build a quick lookup for the kept set so we can trim the
// LogSourcesInfo to only those providers the policy allowed.
keepSet := make(map[string]struct{}, len(keptNames))
for _, name := range keptNames {
keepSet[name] = struct{}{}
}

// Detect trimming by scanning requested names against
// keepSet. We cannot use len(kept) != len(requested):
// the rego enforcer returns providers_to_keep via a set
// (see getProvidersToKeep → keepSet.toArray()), so a
// duplicate-name request like [A, A, B] returns [A, B]
// even when nothing was dropped, which would otherwise
// trip a false-positive warning and a needless re-marshal.
dropped := make([]string, 0)
seenDropped := make(map[string]struct{})
for _, name := range requestedNames {
if _, ok := keepSet[name]; ok {
continue
}
if _, dup := seenDropped[name]; dup {
continue
}
seenDropped[name] = struct{}{}
dropped = append(dropped, name)
}

// Trim happens in-place on the parsed structure so we can
// hand it to UpdateLogSourcesFromInfo without a redundant
// base64-decode + JSON-unmarshal round-trip (we already
// decoded above for enforcement).
trimmed := logSources
if len(dropped) > 0 {
// Surface the drop so operators have a breadcrumb —
// under allow_log_provider_dropping the pod boots
// silently, and forwardlogs may itself be off, so
// without this warning the trim is invisible.
log.G(req.ctx).WithFields(map[string]interface{}{
"requested": requestedNames,
"kept": keptNames,
"dropped": dropped,
}).Warn("log providers trimmed by policy (allow_log_provider_dropping)")

// Trim each source's provider list to only the
// allowed names. Empty sources are preserved to keep
// the shape stable; inbox GCS handles them as no-ops.
for i := range trimmed.LogConfig.Sources {
src := &trimmed.LogConfig.Sources[i]
filtered := make([]etw.EtwProvider, 0, len(src.Providers))
for _, p := range src.Providers {
if _, ok := keepSet[p.ProviderName]; ok {
filtered = append(filtered, p)
}
}
src.Providers = filtered
}
}

// Apply GUID resolution (and any other inbox-GCS prep)
// against the policy-trimmed payload and hand off to
// inbox GCS.
allowedLogSources, err := etw.UpdateLogSourcesFromInfo(trimmed, false, true)
Comment thread
anmaxvl marked this conversation as resolved.
if err != nil {
return fmt.Errorf("failed to update log sources: %w", err)
}
settings.Settings = allowedLogSources
}
default:
log.G(req.ctx).Warningf("modifyServiceSettings for LogForwardService with RPCType: %v, skipping policy enforcement", settings.RPCType)
return fmt.Errorf("modifyServiceSettings for LogForwardService: unsupported RPCType %q", settings.RPCType)
}
modifyRequest.Settings = settings
buf, err := json.Marshal(modifyRequest)
Expand All @@ -589,12 +664,59 @@ func (b *Bridge) modifyServiceSettings(req *request) (err error) {
log.G(req.ctx).Warningf("modifyServiceSettings for LogForwardService with empty settings, skipping policy enforcement")
}
default:
log.G(req.ctx).Warningf("modifyServiceSettings with PropertyType: %v, skipping policy enforcement", modifyRequest.PropertyType)
return fmt.Errorf("modifyServiceSettings: unsupported PropertyType %q", modifyRequest.PropertyType)
}
b.forwardRequestToGcs(req)
return nil
}

// validateLogProviders validates host-supplied log providers before they
// reach the name-based policy enforcer.
//
// CWCOW policy approves provider names, but inbox GCS subscribes by GUID. If
// the host could send {Name: "allowed", GUID: "<disallowed>"} the name-based
// enforcer would approve and the disallowed GUID would still be forwarded
// (resolveGUIDsWithLookup keeps any GUID the host set). To close that bypass
// the sidecar rejects, before enforcement, any entry whose (Name, GUID) pair
// is not verifiable against the well-known ETW map:
//
// - Name == "": rejected. Policy is name-based; a GUID-only entry has
// nothing for the enforcer to evaluate.
// - Name + GUID where Name is not in the well-known map: rejected. We have
// no ground truth to compare the GUID against, so we cannot verify the
// host's claim. Name-only is still accepted for downstream resolution to
// stay best-effort.
// - Name + GUID where the GUID disagrees with the well-known lookup for
// Name: rejected.
//
// Name-only entries are passed through unchanged; the sidecar fills in the
// canonical GUID after enforcement via etw.UpdateLogSourcesFromInfo.
func validateLogProviders(sources []etw.Source) error {
for _, src := range sources {
for _, p := range src.Providers {
if p.ProviderName == "" {
return fmt.Errorf("provider with no name is not allowed (GUID %q)", p.ProviderGUID)
}
if p.ProviderGUID == "" {
continue
}
well := etw.GetProviderGUIDFromName(p.ProviderName)
if well == "" {
return fmt.Errorf("provider %q: name not in well-known ETW map; cannot verify supplied GUID %q", p.ProviderName, p.ProviderGUID)
}
suppliedTrimmed := strings.TrimSuffix(strings.TrimPrefix(strings.TrimSpace(p.ProviderGUID), "{"), "}")
supplied, err := guid.FromString(suppliedTrimmed)
if err != nil {
return fmt.Errorf("provider %q: invalid GUID %q: %w", p.ProviderName, p.ProviderGUID, err)
}
if !strings.EqualFold(supplied.String(), well) {
return fmt.Errorf("provider %q: supplied GUID %q does not match well-known GUID %q", p.ProviderName, p.ProviderGUID, well)
}
}
}
return nil
}

func volumeGUIDFromLayerPath(path string) (string, bool) {
if p, ok := strings.CutPrefix(path, `\\?\Volume{`); ok {
if q, ok := strings.CutSuffix(p, `}\Files`); ok {
Expand Down
Loading
Loading