//go:build linux && rego // +build linux,rego package securitypolicy import ( "context" _ "embed" "encoding/base64" "encoding/json" "fmt" "os" "path/filepath" "strconv" "strings" "syscall" "github.com/Microsoft/hcsshim/internal/guest/spec" "github.com/Microsoft/hcsshim/internal/guestpath" "github.com/Microsoft/hcsshim/internal/log" rpi "github.com/Microsoft/hcsshim/internal/regopolicyinterpreter" "github.com/opencontainers/runc/libcontainer/user" oci "github.com/opencontainers/runtime-spec/specs-go" "github.com/pkg/errors" ) const regoEnforcerName = "rego" func init() { registeredEnforcers[regoEnforcerName] = createRegoEnforcer // Overriding the value inside init guarantees that this assignment happens // after the variable has been initialized in securitypolicy.go and there // are no race conditions. When multiple init functions are defined in a // single package, the order of their execution is determined by the // filename. defaultEnforcer = regoEnforcerName defaultMarshaller = regoMarshaller } const capabilitiesNilError = "capabilities object provided by the UVM to the policy engine is nil" const invalidPolicyMessage = "Security policy is not valid. Please check security policy or re-generate with tooling." const noReasonMessage = "Security policy is either not valid or did not provide a reason for denial. Please check security policy or re-generate with tooling." const noAPIVersionError = "policy does not define api_version" // RegoEnforcer is a stub implementation of a security policy, which will be // based on [Rego] policy language. The detailed implementation will be // introduced in the subsequent PRs and documentation updated accordingly. // // [Rego]: https://www.openpolicyagent.org/docs/latest/policy-language/ type regoEnforcer struct { // Base64 encoded (JSON) policy base64policy string // Rego interpreter rego *rpi.RegoPolicyInterpreter // Default mount data defaultMounts []oci.Mount // Stdio allowed state on a per container id basis stdio map[string]bool // Maximum error message length maxErrorMessageLength int } var _ SecurityPolicyEnforcer = (*regoEnforcer)(nil) //nolint:unused func (sp SecurityPolicy) toInternal() (*securityPolicyInternal, error) { policy := new(securityPolicyInternal) var err error if policy.Containers, err = sp.Containers.toInternal(); err != nil { return nil, err } return policy, nil } func toStringSet(items []string) stringSet { s := make(stringSet) for _, item := range items { s.add(item) } return s } func (s stringSet) toArray() []string { a := make([]string, 0, len(s)) for item := range s { a = append(a, item) } return a } func (a stringSet) intersect(b stringSet) stringSet { s := make(stringSet) for item := range a { if b.contains(item) { s.add(item) } } return s } type inputData map[string]interface{} func createRegoEnforcer(base64EncodedPolicy string, defaultMounts []oci.Mount, privilegedMounts []oci.Mount, maxErrorMessageLength int, ) (SecurityPolicyEnforcer, error) { // base64 decode the incoming policy string // It will either be (legacy) JSON or Rego. rawPolicy, err := base64.StdEncoding.DecodeString(base64EncodedPolicy) if err != nil { return nil, fmt.Errorf("unable to decode policy from Base64 format: %w", err) } // Try to unmarshal the JSON var code string securityPolicy := new(SecurityPolicy) err = json.Unmarshal(rawPolicy, securityPolicy) if err == nil { if securityPolicy.AllowAll { return createOpenDoorEnforcer(base64EncodedPolicy, defaultMounts, privilegedMounts, maxErrorMessageLength) } containers := make([]*Container, securityPolicy.Containers.Length) for i := 0; i < securityPolicy.Containers.Length; i++ { index := strconv.Itoa(i) cConf, ok := securityPolicy.Containers.Elements[index] if !ok { return nil, fmt.Errorf("container constraint with index %q not found", index) } cConf.AllowStdioAccess = true cConf.NoNewPrivileges = false cConf.User = UserConfig{ UserIDName: IDNameConfig{Strategy: IDNameStrategyAny}, GroupIDNames: []IDNameConfig{{Strategy: IDNameStrategyAny}}, Umask: "0022", } cConf.SeccompProfileSHA256 = "" containers[i] = &cConf } code, err = marshalRego( securityPolicy.AllowAll, containers, []ExternalProcessConfig{}, []FragmentConfig{}, true, true, true, false, true, false, ) if err != nil { return nil, fmt.Errorf("error marshaling the policy to Rego: %w", err) } } else { // this is either a Rego policy or malformed JSON code = string(rawPolicy) } regoPolicy, err := newRegoPolicy(code, defaultMounts, privilegedMounts) if err != nil { return nil, fmt.Errorf("error creating Rego policy: %w", err) } regoPolicy.base64policy = base64EncodedPolicy regoPolicy.maxErrorMessageLength = maxErrorMessageLength return regoPolicy, nil } func (policy *regoEnforcer) enableLogging(path string, logLevel rpi.LogLevel) { policy.rego.EnableLogging(path, logLevel) } func newRegoPolicy(code string, defaultMounts []oci.Mount, privilegedMounts []oci.Mount) (policy *regoEnforcer, err error) { policy = new(regoEnforcer) policy.defaultMounts = make([]oci.Mount, len(defaultMounts)) copy(policy.defaultMounts, defaultMounts) defaultMountData := make([]interface{}, 0, len(defaultMounts)) privilegedMountData := make([]interface{}, 0, len(privilegedMounts)) data := map[string]interface{}{ "defaultMounts": appendMountData(defaultMountData, defaultMounts), "privilegedMounts": appendMountData(privilegedMountData, privilegedMounts), "sandboxPrefix": guestpath.SandboxMountPrefix, "hugePagesPrefix": guestpath.HugePagesMountPrefix, "plan9Prefix": plan9Prefix, "defaultUnprivilegedCapabilities": DefaultUnprivilegedCapabilities(), "defaultPrivilegedCapabilities": DefaultPrivilegedCapabilities(), } policy.rego, err = rpi.NewRegoPolicyInterpreter(code, data) if err != nil { return nil, err } policy.stdio = map[string]bool{} policy.base64policy = "" policy.rego.AddModule("framework.rego", &rpi.RegoModule{Namespace: "framework", Code: FrameworkCode}) policy.rego.AddModule("api.rego", &rpi.RegoModule{Namespace: "api", Code: APICode}) err = policy.rego.Compile() if err != nil { return nil, fmt.Errorf("rego compilation failed: %w", err) } // by default we do not perform message truncation policy.maxErrorMessageLength = 0 return policy, nil } func (policy *regoEnforcer) applyDefaults(enforcementPoint string, results rpi.RegoQueryResult) (rpi.RegoQueryResult, error) { deny := rpi.RegoQueryResult{"allowed": false} info, err := policy.queryEnforcementPoint(enforcementPoint) if err != nil { return deny, err } if results.IsEmpty() && info.availableByPolicyVersion { // policy should define this rule but it is missing return deny, fmt.Errorf("rule for %s is missing from policy", enforcementPoint) } return info.defaultResults.Union(results), nil } type enforcementPointInfo struct { availableByPolicyVersion bool defaultResults rpi.RegoQueryResult } func (policy *regoEnforcer) queryEnforcementPoint(enforcementPoint string) (*enforcementPointInfo, error) { input := inputData{ "name": enforcementPoint, "rule": enforcementPoint, } result, err := policy.rego.Query("data.framework.enforcement_point_info", input) if err != nil { return nil, fmt.Errorf("error querying enforcement point information: %w", err) } unknown, err := result.Bool("unknown") if err != nil { return nil, err } if unknown { return nil, fmt.Errorf("enforcement point rule %s does not exist", enforcementPoint) } invalid, err := result.Bool("invalid") if err != nil { return nil, err } if invalid { return nil, fmt.Errorf("enforcement point rule %s is invalid", enforcementPoint) } versionMissing, err := result.Bool("version_missing") if err != nil { return nil, err } if versionMissing { return nil, errors.New(noAPIVersionError) } defaultResults, err := result.Object("default_results") if err != nil { return nil, errors.New("enforcement point result missing defaults") } availableByPolicyVersion, err := result.Bool("available") if err != nil { return nil, errors.New("enforcement point result missing availability info") } return &enforcementPointInfo{ availableByPolicyVersion: availableByPolicyVersion, defaultResults: defaultResults, }, nil } func (policy *regoEnforcer) enforce(ctx context.Context, enforcementPoint string, input inputData) (rpi.RegoQueryResult, error) { rule := "data.policy." + enforcementPoint result, err := policy.rego.Query(rule, input) if err != nil { return nil, policy.denyWithError(ctx, err, input) } result, err = policy.applyDefaults(enforcementPoint, result) if err != nil { return result, policy.denyWithError(ctx, err, input) } allowed, err := result.Bool("allowed") if err != nil { return nil, policy.denyWithError(ctx, err, input) } if !allowed { return nil, policy.denyWithReason(ctx, enforcementPoint, input) } return result, nil } type decisionTruncator func(map[string]interface{}) func truncateErrorObjects(decision map[string]interface{}) { if rawReason, ok := decision["reason"]; ok { // check if it is a framework reason object if reason, ok := rawReason.(rpi.RegoQueryResult); ok { // check if we can remove error_objects if _, ok := reason["error_objects"]; ok { decision["truncated"] = append(decision["truncated"].([]string), "reason.error_objects") delete(reason, "error_objects") decision["reason"] = reason } } } } func truncateInput(decision map[string]interface{}) { if _, ok := decision["input"]; ok { // remove the input decision["truncated"] = append(decision["truncated"].([]string), "input") delete(decision, "input") } } func truncateReason(decision map[string]interface{}) { decision["truncated"] = append(decision["truncated"].([]string), "reason") delete(decision, "reason") } func (policy *regoEnforcer) policyDecisionToError(ctx context.Context, decision map[string]interface{}) error { decisionJSON, err := json.Marshal(decision) if err != nil { log.G(ctx).WithError(err).Error("unable to marshal error object") decisionJSON = []byte(`"Unable to marshal error object"`) } log.G(ctx).WithField("policyDecision", string(decisionJSON)) base64EncodedDecisionJSON := base64.StdEncoding.EncodeToString(decisionJSON) errorMessage := fmt.Errorf(policyDecisionPattern, base64EncodedDecisionJSON) if policy.maxErrorMessageLength == 0 { // indicates no message truncation return fmt.Errorf(policyDecisionPattern, base64EncodedDecisionJSON) } if len(errorMessage.Error()) <= policy.maxErrorMessageLength { return errorMessage } decision["truncated"] = []string{} truncators := []decisionTruncator{truncateErrorObjects, truncateInput, truncateReason} for _, truncate := range truncators { truncate(decision) decisionJSON, err := json.Marshal(decision) if err != nil { log.G(ctx).WithError(err).Error("unable to marshal error object") decisionJSON = []byte(`"Unable to marshal error object"`) } base64EncodedDecisionJSON = base64.StdEncoding.EncodeToString(decisionJSON) errorMessage = fmt.Errorf(policyDecisionPattern, base64EncodedDecisionJSON) if len(errorMessage.Error()) <= policy.maxErrorMessageLength { break } } return errorMessage } func (policy *regoEnforcer) denyWithError(ctx context.Context, policyError error, input inputData) error { input = policy.redactSensitiveData(input) input = replaceCapabilitiesWithPlaceholders(input) policyDecision := map[string]interface{}{ "input": input, "decision": "deny", "reason": invalidPolicyMessage, "policyError": policyError.Error(), } return policy.policyDecisionToError(ctx, policyDecision) } func (policy *regoEnforcer) denyWithReason(ctx context.Context, enforcementPoint string, input inputData) error { cleaned_input := policy.redactSensitiveData(input) cleaned_input = replaceCapabilitiesWithPlaceholders(cleaned_input) input["rule"] = enforcementPoint policyDecision := map[string]interface{}{ "input": cleaned_input, "decision": "deny", } result, err := policy.rego.Query("data.policy.reason", input) if err == nil { if result.IsEmpty() { policyDecision["reason"] = noReasonMessage } else { policyDecision["reason"] = replaceCapabilitiesWithPlaceholdersInReason(result) } } else { log.G(ctx).WithError(err).Warn("unable to obtain reason for policy decision") policyDecision["reason"] = noReasonMessage } return policy.policyDecisionToError(ctx, policyDecision) } func areCapsEqual(actual map[string]interface{}, expected map[string][]string) bool { for key, caps := range expected { values, ok := actual[key].([]interface{}) if !ok { return false } if len(values) != len(caps) { return false } for i, value := range values { cap, ok := value.(string) if !ok { return false } if cap != caps[i] { return false } } } return true } var privilegedCapabilities = map[string][]string{ "bounding": DefaultPrivilegedCapabilities(), "effective": DefaultPrivilegedCapabilities(), "inheritable": DefaultPrivilegedCapabilities(), "permitted": DefaultPrivilegedCapabilities(), "ambient": EmptyCapabiltiesSet(), } var unprivilegedCapabilities = map[string][]string{ "bounding": DefaultUnprivilegedCapabilities(), "effective": DefaultUnprivilegedCapabilities(), "inheritable": EmptyCapabiltiesSet(), "permitted": DefaultUnprivilegedCapabilities(), "ambient": EmptyCapabiltiesSet(), } // as capability lists are repetitive and take up a lot of room in the error // message, we can replace the defaults with placeholders to save space func replaceCapabilitiesWithPlaceholders(object map[string]interface{}) map[string]interface{} { capabilities, ok := object["capabilities"].(map[string]interface{}) if !ok { return object } if areCapsEqual(capabilities, privilegedCapabilities) { object["capabilities"] = "[privileged]" } else if areCapsEqual(capabilities, unprivilegedCapabilities) { object["capabilities"] = "[unprivileged]" } return object } func replaceCapabilitiesWithPlaceholdersInReason(reason rpi.RegoQueryResult) rpi.RegoQueryResult { errorObjectsRaw, err := reason.Value("error_objects") if err != nil { return reason } errorObjects, ok := errorObjectsRaw.([]interface{}) if !ok { return reason } objects := make([]interface{}, len(errorObjects)) for i, objectRaw := range errorObjects { object, ok := objectRaw.(map[string]interface{}) if !ok { objects[i] = objectRaw continue } objects[i] = replaceCapabilitiesWithPlaceholders(object) } reason["error_objects"] = objects return reason } func (policy *regoEnforcer) redactSensitiveData(input inputData) inputData { if v, k := input["envList"]; k { newInput := make(inputData) for k, v := range input { newInput[k] = v } newEnvList := make([]string, 0) cast, ok := v.([]string) if ok { for _, env := range cast { parts := strings.Split(env, "=") redacted := parts[0] + "=<>" newEnvList = append(newEnvList, redacted) } } newInput["envList"] = newEnvList return newInput } return input } func (policy *regoEnforcer) EnforceDeviceMountPolicy(ctx context.Context, target string, deviceHash string) error { input := inputData{ "target": target, "deviceHash": deviceHash, } _, err := policy.enforce(ctx, "mount_device", input) return err } func (policy *regoEnforcer) EnforceOverlayMountPolicy(ctx context.Context, containerID string, layerPaths []string, target string) error { input := inputData{ "containerID": containerID, "layerPaths": layerPaths, "target": target, } _, err := policy.enforce(ctx, "mount_overlay", input) return err } func (policy *regoEnforcer) EnforceOverlayUnmountPolicy(ctx context.Context, target string) error { input := inputData{ "unmountTarget": target, } _, err := policy.enforce(ctx, "unmount_overlay", input) return err } func getEnvsToKeep(envList []string, results rpi.RegoQueryResult) ([]string, error) { value, err := results.Value("env_list") if err != nil || value == nil { // policy did not return an 'env_list'. This is interpreted // as "proceed with provided env list". return envList, nil } envsAsInterfaces, ok := value.([]interface{}) if !ok { return nil, fmt.Errorf("policy returned incorrect type for 'env_list', expected []interface{}, received %T", value) } keepSet := make(stringSet) for _, envAsInterface := range envsAsInterfaces { if env, ok := envAsInterface.(string); ok { keepSet.add(env) } else { return nil, fmt.Errorf("members of env_list from policy must be strings, received %T", envAsInterface) } } keepSet = keepSet.intersect(toStringSet(envList)) return keepSet.toArray(), nil } func getCapsToKeep(capsList *oci.LinuxCapabilities, results rpi.RegoQueryResult) (*oci.LinuxCapabilities, error) { value, err := results.Value("caps_list") if err != nil || value == nil { // policy did not return an 'caps_list'. This is interpreted // as "proceed with provided caps list". return capsList, nil } capsMap, ok := value.(map[string]interface{}) if !ok { return nil, fmt.Errorf("policy returned incorrect type for 'caps_list', expected map[string]interface{}, received %T", value) } bounding, err := filterCapabilities(capsList.Bounding, capsMap["bounding"]) if err != nil { return nil, err } effective, err := filterCapabilities(capsList.Effective, capsMap["effective"]) if err != nil { return nil, err } inheritable, err := filterCapabilities(capsList.Inheritable, capsMap["inheritable"]) if err != nil { return nil, err } permitted, err := filterCapabilities(capsList.Permitted, capsMap["permitted"]) if err != nil { return nil, err } ambient, err := filterCapabilities(capsList.Ambient, capsMap["ambient"]) if err != nil { return nil, err } return &oci.LinuxCapabilities{ Bounding: bounding, Effective: effective, Inheritable: inheritable, Permitted: permitted, Ambient: ambient, }, nil } func filterCapabilities(suppliedList []string, fromRegoCapsList interface{}) ([]string, error) { keepSet := make(stringSet) if capsList, ok := fromRegoCapsList.([]interface{}); ok { for _, capAsInterface := range capsList { if cap, ok := capAsInterface.(string); ok { keepSet.add(cap) } else { return nil, fmt.Errorf("members of capability sets from policy must be strings, received %T", capAsInterface) } } } else { return nil, fmt.Errorf("capability sets of caps_list from policy must be an array of interface{}, received %T", fromRegoCapsList) } keepSet = keepSet.intersect(toStringSet(suppliedList)) return keepSet.toArray(), nil } func (idName IDName) toInput() interface{} { return map[string]interface{}{ "id": idName.ID, "name": idName.Name, } } func groupsToInputs(groups []IDName) []interface{} { inputs := []interface{}{} for _, group := range groups { inputs = append(inputs, group.toInput()) } return inputs } func handleNilOrEmptyCaps(caps []string) interface{} { if len(caps) > 0 { result := make([]interface{}, len(caps)) for i, cap := range caps { result[i] = cap } return result } // caps is either nil or empty. // In either case, we want to return an empty array. return make([]interface{}, 0) } func mapifyCapabilities(caps *oci.LinuxCapabilities) map[string]interface{} { out := make(map[string]interface{}) out["bounding"] = handleNilOrEmptyCaps(caps.Bounding) out["effective"] = handleNilOrEmptyCaps(caps.Effective) out["inheritable"] = handleNilOrEmptyCaps(caps.Inheritable) out["permitted"] = handleNilOrEmptyCaps(caps.Permitted) out["ambient"] = handleNilOrEmptyCaps(caps.Ambient) return out } func (policy *regoEnforcer) EnforceCreateContainerPolicy( ctx context.Context, sandboxID string, containerID string, argList []string, envList []string, workingDir string, mounts []oci.Mount, privileged bool, noNewPrivileges bool, user IDName, groups []IDName, umask string, capabilities *oci.LinuxCapabilities, seccompProfileSHA256 string, ) (envToKeep EnvList, capsToKeep *oci.LinuxCapabilities, stdioAccessAllowed bool, err error) { if capabilities == nil { return nil, nil, false, errors.New(capabilitiesNilError) } input := inputData{ "containerID": containerID, "argList": argList, "envList": envList, "workingDir": workingDir, "sandboxDir": spec.SandboxMountsDir(sandboxID), "hugePagesDir": spec.HugePagesMountsDir(sandboxID), "mounts": appendMountData([]interface{}{}, mounts), "privileged": privileged, "noNewPrivileges": noNewPrivileges, "user": user.toInput(), "groups": groupsToInputs(groups), "umask": umask, "capabilities": mapifyCapabilities(capabilities), "seccompProfileSHA256": seccompProfileSHA256, } results, err := policy.enforce(ctx, "create_container", input) if err != nil { return nil, nil, false, err } envToKeep, err = getEnvsToKeep(envList, results) if err != nil { return nil, nil, false, err } capsToKeep, err = getCapsToKeep(capabilities, results) if err != nil { return nil, nil, false, err } stdioAccessAllowed, err = results.Bool("allow_stdio_access") if err != nil { return nil, nil, false, err } // Store the result of stdio access allowed for this container so we can use // it if we get queried about allowing exec in container access. Stdio access // is on a per-container, not per-process basis. policy.stdio[containerID] = stdioAccessAllowed return envToKeep, capsToKeep, stdioAccessAllowed, nil } func (policy *regoEnforcer) EnforceDeviceUnmountPolicy(ctx context.Context, unmountTarget string) error { input := inputData{ "unmountTarget": unmountTarget, } _, err := policy.enforce(ctx, "unmount_device", input) return err } func appendMountData(mountData []interface{}, mounts []oci.Mount) []interface{} { for _, mount := range mounts { mountData = append(mountData, inputData{ "destination": mount.Destination, "source": mount.Source, "options": mount.Options, "type": mount.Type, }) } return mountData } func (policy *regoEnforcer) ExtendDefaultMounts(mounts []oci.Mount) error { policy.defaultMounts = append(policy.defaultMounts, mounts...) defaultMounts := appendMountData([]interface{}{}, policy.defaultMounts) return policy.rego.UpdateData("defaultMounts", defaultMounts) } func (policy *regoEnforcer) EncodedSecurityPolicy() string { return policy.base64policy } func (policy *regoEnforcer) EnforceExecInContainerPolicy( ctx context.Context, containerID string, argList []string, envList []string, workingDir string, noNewPrivileges bool, user IDName, groups []IDName, umask string, capabilities *oci.LinuxCapabilities, ) (envToKeep EnvList, capsToKeep *oci.LinuxCapabilities, stdioAccessAllowed bool, err error) { if capabilities == nil { return nil, nil, false, errors.New(capabilitiesNilError) } input := inputData{ "containerID": containerID, "argList": argList, "envList": envList, "workingDir": workingDir, "noNewPrivileges": noNewPrivileges, "user": user.toInput(), "groups": groupsToInputs(groups), "umask": umask, "capabilities": mapifyCapabilities(capabilities), } results, err := policy.enforce(ctx, "exec_in_container", input) if err != nil { return nil, nil, false, err } envToKeep, err = getEnvsToKeep(envList, results) if err != nil { return nil, nil, false, err } capsToKeep, err = getCapsToKeep(capabilities, results) if err != nil { return nil, nil, false, err } return envToKeep, capsToKeep, policy.stdio[containerID], nil } func (policy *regoEnforcer) EnforceExecExternalProcessPolicy(ctx context.Context, argList []string, envList []string, workingDir string) (toKeep EnvList, stdioAccessAllowed bool, err error) { input := map[string]interface{}{ "argList": argList, "envList": envList, "workingDir": workingDir, } results, err := policy.enforce(ctx, "exec_external", input) if err != nil { return nil, false, err } toKeep, err = getEnvsToKeep(envList, results) if err != nil { return nil, false, err } stdioAccessAllowed, err = results.Bool("allow_stdio_access") if err != nil { return nil, false, err } return toKeep, stdioAccessAllowed, nil } func (policy *regoEnforcer) EnforceShutdownContainerPolicy(ctx context.Context, containerID string) error { input := inputData{ "containerID": containerID, } _, err := policy.enforce(ctx, "shutdown_container", input) return err } func (policy *regoEnforcer) EnforceSignalContainerProcessPolicy(ctx context.Context, containerID string, signal syscall.Signal, isInitProcess bool, startupArgList []string) error { input := inputData{ "containerID": containerID, "signal": signal, "isInitProcess": isInitProcess, "argList": startupArgList, } _, err := policy.enforce(ctx, "signal_container_process", input) return err } func (policy *regoEnforcer) EnforcePlan9MountPolicy(ctx context.Context, target string) error { mountPathPrefix := strings.Replace(guestpath.LCOWMountPathPrefixFmt, "%d", "[0-9]+", 1) input := inputData{ "rootPrefix": guestpath.LCOWRootPrefixInUVM, "mountPathPrefix": mountPathPrefix, "target": target, } _, err := policy.enforce(ctx, "plan9_mount", input) return err } func (policy *regoEnforcer) EnforcePlan9UnmountPolicy(ctx context.Context, target string) error { input := map[string]interface{}{ "unmountTarget": target, } _, err := policy.enforce(ctx, "plan9_unmount", input) return err } func (policy *regoEnforcer) EnforceGetPropertiesPolicy(ctx context.Context) error { input := make(inputData) _, err := policy.enforce(ctx, "get_properties", input) return err } func (policy *regoEnforcer) EnforceDumpStacksPolicy(ctx context.Context) error { input := make(inputData) _, err := policy.enforce(ctx, "dump_stacks", input) return err } func (policy *regoEnforcer) EnforceRuntimeLoggingPolicy(ctx context.Context) error { input := make(inputData) _, err := policy.enforce(ctx, "runtime_logging", input) return err } func parseNamespace(rego string) (string, error) { lines := strings.Split(rego, "\n") parts := strings.Split(lines[0], " ") if parts[0] != "package" { return "", errors.New("package definition required on first line") } return strings.TrimSpace(parts[1]), nil } func (policy *regoEnforcer) LoadFragment(ctx context.Context, issuer string, feed string, rego string) error { namespace, err := parseNamespace(rego) if err != nil { return fmt.Errorf("unable to load fragment: %w", err) } fragment := &rpi.RegoModule{ Issuer: issuer, Feed: feed, Code: rego, Namespace: namespace, } policy.rego.AddModule(fragment.ID(), fragment) input := inputData{ "issuer": issuer, "feed": feed, "namespace": namespace, } results, err := policy.enforce(ctx, "load_fragment", input) addModule, _ := results.Bool("add_module") if !addModule { policy.rego.RemoveModule(fragment.ID()) } return err } func (policy *regoEnforcer) EnforceScratchMountPolicy(ctx context.Context, scratchPath string, encrypted bool) error { input := map[string]interface{}{ "target": scratchPath, "encrypted": encrypted, } _, err := policy.enforce(ctx, "scratch_mount", input) if err != nil { return err } return nil } func (policy *regoEnforcer) EnforceScratchUnmountPolicy(ctx context.Context, scratchPath string) error { input := map[string]interface{}{ "unmountTarget": scratchPath, } _, err := policy.enforce(ctx, "scratch_unmount", input) if err != nil { return err } return nil } func getUser(passwdPath string, filter func(user.User) bool) (user.User, error) { users, err := user.ParsePasswdFileFilter(passwdPath, filter) if err != nil { return user.User{}, err } if len(users) != 1 { return user.User{}, errors.Errorf("expected exactly 1 user matched '%d'", len(users)) } return users[0], nil } func getGroup(groupPath string, filter func(user.Group) bool) (user.Group, error) { groups, err := user.ParseGroupFileFilter(groupPath, filter) if err != nil { return user.Group{}, err } if len(groups) != 1 { return user.Group{}, errors.Errorf("expected exactly 1 group matched '%d'", len(groups)) } return groups[0], nil } func (policy *regoEnforcer) GetUserInfo(containerID string, process *oci.Process) (IDName, []IDName, string, error) { rootPath := filepath.Join(guestpath.LCOWRootPrefixInUVM, containerID, guestpath.RootfsPath) passwdPath := filepath.Join(rootPath, "/etc/passwd") groupPath := filepath.Join(rootPath, "/etc/group") if process == nil { return IDName{}, nil, "", errors.New("spec.Process is nil") } uid := process.User.UID userIDName := IDName{ID: strconv.FormatUint(uint64(uid), 10), Name: ""} if _, err := os.Stat(passwdPath); err == nil { userInfo, err := getUser(passwdPath, func(user user.User) bool { return uint32(user.Uid) == uid }) if err != nil { return userIDName, nil, "", err } userIDName.Name = userInfo.Name } gid := process.User.GID groupIDName := IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} checkGroup := true if _, err := os.Stat(groupPath); err == nil { groupInfo, err := getGroup(groupPath, func(group user.Group) bool { return uint32(group.Gid) == gid }) if err != nil { return userIDName, nil, "", err } groupIDName.Name = groupInfo.Name } else { checkGroup = false } groupIDNames := []IDName{groupIDName} additionalGIDs := process.User.AdditionalGids if len(additionalGIDs) > 0 { for _, gid := range additionalGIDs { groupIDName = IDName{ID: strconv.FormatUint(uint64(gid), 10), Name: ""} if checkGroup { groupInfo, err := getGroup(groupPath, func(group user.Group) bool { return uint32(group.Gid) == gid }) if err != nil { return userIDName, nil, "", err } groupIDName.Name = groupInfo.Name } groupIDNames = append(groupIDNames, groupIDName) } } // this default value is used in the Linux kernel if no umask is specified umask := "0022" if process.User.Umask != nil { umask = fmt.Sprintf("%04o", *process.User.Umask) } return userIDName, groupIDNames, umask, nil }