agent: runtime: add the Agent Policy feature

Fixes: #7573

To enable this feature, build your rootfs using AGENT_POLICY=yes. The
default is AGENT_POLICY=no.

Building rootfs using AGENT_POLICY=yes has the following effects:

1. The kata-opa service gets included in the Guest image.

2. The agent gets built using AGENT_POLICY=yes.

After this patch, the shim calls SetPolicy if and only if a Policy
annotation is attached to the sandbox/pod. When creating a sandbox/pod
that doesn't have an attached Policy annotation:

1. If the agent was built using AGENT_POLICY=yes, the new sandbox uses
   the default agent settings, that might include a default Policy too.

2. If the agent was built using AGENT_POLICY=no, the new sandbox is
   executed the same way as before this patch.

Any SetPolicy calls from the shim to the agent fail if the agent was
built using AGENT_POLICY=no.

If the agent was built using AGENT_POLICY=yes:

1. The agent reads the contents of a default policy file during sandbox
   start-up.

2. The agent then connects to the OPA service on localhost and sends
   the default policy to OPA.

3. If the shim calls SetPolicy:

   a. The agent checks if SetPolicy is allowed by the current
      policy (the current policy is typically the default policy
      mentioned above).

   b. If SetPolicy is allowed, the agent deletes the current policy
      from OPA and replaces it with the new policy it received from
      the shim.

   A typical new policy from the shim doesn't allow any future SetPolicy
   calls.

4. For every agent rpc API call, the agent asks OPA if that call
   should be allowed. OPA allows or not a call based on the current
   policy, the name of the agent API, and the API call's inputs. The
   agent rejects any calls that are rejected by OPA.

When building using AGENT_POLICY_DEBUG=yes, additional Policy logging
gets enabled in the agent. In particular, information about the inputs
for agent rpc API calls is logged in /tmp/policy.txt, on the Guest VM.
These inputs can be useful for investigating API calls that might have
been rejected by the Policy. Examples:

1. Load a failing policy file test1.rego on a different machine:

opa run --server --addr 127.0.0.1:8181 test1.rego

2. Collect the API inputs from Guest's /tmp/policy.txt and test on the
   machine where the failing policy has been loaded:

curl -X POST http://localhost:8181/v1/data/agent_policy/CreateContainerRequest \
--data-binary @test1-inputs.json

Signed-off-by: Dan Mihai <dmihai@microsoft.com>
This commit is contained in:
Dan Mihai 2023-08-11 15:32:25 +00:00
parent a89c9cd620
commit ab829d1038
20 changed files with 1529 additions and 317 deletions

461
src/agent/Cargo.lock generated
View File

@ -163,6 +163,12 @@ version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
[[package]]
name = "base64"
version = "0.21.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d"
[[package]]
name = "bincode"
version = "1.3.3"
@ -258,9 +264,12 @@ dependencies = [
[[package]]
name = "cc"
version = "1.0.73"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
checksum = "6c6b2562119bf28c3439f7f02db99faf0aa1a8cdfe5772a2ee155d32227239f0"
dependencies = [
"libc",
]
[[package]]
name = "cfg-if"
@ -356,6 +365,16 @@ dependencies = [
"cache-padded",
]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@ -460,6 +479,15 @@ version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
version = "0.8.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "enumflags2"
version = "0.7.5"
@ -550,6 +578,31 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]]
name = "form_urlencoded"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191"
dependencies = [
"matches",
"percent-encoding",
]
[[package]]
name = "futures"
version = "0.3.21"
@ -671,6 +724,25 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "h2"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
dependencies = [
"bytes 1.1.0",
"fnv",
"futures-core",
"futures-sink",
"futures-util",
"http",
"indexmap",
"slab",
"tokio",
"tokio-util 0.7.8",
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.12.1"
@ -707,6 +779,77 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "http"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482"
dependencies = [
"bytes 1.1.0",
"fnv",
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1"
dependencies = [
"bytes 1.1.0",
"http",
"pin-project-lite",
]
[[package]]
name = "httparse"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904"
[[package]]
name = "httpdate"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "hyper"
version = "0.14.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffb1cfd654a8219eaef89881fdb3bb3b1cdc5fa75ded05d6933b2b382e395468"
dependencies = [
"bytes 1.1.0",
"futures-channel",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"socket2",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper-tls"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905"
dependencies = [
"bytes 1.1.0",
"hyper",
"native-tls",
"tokio",
"tokio-native-tls",
]
[[package]]
name = "iana-time-zone"
version = "0.1.46"
@ -720,6 +863,17 @@ dependencies = [
"winapi",
]
[[package]]
name = "idna"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8"
dependencies = [
"matches",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "indexmap"
version = "1.9.1"
@ -770,6 +924,12 @@ dependencies = [
"libc",
]
[[package]]
name = "ipnet"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6"
[[package]]
name = "ipnetwork"
version = "0.17.0"
@ -815,6 +975,7 @@ dependencies = [
"cgroups-rs",
"clap",
"futures",
"http",
"ipnetwork",
"kata-sys-util",
"kata-types",
@ -826,12 +987,14 @@ dependencies = [
"netlink-sys",
"nix 0.24.2",
"oci",
"openssl",
"opentelemetry",
"procfs",
"prometheus",
"protobuf 3.2.0",
"protocols",
"regex",
"reqwest",
"rtnetlink",
"rustjail",
"scan_fmt",
@ -886,7 +1049,7 @@ name = "kata-types"
version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"base64 0.13.0",
"bitmask-enum",
"byte-unit",
"glob",
@ -973,6 +1136,12 @@ dependencies = [
"regex-automata",
]
[[package]]
name = "matches"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2532096657941c2fea9c289d370a250971c689d4f143798ff67113ec042024a5"
[[package]]
name = "memchr"
version = "2.5.0"
@ -988,6 +1157,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "mime"
version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
[[package]]
name = "miniz_oxide"
version = "0.5.3"
@ -1015,6 +1190,24 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
]
[[package]]
name = "netlink-packet-core"
version = "0.2.4"
@ -1065,7 +1258,7 @@ dependencies = [
"netlink-packet-core",
"netlink-sys",
"tokio",
"tokio-util",
"tokio-util 0.6.10",
]
[[package]]
@ -1184,6 +1377,60 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
[[package]]
name = "openssl"
version = "0.10.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
dependencies = [
"bitflags",
"cfg-if 1.0.0",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.16",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.27.0+1.1.1v"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e8f197c82d7511c5b014030c9b1efeda40d7d5f99d23b4ceed3524a5e63f02"
dependencies = [
"cc",
]
[[package]]
name = "openssl-sys"
version = "0.9.90"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
dependencies = [
"cc",
"libc",
"openssl-src",
"pkg-config",
"vcpkg",
]
[[package]]
name = "opentelemetry"
version = "0.14.0"
@ -1573,6 +1820,8 @@ dependencies = [
"async-trait",
"oci",
"protobuf 3.2.0",
"serde",
"serde_json",
"ttrpc",
"ttrpc-codegen",
]
@ -1671,6 +1920,43 @@ dependencies = [
"winapi",
]
[[package]]
name = "reqwest"
version = "0.11.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
dependencies = [
"base64 0.21.2",
"bytes 1.1.0",
"encoding_rs",
"futures-core",
"futures-util",
"h2",
"http",
"http-body",
"hyper",
"hyper-tls",
"ipnet",
"js-sys",
"log",
"mime",
"native-tls",
"once_cell",
"percent-encoding",
"pin-project-lite",
"serde",
"serde_json",
"serde_urlencoded",
"tokio",
"tokio-native-tls",
"tower-service",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"web-sys",
"winreg",
]
[[package]]
name = "rlimit"
version = "0.5.4"
@ -1762,12 +2048,44 @@ dependencies = [
"regex",
]
[[package]]
name = "schannel"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "security-framework"
version = "2.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de"
dependencies = [
"bitflags",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "serde"
version = "1.0.137"
@ -1810,6 +2128,18 @@ dependencies = [
"syn 1.0.98",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serial_test"
version = "0.5.1"
@ -2113,6 +2443,21 @@ version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792"
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.28.1"
@ -2143,6 +2488,16 @@ dependencies = [
"syn 2.0.16",
]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]]
name = "tokio-stream"
version = "0.1.9"
@ -2168,6 +2523,20 @@ dependencies = [
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
dependencies = [
"bytes 1.1.0",
"futures-core",
"futures-sink",
"pin-project-lite",
"tokio",
"tracing",
]
[[package]]
name = "tokio-vsock"
version = "0.3.1"
@ -2190,6 +2559,12 @@ dependencies = [
"serde",
]
[[package]]
name = "tower-service"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52"
[[package]]
name = "tracing"
version = "0.1.35"
@ -2279,6 +2654,12 @@ dependencies = [
"tracing-serde",
]
[[package]]
name = "try-lock"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
[[package]]
name = "ttrpc"
version = "0.7.1"
@ -2335,24 +2716,56 @@ dependencies = [
"winapi",
]
[[package]]
name = "unicode-bidi"
version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
[[package]]
name = "unicode-ident"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-segmentation"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "url"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22fe195a4f217c25b25cb5058ced57059824a678474874038dc88d211bf508d3"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
]
[[package]]
name = "valuable"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
[[package]]
name = "vcpkg"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "version_check"
version = "0.9.4"
@ -2392,6 +2805,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca"
[[package]]
name = "want"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
dependencies = [
"try-lock",
]
[[package]]
name = "wasi"
version = "0.10.0+wasi-snapshot-preview1"
@ -2429,6 +2851,18 @@ dependencies = [
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f"
dependencies = [
"cfg-if 1.0.0",
"js-sys",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.81"
@ -2458,6 +2892,16 @@ version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
[[package]]
name = "web-sys"
version = "0.3.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90"
dependencies = [
"js-sys",
"wasm-bindgen",
]
[[package]]
name = "wepoll-ffi"
version = "0.1.2"
@ -2618,6 +3062,15 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winreg"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
[[package]]
name = "xattr"
version = "0.2.3"

View File

@ -8,7 +8,7 @@ license = "Apache-2.0"
[dependencies]
oci = { path = "../libs/oci" }
rustjail = { path = "rustjail" }
protocols = { path = "../libs/protocols", features = ["async"] }
protocols = { path = "../libs/protocols", features = ["async", "with-serde"] }
lazy_static = "1.3.0"
ttrpc = { version = "0.7.1", features = ["async"], default-features = false }
protobuf = "3.2.0"
@ -67,6 +67,12 @@ serde = { version = "1.0.129", features = ["derive"] }
toml = "0.5.8"
clap = { version = "3.0.1", features = ["derive"] }
# Communication with the OPA service
http = { version = "0.2.8", optional = true }
reqwest = { version = "0.11.14", optional = true }
# The "vendored" feature for openssl is required for musl build
openssl = { version = "0.10.54", features = ["vendored"], optional = true }
[dev-dependencies]
tempfile = "3.1.0"
test-utils = { path = "../libs/test-utils" }
@ -83,6 +89,7 @@ lto = true
[features]
seccomp = ["rustjail/seccomp"]
standard-oci-runtime = ["rustjail/standard-oci-runtime"]
agent-policy = ["http", "openssl", "reqwest"]
[[bin]]
name = "kata-agent"

View File

@ -33,6 +33,14 @@ ifeq ($(SECCOMP),yes)
override EXTRA_RUSTFEATURES += seccomp
endif
##VAR AGENT_POLICY=yes|no define if agent enables the policy feature
AGENT_POLICY := no
# Enable the policy feature of rust build
ifeq ($(AGENT_POLICY),yes)
override EXTRA_RUSTFEATURES += agent-policy
endif
include ../../utils.mk
ifeq ($(ARCH), ppc64le)

View File

@ -73,6 +73,9 @@ use tokio::{
mod rpc;
mod tracer;
#[cfg(feature = "agent-policy")]
mod policy;
cfg_if! {
if #[cfg(target_arch = "s390x")] {
mod ap;
@ -90,6 +93,11 @@ lazy_static! {
AgentConfig::from_cmdline("/proc/cmdline", env::args().collect()).unwrap();
}
#[cfg(feature = "agent-policy")]
lazy_static! {
static ref AGENT_POLICY: Mutex<policy::AgentPolicy> = Mutex::new(AgentPolicy::new());
}
#[derive(Parser)]
// The default clap version info doesn't match our form, so we need to override it
#[clap(global_setting(AppSettings::DisableVersionFlag))]
@ -221,6 +229,27 @@ async fn real_main() -> std::result::Result<(), Box<dyn std::error::Error>> {
let root_span = span!(tracing::Level::TRACE, "root-span");
#[cfg(feature = "agent-policy")]
{
let debug_policy =
config.log_level == slog::Level::Debug || config.log_level == slog::Level::Trace;
if let Err(e) = AGENT_POLICY
.lock()
.await
.initialize(
debug_policy,
"http://localhost:8181/v1",
"/agent_policy",
"/etc/kata-opa/default-policy.rego",
)
.await
{
error!(logger, "Failed to initialize agent policy: {:?}", e);
// Continuing execution without a security policy could be dangerous.
std::process::abort();
}
}
// XXX: Start the root trace transaction.
//
// XXX: Note that *ALL* spans needs to start after this point!!
@ -401,6 +430,9 @@ fn reset_sigpipe() {
use crate::config::AgentConfig;
use std::os::unix::io::{FromRawFd, RawFd};
#[cfg(feature = "agent-policy")]
use crate::policy::AgentPolicy;
#[cfg(test)]
mod tests {
use super::*;

240
src/agent/src/policy.rs Normal file
View File

@ -0,0 +1,240 @@
// Copyright (c) 2023 Microsoft Corporation
//
// SPDX-License-Identifier: Apache-2.0
//
use anyhow::{bail, Result};
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use tokio::time::{sleep, Duration};
static EMPTY_JSON_INPUT: &str = "{\"input\":{}}";
static OPA_DATA_PATH: &str = "/data";
static OPA_POLICIES_PATH: &str = "/policies";
static POLICY_LOG_FILE: &str = "/tmp/policy.txt";
/// Convenience macro to obtain the scope logger
macro_rules! sl {
() => {
slog_scope::logger()
};
}
/// Example of HTTP response from OPA: {"result":true}
#[derive(Debug, Serialize, Deserialize)]
struct AllowResponse {
result: bool,
}
/// Singleton policy object.
#[derive(Debug, Default)]
pub struct AgentPolicy {
/// When true policy errors are ignored, for debug purposes.
allow_failures: bool,
/// OPA path used to query if an Agent gRPC request should be allowed.
/// The request name (e.g., CreateContainerRequest) must be added to
/// this path.
query_path: String,
/// OPA path used to add or delete a rego format Policy.
policy_path: String,
/// Client used to connect a single time to the OPA service and reused
/// for all the future communication with OPA.
opa_client: Option<reqwest::Client>,
/// "/tmp/policy.txt" log file for policy activity.
log_file: Option<tokio::fs::File>,
}
impl AgentPolicy {
/// Create AgentPolicy object.
pub fn new() -> Self {
Self {
allow_failures: false,
..Default::default()
}
}
/// Wait for OPA to start and connect to it.
pub async fn initialize(
&mut self,
debug_enabled: bool,
opa_uri: &str,
policy_name: &str,
default_policy: &str,
) -> Result<()> {
if debug_enabled {
self.log_file = Some(
tokio::fs::OpenOptions::new()
.write(true)
.truncate(true)
.create(true)
.open(POLICY_LOG_FILE)
.await?,
);
debug!(sl!(), "policy: log file: {}", POLICY_LOG_FILE);
}
self.query_path = format!("{opa_uri}{OPA_DATA_PATH}{policy_name}/");
self.policy_path = format!("{opa_uri}{OPA_POLICIES_PATH}{policy_name}");
let opa_client = reqwest::Client::builder().http1_only().build()?;
let policy = tokio::fs::read_to_string(default_policy).await?;
// This loop is necessary to get the opa_client connected to the
// OPA service while that service is starting. Future requests to
// OPA are expected to work without retrying, after connecting
// successfully for the first time.
for i in 0..50 {
if i > 0 {
sleep(Duration::from_millis(100)).await;
debug!(sl!(), "policy initialize: PUT failed, retrying");
}
// Set-up the default policy.
if opa_client
.put(&self.policy_path)
.body(policy.clone())
.send()
.await
.is_ok()
{
// Check if requests causing policy errors should actually
// be allowed. That is an insecure configuration but is
// useful for allowing insecure pods to start, then connect to
// them and inspect Guest logs for the root cause of a failure.
if let Ok(allow_failures) = self
.post_query("AllowRequestsFailingPolicy", EMPTY_JSON_INPUT)
.await
{
self.allow_failures = allow_failures;
} else {
// post_query failed so the the default, secure, value
// allow_failures: false will be used.
}
self.opa_client = Some(opa_client);
return Ok(());
}
}
bail!("Failed to connect to OPA")
}
/// Ask OPA to check if an API call should be allowed or not.
pub async fn is_allowed_endpoint(&mut self, ep: &str, request: &str) -> bool {
let post_input = format!("{{\"input\":{request}}}");
self.log_opa_input(ep, &post_input).await;
match self.post_query(ep, &post_input).await {
Err(e) => {
debug!(
sl!(),
"policy: failed to query endpoint {}: {:?}. Returning false.", ep, e
);
false
}
Ok(allowed) => allowed,
}
}
/// Replace the Policy in OPA.
pub async fn set_policy(&mut self, policy: &str) -> Result<()> {
if let Some(opa_client) = &mut self.opa_client {
// Delete the old rules.
opa_client.delete(&self.policy_path).send().await?;
// Put the new rules.
opa_client
.put(&self.policy_path)
.body(policy.to_string())
.send()
.await?;
// Check if requests causing policy errors should actually be allowed.
// That is an insecure configuration but is useful for allowing insecure
// pods to start, then connect to them and inspect Guest logs for the
// root cause of a failure.
self.allow_failures = self
.post_query("AllowRequestsFailingPolicy", EMPTY_JSON_INPUT)
.await?;
Ok(())
} else {
bail!("Agent Policy is not initialized")
}
}
// Post query to OPA.
async fn post_query(&mut self, ep: &str, post_input: &str) -> Result<bool> {
debug!(sl!(), "policy check: {ep}");
if let Some(opa_client) = &mut self.opa_client {
let uri = format!("{}{ep}", &self.query_path);
let response = opa_client
.post(uri)
.body(post_input.to_string())
.send()
.await?;
if response.status() != http::StatusCode::OK {
bail!("policy: POST {} response status {}", ep, response.status());
}
let http_response = response.text().await?;
let opa_response: serde_json::Result<AllowResponse> =
serde_json::from_str(&http_response);
match opa_response {
Ok(resp) => {
if !resp.result {
if self.allow_failures {
warn!(
sl!(),
"policy: POST {} response <{}>. Ignoring error!", ep, http_response
);
return Ok(true);
} else {
error!(sl!(), "policy: POST {} response <{}>", ep, http_response);
}
}
Ok(resp.result)
}
Err(_) => {
warn!(
sl!(),
"policy: endpoint {} not found in policy. Returning false.", ep,
);
Ok(false)
}
}
} else {
bail!("Agent Policy is not initialized")
}
}
async fn log_opa_input(&mut self, ep: &str, input: &str) {
if let Some(log_file) = &mut self.log_file {
match ep {
"StatsContainerRequest" | "ReadStreamRequest" | "SetPolicyRequest" => {
// - StatsContainerRequest and ReadStreamRequest are called
// relatively often, so we're not logging them, to avoid
// growing this log file too much.
// - Confidential Containers Policy documents are relatively
// large, so we're not logging them here, for SetPolicyRequest.
// The Policy text can be obtained directly from the pod YAML.
}
_ => {
let log_entry = format!("[\"ep\":\"{ep}\",{input}],\n\n");
if let Err(e) = log_file.write_all(log_entry.as_bytes()).await {
warn!(sl!(), "policy: log_opa_input: write_all failed: {}", e);
} else if let Err(e) = log_file.flush().await {
warn!(sl!(), "policy: log_opa_input: flush failed: {}", e);
}
}
}
}
}
}

View File

@ -66,6 +66,10 @@ use crate::AGENT_CONFIG;
use crate::trace_rpc_call;
use crate::tracer::extract_carrier_from_ttrpc;
#[cfg(feature = "agent-policy")]
use crate::AGENT_POLICY;
use opentelemetry::global;
use tracing::span;
use tracing_opentelemetry::OpenTelemetrySpanExt;
@ -120,7 +124,7 @@ fn ttrpc_error(code: ttrpc::Code, err: impl std::fmt::Debug) -> ttrpc::Error {
get_rpc_status(code, format!("{:?}", err))
}
fn is_allowed(req: &impl MessageDyn) -> ttrpc::Result<()> {
fn config_allows(req: &impl MessageDyn) -> ttrpc::Result<()> {
if !AGENT_CONFIG.is_allowed_endpoint(req.descriptor_dyn().name()) {
Err(ttrpc_error(
ttrpc::Code::UNIMPLEMENTED,
@ -131,6 +135,35 @@ fn is_allowed(req: &impl MessageDyn) -> ttrpc::Result<()> {
}
}
#[cfg(feature = "agent-policy")]
async fn policy_allows(req: &(impl MessageDyn + serde::Serialize)) -> ttrpc::Result<()> {
let request = serde_json::to_string(req).unwrap();
let mut policy = AGENT_POLICY.lock().await;
if !policy
.is_allowed_endpoint(req.descriptor_dyn().name(), &request)
.await
{
warn!(sl(), "{} is blocked by policy", req.descriptor_dyn().name());
Err(ttrpc_error(
ttrpc::Code::PERMISSION_DENIED,
format!("{} is blocked by policy", req.descriptor_dyn().name()),
))
} else {
Ok(())
}
}
async fn is_allowed(req: &(impl MessageDyn + serde::Serialize)) -> ttrpc::Result<()> {
let res = config_allows(req);
#[cfg(feature = "agent-policy")]
if res.is_ok() {
return policy_allows(req).await;
}
res
}
#[derive(Clone, Debug)]
pub struct AgentService {
sandbox: Arc<Mutex<Sandbox>>,
@ -607,7 +640,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::CreateContainerRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "create_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match self.do_create_container(req).await {
Err(e) => Err(ttrpc_error(ttrpc::Code::INTERNAL, e)),
Ok(_) => Ok(Empty::new()),
@ -620,7 +653,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::StartContainerRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "start_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match self.do_start_container(req).await {
Err(e) => Err(ttrpc_error(ttrpc::Code::INTERNAL, e)),
Ok(_) => Ok(Empty::new()),
@ -633,7 +666,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::RemoveContainerRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "remove_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match self.do_remove_container(req).await {
Err(e) => Err(ttrpc_error(ttrpc::Code::INTERNAL, e)),
Ok(_) => Ok(Empty::new()),
@ -646,7 +679,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::ExecProcessRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "exec_process", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match self.do_exec_process(req).await {
Err(e) => Err(ttrpc_error(ttrpc::Code::INTERNAL, e)),
Ok(_) => Ok(Empty::new()),
@ -659,7 +692,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::SignalProcessRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "signal_process", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match self.do_signal_process(req).await {
Err(e) => Err(ttrpc_error(ttrpc::Code::INTERNAL, e)),
Ok(_) => Ok(Empty::new()),
@ -672,7 +705,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::WaitProcessRequest,
) -> ttrpc::Result<WaitProcessResponse> {
trace_rpc_call!(ctx, "wait_process", req);
is_allowed(&req)?;
is_allowed(&req).await?;
self.do_wait_process(req)
.await
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))
@ -684,7 +717,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::UpdateContainerRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "update_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mut sandbox = self.sandbox.lock().await;
let ctr = sandbox.get_container(&req.container_id).ok_or_else(|| {
@ -710,7 +743,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::StatsContainerRequest,
) -> ttrpc::Result<StatsContainerResponse> {
trace_rpc_call!(ctx, "stats_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mut sandbox = self.sandbox.lock().await;
let ctr = sandbox.get_container(&req.container_id).ok_or_else(|| {
@ -730,7 +763,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::PauseContainerRequest,
) -> ttrpc::Result<protocols::empty::Empty> {
trace_rpc_call!(ctx, "pause_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mut sandbox = self.sandbox.lock().await;
let ctr = sandbox.get_container(req.container_id()).ok_or_else(|| {
@ -752,7 +785,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::ResumeContainerRequest,
) -> ttrpc::Result<protocols::empty::Empty> {
trace_rpc_call!(ctx, "resume_container", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mut sandbox = self.sandbox.lock().await;
let ctr = sandbox.get_container(req.container_id()).ok_or_else(|| {
@ -774,7 +807,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::RemoveStaleVirtiofsShareMountsRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "remove_stale_virtiofs_share_mounts", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mount_infos = parse_mount_table("/proc/self/mountinfo")
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))?;
for m in &mount_infos {
@ -796,7 +829,7 @@ impl agent_ttrpc::AgentService for AgentService {
_ctx: &TtrpcContext,
req: protocols::agent::WriteStreamRequest,
) -> ttrpc::Result<WriteStreamResponse> {
is_allowed(&req)?;
is_allowed(&req).await?;
self.do_write_stream(req)
.await
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))
@ -807,7 +840,7 @@ impl agent_ttrpc::AgentService for AgentService {
_ctx: &TtrpcContext,
req: protocols::agent::ReadStreamRequest,
) -> ttrpc::Result<ReadStreamResponse> {
is_allowed(&req)?;
is_allowed(&req).await?;
self.do_read_stream(req, true)
.await
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))
@ -818,7 +851,7 @@ impl agent_ttrpc::AgentService for AgentService {
_ctx: &TtrpcContext,
req: protocols::agent::ReadStreamRequest,
) -> ttrpc::Result<ReadStreamResponse> {
is_allowed(&req)?;
is_allowed(&req).await?;
self.do_read_stream(req, false)
.await
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))
@ -830,7 +863,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::CloseStdinRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "close_stdin", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let cid = req.container_id;
let eid = req.exec_id;
@ -856,7 +889,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::TtyWinResizeRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "tty_win_resize", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mut sandbox = self.sandbox.lock().await;
let p = sandbox
@ -895,7 +928,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::UpdateInterfaceRequest,
) -> ttrpc::Result<Interface> {
trace_rpc_call!(ctx, "update_interface", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let interface = req.interface.into_option().ok_or_else(|| {
ttrpc_error(
@ -923,7 +956,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::UpdateRoutesRequest,
) -> ttrpc::Result<Routes> {
trace_rpc_call!(ctx, "update_routes", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let new_routes = req.routes.into_option().map(|r| r.Routes).ok_or_else(|| {
ttrpc_error(
@ -960,7 +993,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::UpdateEphemeralMountsRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "update_mounts", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match update_ephemeral_mounts(sl(), &req.storages, &self.sandbox).await {
Ok(_) => Ok(Empty::new()),
@ -977,7 +1010,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: GetIPTablesRequest,
) -> ttrpc::Result<GetIPTablesResponse> {
trace_rpc_call!(ctx, "get_iptables", req);
is_allowed(&req)?;
is_allowed(&req).await?;
info!(sl(), "get_ip_tables: request received");
@ -1016,7 +1049,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: SetIPTablesRequest,
) -> ttrpc::Result<SetIPTablesResponse> {
trace_rpc_call!(ctx, "set_iptables", req);
is_allowed(&req)?;
is_allowed(&req).await?;
info!(sl(), "set_ip_tables request received");
@ -1131,7 +1164,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::ListInterfacesRequest,
) -> ttrpc::Result<Interfaces> {
trace_rpc_call!(ctx, "list_interfaces", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let list = self
.sandbox
@ -1159,7 +1192,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::ListRoutesRequest,
) -> ttrpc::Result<Routes> {
trace_rpc_call!(ctx, "list_routes", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let list = self
.sandbox
@ -1182,7 +1215,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::CreateSandboxRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "create_sandbox", req);
is_allowed(&req)?;
is_allowed(&req).await?;
{
let mut s = self.sandbox.lock().await;
@ -1241,7 +1274,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::DestroySandboxRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "destroy_sandbox", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let mut sandbox = self.sandbox.lock().await;
// destroy all containers, clean up, notify agent to exit etc.
@ -1274,7 +1307,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::AddARPNeighborsRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "add_arp_neighbors", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let neighs = req
.neighbors
@ -1309,7 +1342,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::OnlineCPUMemRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "online_cpu_mem", req);
is_allowed(&req)?;
is_allowed(&req).await?;
let sandbox = self.sandbox.lock().await;
sandbox
@ -1325,7 +1358,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::ReseedRandomDevRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "reseed_random_dev", req);
is_allowed(&req)?;
is_allowed(&req).await?;
random::reseed_rng(req.data.as_slice())
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))?;
@ -1339,7 +1372,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::GuestDetailsRequest,
) -> ttrpc::Result<GuestDetailsResponse> {
trace_rpc_call!(ctx, "get_guest_details", req);
is_allowed(&req)?;
is_allowed(&req).await?;
info!(sl(), "get guest details!");
let mut resp = GuestDetailsResponse::new();
@ -1373,7 +1406,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::MemHotplugByProbeRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "mem_hotplug_by_probe", req);
is_allowed(&req)?;
is_allowed(&req).await?;
do_mem_hotplug_by_probe(&req.memHotplugProbeAddr)
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))?;
@ -1387,7 +1420,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::SetGuestDateTimeRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "set_guest_date_time", req);
is_allowed(&req)?;
is_allowed(&req).await?;
do_set_guest_date_time(req.Sec, req.Usec)
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))?;
@ -1401,7 +1434,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::CopyFileRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "copy_file", req);
is_allowed(&req)?;
is_allowed(&req).await?;
do_copy_file(&req).map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))?;
@ -1414,7 +1447,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::GetMetricsRequest,
) -> ttrpc::Result<Metrics> {
trace_rpc_call!(ctx, "get_metrics", req);
is_allowed(&req)?;
is_allowed(&req).await?;
match get_metrics(&req) {
Err(e) => Err(ttrpc_error(ttrpc::Code::INTERNAL, e)),
@ -1431,7 +1464,7 @@ impl agent_ttrpc::AgentService for AgentService {
_ctx: &TtrpcContext,
req: protocols::agent::GetOOMEventRequest,
) -> ttrpc::Result<OOMEvent> {
is_allowed(&req)?;
is_allowed(&req).await?;
let s = self.sandbox.lock().await;
let event_rx = &s.event_rx.clone();
let mut event_rx = event_rx.lock().await;
@ -1455,7 +1488,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: VolumeStatsRequest,
) -> ttrpc::Result<VolumeStatsResponse> {
trace_rpc_call!(ctx, "get_volume_stats", req);
is_allowed(&req)?;
is_allowed(&req).await?;
info!(sl(), "get volume stats!");
let mut resp = VolumeStatsResponse::new();
@ -1495,7 +1528,7 @@ impl agent_ttrpc::AgentService for AgentService {
req: protocols::agent::AddSwapRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "add_swap", req);
is_allowed(&req)?;
is_allowed(&req).await?;
do_add_swap(&self.sandbox, &req)
.await
@ -1503,6 +1536,25 @@ impl agent_ttrpc::AgentService for AgentService {
Ok(Empty::new())
}
#[cfg(feature = "agent-policy")]
async fn set_policy(
&self,
ctx: &TtrpcContext,
req: protocols::agent::SetPolicyRequest,
) -> ttrpc::Result<Empty> {
trace_rpc_call!(ctx, "set_policy", req);
is_allowed(&req).await?;
AGENT_POLICY
.lock()
.await
.set_policy(&req.policy)
.await
.map_err(|e| ttrpc_error(ttrpc::Code::INTERNAL, e))?;
Ok(Empty::new())
}
}
#[derive(Clone)]

View File

@ -0,0 +1,38 @@
package agent_policy
default AddARPNeighborsRequest := true
default AddSwapRequest := true
default CloseStdinRequest := true
default CopyFileRequest := true
default CreateContainerRequest := true
default CreateSandboxRequest := true
default DestroySandboxRequest := true
default ExecProcessRequest = true
default GetMetricsRequest := true
default GetOOMEventRequest := true
default GuestDetailsRequest := true
default ListInterfacesRequest := true
default ListRoutesRequest := true
default MemHotplugByProbeRequest := true
default OnlineCPUMemRequest := true
default PauseContainerRequest := true
default PullImageRequest := true
default ReadStreamRequest := true
default RemoveContainerRequest := true
default RemoveStaleVirtiofsShareMountsRequest := true
default ReseedRandomDevRequest := false
default ResumeContainerRequest := true
default SetGuestDateTimeRequest := true
default SetPolicyRequest := true
default SignalProcessRequest := true
default StartContainerRequest := true
default StartTracingRequest := true
default StatsContainerRequest := true
default StopTracingRequest := true
default TtyWinResizeRequest := true
default UpdateContainerRequest := true
default UpdateEphemeralMountsRequest := true
default UpdateInterfaceRequest := true
default UpdateRoutesRequest := true
default WaitProcessRequest := true
default WriteStreamRequest := true

View File

@ -0,0 +1,29 @@
#
# Copyright (c) 2023 Microsoft Corporation
#
# SPDX-License-Identifier: Apache-2.0
#
[Unit]
Description=Open Policy Agent for Kata Containers
Documentation=https://github.com/kata-containers
ConditionPathExists=@SETTINGSDIR@/default-policy.rego
# kata-agent connects to OPA while starting up.
Before=kata-agent.service
[Service]
Type=simple
ExecStart=@BINDIR@/opa run --server --disable-telemetry --addr 127.0.0.1:8181 --log-level info
DynamicUser=yes
RuntimeDirectory=kata-opa
LimitNOFILE=1048576
# Don't restart because there may be an active policy that would be lost.
Restart=no
# Send log output to tty to allow capturing debug logs from a VM vsock port.
StandardError=tty
# Discourage OOM-killer from touching the policy service.
OOMScoreAdjust=-997

View File

@ -72,6 +72,7 @@ service AgentService {
rpc AddSwap(AddSwapRequest) returns (google.protobuf.Empty);
rpc GetVolumeStats(VolumeStatsRequest) returns (VolumeStatsResponse);
rpc ResizeVolume(ResizeVolumeRequest) returns (google.protobuf.Empty);
rpc SetPolicy(SetPolicyRequest) returns (google.protobuf.Empty);
}
message CreateContainerRequest {
@ -566,3 +567,7 @@ message ResizeVolumeRequest {
string volume_guest_path = 1;
uint64 size = 2;
}
message SetPolicyRequest {
string policy = 1;
}

View File

@ -18,6 +18,7 @@ import (
"github.com/kata-containers/kata-containers/src/runtime/pkg/oci"
vc "github.com/kata-containers/kata-containers/src/runtime/virtcontainers"
vf "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/factory"
vcAnnotations "github.com/kata-containers/kata-containers/src/runtime/virtcontainers/pkg/annotations"
specs "github.com/opencontainers/runtime-spec/specs-go"
)
@ -162,6 +163,10 @@ func CreateSandbox(ctx context.Context, vci vc.VC, ociSpec specs.Spec, runtimeCo
ociSpec.Annotations["nerdctl/network-namespace"] = sandboxConfig.NetworkConfig.NetworkID
sandboxConfig.Annotations["nerdctl/network-namespace"] = ociSpec.Annotations["nerdctl/network-namespace"]
// The value of this annotation is sent to the sandbox using SetPolicy.
delete(ociSpec.Annotations, vcAnnotations.Policy)
delete(sandboxConfig.Annotations, vcAnnotations.Policy)
sandbox, err := vci.CreateSandbox(ctx, sandboxConfig, func(ctx context.Context) error {
// Run pre-start OCI hooks, in the runtime namespace.
if err := PreStartHooks(ctx, ociSpec, containerID, bundlePath); err != nil {
@ -228,6 +233,9 @@ func CreateContainer(ctx context.Context, sandbox vc.VCSandbox, ociSpec specs.Sp
katatrace.AddTags(span, "container_id", containerID)
defer span.End()
// The value of this annotation is sent to the sandbox using SetPolicy.
delete(ociSpec.Annotations, vcAnnotations.Policy)
ociSpec = SetEphemeralStorageType(ociSpec, disableGuestEmptyDir)
contConfig, err := oci.ContainerConfig(ociSpec, bundlePath, containerID, disableOutput)

View File

@ -8,6 +8,7 @@ package oci
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -908,10 +909,24 @@ func addRuntimeConfigOverrides(ocispec specs.Spec, sbConfig *vc.SandboxConfig, r
func addAgentConfigOverrides(ocispec specs.Spec, config *vc.SandboxConfig) error {
c := config.AgentConfig
updateConfig := false
if value, ok := ocispec.Annotations[vcAnnotations.KernelModules]; ok {
modules := strings.Split(value, KernelModulesSeparator)
c.KernelModules = modules
updateConfig = true
}
if value, ok := ocispec.Annotations[vcAnnotations.Policy]; ok {
if decoded_rules, err := base64.StdEncoding.DecodeString(value); err == nil {
c.Policy = string(decoded_rules)
updateConfig = true
} else {
return err
}
}
if updateConfig {
config.AgentConfig = c
}

View File

@ -208,4 +208,7 @@ type agent interface {
// setIPTables sets the iptables from the guest
setIPTables(ctx context.Context, isIPv6 bool, data []byte) error
// setPolicy sends a new policy to the guest agent
setPolicy(ctx context.Context, policy string) error
}

View File

@ -150,6 +150,7 @@ const (
grpcResizeVolumeRequest = "grpc.ResizeVolumeRequest"
grpcGetIPTablesRequest = "grpc.GetIPTablesRequest"
grpcSetIPTablesRequest = "grpc.SetIPTablesRequest"
grpcSetPolicyRequest = "grpc.SetPolicyRequest"
)
// newKataAgent returns an agent from an agent type.
@ -275,6 +276,7 @@ type KataAgentConfig struct {
Debug bool
Trace bool
EnableDebugConsole bool
Policy string
}
// KataAgentState is the structure describing the data stored from this
@ -747,6 +749,13 @@ func (k *kataAgent) startSandbox(ctx context.Context, sandbox *Sandbox) error {
return err
}
// If a Policy has been specified, send it to the agent.
if len(sandbox.config.AgentConfig.Policy) > 0 {
if err := sandbox.agent.setPolicy(ctx, sandbox.config.AgentConfig.Policy); err != nil {
return err
}
}
// Setup network interfaces and routes
interfaces, routes, neighs, err := generateVCNetworkStructures(ctx, sandbox.network)
if err != nil {
@ -2081,6 +2090,9 @@ func (k *kataAgent) installReqFunc(c *kataclient.AgentClient) {
k.reqHandlers[grpcRemoveStaleVirtiofsShareMountsRequest] = func(ctx context.Context, req interface{}) (interface{}, error) {
return k.client.AgentServiceClient.RemoveStaleVirtiofsShareMounts(ctx, req.(*grpc.RemoveStaleVirtiofsShareMountsRequest))
}
k.reqHandlers[grpcSetPolicyRequest] = func(ctx context.Context, req interface{}) (interface{}, error) {
return k.client.AgentServiceClient.SetPolicy(ctx, req.(*grpc.SetPolicyRequest))
}
}
func (k *kataAgent) getReqContext(ctx context.Context, reqName string) (newCtx context.Context, cancel context.CancelFunc) {
@ -2339,3 +2351,8 @@ func (k *kataAgent) resizeGuestVolume(ctx context.Context, volumeGuestPath strin
_, err := k.sendReq(ctx, &grpc.ResizeVolumeRequest{VolumeGuestPath: volumeGuestPath, Size_: size})
return err
}
func (k *kataAgent) setPolicy(ctx context.Context, policy string) error {
_, err := k.sendReq(ctx, &grpc.SetPolicyRequest{Policy: policy})
return err
}

View File

@ -267,3 +267,7 @@ func (k *mockAgent) getIPTables(ctx context.Context, isIPv6 bool) ([]byte, error
func (k *mockAgent) setIPTables(ctx context.Context, isIPv6 bool, data []byte) error {
return nil
}
func (k *mockAgent) setPolicy(ctx context.Context, policy string) error {
return nil
}

View File

@ -297,6 +297,9 @@ const (
AgentContainerPipeSize = kataAnnotAgentPrefix + ContainerPipeSizeOption
ContainerPipeSizeOption = "container_pipe_size"
ContainerPipeSizeKernelParam = "agent." + ContainerPipeSizeOption
// Policy is an annotation containing the contents of an agent policy file, base64 encoded.
Policy = kataAnnotAgentPrefix + "policy"
)
// Container resource related annotations

View File

@ -256,3 +256,7 @@ func (p *HybridVSockTTRPCMockImp) GetIPTables(ctx context.Context, req *pb.GetIP
func (p *HybridVSockTTRPCMockImp) SetIPTables(ctx context.Context, req *pb.SetIPTablesRequest) (*pb.SetIPTablesResponse, error) {
return &pb.SetIPTablesResponse{}, nil
}
func (p *HybridVSockTTRPCMockImp) SetPolicy(ctx context.Context, req *pb.SetPolicyRequest) (*gpb.Empty, error) {
return &gpb.Empty{}, nil
}

View File

@ -8,3 +8,4 @@ LIBC="gnu"
PACKAGES="core-packages-base-image ca-certificates"
[ "$AGENT_INIT" = no ] && PACKAGES+=" systemd"
[ "$SECCOMP" = yes ] && PACKAGES+=" libseccomp"
[ "$AGENT_POLICY" = yes ] && PACKAGES+=" opa" || true

View File

@ -27,6 +27,7 @@ LIBC=${LIBC:-musl}
# However, it is not enforced by default: you need to enable that in the main configuration file.
SECCOMP=${SECCOMP:-"yes"}
SELINUX=${SELINUX:-"no"}
AGENT_POLICY=${AGENT_POLICY:-no}
lib_file="${script_dir}/../scripts/lib.sh"
source "$lib_file"
@ -315,6 +316,9 @@ check_env_variables()
[ "$AGENT_INIT" == "yes" -o "$AGENT_INIT" == "no" ] || die "AGENT_INIT($AGENT_INIT) is invalid (must be yes or no)"
[ "$AGENT_POLICY" == "yes" -o "$AGENT_POLICY" == "no" ] || die "AGENT_POLICY($AGENT_POLICY) is invalid (must be yes or no)"
[ "$AGENT_POLICY" == "no" -o "$AGENT_INIT" == "no" ] || die "AGENT_POLICY($AGENT_POLICY) and AGENT_INIT($AGENT_INIT) is an invalid combination (at least one must be no)"
[ -n "${KERNEL_MODULES_DIR}" ] && [ ! -d "${KERNEL_MODULES_DIR}" ] && die "KERNEL_MODULES_DIR defined but is not an existing directory"
[ -n "${OSBUILDER_VERSION}" ] || die "need osbuilder version"
@ -456,6 +460,7 @@ build_rootfs_distro()
--env SELINUX="${SELINUX}" \
--env DEBUG="${DEBUG}" \
--env HOME="/root" \
--env AGENT_POLICY="${AGENT_POLICY}" \
-v "${repo_dir}":"/kata-containers" \
-v "${ROOTFS_DIR}":"/rootfs" \
-v "${script_dir}/../scripts":"/scripts" \
@ -614,7 +619,7 @@ EOF
git checkout "${AGENT_VERSION}" && OK "git checkout successful" || die "checkout agent ${AGENT_VERSION} failed!"
fi
make clean
make LIBC=${LIBC} INIT=${AGENT_INIT} SECCOMP=${SECCOMP}
make LIBC=${LIBC} INIT=${AGENT_INIT} SECCOMP=${SECCOMP} AGENT_POLICY=${AGENT_POLICY}
make install DESTDIR="${ROOTFS_DIR}" LIBC=${LIBC} INIT=${AGENT_INIT}
if [ "${SECCOMP}" == "yes" ]; then
rm -rf "${libseccomp_install_dir}" "${gperf_install_dir}"
@ -640,6 +645,55 @@ EOF
chmod g+rx,o+x "${ROOTFS_DIR}"
fi
if [ "${AGENT_POLICY}" == "yes" ]; then
# Setup systemd-based environment for kata-opa.
local opa_bin_dir="$(get_opa_bin_dir "${ROOTFS_DIR}")"
if [ -z "${opa_bin_dir}" ]; then
# OPA was not installed already, so download it here.
#
# TODO: if an OPA package is not available for the Guest image distro,
# Kata should cache the OPA source code, toolchain information, etc.
# OPA should be built from the cached source code instead of downloading
# this binary.
#
opa_bin_url="$(get_package_version_from_kata_yaml externals.open-policy-agent.meta.binary)"
info "Downloading OPA binary from ${opa_bin_url}"
curl --fail -L "${opa_bin_url}" -o opa || die "Failed to download OPA"
# Install the OPA binary.
opa_bin_dir="/usr/local/bin"
local opa_bin="${ROOTFS_DIR}${opa_bin_dir}/opa"
info "Installing OPA binary to ${opa_bin}"
install -D -o root -g root -m 0755 opa -T "${opa_bin}"
else
info "OPA binary already exists in ${opa_bin_dir}"
fi
# Install default settings for the kata-opa service.
local kata_opa_in_dir="${script_dir}/../../../src/kata-opa"
local opa_settings_dir="/etc/kata-opa"
local policy_file="allow-all.rego"
local policy_dir="${ROOTFS_DIR}/${opa_settings_dir}"
mkdir -p "${policy_dir}"
install -D -o root -g root -m 0644 "${kata_opa_in_dir}/${policy_file}" -T "${policy_dir}/${policy_file}"
ln -sf "${policy_file}" "${policy_dir}/default-policy.rego"
# Install the unit file for the kata-opa service.
local kata_opa_unit="kata-opa.service"
local kata_opa_unit_path="${ROOTFS_DIR}/usr/lib/systemd/system/${kata_opa_unit}"
local kata_containers_wants="${ROOTFS_DIR}/etc/systemd/system/kata-containers.target.wants"
opa_settings_dir="${opa_settings_dir//\//\\/}"
sed -e "s/@SETTINGSDIR@/${opa_settings_dir}/g" "${kata_opa_in_dir}/${kata_opa_unit}.in" > "${kata_opa_unit}"
opa_bin_dir="${opa_bin_dir//\//\\/}"
sed -i -e "s/@BINDIR@/${opa_bin_dir}/g" "${kata_opa_unit}"
install -D -o root -g root -m 0644 "${kata_opa_unit}" -T "${kata_opa_unit_path}"
mkdir -p "${kata_containers_wants}"
ln -sf "${kata_opa_unit_path}" "${kata_containers_wants}/${kata_opa_unit}"
fi
info "Check init is installed"
[ -x "${init}" ] || [ -L "${init}" ] || die "/sbin/init is not installed in ${ROOTFS_DIR}"
OK "init is installed"
@ -657,6 +711,24 @@ EOF
create_summary_file "${ROOTFS_DIR}"
}
get_opa_bin_dir()
{
local rootfs_dir="$1"
local -a bin_dirs=(
"/bin"
"/usr/bin"
"/usr/local/bin"
)
for bin_dir in "${bin_dirs[@]}"
do
local opa_bin="${rootfs_dir}${bin_dir}/opa"
if [ -f "${opa_bin}" ]; then
echo "${bin_dir}"
return 0
fi
done
}
parse_arguments()
{
[ "$#" -eq 0 ] && usage && return 0

View File

@ -284,6 +284,22 @@ externals:
url: "https://github.com/containerd/nydus-snapshotter"
version: "v0.3.3"
open-policy-agent:
description: "Open Policy Agent"
url: "https://github.com/open-policy-agent/opa"
version: "v0.55.0"
meta:
# - If an OPA package is available for the Guest image distro, that
# package is used instead of the binary below.
#
# - TODO: if an OPA package is not available for the Guest image distro,
# Kata should cache the OPA source code, toolchain information, etc.
# OPA should be built from the cached source code instead of downloading
# this binary.
#
# yamllint disable-line rule:line-length
binary: "https://github.com/open-policy-agent/opa/releases/download/v0.55.0/opa_linux_amd64_static"
ovmf:
description: "Firmware, implementation of UEFI for virtual machines."
url: "https://github.com/tianocore/edk2"