|
| 1 | +--- |
| 2 | +description: Use this when working with confidential containers (C-LCOW or C-WCOW). |
| 3 | +--- |
| 4 | + |
| 5 | +# Confidential Containers (C-LCOW and C-WCOW) |
| 6 | + |
| 7 | +C-LCOW (Confidential Linux Containers on Windows) and C-WCOW (Confidential |
| 8 | +Windows Containers on Windows) are containers running in an SNP UVM (a trusted |
| 9 | +execution environment backed by AMD SEV-SNP). Inside the UVM, all sensitive |
| 10 | +operations coming from the (untrusted) host are gated by a Rego-based security |
| 11 | +policy. This functionality backs C-ACI, the confidential SKU of ACI. |
| 12 | + |
| 13 | +## Trust model |
| 14 | + |
| 15 | +- The UVM is launched with a "host data" field set to the SHA-256 of the |
| 16 | + customer's security policy. When the GCS (on Linux) or gcs-sidecar (on |
| 17 | + Windows) starts, it receives the policy from the host via an RPC (a |
| 18 | + `ModifySettings` request for `guestresource.ResourceTypeSecurityPolicy`). |
| 19 | + That RPC is itself untrusted — the host could send any policy — but before |
| 20 | + the GCS constructs a `regoEnforcer` from it, it requests an SNP attestation |
| 21 | + report from the PSP and verifies that the SHA-256 of the received policy |
| 22 | + matches the `host_data` field baked into the report at launch. If the |
| 23 | + hashes don't match, the policy is rejected. Until this RPC has been |
| 24 | + processed, the enforcer in use is the `ClosedDoorSecurityPolicyEnforcer`, |
| 25 | + so nothing meaningful can happen. |
| 26 | +- After that point, **every** subsequent request from the host is treated as |
| 27 | + untrusted input. The policy describes which containers may run (layer hashes, command, |
| 28 | + env, mounts, user, capabilities, seccomp, privileged, etc.), which external |
| 29 | + processes may exec, which fragments may be loaded, and so on. |
| 30 | +- For C-LCOW the GCS itself performs the requested operation after enforcement |
| 31 | + (mount filesystems, spawn processes, invoke runc). For C-WCOW the |
| 32 | + gcs-sidecar validates and rewrites the request, then forwards it to the |
| 33 | + Windows "inbox GCS" (a built-in C++ component) which performs the operation. |
| 34 | + The sidecar is the only enforcement boundary — anything passed unchecked |
| 35 | + through to the inbox GCS is effectively trusted. |
| 36 | + |
| 37 | +## Rego modules |
| 38 | + |
| 39 | +The interpreter (see [internal/regopolicyinterpreter](internal/regopolicyinterpreter)) |
| 40 | +loads several Rego modules into one OPA evaluation: |
| 41 | + |
| 42 | +- `policy` — the customer's policy, generated by tooling from the container |
| 43 | + group spec. Declares allowed containers, external processes, fragments, and |
| 44 | + a set of base flags (`allow_properties_access`, `allow_unencrypted_scratch`, |
| 45 | + `allow_environment_variable_dropping`, `allow_capability_dropping`, …). |
| 46 | + Customer policies typically just **delegate** each enforcement point to |
| 47 | + `data.framework.<point>`; the policy mainly supplies data. |
| 48 | +- `framework` — [pkg/securitypolicy/framework.rego](pkg/securitypolicy/framework.rego), |
| 49 | + bundled with GCS. Contains all the actual matching/narrowing logic for |
| 50 | + every enforcement point, plus the error-reason rules used to build denial |
| 51 | + messages. Has a `version` (see `version_framework`) that the policy's |
| 52 | + `framework_version` is compared against; the framework includes backward |
| 53 | + compatibility shims (`check_container`, `apply_defaults`, etc.) so newer |
| 54 | + GCS can run older policies. |
| 55 | + > **The framework must never break existing policies.** Customer |
| 56 | + > policies are generated by tooling and bundled with images that may not |
| 57 | + > be regenerated for a long time. Any framework change must preserve the |
| 58 | + > exact decision for every previously-valid policy: gate new behavior on |
| 59 | + > `policy_framework_version` / `policy_api_version`, add an |
| 60 | + > `apply_defaults` / `check_*` shim for any new object field, and bump |
| 61 | + > `version_framework` / `version_api` accordingly. The same rule applies |
| 62 | + > to fragments (`fragment_framework_version`). |
| 63 | +- `api` — [pkg/securitypolicy/api.rego](pkg/securitypolicy/api.rego). Declares |
| 64 | + every enforcement point, the version it was introduced in, its default |
| 65 | + result if missing, and whether `use_framework` is set (so the enforcer can |
| 66 | + fall back to `data.framework.<point>` when the policy itself does not |
| 67 | + define the rule). |
| 68 | +- **Fragments** — additional Rego modules loaded at runtime after doing |
| 69 | + signature verification and having the issuer checked by the `load_fragment` |
| 70 | + enforcement point. Each fragment lives in its own namespace (parsed from the |
| 71 | + required `package <ns>` first line; reserved namespaces `framework`, `api`, |
| 72 | + `policy`, `metadata` are rejected — see `parseNamespace`). Fragments can |
| 73 | + contribute additional allowed `containers`, `external_processes`, and nested |
| 74 | + `fragments`. |
| 75 | + |
| 76 | +## Enforcement point lifecycle |
| 77 | + |
| 78 | +The call path for a host request to a policy decision: |
| 79 | + |
| 80 | +1. The host sends a bridge message. For C-LCOW this lands in the bridge in |
| 81 | + [internal/guest/bridge](internal/guest/bridge); for C-WCOW it lands in |
| 82 | + [internal/gcs-sidecar](internal/gcs-sidecar). |
| 83 | +2. The request is unmarshalled and dispatched, typically into |
| 84 | + [internal/guest/runtime/hcsv2/uvm.go](internal/guest/runtime/hcsv2/uvm.go) |
| 85 | + (`Host.CreateContainer`, `Host.modifyMappedVirtualDisk`, |
| 86 | + `Host.modifyCombinedLayers`, `Host.ExecProcess`, …) on Linux, or |
| 87 | + equivalent sidecar handlers on Windows. |
| 88 | +3. The handler calls into a `SecurityPolicyEnforcer` method (see |
| 89 | + [pkg/securitypolicy/securitypolicyenforcer.go](pkg/securitypolicy/securitypolicyenforcer.go)) |
| 90 | + such as `EnforceDeviceMountPolicy`, `EnforceOverlayMountPolicy`, |
| 91 | + `EnforceCreateContainerPolicyV2`, `EnforceExecInContainerPolicyV2`, etc. |
| 92 | +4. The `regoEnforcer` (see |
| 93 | + [pkg/securitypolicy/securitypolicyenforcer_rego.go](pkg/securitypolicy/securitypolicyenforcer_rego.go)) |
| 94 | + builds an `inputData` map for the enforcement point and calls |
| 95 | + `policy.enforce(ctx, name, input)`, which queries |
| 96 | + `data.policy.<name>` (with fallback to `data.framework.<name>` via |
| 97 | + `applyDefaults` when the policy delegates or the rule pre-dates the |
| 98 | + policy's `api_version`). |
| 99 | +5. The Rego result is either `{"allowed": true, ...extra}` or `{"allowed": false}`. |
| 100 | + On deny, the enforcer queries `data.policy.reason` to build a structured |
| 101 | + error (`denyWithReason` / `policyDecisionToError`, truncating |
| 102 | + `error_objects`, then `input`, then `reason` until under |
| 103 | + `maxErrorMessageLength`). |
| 104 | +6. Extra outputs are interpreted by the Go side: e.g. `env_list` filters the |
| 105 | + env that survives "env dropping", `caps_list` filters capabilities, |
| 106 | + `allow_stdio_access` decides whether the container's stdio uses the real |
| 107 | + vsock transport or `/dev/null`, `add_module` decides whether a loaded |
| 108 | + fragment is kept. |
| 109 | + |
| 110 | +## Metadata and revertable sections |
| 111 | + |
| 112 | +Rego is functional, but enforcement needs state (which devices are mounted, |
| 113 | +which overlays exist, which containers have been started, which fragments |
| 114 | +have been loaded, fragment parameter sets, …). We invented **metadata** for |
| 115 | +this: a Rego rule can return a `metadata` array of operations |
| 116 | +(`add`/`update`/`remove` on named maps, plus the newer "set" type) and the |
| 117 | +interpreter applies them after a successful query. See `framework.rego` |
| 118 | +usage of `data.metadata.devices`, `data.metadata.matches`, |
| 119 | +`data.metadata.started`, `data.metadata.overlayTargets`, |
| 120 | +`data.metadata.scratch_mounts`, `data.metadata.issuers`, |
| 121 | +`data.metadata.fragment_parameters`, etc. |
| 122 | + |
| 123 | +Because metadata mutates real state, mount/unmount handlers wrap their work |
| 124 | +in a **revertable section** (`StartRevertableSection` / `Commit` / `Rollback`, |
| 125 | +see the methods on `regoEnforcer` and `commitOrRollbackPolicyRevSection` in |
| 126 | +`hcsv2/uvm.go`). On any error the metadata is rolled back so it stays in |
| 127 | +sync with reality. Any new side-effecting handler **must** use this pattern, |
| 128 | +and the underlying operation **must** clean up its own partial state on |
| 129 | +failure (the revert only undoes the Rego metadata, not files/mounts/dm |
| 130 | +devices). If a clean revert is not possible, there should be a mechanism to |
| 131 | +stop further operations to prevent exploits (e.g. the `mountsBroken` mechanism |
| 132 | +for mounts). |
| 133 | + |
| 134 | +## Threat-model rule of thumb |
| 135 | + |
| 136 | +Treat **everything** coming from the host as adversarial. When changing |
| 137 | +any in-guest code path, work through the checklist in the |
| 138 | +[review-confidential-security](../skills/review-confidential-security/SKILL.md) |
| 139 | +skill. |
| 140 | + |
| 141 | +## C-WCOW specifics |
| 142 | + |
| 143 | +- The enforcement boundary is [internal/gcs-sidecar](internal/gcs-sidecar), |
| 144 | + not the inbox GCS. The sidecar unmarshals the request, runs policy |
| 145 | + enforcement, potentially rewrites the payload (e.g. filtering env or ETW |
| 146 | + providers), and only then forwards it. |
| 147 | +- Anything the sidecar does **not** validate is effectively delegated to the |
| 148 | + inbox GCS, which is not part of this repo. New resource types added to the |
| 149 | + sidecar dispatcher must come with explicit enforcement, not a pass-through. |
| 150 | +- The same Rego enforcer is shared with C-LCOW (`policy.osType` switches |
| 151 | + per-input shape inside `EnforceCreateContainerPolicyV2` etc.), so changes |
| 152 | + to framework.rego must consider both Linux and Windows code paths. |
| 153 | + |
| 154 | +## Where to look |
| 155 | + |
| 156 | +- Rego: [pkg/securitypolicy/framework.rego](pkg/securitypolicy/framework.rego), |
| 157 | + [pkg/securitypolicy/api.rego](pkg/securitypolicy/api.rego), |
| 158 | + [pkg/securitypolicy/policy.rego](pkg/securitypolicy/policy.rego) |
| 159 | +- Go enforcer: [pkg/securitypolicy/securitypolicyenforcer_rego.go](pkg/securitypolicy/securitypolicyenforcer_rego.go), |
| 160 | + [pkg/securitypolicy/securitypolicyenforcer.go](pkg/securitypolicy/securitypolicyenforcer.go) |
| 161 | +- Interpreter: [internal/regopolicyinterpreter](internal/regopolicyinterpreter) |
| 162 | +- C-LCOW handlers: [internal/guest/runtime/hcsv2/uvm.go](internal/guest/runtime/hcsv2/uvm.go) |
| 163 | +- C-WCOW handlers: [internal/gcs-sidecar](internal/gcs-sidecar) |
| 164 | +- Bridge sequential mode: [internal/guest/bridge](internal/guest/bridge) |
| 165 | +- Tests: `regopolicy_linux_test.go`, `regopolicy_windows_test.go`, `api_test.rego` |
| 166 | + |
| 167 | +## Build tag |
| 168 | + |
| 169 | +The Rego enforcer is gated behind the `rego` Go build tag (see the |
| 170 | +`//go:build rego` headers on `securitypolicyenforcer_rego.go` and the |
| 171 | +test files). Non-confidential LCOW builds of GCS deliberately do not |
| 172 | +include any Rego code. When building or testing any confidential change |
| 173 | +you must pass `-tags rego`, e.g. `go build -tags rego ./...` or |
| 174 | +`go test -tags rego ./pkg/securitypolicy/...`. Without it, your changes |
| 175 | +to the Rego enforcer will not be compiled and the tests will silently |
| 176 | +not run. |
0 commit comments