mirror of
https://github.com/kata-containers/kata-containers.git
synced 2025-07-30 23:06:27 +00:00
policy: initial genpolicy commit
Add application that infers K8s user's intentions based on user's K8s YAML file, and generates a Rego/OPA based policy for that YAML. Just Pod YAML files are supported as input using this initial source code. Support for other types of YAML files will come with upcoming commits. Fixes: #7673 Signed-off-by: Dan Mihai <dmihai@microsoft.com>
This commit is contained in:
parent
555136c1a5
commit
48829120b6
2196
src/tools/genpolicy/Cargo.lock
generated
Normal file
2196
src/tools/genpolicy/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
56
src/tools/genpolicy/Cargo.toml
Normal file
56
src/tools/genpolicy/Cargo.toml
Normal file
@ -0,0 +1,56 @@
|
||||
[package]
|
||||
name = "genpolicy"
|
||||
version = "0.1.0"
|
||||
authors = ["The Confidential Containers community https://github.com/confidential-containers"]
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
# Logging.
|
||||
env_logger = "0.10.0"
|
||||
log = "0.4.17"
|
||||
|
||||
# Command line parsing.
|
||||
clap = { version = "4.1.8", features = ["derive"] }
|
||||
|
||||
# YAML file serialization/deserialization.
|
||||
base64 = "0.21.0"
|
||||
serde = { version = "1.0.159", features = ["derive"] }
|
||||
|
||||
# Newer serde_yaml versions are using unsafe-libyaml instead of yaml-rust,
|
||||
# and incorrectly change on serialization:
|
||||
#
|
||||
# value: "yes"
|
||||
#
|
||||
# to:
|
||||
#
|
||||
# value: yes
|
||||
#
|
||||
# In YAML, the value yes without quotes is reserved for boolean,
|
||||
# and confuses kubectl, that expects a string value.
|
||||
serde_yaml = "0.8"
|
||||
|
||||
# Container repository.
|
||||
anyhow = "1.0.32"
|
||||
async-trait = "0.1.68"
|
||||
docker_credential = "1.2.0"
|
||||
flate2 = { version = "1.0.26", features = ["zlib-ng"], default-features = false }
|
||||
oci-distribution = { version = "0.9.4" }
|
||||
openssl = { version = "0.10.54", features = ["vendored"] }
|
||||
serde_ignored = "0.1.7"
|
||||
serde_json = "1.0.39"
|
||||
serde-transcode = "1.1.1"
|
||||
tokio = {version = "1.33.0", features = ["rt-multi-thread"]}
|
||||
|
||||
# OCI container specs.
|
||||
oci = { path = "../../libs/oci" }
|
||||
|
||||
# Kata Agent prototol.
|
||||
protocols = { path = "../../libs/protocols", features = ["with-serde"] }
|
||||
protobuf = "3.2.0"
|
||||
|
||||
# dm-verity root hash support
|
||||
generic-array = "0.14.6"
|
||||
sha2 = "0.10.6"
|
||||
tarindex = { git = "https://github.com/kata-containers/tardev-snapshotter", rev = "06183a5" }
|
||||
tempfile = "3.5.0"
|
||||
zerocopy = "0.6.1"
|
40
src/tools/genpolicy/Makefile
Normal file
40
src/tools/genpolicy/Makefile
Normal file
@ -0,0 +1,40 @@
|
||||
# Copyright (c) 2020 Intel Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
|
||||
include ../../../utils.mk
|
||||
|
||||
ifeq ($(ARCH), ppc64le)
|
||||
override ARCH = powerpc64le
|
||||
endif
|
||||
|
||||
.DEFAULT_GOAL := default
|
||||
default: build
|
||||
|
||||
build:
|
||||
@RUSTFLAGS="$(EXTRA_RUSTFLAGS) --deny warnings" cargo build --target $(TRIPLE) --$(BUILD_TYPE)
|
||||
|
||||
static-checks-build:
|
||||
@echo "INFO: static-checks-build do nothing.."
|
||||
|
||||
clean:
|
||||
cargo clean
|
||||
|
||||
vendor:
|
||||
cargo vendor
|
||||
|
||||
test:
|
||||
|
||||
install:
|
||||
@RUSTFLAGS="$(EXTRA_RUSTFLAGS) --deny warnings" cargo install --locked --target $(TRIPLE) --path .
|
||||
|
||||
check: standard_rust_check
|
||||
|
||||
.PHONY: \
|
||||
build \
|
||||
check \
|
||||
clean \
|
||||
install \
|
||||
test \
|
||||
vendor
|
29
src/tools/genpolicy/README.md
Normal file
29
src/tools/genpolicy/README.md
Normal file
@ -0,0 +1,29 @@
|
||||
# Agent Policy generation tool
|
||||
|
||||
The Kata Containers policy generation tool (`genpolicy`):
|
||||
|
||||
1. Reads user's Kubernetes YAML file.
|
||||
|
||||
1. Infers user's intentions based on the contents of that file.
|
||||
|
||||
1. Generates a Kata Containers Agent (`kata-agent`) policy file
|
||||
corresponding to the input YAML, using the Rego/Open Policy Agent
|
||||
format.
|
||||
|
||||
1. Appends the policy as an annotation to user's YAML file.
|
||||
|
||||
When the user deploys that YAML file, the Kata Agent uses the attached
|
||||
policy to reject possible Agent API calls that are not consistent with
|
||||
the policy.
|
||||
|
||||
Example:
|
||||
|
||||
```sh
|
||||
$ genpolicy -y samples/pod-one-container.yaml
|
||||
```
|
||||
|
||||
For a usage statement, run:
|
||||
|
||||
```sh
|
||||
$ genpolicy --help
|
||||
```
|
291
src/tools/genpolicy/genpolicy-settings.json
Normal file
291
src/tools/genpolicy/genpolicy-settings.json
Normal file
@ -0,0 +1,291 @@
|
||||
{
|
||||
"pause_container": {
|
||||
"Root": {
|
||||
"Path": "$(cpath)/$(bundle-id)",
|
||||
"Readonly": true
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"destination": "/dev/shm",
|
||||
"type_": "bind",
|
||||
"source": "/run/kata-containers/sandbox/shm",
|
||||
"options": [
|
||||
"rbind"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/etc/resolv.conf",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"ro",
|
||||
"nosuid",
|
||||
"nodev",
|
||||
"noexec"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Annotations": {
|
||||
"io.kubernetes.cri.container-type": "sandbox",
|
||||
"io.kubernetes.cri.sandbox-id": "^[a-z0-9]{64}$",
|
||||
"io.kubernetes.cri.sandbox-log-directory": "^/var/log/pods/$(sandbox-namespace)_$(sandbox-name)_[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$",
|
||||
"io.katacontainers.pkg.oci.container_type": "pod_sandbox",
|
||||
"io.kubernetes.cri.sandbox-namespace": "default",
|
||||
"io.katacontainers.pkg.oci.bundle_path": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/$(bundle-id)"
|
||||
},
|
||||
"Process": {
|
||||
"Args": [
|
||||
"/pause"
|
||||
]
|
||||
},
|
||||
"Linux": {
|
||||
"MaskedPaths": [
|
||||
"/proc/acpi",
|
||||
"/proc/asound",
|
||||
"/proc/kcore",
|
||||
"/proc/keys",
|
||||
"/proc/latency_stats",
|
||||
"/proc/timer_list",
|
||||
"/proc/timer_stats",
|
||||
"/proc/sched_debug",
|
||||
"/sys/firmware",
|
||||
"/proc/scsi"
|
||||
],
|
||||
"ReadonlyPaths": [
|
||||
"/proc/bus",
|
||||
"/proc/fs",
|
||||
"/proc/irq",
|
||||
"/proc/sys",
|
||||
"/proc/sysrq-trigger"
|
||||
]
|
||||
}
|
||||
},
|
||||
"other_container": {
|
||||
"Root": {
|
||||
"Path": "$(cpath)/$(bundle-id)"
|
||||
},
|
||||
"Mounts": [
|
||||
{
|
||||
"destination": "/etc/hosts",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate",
|
||||
"rw"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/dev/termination-log",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate",
|
||||
"rw"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/etc/hostname",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/etc/resolv.conf",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/dev/shm",
|
||||
"type_": "bind",
|
||||
"source": "/run/kata-containers/sandbox/shm",
|
||||
"options": [
|
||||
"rbind"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate",
|
||||
"ro"
|
||||
]
|
||||
},
|
||||
{
|
||||
"destination": "/var/run/secrets/azure/tokens",
|
||||
"source": "$(sfprefix)tokens$",
|
||||
"type_": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate",
|
||||
"ro"
|
||||
]
|
||||
}
|
||||
],
|
||||
"Annotations": {
|
||||
"io.katacontainers.pkg.oci.bundle_path": "/run/containerd/io.containerd.runtime.v2.task/k8s.io/$(bundle-id)",
|
||||
"io.kubernetes.cri.sandbox-id": "^[a-z0-9]{64}$",
|
||||
"io.katacontainers.pkg.oci.container_type": "pod_container",
|
||||
"io.kubernetes.cri.container-type": "container"
|
||||
}
|
||||
},
|
||||
"volumes": {
|
||||
"emptyDir": {
|
||||
"mount_type": "local",
|
||||
"mount_source": "^$(cpath)/$(sandbox-id)/local/",
|
||||
"mount_point": "^$(cpath)/$(sandbox-id)/local/",
|
||||
"driver": "local",
|
||||
"source": "local",
|
||||
"fstype": "local",
|
||||
"options": [
|
||||
"mode=0777"
|
||||
]
|
||||
},
|
||||
"emptyDir_memory": {
|
||||
"mount_type": "bind",
|
||||
"mount_source": "^/run/kata-containers/sandbox/ephemeral/",
|
||||
"mount_point": "^/run/kata-containers/sandbox/ephemeral/",
|
||||
"driver": "ephemeral",
|
||||
"source": "tmpfs",
|
||||
"fstype": "tmpfs",
|
||||
"options": []
|
||||
},
|
||||
"configMap": {
|
||||
"mount_type": "bind",
|
||||
"mount_source": "$(sfprefix)",
|
||||
"mount_point": "^$(cpath)/watchable/$(bundle-id)-[a-z0-9]{16}-",
|
||||
"driver": "watchable-bind",
|
||||
"fstype": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate",
|
||||
"ro"
|
||||
]
|
||||
},
|
||||
"confidential_configMap": {
|
||||
"mount_type": "bind",
|
||||
"mount_source": "$(sfprefix)",
|
||||
"mount_point": "$(sfprefix)",
|
||||
"driver": "local",
|
||||
"fstype": "bind",
|
||||
"options": [
|
||||
"rbind",
|
||||
"rprivate",
|
||||
"ro"
|
||||
]
|
||||
}
|
||||
},
|
||||
"mount_destinations": [
|
||||
"/sys/fs/cgroup",
|
||||
"/etc/hosts",
|
||||
"/dev/termination-log",
|
||||
"/etc/hostname",
|
||||
"/etc/resolv.conf",
|
||||
"/dev/shm",
|
||||
"/var/run/secrets/kubernetes.io/serviceaccount",
|
||||
"/var/run/secrets/azure/tokens"
|
||||
],
|
||||
"common": {
|
||||
"cpath": "/run/kata-containers/shared/containers",
|
||||
"sfprefix": "^$(cpath)/$(bundle-id)-[a-z0-9]{16}-",
|
||||
"ip_p": "[0-9]{1,5}",
|
||||
"ipv4_a": "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])",
|
||||
"svc_name": "[A-Z_\\.\\-]+",
|
||||
"dns_label": "[a-zA-Z0-9_\\.\\-]+",
|
||||
"default_caps": [
|
||||
"CAP_CHOWN",
|
||||
"CAP_DAC_OVERRIDE",
|
||||
"CAP_FSETID",
|
||||
"CAP_FOWNER",
|
||||
"CAP_MKNOD",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_SETGID",
|
||||
"CAP_SETUID",
|
||||
"CAP_SETFCAP",
|
||||
"CAP_SETPCAP",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_SYS_CHROOT",
|
||||
"CAP_KILL",
|
||||
"CAP_AUDIT_WRITE"
|
||||
],
|
||||
"privileged_caps": [
|
||||
"CAP_CHOWN",
|
||||
"CAP_DAC_OVERRIDE",
|
||||
"CAP_DAC_READ_SEARCH",
|
||||
"CAP_FOWNER",
|
||||
"CAP_FSETID",
|
||||
"CAP_KILL",
|
||||
"CAP_SETGID",
|
||||
"CAP_SETUID",
|
||||
"CAP_SETPCAP",
|
||||
"CAP_LINUX_IMMUTABLE",
|
||||
"CAP_NET_BIND_SERVICE",
|
||||
"CAP_NET_BROADCAST",
|
||||
"CAP_NET_ADMIN",
|
||||
"CAP_NET_RAW",
|
||||
"CAP_IPC_LOCK",
|
||||
"CAP_IPC_OWNER",
|
||||
"CAP_SYS_MODULE",
|
||||
"CAP_SYS_RAWIO",
|
||||
"CAP_SYS_CHROOT",
|
||||
"CAP_SYS_PTRACE",
|
||||
"CAP_SYS_PACCT",
|
||||
"CAP_SYS_ADMIN",
|
||||
"CAP_SYS_BOOT",
|
||||
"CAP_SYS_NICE",
|
||||
"CAP_SYS_RESOURCE",
|
||||
"CAP_SYS_TIME",
|
||||
"CAP_SYS_TTY_CONFIG",
|
||||
"CAP_MKNOD",
|
||||
"CAP_LEASE",
|
||||
"CAP_AUDIT_WRITE",
|
||||
"CAP_AUDIT_CONTROL",
|
||||
"CAP_SETFCAP",
|
||||
"CAP_MAC_OVERRIDE",
|
||||
"CAP_MAC_ADMIN",
|
||||
"CAP_SYSLOG",
|
||||
"CAP_WAKE_ALARM",
|
||||
"CAP_BLOCK_SUSPEND",
|
||||
"CAP_AUDIT_READ",
|
||||
"CAP_PERFMON",
|
||||
"CAP_BPF",
|
||||
"CAP_CHECKPOINT_RESTORE"
|
||||
]
|
||||
},
|
||||
"kata_config": {
|
||||
"confidential_guest": true
|
||||
},
|
||||
"request_defaults": {
|
||||
"CreateContainerRequest": {
|
||||
"allow_env_regex": [
|
||||
"^HOSTNAME=$(dns_label)$",
|
||||
"^$(svc_name)_PORT_$(ip_p)_TCP=tcp://$(ipv4_a):$(ip_p)$",
|
||||
"^$(svc_name)_PORT_$(ip_p)_TCP_PROTO=tcp$",
|
||||
"^$(svc_name)_PORT_$(ip_p)_TCP_PORT=$(ip_p)$",
|
||||
"^$(svc_name)_PORT_$(ip_p)_TCP_ADDR=$(ipv4_a)$",
|
||||
"^$(svc_name)_SERVICE_HOST=$(ipv4_a)$",
|
||||
"^$(svc_name)_SERVICE_PORT=$(ip_p)$",
|
||||
"^$(svc_name)_SERVICE_PORT_$(dns_label)=$(ip_p)$",
|
||||
"^$(svc_name)_PORT=tcp://$(ipv4_a):$(ip_p)$",
|
||||
"^AZURE_CLIENT_ID=[A-Fa-f0-9-]+$",
|
||||
"^AZURE_TENANT_ID=[A-Fa-f0-9-]+$",
|
||||
"^AZURE_FEDERATED_TOKEN_FILE=/var/run/secrets/azure/tokens/azure-identity-token$",
|
||||
"^AZURE_AUTHORITY_HOST=https://login\\.microsoftonline\\.com/$"
|
||||
]
|
||||
},
|
||||
"CopyFileRequest": [
|
||||
"^$(cpath)/"
|
||||
],
|
||||
"ExecProcessRequest": {
|
||||
"commands": [],
|
||||
"regex": []
|
||||
},
|
||||
"ReadStreamRequest": false,
|
||||
"WriteStreamRequest": false
|
||||
}
|
||||
}
|
1120
src/tools/genpolicy/rules.rego
Normal file
1120
src/tools/genpolicy/rules.rego
Normal file
File diff suppressed because it is too large
Load Diff
57
src/tools/genpolicy/samples/pod-one-container.yaml
Normal file
57
src/tools/genpolicy/samples/pod-one-container.yaml
Normal file
File diff suppressed because one or more lines are too long
128
src/tools/genpolicy/src/config_map.rs
Normal file
128
src/tools/genpolicy/src/config_map.rs
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::obj_meta;
|
||||
use crate::pod;
|
||||
use crate::policy;
|
||||
use crate::settings;
|
||||
use crate::yaml;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::debug;
|
||||
use protocols::agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::File;
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / ConfigMap.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigMap {
|
||||
apiVersion: String,
|
||||
kind: String,
|
||||
pub metadata: obj_meta::ObjectMeta,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub data: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub binaryData: Option<BTreeMap<String, Vec<u8>>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
immutable: Option<bool>,
|
||||
|
||||
#[serde(skip)]
|
||||
doc_mapping: serde_yaml::Value,
|
||||
}
|
||||
|
||||
impl ConfigMap {
|
||||
pub fn new(file: &str) -> anyhow::Result<Self> {
|
||||
debug!("Reading ConfigMap...");
|
||||
let config_map: ConfigMap = serde_yaml::from_reader(File::open(file)?)?;
|
||||
debug!("\nRead ConfigMap => {:#?}", config_map);
|
||||
|
||||
Ok(config_map)
|
||||
}
|
||||
|
||||
pub fn get_value(&self, value_from: &pod::EnvVarSource) -> Option<String> {
|
||||
if let Some(key_ref) = &value_from.configMapKeyRef {
|
||||
if let Some(name) = &key_ref.name {
|
||||
if let Some(my_name) = &self.metadata.name {
|
||||
if my_name.eq(name) {
|
||||
if let Some(data) = &self.data {
|
||||
if let Some(value) = data.get(&key_ref.key) {
|
||||
return Some(value.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_value(value_from: &pod::EnvVarSource, config_maps: &Vec<ConfigMap>) -> Option<String> {
|
||||
for config_map in config_maps {
|
||||
if let Some(value) = config_map.get_value(value_from) {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl yaml::K8sResource for ConfigMap {
|
||||
async fn init(
|
||||
&mut self,
|
||||
_use_cache: bool,
|
||||
doc_mapping: &serde_yaml::Value,
|
||||
_silent_unsupported_fields: bool,
|
||||
) {
|
||||
self.doc_mapping = doc_mapping.clone();
|
||||
}
|
||||
|
||||
fn get_sandbox_name(&self) -> Option<String> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_namespace(&self) -> String {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_container_mounts_and_storages(
|
||||
&self,
|
||||
_policy_mounts: &mut Vec<policy::KataMount>,
|
||||
_storages: &mut Vec<agent::Storage>,
|
||||
_container: &pod::Container,
|
||||
_settings: &settings::Settings,
|
||||
) {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn generate_policy(&self, _agent_policy: &policy::AgentPolicy) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn serialize(&mut self, _policy: &str) -> String {
|
||||
serde_yaml::to_string(&self.doc_mapping).unwrap()
|
||||
}
|
||||
|
||||
fn get_containers(&self) -> &Vec<pod::Container> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_annotations(&self) -> &Option<BTreeMap<String, String>> {
|
||||
&self.metadata.annotations
|
||||
}
|
||||
|
||||
fn use_host_network(&self) -> bool {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
}
|
163
src/tools/genpolicy/src/containerd.rs
Normal file
163
src/tools/genpolicy/src/containerd.rs
Normal file
@ -0,0 +1,163 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use crate::policy;
|
||||
|
||||
// Default process field from containerd.
|
||||
pub fn get_process(privileged_container: bool, common: &policy::CommonData) -> policy::KataProcess {
|
||||
let capabilities = if privileged_container {
|
||||
policy::KataLinuxCapabilities {
|
||||
Ambient: vec![],
|
||||
Bounding: common.privileged_caps.clone(),
|
||||
Effective: common.privileged_caps.clone(),
|
||||
Inheritable: vec![],
|
||||
Permitted: common.privileged_caps.clone(),
|
||||
}
|
||||
} else {
|
||||
policy::KataLinuxCapabilities {
|
||||
Ambient: vec![],
|
||||
Bounding: common.default_caps.clone(),
|
||||
Effective: common.default_caps.clone(),
|
||||
Inheritable: vec![],
|
||||
Permitted: common.default_caps.clone(),
|
||||
}
|
||||
};
|
||||
|
||||
policy::KataProcess {
|
||||
Terminal: false,
|
||||
User: Default::default(),
|
||||
Args: Vec::new(),
|
||||
Env: Vec::new(),
|
||||
Cwd: "/".to_string(),
|
||||
Capabilities: capabilities,
|
||||
NoNewPrivileges: true,
|
||||
}
|
||||
}
|
||||
|
||||
// Default mounts field from containerd.
|
||||
pub fn get_mounts(is_pause_container: bool, privileged_container: bool) -> Vec<policy::KataMount> {
|
||||
let sysfs_read_write_option = if privileged_container { "rw" } else { "ro" };
|
||||
|
||||
let mut mounts = vec![
|
||||
policy::KataMount {
|
||||
destination: "/proc".to_string(),
|
||||
type_: "proc".to_string(),
|
||||
source: "proc".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"noexec".to_string(),
|
||||
"nodev".to_string(),
|
||||
],
|
||||
},
|
||||
policy::KataMount {
|
||||
destination: "/dev".to_string(),
|
||||
type_: "tmpfs".to_string(),
|
||||
source: "tmpfs".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"strictatime".to_string(),
|
||||
"mode=755".to_string(),
|
||||
"size=65536k".to_string(),
|
||||
],
|
||||
},
|
||||
policy::KataMount {
|
||||
destination: "/dev/pts".to_string(),
|
||||
type_: "devpts".to_string(),
|
||||
source: "devpts".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"noexec".to_string(),
|
||||
"newinstance".to_string(),
|
||||
"ptmxmode=0666".to_string(),
|
||||
"mode=0620".to_string(),
|
||||
"gid=5".to_string(),
|
||||
],
|
||||
},
|
||||
policy::KataMount {
|
||||
destination: "/dev/shm".to_string(),
|
||||
type_: "tmpfs".to_string(),
|
||||
source: "shm".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"noexec".to_string(),
|
||||
"nodev".to_string(),
|
||||
"mode=1777".to_string(),
|
||||
"size=65536k".to_string(),
|
||||
],
|
||||
},
|
||||
policy::KataMount {
|
||||
destination: "/dev/mqueue".to_string(),
|
||||
type_: "mqueue".to_string(),
|
||||
source: "mqueue".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"noexec".to_string(),
|
||||
"nodev".to_string(),
|
||||
],
|
||||
},
|
||||
policy::KataMount {
|
||||
destination: "/sys".to_string(),
|
||||
type_: "sysfs".to_string(),
|
||||
source: "sysfs".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"noexec".to_string(),
|
||||
"nodev".to_string(),
|
||||
sysfs_read_write_option.to_string(),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
if !is_pause_container {
|
||||
mounts.push(policy::KataMount {
|
||||
destination: "/sys/fs/cgroup".to_string(),
|
||||
type_: "cgroup".to_string(),
|
||||
source: "cgroup".to_string(),
|
||||
options: vec![
|
||||
"nosuid".to_string(),
|
||||
"noexec".to_string(),
|
||||
"nodev".to_string(),
|
||||
"relatime".to_string(),
|
||||
sysfs_read_write_option.to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
mounts
|
||||
}
|
||||
|
||||
// Default policy::KataLinux field from containerd.
|
||||
pub fn get_linux(privileged_container: bool) -> policy::KataLinux {
|
||||
if !privileged_container {
|
||||
policy::KataLinux {
|
||||
Namespaces: vec![],
|
||||
MaskedPaths: vec![
|
||||
"/proc/acpi".to_string(),
|
||||
"/proc/kcore".to_string(),
|
||||
"/proc/keys".to_string(),
|
||||
"/proc/latency_stats".to_string(),
|
||||
"/proc/timer_list".to_string(),
|
||||
"/proc/timer_stats".to_string(),
|
||||
"/proc/sched_debug".to_string(),
|
||||
"/proc/scsi".to_string(),
|
||||
"/sys/firmware".to_string(),
|
||||
],
|
||||
ReadonlyPaths: vec![
|
||||
"/proc/asound".to_string(),
|
||||
"/proc/bus".to_string(),
|
||||
"/proc/fs".to_string(),
|
||||
"/proc/irq".to_string(),
|
||||
"/proc/sys".to_string(),
|
||||
"/proc/sysrq-trigger".to_string(),
|
||||
],
|
||||
}
|
||||
} else {
|
||||
policy::KataLinux {
|
||||
Namespaces: vec![],
|
||||
MaskedPaths: vec![],
|
||||
ReadonlyPaths: vec![],
|
||||
}
|
||||
}
|
||||
}
|
116
src/tools/genpolicy/src/main.rs
Normal file
116
src/tools/genpolicy/src/main.rs
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use clap::Parser;
|
||||
use env_logger;
|
||||
use log::{debug, info};
|
||||
|
||||
mod config_map;
|
||||
mod containerd;
|
||||
mod mount_and_storage;
|
||||
mod no_policy;
|
||||
mod obj_meta;
|
||||
mod persistent_volume_claim;
|
||||
mod pod;
|
||||
mod pod_template;
|
||||
mod policy;
|
||||
mod registry;
|
||||
mod secret;
|
||||
mod settings;
|
||||
mod utils;
|
||||
mod verity;
|
||||
mod volume;
|
||||
mod yaml;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct CommandLineOptions {
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Kubernetes input/output YAML file path. stdin/stdout get used if this option is not specified."
|
||||
)]
|
||||
yaml_file: Option<String>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Optional Kubernetes config map YAML input file path"
|
||||
)]
|
||||
config_map_file: Option<String>,
|
||||
|
||||
#[clap(
|
||||
short = 'j',
|
||||
long,
|
||||
default_value_t = String::from("genpolicy-settings.json"),
|
||||
help = "genpolicy settings file name"
|
||||
)]
|
||||
settings_file_name: String,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
default_value_t = String::from("."),
|
||||
help = "Path to the rules.rego and settings input files"
|
||||
)]
|
||||
input_files_path: String,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Create and use a cache of container image layer contents and dm-verity information (in ./layers_cache/)"
|
||||
)]
|
||||
use_cached_files: bool,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Print the output Rego policy text to standard output"
|
||||
)]
|
||||
raw_out: bool,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Print the base64 encoded output Rego policy to standard output"
|
||||
)]
|
||||
base64_out: bool,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Ignore unsupported input Kubernetes YAML fields. This is not recommeded unless you understand exactly how genpolicy works!"
|
||||
)]
|
||||
silent_unsupported_fields: bool,
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
env_logger::init();
|
||||
|
||||
let args = CommandLineOptions::parse();
|
||||
|
||||
let mut config_map_files = Vec::new();
|
||||
if let Some(config_map_file) = &args.config_map_file {
|
||||
config_map_files.push(config_map_file.clone());
|
||||
}
|
||||
|
||||
let config = utils::Config::new(
|
||||
args.use_cached_files,
|
||||
args.yaml_file,
|
||||
&args.input_files_path,
|
||||
&args.settings_file_name,
|
||||
&config_map_files,
|
||||
args.silent_unsupported_fields,
|
||||
args.raw_out,
|
||||
args.base64_out,
|
||||
);
|
||||
|
||||
debug!("Creating policy from yaml, settings, and rules.rego files...");
|
||||
let mut policy = policy::AgentPolicy::from_files(&config).await.unwrap();
|
||||
|
||||
debug!("Exporting policy to yaml file...");
|
||||
policy.export_policy();
|
||||
info!("Success!");
|
||||
}
|
340
src/tools/genpolicy/src/mount_and_storage.rs
Normal file
340
src/tools/genpolicy/src/mount_and_storage.rs
Normal file
@ -0,0 +1,340 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow OCI spec field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::pod;
|
||||
use crate::policy;
|
||||
use crate::settings;
|
||||
use crate::volume;
|
||||
|
||||
use log::debug;
|
||||
use protocols::agent;
|
||||
use std::ffi::OsString;
|
||||
use std::path::Path;
|
||||
use std::str;
|
||||
|
||||
pub fn get_policy_mounts(
|
||||
settings: &settings::Settings,
|
||||
p_mounts: &mut Vec<policy::KataMount>,
|
||||
yaml_container: &pod::Container,
|
||||
is_pause_container: bool,
|
||||
) {
|
||||
let c_settings = settings.get_container_settings(is_pause_container);
|
||||
let settings_mounts = &c_settings.Mounts;
|
||||
let rootfs_access = if yaml_container.read_only_root_filesystem() {
|
||||
"ro"
|
||||
} else {
|
||||
"rw"
|
||||
};
|
||||
|
||||
for s_mount in settings_mounts {
|
||||
if keep_settings_mount(settings, &s_mount, &yaml_container.volumeMounts) {
|
||||
let mut mount = s_mount.clone();
|
||||
adjust_termination_path(&mut mount, &yaml_container);
|
||||
|
||||
if mount.source.is_empty() && mount.type_.eq("bind") {
|
||||
if let Some(file_name) = Path::new(&mount.destination).file_name() {
|
||||
if let Some(file_name) = file_name.to_str() {
|
||||
mount.source = format!("$(sfprefix){file_name}$");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(policy_mount) = p_mounts
|
||||
.iter_mut()
|
||||
.find(|m| m.destination.eq(&s_mount.destination))
|
||||
{
|
||||
// Update an already existing mount.
|
||||
policy_mount.type_ = mount.type_.clone();
|
||||
policy_mount.source = mount.source.clone();
|
||||
policy_mount.options = mount.options.iter().map(String::from).collect();
|
||||
} else {
|
||||
// Add a new mount.
|
||||
if !is_pause_container {
|
||||
if s_mount.destination.eq("/etc/hostname")
|
||||
|| s_mount.destination.eq("/etc/resolv.conf")
|
||||
{
|
||||
mount.options.push(rootfs_access.to_string());
|
||||
}
|
||||
}
|
||||
p_mounts.push(mount);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn keep_settings_mount(
|
||||
settings: &settings::Settings,
|
||||
s_mount: &policy::KataMount,
|
||||
yaml_mounts: &Option<Vec<pod::VolumeMount>>,
|
||||
) -> bool {
|
||||
let destinations = &settings.mount_destinations;
|
||||
let mut keep = destinations.iter().any(|d| s_mount.destination.eq(d));
|
||||
|
||||
if !keep {
|
||||
if let Some(mounts) = yaml_mounts {
|
||||
keep = mounts.iter().any(|m| m.mountPath.eq(&s_mount.destination));
|
||||
}
|
||||
}
|
||||
|
||||
keep
|
||||
}
|
||||
|
||||
fn adjust_termination_path(mount: &mut policy::KataMount, yaml_container: &pod::Container) {
|
||||
if mount.destination == "/dev/termination-log" {
|
||||
if let Some(path) = &yaml_container.terminationMessagePath {
|
||||
mount.destination = path.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_mount_and_storage(
|
||||
settings: &settings::Settings,
|
||||
p_mounts: &mut Vec<policy::KataMount>,
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
yaml_volume: &volume::Volume,
|
||||
yaml_mount: &pod::VolumeMount,
|
||||
) {
|
||||
if let Some(emptyDir) = &yaml_volume.emptyDir {
|
||||
let memory_medium = if let Some(medium) = &emptyDir.medium {
|
||||
medium == "Memory"
|
||||
} else {
|
||||
false
|
||||
};
|
||||
get_empty_dir_mount_and_storage(settings, p_mounts, storages, yaml_mount, memory_medium);
|
||||
} else if yaml_volume.persistentVolumeClaim.is_some() || yaml_volume.azureFile.is_some() {
|
||||
get_shared_bind_mount(yaml_mount, p_mounts, "rprivate", "rw");
|
||||
} else if yaml_volume.hostPath.is_some() {
|
||||
get_host_path_mount(yaml_mount, yaml_volume, p_mounts);
|
||||
} else if yaml_volume.configMap.is_some() || yaml_volume.secret.is_some() {
|
||||
get_config_map_mount_and_storage(settings, p_mounts, storages, yaml_mount);
|
||||
} else if yaml_volume.projected.is_some() {
|
||||
get_shared_bind_mount(yaml_mount, p_mounts, "rprivate", "ro");
|
||||
} else if yaml_volume.downwardAPI.is_some() {
|
||||
get_downward_api_mount(yaml_mount, p_mounts);
|
||||
} else {
|
||||
todo!("Unsupported volume type {:?}", yaml_volume);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_empty_dir_mount_and_storage(
|
||||
settings: &settings::Settings,
|
||||
p_mounts: &mut Vec<policy::KataMount>,
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
yaml_mount: &pod::VolumeMount,
|
||||
memory_medium: bool,
|
||||
) {
|
||||
let settings_volumes = &settings.volumes;
|
||||
let settings_empty_dir = if memory_medium {
|
||||
&settings_volumes.emptyDir_memory
|
||||
} else {
|
||||
&settings_volumes.emptyDir
|
||||
};
|
||||
debug!("Settings emptyDir: {:?}", settings_empty_dir);
|
||||
|
||||
if yaml_mount.subPathExpr.is_none() {
|
||||
storages.push(agent::Storage {
|
||||
driver: settings_empty_dir.driver.clone(),
|
||||
driver_options: Vec::new(),
|
||||
source: settings_empty_dir.source.clone(),
|
||||
fstype: settings_empty_dir.fstype.clone(),
|
||||
options: settings_empty_dir.options.clone(),
|
||||
mount_point: format!("{}{}$", &settings_empty_dir.mount_point, &yaml_mount.name),
|
||||
fs_group: protobuf::MessageField::none(),
|
||||
special_fields: ::protobuf::SpecialFields::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let source = if yaml_mount.subPathExpr.is_some() {
|
||||
let file_name = Path::new(&yaml_mount.mountPath).file_name().unwrap();
|
||||
let name = OsString::from(file_name).into_string().unwrap();
|
||||
format!("{}{name}$", &settings_volumes.configMap.mount_source)
|
||||
} else {
|
||||
format!("{}{}$", &settings_empty_dir.mount_source, &yaml_mount.name)
|
||||
};
|
||||
|
||||
let mount_type = if yaml_mount.subPathExpr.is_some() {
|
||||
"bind"
|
||||
} else {
|
||||
&settings_empty_dir.mount_type
|
||||
};
|
||||
|
||||
p_mounts.push(policy::KataMount {
|
||||
destination: yaml_mount.mountPath.to_string(),
|
||||
type_: mount_type.to_string(),
|
||||
source,
|
||||
options: vec![
|
||||
"rbind".to_string(),
|
||||
"rprivate".to_string(),
|
||||
"rw".to_string(),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
fn get_host_path_mount(
|
||||
yaml_mount: &pod::VolumeMount,
|
||||
yaml_volume: &volume::Volume,
|
||||
p_mounts: &mut Vec<policy::KataMount>,
|
||||
) {
|
||||
let host_path = yaml_volume.hostPath.as_ref().unwrap().path.clone();
|
||||
let path = Path::new(&host_path);
|
||||
|
||||
let mut biderectional = false;
|
||||
if let Some(mount_propagation) = &yaml_mount.mountPropagation {
|
||||
if mount_propagation.eq("Bidirectional") {
|
||||
debug!("get_host_path_mount: Bidirectional");
|
||||
biderectional = true;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO:
|
||||
//
|
||||
// - When volume.hostPath.path: /dev/ttyS0
|
||||
// "source": "/dev/ttyS0"
|
||||
// - When volume.hostPath.path: /tmp/results
|
||||
// "source": "^/run/kata-containers/shared/containers/$(bundle-id)-[a-z0-9]{16}-results$"
|
||||
//
|
||||
// What is the reason for this source path difference in the Guest OS?
|
||||
if !path.starts_with("/dev/") && !path.starts_with("/sys/") {
|
||||
debug!("get_host_path_mount: calling get_shared_bind_mount");
|
||||
let propagation = if biderectional { "rshared" } else { "rprivate" };
|
||||
get_shared_bind_mount(yaml_mount, p_mounts, propagation, "rw");
|
||||
} else {
|
||||
let dest = yaml_mount.mountPath.clone();
|
||||
let type_ = "bind".to_string();
|
||||
let mount_option = if biderectional { "rshared" } else { "rprivate" };
|
||||
let options = vec![
|
||||
"rbind".to_string(),
|
||||
mount_option.to_string(),
|
||||
"rw".to_string(),
|
||||
];
|
||||
|
||||
if let Some(policy_mount) = p_mounts.iter_mut().find(|m| m.destination.eq(&dest)) {
|
||||
debug!("get_host_path_mount: updating dest = {dest}, source = {host_path}");
|
||||
policy_mount.type_ = type_;
|
||||
policy_mount.source = host_path;
|
||||
policy_mount.options = options;
|
||||
} else {
|
||||
debug!("get_host_path_mount: adding dest = {dest}, source = {host_path}");
|
||||
p_mounts.push(policy::KataMount {
|
||||
destination: dest,
|
||||
type_,
|
||||
source: host_path,
|
||||
options,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_config_map_mount_and_storage(
|
||||
settings: &settings::Settings,
|
||||
p_mounts: &mut Vec<policy::KataMount>,
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
yaml_mount: &pod::VolumeMount,
|
||||
) {
|
||||
let settings_volumes = &settings.volumes;
|
||||
let settings_config_map = if settings.kata_config.confidential_guest {
|
||||
&settings_volumes.confidential_configMap
|
||||
} else {
|
||||
&settings_volumes.configMap
|
||||
};
|
||||
debug!("Settings configMap: {:?}", settings_config_map);
|
||||
|
||||
if !settings.kata_config.confidential_guest {
|
||||
let mount_path = Path::new(&yaml_mount.mountPath).file_name().unwrap();
|
||||
let mount_path_str = OsString::from(mount_path).into_string().unwrap();
|
||||
|
||||
storages.push(agent::Storage {
|
||||
driver: settings_config_map.driver.clone(),
|
||||
driver_options: Vec::new(),
|
||||
source: format!("{}{}$", &settings_config_map.mount_source, &yaml_mount.name),
|
||||
fstype: settings_config_map.fstype.clone(),
|
||||
options: settings_config_map.options.clone(),
|
||||
mount_point: format!("{}{mount_path_str}$", &settings_config_map.mount_point),
|
||||
fs_group: protobuf::MessageField::none(),
|
||||
special_fields: ::protobuf::SpecialFields::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let file_name = Path::new(&yaml_mount.mountPath).file_name().unwrap();
|
||||
let name = OsString::from(file_name).into_string().unwrap();
|
||||
p_mounts.push(policy::KataMount {
|
||||
destination: yaml_mount.mountPath.clone(),
|
||||
type_: settings_config_map.mount_type.clone(),
|
||||
source: format!("{}{name}$", &settings_config_map.mount_point),
|
||||
options: settings_config_map.options.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
fn get_shared_bind_mount(
|
||||
yaml_mount: &pod::VolumeMount,
|
||||
p_mounts: &mut Vec<policy::KataMount>,
|
||||
propagation: &str,
|
||||
access: &str,
|
||||
) {
|
||||
let mount_path = if let Some(byte_index) = str::rfind(&yaml_mount.mountPath, '/') {
|
||||
str::from_utf8(&yaml_mount.mountPath.as_bytes()[byte_index + 1..]).unwrap()
|
||||
} else {
|
||||
&yaml_mount.mountPath
|
||||
};
|
||||
let source = format!("$(sfprefix){mount_path}$");
|
||||
|
||||
let dest = yaml_mount.mountPath.clone();
|
||||
let type_ = "bind".to_string();
|
||||
let options = vec![
|
||||
"rbind".to_string(),
|
||||
propagation.to_string(),
|
||||
access.to_string(),
|
||||
];
|
||||
|
||||
if let Some(policy_mount) = p_mounts.iter_mut().find(|m| m.destination.eq(&dest)) {
|
||||
debug!("get_shared_bind_mount: updating dest = {dest}, source = {source}");
|
||||
policy_mount.type_ = type_;
|
||||
policy_mount.source = source;
|
||||
policy_mount.options = options;
|
||||
} else {
|
||||
debug!("get_shared_bind_mount: adding dest = {dest}, source = {source}");
|
||||
p_mounts.push(policy::KataMount {
|
||||
destination: dest,
|
||||
type_,
|
||||
source,
|
||||
options,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn get_downward_api_mount(yaml_mount: &pod::VolumeMount, p_mounts: &mut Vec<policy::KataMount>) {
|
||||
let mount_path = if let Some(byte_index) = str::rfind(&yaml_mount.mountPath, '/') {
|
||||
str::from_utf8(&yaml_mount.mountPath.as_bytes()[byte_index + 1..]).unwrap()
|
||||
} else {
|
||||
&yaml_mount.mountPath
|
||||
};
|
||||
let source = format!("$(sfprefix){mount_path}$");
|
||||
|
||||
let dest = yaml_mount.mountPath.clone();
|
||||
let type_ = "bind".to_string();
|
||||
let options = vec![
|
||||
"rbind".to_string(),
|
||||
"rprivate".to_string(),
|
||||
"ro".to_string(),
|
||||
];
|
||||
|
||||
if let Some(policy_mount) = p_mounts.iter_mut().find(|m| m.destination.eq(&dest)) {
|
||||
debug!("get_downward_api_mount: updating dest = {dest}, source = {source}");
|
||||
policy_mount.type_ = type_;
|
||||
policy_mount.source = source;
|
||||
policy_mount.options = options;
|
||||
} else {
|
||||
debug!("get_downward_api_mount: adding dest = {dest}, source = {source}");
|
||||
p_mounts.push(policy::KataMount {
|
||||
destination: dest,
|
||||
type_,
|
||||
source,
|
||||
options,
|
||||
});
|
||||
}
|
||||
}
|
70
src/tools/genpolicy/src/no_policy.rs
Normal file
70
src/tools/genpolicy/src/no_policy.rs
Normal file
@ -0,0 +1,70 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::pod;
|
||||
use crate::policy;
|
||||
use crate::settings;
|
||||
use crate::yaml;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use protocols::agent;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct NoPolicyResource {
|
||||
pub yaml: String,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl yaml::K8sResource for NoPolicyResource {
|
||||
async fn init(
|
||||
&mut self,
|
||||
_use_cache: bool,
|
||||
_doc_mapping: &serde_yaml::Value,
|
||||
_silent_unsupported_fields: bool,
|
||||
) {
|
||||
}
|
||||
|
||||
fn get_sandbox_name(&self) -> Option<String> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_namespace(&self) -> String {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_container_mounts_and_storages(
|
||||
&self,
|
||||
_policy_mounts: &mut Vec<policy::KataMount>,
|
||||
_storages: &mut Vec<agent::Storage>,
|
||||
_container: &pod::Container,
|
||||
_settings: &settings::Settings,
|
||||
) {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn generate_policy(&self, _agent_policy: &policy::AgentPolicy) -> String {
|
||||
return "".to_string();
|
||||
}
|
||||
|
||||
fn serialize(&mut self, _policy: &str) -> String {
|
||||
self.yaml.clone()
|
||||
}
|
||||
|
||||
fn get_containers(&self) -> &Vec<pod::Container> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_annotations(&self) -> &Option<BTreeMap<String, String>> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn use_host_network(&self) -> bool {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
}
|
49
src/tools/genpolicy/src/obj_meta.rs
Normal file
49
src/tools/genpolicy/src/obj_meta.rs
Normal file
@ -0,0 +1,49 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// See ObjectMeta in the Kubernetes API reference.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ObjectMeta {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
generateName: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
labels: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub annotations: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
impl ObjectMeta {
|
||||
pub fn get_name(&self) -> String {
|
||||
if let Some(name) = &self.name {
|
||||
name.clone()
|
||||
} else if self.generateName.is_some() {
|
||||
return "$(generated-name)".to_string();
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_namespace(&self) -> String {
|
||||
if let Some(namespace) = &self.namespace {
|
||||
namespace.clone()
|
||||
} else {
|
||||
"default".to_string()
|
||||
}
|
||||
}
|
||||
}
|
45
src/tools/genpolicy/src/persistent_volume_claim.rs
Normal file
45
src/tools/genpolicy/src/persistent_volume_claim.rs
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::obj_meta;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / PersistentVolumeClaim.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct PersistentVolumeClaim {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
apiVersion: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
kind: Option<String>,
|
||||
|
||||
pub metadata: obj_meta::ObjectMeta,
|
||||
spec: PersistentVolumeClaimSpec,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / PersistentVolumeClaim.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
struct PersistentVolumeClaimSpec {
|
||||
resources: ResourceRequirements,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
accessModes: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
storageClassName: Option<String>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / PersistentVolumeClaim.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct ResourceRequirements {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
requests: Option<BTreeMap<String, String>>,
|
||||
}
|
808
src/tools/genpolicy/src/pod.rs
Normal file
808
src/tools/genpolicy/src/pod.rs
Normal file
@ -0,0 +1,808 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::config_map;
|
||||
use crate::obj_meta;
|
||||
use crate::policy;
|
||||
use crate::registry;
|
||||
use crate::secret;
|
||||
use crate::settings;
|
||||
use crate::volume;
|
||||
use crate::yaml;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, warn};
|
||||
use protocols::agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Pod {
|
||||
apiVersion: String,
|
||||
kind: String,
|
||||
pub metadata: obj_meta::ObjectMeta,
|
||||
pub spec: PodSpec,
|
||||
|
||||
#[serde(skip)]
|
||||
doc_mapping: serde_yaml::Value,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PodSpec {
|
||||
pub containers: Vec<Container>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
nodeSelector: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
restartPolicy: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
runtimeClassName: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub initContainers: Option<Vec<Container>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
imagePullSecrets: Option<Vec<LocalObjectReference>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
affinity: Option<Affinity>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volumes: Option<Vec<volume::Volume>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
serviceAccountName: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
serviceAccount: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
terminationGracePeriodSeconds: Option<i64>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tolerations: Option<Vec<Toleration>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hostname: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hostNetwork: Option<bool>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct Container {
|
||||
/// Container image registry information.
|
||||
#[serde(skip)]
|
||||
pub registry: registry::Container,
|
||||
|
||||
pub name: String,
|
||||
pub image: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
imagePullPolicy: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
securityContext: Option<SecurityContext>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub volumeMounts: Option<Vec<VolumeMount>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
env: Option<Vec<EnvVar>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resources: Option<ResourceRequirements>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
ports: Option<Vec<ContainerPort>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub command: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub args: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
lifecycle: Option<Lifecycle>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
livenessProbe: Option<Probe>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
readinessProbe: Option<Probe>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
startupProbe: Option<Probe>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub serviceAccountName: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
stdin: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tty: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub terminationMessagePath: Option<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Affinity {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub podAntiAffinity: Option<PodAntiAffinity>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub podAffinity: Option<PodAffinity>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct PodAffinity {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
requiredDuringSchedulingIgnoredDuringExecution: Option<Vec<PodAffinityTerm>>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct PodAntiAffinity {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
preferredDuringSchedulingIgnoredDuringExecution: Option<Vec<WeightedPodAffinityTerm>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
requiredDuringSchedulingIgnoredDuringExecution: Option<Vec<PodAffinityTerm>>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct WeightedPodAffinityTerm {
|
||||
weight: i32,
|
||||
podAffinityTerm: PodAffinityTerm,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct PodAffinityTerm {
|
||||
topologyKey: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
labelSelector: Option<yaml::LabelSelector>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Probe {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
exec: Option<ExecAction>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
initialDelaySeconds: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
timeoutSeconds: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
periodSeconds: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
failureThreshold: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
successThreshold: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
httpGet: Option<HTTPGetAction>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tcpSocket: Option<TCPSocketAction>,
|
||||
// TODO: additional fiels.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct TCPSocketAction {
|
||||
port: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
host: Option<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct HTTPGetAction {
|
||||
port: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
host: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
path: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
scheme: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
httpHeaders: Option<Vec<HTTPHeader>>,
|
||||
// TODO: additional fiels.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct HTTPHeader {
|
||||
name: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct SecurityContext {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
readOnlyRootFilesystem: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
allowPrivilegeEscalation: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
privileged: Option<bool>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
capabilities: Option<Capabilities>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
runAsUser: Option<i64>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Lifecycle {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
postStart: Option<LifecycleHandler>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
preStop: Option<LifecycleHandler>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct LifecycleHandler {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
exec: Option<ExecAction>,
|
||||
// TODO: additional fiels.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct ExecAction {
|
||||
command: Vec<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Capabilities {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
add: Option<Vec<String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
drop: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct ContainerPort {
|
||||
containerPort: i32,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hostIP: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
hostPort: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
name: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
protocol: Option<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct EnvVar {
|
||||
name: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
value: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
valueFrom: Option<EnvVarSource>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EnvVarSource {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub configMapKeyRef: Option<ConfigMapKeySelector>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
fieldRef: Option<ObjectFieldSelector>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub secretKeyRef: Option<SecretKeySelector>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
resourceFieldRef: Option<ResourceFieldSelector>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SecretKeySelector {
|
||||
pub key: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
optional: Option<bool>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigMapKeySelector {
|
||||
pub key: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub name: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
optional: Option<bool>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Common Definitions / ResourceFieldSelector.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct ResourceFieldSelector {
|
||||
resource: String,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Common Definitions / ObjectFieldSelector.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ObjectFieldSelector {
|
||||
fieldPath: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
apiVersion: Option<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct VolumeMount {
|
||||
pub mountPath: String,
|
||||
pub name: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mountPropagation: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub subPathExpr: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub readOnly: Option<bool>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct ResourceRequirements {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
requests: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
limits: Option<BTreeMap<String, String>>,
|
||||
// TODO: claims field.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Workload Resources / Pod.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct Toleration {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
key: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
operator: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
value: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
effect: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
tolerationSeconds: Option<i64>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Common Definitions / LocalObjectReference.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct LocalObjectReference {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub async fn init(&mut self, use_cache: bool) {
|
||||
// Load container image properties from the registry.
|
||||
self.registry = registry::get_container(use_cache, &self.image)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn get_env_variables(
|
||||
&self,
|
||||
dest_env: &mut Vec<String>,
|
||||
config_maps: &Vec<config_map::ConfigMap>,
|
||||
secrets: &Vec<secret::Secret>,
|
||||
namespace: &str,
|
||||
annotations: &Option<BTreeMap<String, String>>,
|
||||
service_account_name: &str,
|
||||
) {
|
||||
if let Some(source_env) = &self.env {
|
||||
for env_variable in source_env {
|
||||
let value = env_variable.get_value(
|
||||
config_maps,
|
||||
secrets,
|
||||
namespace,
|
||||
annotations,
|
||||
service_account_name,
|
||||
);
|
||||
let src_string = format!("{}={value}", &env_variable.name);
|
||||
|
||||
if !dest_env.contains(&src_string) {
|
||||
dest_env.push(src_string.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_privileged(&self) -> bool {
|
||||
if let Some(context) = &self.securityContext {
|
||||
if let Some(privileged) = context.privileged {
|
||||
return privileged;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn allow_privilege_escalation(&self) -> bool {
|
||||
if let Some(context) = &self.securityContext {
|
||||
if let Some(allow) = context.allowPrivilegeEscalation {
|
||||
return allow;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn read_only_root_filesystem(&self) -> bool {
|
||||
if let Some(context) = &self.securityContext {
|
||||
if let Some(read_only) = context.readOnlyRootFilesystem {
|
||||
return read_only;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_process_args(&self, policy_args: &mut Vec<String>) -> (bool, bool) {
|
||||
let mut yaml_has_command = true;
|
||||
let mut yaml_has_args = true;
|
||||
|
||||
if let Some(commands) = &self.command {
|
||||
for command in commands {
|
||||
policy_args.push(command.clone());
|
||||
}
|
||||
} else {
|
||||
yaml_has_command = false;
|
||||
}
|
||||
|
||||
if let Some(args) = &self.args {
|
||||
for arg in args {
|
||||
policy_args.push(arg.clone());
|
||||
}
|
||||
} else {
|
||||
yaml_has_args = false;
|
||||
}
|
||||
|
||||
(yaml_has_command, yaml_has_args)
|
||||
}
|
||||
|
||||
pub fn get_exec_commands(&self) -> Vec<String> {
|
||||
let mut commands = Vec::new();
|
||||
|
||||
if let Some(probe) = &self.livenessProbe {
|
||||
if let Some(exec) = &probe.exec {
|
||||
commands.push(exec.command.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(probe) = &self.readinessProbe {
|
||||
if let Some(exec) = &probe.exec {
|
||||
commands.push(exec.command.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(probe) = &self.startupProbe {
|
||||
if let Some(exec) = &probe.exec {
|
||||
commands.push(exec.command.join(" "));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(lifecycle) = &self.lifecycle {
|
||||
if let Some(postStart) = &lifecycle.postStart {
|
||||
if let Some(exec) = &postStart.exec {
|
||||
commands.push(exec.command.join(" "));
|
||||
}
|
||||
}
|
||||
if let Some(preStop) = &lifecycle.preStop {
|
||||
if let Some(exec) = &preStop.exec {
|
||||
commands.push(exec.command.join(" "));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commands
|
||||
}
|
||||
}
|
||||
|
||||
impl EnvVar {
|
||||
pub fn get_value(
|
||||
&self,
|
||||
config_maps: &Vec<config_map::ConfigMap>,
|
||||
secrets: &Vec<secret::Secret>,
|
||||
namespace: &str,
|
||||
annotations: &Option<BTreeMap<String, String>>,
|
||||
service_account_name: &str,
|
||||
) -> String {
|
||||
if let Some(value) = &self.value {
|
||||
return value.clone();
|
||||
}
|
||||
|
||||
if let Some(value_from) = &self.valueFrom {
|
||||
if let Some(value) = config_map::get_value(value_from, config_maps) {
|
||||
return value.clone();
|
||||
}
|
||||
|
||||
if let Some(value) = secret::get_value(value_from, secrets) {
|
||||
return value.clone();
|
||||
}
|
||||
|
||||
if let Some(field_ref) = &value_from.fieldRef {
|
||||
let path: &str = &field_ref.fieldPath;
|
||||
match path {
|
||||
"metadata.name" => return "$(sandbox-name)".to_string(),
|
||||
"metadata.namespace" => return namespace.to_string(),
|
||||
"metadata.uid" => return "$(pod-uid)".to_string(),
|
||||
"status.hostIP" => return "$(host-ip)".to_string(),
|
||||
"status.podIP" => return "$(pod-ip)".to_string(),
|
||||
"spec.nodeName" => return "$(node-name)".to_string(),
|
||||
"spec.serviceAccountName" => return service_account_name.to_string(),
|
||||
_ => {
|
||||
if let Some(value) = self.get_annotation_value(path, annotations) {
|
||||
return value;
|
||||
} else {
|
||||
panic!(
|
||||
"Env var: unsupported field reference: {}",
|
||||
&field_ref.fieldPath
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if value_from.resourceFieldRef.is_some() {
|
||||
// TODO: should resource fields such as "limits.cpu" or "limits.memory"
|
||||
// be handled in a different way?
|
||||
return "$(resource-field)".to_string();
|
||||
}
|
||||
} else {
|
||||
panic!("Environment variable without value or valueFrom!");
|
||||
}
|
||||
|
||||
panic!("Couldn't get the value of env var: {}", &self.name);
|
||||
}
|
||||
|
||||
fn get_annotation_value(
|
||||
&self,
|
||||
reference: &str,
|
||||
anno: &Option<BTreeMap<String, String>>,
|
||||
) -> Option<String> {
|
||||
let prefix = "metadata.annotations['";
|
||||
let suffix = "']";
|
||||
if reference.starts_with(prefix) && reference.ends_with(suffix) {
|
||||
if let Some(annotations) = anno {
|
||||
let start = prefix.len();
|
||||
let end = reference.len() - 2;
|
||||
let annotation = reference[start..end].to_string();
|
||||
|
||||
if let Some(value) = annotations.get(&annotation) {
|
||||
return Some(value.clone());
|
||||
} else {
|
||||
warn!(
|
||||
"Can't find the value of annotation {}. Allowing any value.",
|
||||
&annotation
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: should missing annotations be handled differently?
|
||||
return Some("$(todo-annotation)".to_string());
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl yaml::K8sResource for Pod {
|
||||
async fn init(&mut self, use_cache: bool, doc_mapping: &serde_yaml::Value, _silent: bool) {
|
||||
yaml::k8s_resource_init(&mut self.spec, use_cache).await;
|
||||
self.doc_mapping = doc_mapping.clone();
|
||||
}
|
||||
|
||||
fn get_sandbox_name(&self) -> Option<String> {
|
||||
let name = self.metadata.get_name();
|
||||
if !name.is_empty() {
|
||||
return Some(name);
|
||||
}
|
||||
panic!("No pod name.");
|
||||
}
|
||||
|
||||
fn get_namespace(&self) -> String {
|
||||
self.metadata.get_namespace()
|
||||
}
|
||||
|
||||
fn get_container_mounts_and_storages(
|
||||
&self,
|
||||
policy_mounts: &mut Vec<policy::KataMount>,
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
container: &Container,
|
||||
settings: &settings::Settings,
|
||||
) {
|
||||
if let Some(volumes) = &self.spec.volumes {
|
||||
yaml::get_container_mounts_and_storages(
|
||||
policy_mounts,
|
||||
storages,
|
||||
container,
|
||||
settings,
|
||||
volumes,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_policy(&self, agent_policy: &policy::AgentPolicy) -> String {
|
||||
agent_policy.generate_policy(self)
|
||||
}
|
||||
|
||||
fn serialize(&mut self, policy: &str) -> String {
|
||||
yaml::add_policy_annotation(&mut self.doc_mapping, "metadata", policy);
|
||||
serde_yaml::to_string(&self.doc_mapping).unwrap()
|
||||
}
|
||||
|
||||
fn get_containers(&self) -> &Vec<Container> {
|
||||
&self.spec.containers
|
||||
}
|
||||
|
||||
fn get_annotations(&self) -> &Option<BTreeMap<String, String>> {
|
||||
&self.metadata.annotations
|
||||
}
|
||||
|
||||
fn use_host_network(&self) -> bool {
|
||||
if let Some(host_network) = self.spec.hostNetwork {
|
||||
return host_network;
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub fn apply_capabilities(
|
||||
&self,
|
||||
capabilities: &mut policy::KataLinuxCapabilities,
|
||||
defaults: &policy::CommonData,
|
||||
) {
|
||||
assert!(capabilities.Ambient.is_empty());
|
||||
assert!(capabilities.Inheritable.is_empty());
|
||||
|
||||
if let Some(securityContext) = &self.securityContext {
|
||||
if let Some(yaml_capabilities) = &securityContext.capabilities {
|
||||
if let Some(drop) = &yaml_capabilities.drop {
|
||||
for c in drop {
|
||||
if c == "ALL" {
|
||||
capabilities.Bounding.clear();
|
||||
capabilities.Permitted.clear();
|
||||
capabilities.Effective.clear();
|
||||
} else {
|
||||
let cap = "CAP_".to_string() + &c;
|
||||
|
||||
capabilities.Bounding.retain(|x| !x.eq(&cap));
|
||||
capabilities.Permitted.retain(|x| !x.eq(&cap));
|
||||
capabilities.Effective.retain(|x| !x.eq(&cap));
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(add) = &yaml_capabilities.add {
|
||||
for c in add {
|
||||
let cap = "CAP_".to_string() + &c;
|
||||
|
||||
if !capabilities.Bounding.contains(&cap) {
|
||||
capabilities.Bounding.push(cap.clone());
|
||||
}
|
||||
if !capabilities.Permitted.contains(&cap) {
|
||||
capabilities.Permitted.push(cap.clone());
|
||||
}
|
||||
if !capabilities.Effective.contains(&cap) {
|
||||
capabilities.Effective.push(cap.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
compress_default_capabilities(capabilities, defaults);
|
||||
}
|
||||
}
|
||||
|
||||
fn compress_default_capabilities(
|
||||
capabilities: &mut policy::KataLinuxCapabilities,
|
||||
defaults: &policy::CommonData,
|
||||
) {
|
||||
assert!(capabilities.Ambient.is_empty());
|
||||
assert!(capabilities.Inheritable.is_empty());
|
||||
|
||||
compress_capabilities(&mut capabilities.Bounding, defaults);
|
||||
compress_capabilities(&mut capabilities.Permitted, defaults);
|
||||
compress_capabilities(&mut capabilities.Effective, defaults);
|
||||
}
|
||||
|
||||
fn compress_capabilities(capabilities: &mut Vec<String>, defaults: &policy::CommonData) {
|
||||
let default_caps = if capabilities == &defaults.default_caps {
|
||||
"$(default_caps)"
|
||||
} else if capabilities == &defaults.privileged_caps {
|
||||
"$(privileged_caps)"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
if default_caps.len() != 0 {
|
||||
capabilities.clear();
|
||||
capabilities.push(default_caps.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn add_pause_container(containers: &mut Vec<Container>, use_cache: bool) {
|
||||
debug!("Adding pause container...");
|
||||
let mut pause_container = Container {
|
||||
// TODO: load this path from the settings file.
|
||||
image: "mcr.microsoft.com/oss/kubernetes/pause:3.6".to_string(),
|
||||
|
||||
name: String::new(),
|
||||
imagePullPolicy: None,
|
||||
securityContext: Some(SecurityContext {
|
||||
readOnlyRootFilesystem: Some(true),
|
||||
allowPrivilegeEscalation: Some(false),
|
||||
privileged: None,
|
||||
capabilities: None,
|
||||
runAsUser: None,
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
pause_container.init(use_cache).await;
|
||||
containers.insert(0, pause_container);
|
||||
debug!("pause container added.");
|
||||
}
|
28
src/tools/genpolicy/src/pod_template.rs
Normal file
28
src/tools/genpolicy/src/pod_template.rs
Normal file
@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::obj_meta;
|
||||
use crate::pod;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Reference / Kubernetes API / Workload / Resources / PodTemplate.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PodTemplate {
|
||||
apiVersion: String,
|
||||
kind: String,
|
||||
metadata: obj_meta::ObjectMeta,
|
||||
spec: PodTemplateSpec,
|
||||
}
|
||||
|
||||
/// Reference / Kubernetes API / Workload / Resources / PodTemplate.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PodTemplateSpec {
|
||||
pub metadata: obj_meta::ObjectMeta,
|
||||
pub spec: pod::PodSpec,
|
||||
}
|
900
src/tools/genpolicy/src/policy.rs
Normal file
900
src/tools/genpolicy/src/policy.rs
Normal file
@ -0,0 +1,900 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow OCI spec field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::config_map;
|
||||
use crate::containerd;
|
||||
use crate::mount_and_storage;
|
||||
use crate::pod;
|
||||
use crate::policy;
|
||||
use crate::registry;
|
||||
use crate::secret;
|
||||
use crate::settings;
|
||||
use crate::utils;
|
||||
use crate::yaml;
|
||||
|
||||
use anyhow::Result;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use log::debug;
|
||||
use protocols::agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::boxed;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::read_to_string;
|
||||
use std::io::Write;
|
||||
|
||||
// TODO: load this value from the settings file.
|
||||
const DEFAULT_OCI_VERSION: &str = "1.1.0-rc.1";
|
||||
|
||||
/// Intermediary format of policy data.
|
||||
pub struct AgentPolicy {
|
||||
/// K8s resources described by the input YAML file.
|
||||
resources: Vec<boxed::Box<dyn yaml::K8sResource + Send + Sync>>,
|
||||
|
||||
/// K8s ConfigMap resources described by an additional input YAML file
|
||||
/// or by the "main" input YAML file, containing additional pod settings.
|
||||
config_maps: Vec<config_map::ConfigMap>,
|
||||
|
||||
/// K8s Secret resources, containing additional pod settings.
|
||||
secrets: Vec<secret::Secret>,
|
||||
|
||||
/// Rego rules read from a file (rules.rego).
|
||||
pub rules: String,
|
||||
|
||||
/// Settings loaded from genpolicy-settings.json.
|
||||
pub settings: settings::Settings,
|
||||
|
||||
/// Additional Policy settings.
|
||||
pub config: utils::Config,
|
||||
}
|
||||
|
||||
/// Representation of the policy_data field from the output policy text.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct PolicyData {
|
||||
/// Policy properties for each container allowed to be executed in a pod.
|
||||
pub containers: Vec<ContainerPolicy>,
|
||||
|
||||
/// Settings read from genpolicy-settings.json.
|
||||
pub common: CommonData,
|
||||
|
||||
/// Settings read from genpolicy-settings.json, related directly to each
|
||||
/// kata agent endpoint, that get added to the output policy.
|
||||
pub request_defaults: RequestDefaults,
|
||||
}
|
||||
|
||||
/// OCI Container spec. This struct is very similar to the Spec struct from
|
||||
/// Kata Containers. The main difference is that the Annotations field below
|
||||
/// is ordered, thus resulting in the same output policy contents every time
|
||||
/// when this apps runs with the same inputs. Also, it preserves the upper
|
||||
/// case field names, for consistency with the structs used by agent's rpc.rs.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct KataSpec {
|
||||
/// Version of the Open Container Initiative Runtime Specification with which the bundle complies.
|
||||
#[serde(default = "version_default")]
|
||||
pub Version: String,
|
||||
|
||||
/// Process configures the container process.
|
||||
#[serde(default)]
|
||||
pub Process: KataProcess,
|
||||
|
||||
/// Root configures the container's root filesystem.
|
||||
pub Root: KataRoot,
|
||||
|
||||
/// Mounts configures additional mounts (on top of Root).
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub Mounts: Vec<KataMount>,
|
||||
|
||||
/// Hooks configures callbacks for container lifecycle events.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub Hooks: Option<oci::Hooks>,
|
||||
|
||||
/// Annotations contains arbitrary metadata for the container.
|
||||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||||
pub Annotations: BTreeMap<String, String>,
|
||||
|
||||
/// Linux is platform-specific configuration for Linux based containers.
|
||||
#[serde(default)]
|
||||
pub Linux: KataLinux,
|
||||
}
|
||||
|
||||
fn version_default() -> String {
|
||||
DEFAULT_OCI_VERSION.to_string()
|
||||
}
|
||||
|
||||
/// OCI container Process struct. This struct is very similar to the Process
|
||||
/// struct generated from oci.proto. The main difference is that it preserves
|
||||
/// the upper case field names from oci.proto, for consistency with the structs
|
||||
/// used by agent's rpc.rs.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct KataProcess {
|
||||
/// Terminal creates an interactive terminal for the container.
|
||||
#[serde(default)]
|
||||
pub Terminal: bool,
|
||||
|
||||
/// User specifies user information for the process.
|
||||
#[serde(default)]
|
||||
pub User: KataUser,
|
||||
|
||||
/// Args specifies the binary and arguments for the application to execute.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub Args: Vec<String>,
|
||||
|
||||
/// Env populates the process environment for the process.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub Env: Vec<String>,
|
||||
|
||||
/// Cwd is the current working directory for the process and must be
|
||||
/// relative to the container's root.
|
||||
#[serde(default, skip_serializing_if = "String::is_empty")]
|
||||
pub Cwd: String,
|
||||
|
||||
/// Capabilities are Linux capabilities that are kept for the process.
|
||||
#[serde(default)]
|
||||
pub Capabilities: KataLinuxCapabilities,
|
||||
|
||||
/// NoNewPrivileges controls whether additional privileges could be gained by processes in the container.
|
||||
#[serde(default)]
|
||||
pub NoNewPrivileges: bool,
|
||||
}
|
||||
|
||||
/// OCI container User struct. This struct is very similar to the User
|
||||
/// struct generated from oci.proto. The main difference is that it preserves
|
||||
/// the upper case field names from oci.proto, for consistency with the structs
|
||||
/// used by agent's rpc.rs.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct KataUser {
|
||||
/// UID is the user id.
|
||||
pub UID: u32,
|
||||
|
||||
/// GID is the group id.
|
||||
pub GID: u32,
|
||||
|
||||
/// AdditionalGids are additional group ids set for the container's process.
|
||||
pub AdditionalGids: Vec<u32>,
|
||||
|
||||
/// Username is the user name.
|
||||
pub Username: String,
|
||||
}
|
||||
|
||||
/// OCI container Root struct. This struct is very similar to the Root
|
||||
/// struct generated from oci.proto. The main difference is that it preserves the
|
||||
/// upper case field names from oci.proto, for consistency with the structs used
|
||||
/// by agent's rpc.rs.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct KataRoot {
|
||||
/// Path is the absolute path to the container's root filesystem.
|
||||
pub Path: String,
|
||||
|
||||
/// Readonly makes the root filesystem for the container readonly before the process is executed.
|
||||
#[serde(default)]
|
||||
pub Readonly: bool,
|
||||
}
|
||||
|
||||
/// OCI container Linux struct. This struct is similar to the Linux struct
|
||||
/// generated from oci.proto, but includes just the fields that are currently
|
||||
/// relevant for automatic generation of policy.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct KataLinux {
|
||||
/// Namespaces contains the namespaces that are created and/or joined by the container
|
||||
#[serde(default)]
|
||||
pub Namespaces: Vec<KataLinuxNamespace>,
|
||||
|
||||
/// MaskedPaths masks over the provided paths inside the container.
|
||||
pub MaskedPaths: Vec<String>,
|
||||
|
||||
/// ReadonlyPaths sets the provided paths as RO inside the container.
|
||||
pub ReadonlyPaths: Vec<String>,
|
||||
}
|
||||
|
||||
/// OCI container LinuxNamespace struct. This struct is similar to the LinuxNamespace
|
||||
/// struct generated from oci.proto, but includes just the fields that are currently
|
||||
/// relevant for automatic generation of policy.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct KataLinuxNamespace {
|
||||
/// Type is the type of namespace
|
||||
pub Type: String,
|
||||
|
||||
/// Path is a path to an existing namespace persisted on disk that can be joined
|
||||
/// and is of the same type
|
||||
pub Path: String,
|
||||
}
|
||||
|
||||
/// OCI container LinuxCapabilities struct. This struct is very similar to the
|
||||
/// LinuxCapabilities struct generated from oci.proto. The main difference is
|
||||
/// that it preserves the upper case field names from oci.proto, for consistency
|
||||
/// with the structs used by agent's rpc.rs.
|
||||
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
|
||||
pub struct KataLinuxCapabilities {
|
||||
// Ambient is the ambient set of capabilities that are kept.
|
||||
pub Ambient: Vec<String>,
|
||||
|
||||
/// Bounding is the set of capabilities checked by the kernel.
|
||||
pub Bounding: Vec<String>,
|
||||
|
||||
/// Effective is the set of capabilities checked by the kernel.
|
||||
pub Effective: Vec<String>,
|
||||
|
||||
/// Inheritable is the capabilities preserved across execve.
|
||||
pub Inheritable: Vec<String>,
|
||||
|
||||
/// Permitted is the limiting superset for effective capabilities.
|
||||
pub Permitted: Vec<String>,
|
||||
}
|
||||
|
||||
/// OCI container Mount struct. This struct is very similar to the Mount
|
||||
/// struct generated from oci.proto. The main difference is that it preserves
|
||||
/// the field names from oci.proto, for consistency with the structs used by
|
||||
/// agent's rpc.rs.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
|
||||
pub struct KataMount {
|
||||
/// destination is the path inside the container expect when it starts with "tmp:/"
|
||||
pub destination: String,
|
||||
|
||||
/// source is the path inside the container expect when it starts with "vm:/dev/" or "tmp:/"
|
||||
/// the path which starts with "vm:/dev/" refers the guest vm's "/dev",
|
||||
/// especially, "vm:/dev/hostfs/" refers to the shared filesystem.
|
||||
/// "tmp:/" is a temporary directory which is used for temporary mounts.
|
||||
#[serde(default)]
|
||||
pub source: String,
|
||||
|
||||
pub type_: String,
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
/// Policy data for a container, included in the output of this app.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub struct ContainerPolicy {
|
||||
/// Data compared with req.OCI for CreateContainerRequest calls.
|
||||
pub OCI: KataSpec,
|
||||
|
||||
/// Data compared with req.storages for CreateContainerRequest calls.
|
||||
storages: Vec<agent::Storage>,
|
||||
|
||||
/// Allow list of ommand lines that are allowed to be executed using
|
||||
/// ExecProcessRequest. By default, all ExecProcessRequest calls are blocked
|
||||
/// by the policy.
|
||||
exec_commands: Vec<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Volumes {
|
||||
/// K8s EmptyDir Volume.
|
||||
pub emptyDir: Option<EmptyDirVolume>,
|
||||
|
||||
/// K8s PersistentVolumeClaim Volume.
|
||||
pub persistentVolumeClaim: Option<PersistentVolumeClaimVolume>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EmptyDirVolume {
|
||||
pub mount_type: String,
|
||||
pub mount_point: String,
|
||||
pub mount_source: String,
|
||||
pub driver: String,
|
||||
pub source: String,
|
||||
pub fstype: String,
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PersistentVolumeClaimVolume {
|
||||
pub mount_type: String,
|
||||
pub mount_source: String,
|
||||
}
|
||||
|
||||
/// CreateContainerRequest settings from genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CreateContainerRequestDefaults {
|
||||
/// Allow env variables that match any of these regexes.
|
||||
allow_env_regex: Vec<String>,
|
||||
}
|
||||
|
||||
/// ExecProcessRequest settings from genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ExecProcessRequestDefaults {
|
||||
/// Allow these commands to be executed.
|
||||
commands: Vec<String>,
|
||||
|
||||
/// Allow commands matching these regexes to be executed.
|
||||
regex: Vec<String>,
|
||||
}
|
||||
|
||||
/// Settings specific to each kata agent endpoint, loaded from
|
||||
/// genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct RequestDefaults {
|
||||
/// Settings for CreateContainerRequest.
|
||||
pub CreateContainerRequest: CreateContainerRequestDefaults,
|
||||
|
||||
/// Guest file paths matching these regular expressions can be copied by the Host.
|
||||
pub CopyFileRequest: Vec<String>,
|
||||
|
||||
/// Commands allowed to be executed by the Host in all Guest containers.
|
||||
pub ExecProcessRequest: ExecProcessRequestDefaults,
|
||||
|
||||
/// Allow Host reading from Guest containers stdout and stderr.
|
||||
pub ReadStreamRequest: bool,
|
||||
|
||||
/// Allow Host writing to Guest containers stdin.
|
||||
pub WriteStreamRequest: bool,
|
||||
}
|
||||
|
||||
/// Struct used to read data from the settings file and copy that data into the policy.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct CommonData {
|
||||
/// Path to the shared container files - e.g., "/run/kata-containers/shared/containers".
|
||||
pub cpath: String,
|
||||
|
||||
/// Regex prefix for shared file paths - e.g., "^$(cpath)/$(bundle-id)-[a-z0-9]{16}-".
|
||||
pub sfprefix: String,
|
||||
|
||||
/// Regex for an IPv4 address.
|
||||
pub ipv4_a: String,
|
||||
|
||||
/// Regex for an IP port number.
|
||||
pub ip_p: String,
|
||||
|
||||
/// Regex for a K8s service name.
|
||||
pub svc_name: String,
|
||||
|
||||
// Regex for a DNS label (e.g., host name).
|
||||
pub dns_label: String,
|
||||
|
||||
/// Default capabilities for a non-privileged container.
|
||||
pub default_caps: Vec<String>,
|
||||
|
||||
/// Default capabilities for a privileged container.
|
||||
pub privileged_caps: Vec<String>,
|
||||
}
|
||||
|
||||
impl AgentPolicy {
|
||||
pub async fn from_files(config: &utils::Config) -> Result<AgentPolicy> {
|
||||
let mut config_maps = Vec::new();
|
||||
let mut secrets = Vec::new();
|
||||
let mut resources = Vec::new();
|
||||
let yaml_contents = yaml::get_input_yaml(&config.yaml_file)?;
|
||||
|
||||
for document in serde_yaml::Deserializer::from_str(&yaml_contents) {
|
||||
let doc_mapping = Value::deserialize(document)?;
|
||||
let yaml_string = serde_yaml::to_string(&doc_mapping)?;
|
||||
|
||||
let silent = config.silent_unsupported_fields;
|
||||
let (mut resource, kind) = yaml::new_k8s_resource(&yaml_string, silent)?;
|
||||
resource.init(config.use_cache, &doc_mapping, silent).await;
|
||||
|
||||
// ConfigMap and Secret documents contain additional input for policy generation.
|
||||
if kind.eq("ConfigMap") {
|
||||
let config_map: config_map::ConfigMap = serde_yaml::from_str(&yaml_string)?;
|
||||
debug!("{:#?}", &config_map);
|
||||
config_maps.push(config_map);
|
||||
} else if kind.eq("Secret") {
|
||||
let secret: secret::Secret = serde_yaml::from_str(&yaml_string)?;
|
||||
debug!("{:#?}", &secret);
|
||||
secrets.push(secret);
|
||||
}
|
||||
|
||||
// Although copies of ConfigMap and Secret resources get created above,
|
||||
// those resources still have to be present in the resources vector, because
|
||||
// the elements of this vector will eventually be used to create the output
|
||||
// YAML file.
|
||||
resources.push(resource);
|
||||
}
|
||||
|
||||
let settings = settings::Settings::new(&config.settings_file);
|
||||
|
||||
if let Some(config_map_files) = &config.config_map_files {
|
||||
for file in config_map_files {
|
||||
config_maps.push(config_map::ConfigMap::new(&file)?);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(rules) = read_to_string(&config.rules_file) {
|
||||
Ok(AgentPolicy {
|
||||
resources,
|
||||
rules,
|
||||
settings,
|
||||
config_maps,
|
||||
secrets,
|
||||
config: config.clone(),
|
||||
})
|
||||
} else {
|
||||
panic!("Cannot open file {}. Please copy it to the current directory or specify the path to it using the -i parameter.",
|
||||
&config.rules_file);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn export_policy(&mut self) {
|
||||
let mut yaml_string = String::new();
|
||||
for i in 0..self.resources.len() {
|
||||
let policy = self.resources[i].generate_policy(self);
|
||||
if self.config.base64_out {
|
||||
println!("{}", policy);
|
||||
}
|
||||
yaml_string += &self.resources[i].serialize(&policy);
|
||||
}
|
||||
|
||||
if let Some(yaml_file) = &self.config.yaml_file {
|
||||
std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(yaml_file)
|
||||
.unwrap()
|
||||
.write_all(&yaml_string.as_bytes())
|
||||
.unwrap();
|
||||
} else {
|
||||
// When input YAML came through stdin, print the output YAML to stdout.
|
||||
std::io::stdout()
|
||||
.write_all(&yaml_string.as_bytes())
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_policy(&self, resource: &dyn yaml::K8sResource) -> String {
|
||||
let yaml_containers = resource.get_containers();
|
||||
let mut policy_containers = Vec::new();
|
||||
|
||||
for i in 0..yaml_containers.len() {
|
||||
policy_containers.push(self.get_container_policy(
|
||||
resource,
|
||||
&yaml_containers[i],
|
||||
i == 0,
|
||||
));
|
||||
}
|
||||
|
||||
let policy_data = policy::PolicyData {
|
||||
containers: policy_containers,
|
||||
request_defaults: self.settings.request_defaults.clone(),
|
||||
common: self.settings.common.clone(),
|
||||
};
|
||||
|
||||
let json_data = serde_json::to_string_pretty(&policy_data).unwrap();
|
||||
let policy = format!("{}\npolicy_data := {json_data}", &self.rules);
|
||||
if self.config.raw_out {
|
||||
std::io::stdout().write_all(policy.as_bytes()).unwrap();
|
||||
}
|
||||
general_purpose::STANDARD.encode(policy.as_bytes())
|
||||
}
|
||||
|
||||
pub fn get_container_policy(
|
||||
&self,
|
||||
resource: &dyn yaml::K8sResource,
|
||||
yaml_container: &pod::Container,
|
||||
is_pause_container: bool,
|
||||
) -> ContainerPolicy {
|
||||
let c_settings = self.settings.get_container_settings(is_pause_container);
|
||||
let mut root = c_settings.Root.clone();
|
||||
root.Readonly = yaml_container.read_only_root_filesystem();
|
||||
|
||||
let namespace = resource.get_namespace();
|
||||
let use_host_network = resource.use_host_network();
|
||||
let annotations = get_container_annotations(
|
||||
resource,
|
||||
yaml_container,
|
||||
is_pause_container,
|
||||
&namespace,
|
||||
c_settings,
|
||||
use_host_network,
|
||||
);
|
||||
|
||||
let is_privileged = yaml_container.is_privileged();
|
||||
let process = self.get_container_process(
|
||||
resource,
|
||||
yaml_container,
|
||||
is_pause_container,
|
||||
&namespace,
|
||||
c_settings,
|
||||
is_privileged,
|
||||
);
|
||||
|
||||
let mut mounts = containerd::get_mounts(is_pause_container, is_privileged);
|
||||
mount_and_storage::get_policy_mounts(
|
||||
&self.settings,
|
||||
&mut mounts,
|
||||
yaml_container,
|
||||
is_pause_container,
|
||||
);
|
||||
|
||||
let image_layers = yaml_container.registry.get_image_layers();
|
||||
let mut storages = Default::default();
|
||||
get_image_layer_storages(&mut storages, &image_layers, &root);
|
||||
resource.get_container_mounts_and_storages(
|
||||
&mut mounts,
|
||||
&mut storages,
|
||||
yaml_container,
|
||||
&self.settings,
|
||||
);
|
||||
|
||||
let mut linux = containerd::get_linux(is_privileged);
|
||||
linux.Namespaces = get_kata_namespaces(is_pause_container, use_host_network);
|
||||
|
||||
if !c_settings.Linux.MaskedPaths.is_empty() {
|
||||
linux.MaskedPaths = c_settings.Linux.MaskedPaths.clone();
|
||||
}
|
||||
if !c_settings.Linux.ReadonlyPaths.is_empty() {
|
||||
linux.ReadonlyPaths = c_settings.Linux.ReadonlyPaths.clone();
|
||||
}
|
||||
|
||||
let exec_commands = yaml_container.get_exec_commands();
|
||||
|
||||
ContainerPolicy {
|
||||
OCI: KataSpec {
|
||||
Version: version_default(),
|
||||
Process: process,
|
||||
Root: root,
|
||||
Mounts: mounts,
|
||||
Hooks: None,
|
||||
Annotations: annotations,
|
||||
Linux: linux,
|
||||
},
|
||||
storages,
|
||||
exec_commands,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_container_process(
|
||||
&self,
|
||||
resource: &dyn yaml::K8sResource,
|
||||
yaml_container: &pod::Container,
|
||||
is_pause_container: bool,
|
||||
namespace: &str,
|
||||
c_settings: &KataSpec,
|
||||
is_privileged: bool,
|
||||
) -> KataProcess {
|
||||
// Start with the Default Unix Spec from
|
||||
// https://github.com/containerd/containerd/blob/release/1.6/oci/spec.go#L132
|
||||
let mut process = containerd::get_process(is_privileged, &self.settings.common);
|
||||
|
||||
yaml_container.apply_capabilities(&mut process.Capabilities, &self.settings.common);
|
||||
|
||||
let (yaml_has_command, yaml_has_args) = yaml_container.get_process_args(&mut process.Args);
|
||||
yaml_container
|
||||
.registry
|
||||
.get_process(&mut process, yaml_has_command, yaml_has_args);
|
||||
|
||||
if let Some(tty) = yaml_container.tty {
|
||||
process.Terminal = tty;
|
||||
if tty && !is_pause_container {
|
||||
process.Env.push("TERM=xterm".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if !is_pause_container {
|
||||
process.Env.push("HOSTNAME=$(host-name)".to_string());
|
||||
}
|
||||
|
||||
let service_account_name = if let Some(s) = &yaml_container.serviceAccountName {
|
||||
s
|
||||
} else {
|
||||
"default"
|
||||
};
|
||||
|
||||
yaml_container.get_env_variables(
|
||||
&mut process.Env,
|
||||
&self.config_maps,
|
||||
&self.secrets,
|
||||
namespace,
|
||||
resource.get_annotations(),
|
||||
service_account_name,
|
||||
);
|
||||
|
||||
substitute_env_variables(&mut process.Env);
|
||||
substitute_args_env_variables(&mut process.Args, &process.Env);
|
||||
c_settings.get_process_fields(&mut process);
|
||||
process.NoNewPrivileges = !yaml_container.allow_privilege_escalation();
|
||||
|
||||
process
|
||||
}
|
||||
}
|
||||
|
||||
impl KataSpec {
|
||||
fn add_annotations(&self, annotations: &mut BTreeMap<String, String>) {
|
||||
for a in &self.Annotations {
|
||||
annotations.entry(a.0.clone()).or_insert(a.1.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn get_process_fields(&self, process: &mut KataProcess) {
|
||||
if process.User.UID == 0 {
|
||||
process.User.UID = self.Process.User.UID;
|
||||
}
|
||||
if process.User.GID == 0 {
|
||||
process.User.GID = self.Process.User.GID;
|
||||
}
|
||||
|
||||
process.User.AdditionalGids = self.Process.User.AdditionalGids.to_vec();
|
||||
process.User.Username = String::from(&self.Process.User.Username);
|
||||
add_missing_strings(&self.Process.Args, &mut process.Args);
|
||||
|
||||
add_missing_strings(&self.Process.Env, &mut process.Env);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_image_layer_storages(
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
image_layers: &Vec<registry::ImageLayer>,
|
||||
root: &KataRoot,
|
||||
) {
|
||||
let mut new_storages: Vec<agent::Storage> = Vec::new();
|
||||
let mut layer_names: Vec<String> = Vec::new();
|
||||
let mut layer_hashes: Vec<String> = Vec::new();
|
||||
let mut previous_chain_id = String::new();
|
||||
let layers_count = image_layers.len();
|
||||
let mut layer_index = layers_count;
|
||||
|
||||
for layer in image_layers {
|
||||
// See https://github.com/opencontainers/image-spec/blob/main/config.md#layer-chainid
|
||||
let chain_id = if previous_chain_id.is_empty() {
|
||||
layer.diff_id.clone()
|
||||
} else {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(format!("{previous_chain_id} {}", &layer.diff_id));
|
||||
format!("sha256:{:x}", hasher.finalize())
|
||||
};
|
||||
debug!(
|
||||
"previous_chain_id = {}, chain_id = {}",
|
||||
&previous_chain_id, &chain_id
|
||||
);
|
||||
previous_chain_id = chain_id.clone();
|
||||
|
||||
layer_names.push(name_to_hash(&chain_id));
|
||||
layer_hashes.push(layer.verity_hash.to_string());
|
||||
layer_index -= 1;
|
||||
|
||||
new_storages.push(agent::Storage {
|
||||
driver: "blk".to_string(),
|
||||
driver_options: Vec::new(),
|
||||
source: String::new(), // TODO
|
||||
fstype: "tar".to_string(),
|
||||
options: vec![format!("$(hash{layer_index})")],
|
||||
mount_point: format!("$(layer{layer_index})"),
|
||||
fs_group: protobuf::MessageField::none(),
|
||||
special_fields: ::protobuf::SpecialFields::new(),
|
||||
});
|
||||
}
|
||||
|
||||
new_storages.reverse();
|
||||
for storage in new_storages {
|
||||
storages.push(storage);
|
||||
}
|
||||
|
||||
layer_names.reverse();
|
||||
layer_hashes.reverse();
|
||||
|
||||
let overlay_storage = agent::Storage {
|
||||
driver: "overlayfs".to_string(),
|
||||
driver_options: Vec::new(),
|
||||
source: String::new(), // TODO
|
||||
fstype: "fuse3.kata-overlay".to_string(),
|
||||
options: vec![layer_names.join(":"), layer_hashes.join(":")],
|
||||
mount_point: root.Path.clone(),
|
||||
fs_group: protobuf::MessageField::none(),
|
||||
special_fields: ::protobuf::SpecialFields::new(),
|
||||
};
|
||||
|
||||
storages.push(overlay_storage);
|
||||
}
|
||||
|
||||
/// Converts the given name to a string representation of its sha256 hash.
|
||||
fn name_to_hash(name: &str) -> String {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(name);
|
||||
format!("{:x}", hasher.finalize())
|
||||
}
|
||||
|
||||
fn substitute_env_variables(env: &mut Vec<String>) {
|
||||
loop {
|
||||
let mut substituted = false;
|
||||
|
||||
for i in 0..env.len() {
|
||||
let components: Vec<&str> = env[i].split('=').collect();
|
||||
if components.len() == 2 {
|
||||
if let Some((start, end)) = find_subst_target(&components[1]) {
|
||||
if let Some(new_value) = substitute_variable(&components[1], start, end, env) {
|
||||
let new_var = format!("{}={new_value}", &components[0]);
|
||||
debug!("Replacing env variable <{}> with <{new_var}>", &env[i]);
|
||||
env[i] = new_var;
|
||||
substituted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !substituted {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_subst_target(env_value: &str) -> Option<(usize, usize)> {
|
||||
if let Some(mut start) = env_value.find("$(") {
|
||||
start += 2;
|
||||
if env_value.len() > start {
|
||||
if let Some(end) = env_value[start..].find(")") {
|
||||
return Some((start, start + end));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn substitute_variable(
|
||||
env_var: &str,
|
||||
name_start: usize,
|
||||
name_end: usize,
|
||||
env: &Vec<String>,
|
||||
) -> Option<String> {
|
||||
// Variables generated by this application.
|
||||
let internal_vars = vec![
|
||||
"bundle-id",
|
||||
"host-ip",
|
||||
"node-name",
|
||||
"pod-ip",
|
||||
"pod-uid",
|
||||
"sandbox-id",
|
||||
"sandbox-name",
|
||||
"sandbox-namespace",
|
||||
];
|
||||
|
||||
assert!(name_start < name_end);
|
||||
assert!(name_end < env_var.len());
|
||||
let name = env_var[name_start..name_end].to_string();
|
||||
debug!("Searching for the value of <{}>", &name);
|
||||
|
||||
for other_var in env {
|
||||
let components: Vec<&str> = other_var.split('=').collect();
|
||||
if components[0].eq(&name) {
|
||||
debug!("Found {} in <{}>", &name, &other_var);
|
||||
if components.len() == 2 {
|
||||
let mut replace = true;
|
||||
let value = &components[1];
|
||||
|
||||
if let Some((start, end)) = find_subst_target(value) {
|
||||
if internal_vars.contains(&&value[start..end]) {
|
||||
// Variables used internally for Policy don't get expanded
|
||||
// in the current design, so it's OK to use them as replacement
|
||||
// in other env variables or command arguments.
|
||||
} else {
|
||||
// Don't substitute if the value includes variables to be
|
||||
// substituted, to avoid circular substitutions.
|
||||
replace = false;
|
||||
}
|
||||
}
|
||||
|
||||
if replace {
|
||||
let from = format!("$({name})");
|
||||
return Some(env_var.replace(&from, value));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn substitute_args_env_variables(args: &mut Vec<String>, env: &Vec<String>) {
|
||||
for arg in args {
|
||||
substitute_arg_env_variables(arg, env);
|
||||
}
|
||||
}
|
||||
|
||||
fn substitute_arg_env_variables(arg: &mut String, env: &Vec<String>) {
|
||||
loop {
|
||||
let mut substituted = false;
|
||||
|
||||
if let Some((start, end)) = find_subst_target(arg) {
|
||||
if let Some(new_value) = substitute_variable(arg, start, end, env) {
|
||||
debug!(
|
||||
"substitute_arg_env_variables: replacing {} with {}",
|
||||
&arg[start..end],
|
||||
&new_value
|
||||
);
|
||||
*arg = new_value;
|
||||
substituted = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !substituted {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_container_annotations(
|
||||
resource: &dyn yaml::K8sResource,
|
||||
yaml_container: &pod::Container,
|
||||
is_pause_container: bool,
|
||||
namespace: &str,
|
||||
c_settings: &KataSpec,
|
||||
use_host_network: bool,
|
||||
) -> BTreeMap<String, String> {
|
||||
let mut annotations = if let Some(a) = resource.get_annotations() {
|
||||
let mut a_cloned = a.clone();
|
||||
yaml::remove_policy_annotation(&mut a_cloned);
|
||||
a_cloned
|
||||
} else {
|
||||
BTreeMap::new()
|
||||
};
|
||||
|
||||
c_settings.add_annotations(&mut annotations);
|
||||
|
||||
if let Some(name) = resource.get_sandbox_name() {
|
||||
annotations
|
||||
.entry("io.kubernetes.cri.sandbox-name".to_string())
|
||||
.or_insert(name);
|
||||
}
|
||||
|
||||
if !is_pause_container {
|
||||
let mut image_name = yaml_container.image.clone();
|
||||
if image_name.find(':').is_none() {
|
||||
image_name += ":latest";
|
||||
}
|
||||
annotations
|
||||
.entry("io.kubernetes.cri.image-name".to_string())
|
||||
.or_insert(image_name);
|
||||
}
|
||||
|
||||
annotations.insert(
|
||||
"io.kubernetes.cri.sandbox-namespace".to_string(),
|
||||
namespace.to_string(),
|
||||
);
|
||||
|
||||
if !yaml_container.name.is_empty() {
|
||||
annotations
|
||||
.entry("io.kubernetes.cri.container-name".to_string())
|
||||
.or_insert(yaml_container.name.clone());
|
||||
}
|
||||
|
||||
if is_pause_container {
|
||||
let mut network_namespace = "^/var/run/netns/cni".to_string();
|
||||
if use_host_network {
|
||||
network_namespace += "test";
|
||||
}
|
||||
network_namespace += "-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
||||
annotations
|
||||
.entry("nerdctl/network-namespace".to_string())
|
||||
.or_insert(network_namespace);
|
||||
}
|
||||
|
||||
annotations
|
||||
}
|
||||
|
||||
fn add_missing_strings(src: &Vec<String>, dest: &mut Vec<String>) {
|
||||
for src_string in src {
|
||||
if !dest.contains(src_string) {
|
||||
dest.push(src_string.clone());
|
||||
}
|
||||
}
|
||||
debug!("src = {:?}, dest = {:?}", src, dest)
|
||||
}
|
||||
|
||||
pub fn get_kata_namespaces(is_pause_container: bool, use_host_network: bool) -> Vec<KataLinuxNamespace> {
|
||||
let mut namespaces: Vec<KataLinuxNamespace> = vec![KataLinuxNamespace {
|
||||
Type: "ipc".to_string(),
|
||||
Path: "".to_string(),
|
||||
}];
|
||||
|
||||
if !is_pause_container || !use_host_network {
|
||||
namespaces.push(KataLinuxNamespace {
|
||||
Type: "uts".to_string(),
|
||||
Path: "".to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
namespaces.push(KataLinuxNamespace {
|
||||
Type: "mount".to_string(),
|
||||
Path: "".to_string(),
|
||||
});
|
||||
|
||||
namespaces
|
||||
}
|
464
src/tools/genpolicy/src/registry.rs
Normal file
464
src/tools/genpolicy/src/registry.rs
Normal file
@ -0,0 +1,464 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow Docker image config field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::policy;
|
||||
use crate::verity;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use docker_credential::{CredentialRetrievalError, DockerCredential};
|
||||
use log::warn;
|
||||
use log::{debug, info, LevelFilter};
|
||||
use oci_distribution::client::{linux_amd64_resolver, ClientConfig};
|
||||
use oci_distribution::{manifest, secrets::RegistryAuth, Client, Reference};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{digest::typenum::Unsigned, digest::OutputSizeUser, Sha256};
|
||||
use std::{io, io::Seek, io::Write, path::Path};
|
||||
use tokio::{fs, io::AsyncWriteExt};
|
||||
|
||||
/// Container image properties obtained from an OCI repository.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct Container {
|
||||
config_layer: DockerConfigLayer,
|
||||
image_layers: Vec<ImageLayer>,
|
||||
}
|
||||
|
||||
/// Image config layer properties.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
struct DockerConfigLayer {
|
||||
architecture: String,
|
||||
config: DockerImageConfig,
|
||||
rootfs: DockerRootfs,
|
||||
}
|
||||
|
||||
/// Image config properties.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
struct DockerImageConfig {
|
||||
User: Option<String>,
|
||||
Tty: Option<bool>,
|
||||
Env: Vec<String>,
|
||||
Cmd: Option<Vec<String>>,
|
||||
WorkingDir: Option<String>,
|
||||
Entrypoint: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Container rootfs information.
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
struct DockerRootfs {
|
||||
r#type: String,
|
||||
diff_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// This application's image layer properties.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ImageLayer {
|
||||
pub diff_id: String,
|
||||
pub verity_hash: String,
|
||||
}
|
||||
|
||||
impl Container {
|
||||
pub async fn new(use_cached_files: bool, image: &str) -> Result<Self> {
|
||||
info!("============================================");
|
||||
info!("Pulling manifest and config for {:?}", image);
|
||||
let reference: Reference = image.to_string().parse().unwrap();
|
||||
let auth = build_auth(&reference);
|
||||
|
||||
let mut client = Client::new(ClientConfig {
|
||||
platform_resolver: Some(Box::new(linux_amd64_resolver)),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
match client.pull_manifest_and_config(&reference, &auth).await {
|
||||
Ok((manifest, digest_hash, config_layer_str)) => {
|
||||
debug!("digest_hash: {:?}", digest_hash);
|
||||
debug!(
|
||||
"manifest: {}",
|
||||
serde_json::to_string_pretty(&manifest).unwrap()
|
||||
);
|
||||
|
||||
// Log the contents of the config layer.
|
||||
if log::max_level() >= LevelFilter::Debug {
|
||||
let mut deserializer = serde_json::Deserializer::from_str(&config_layer_str);
|
||||
let mut serializer = serde_json::Serializer::pretty(io::stderr());
|
||||
serde_transcode::transcode(&mut deserializer, &mut serializer).unwrap();
|
||||
}
|
||||
|
||||
let config_layer: DockerConfigLayer =
|
||||
serde_json::from_str(&config_layer_str).unwrap();
|
||||
let image_layers = get_image_layers(
|
||||
use_cached_files,
|
||||
&mut client,
|
||||
&reference,
|
||||
&manifest,
|
||||
&config_layer,
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
Ok(Container {
|
||||
config_layer,
|
||||
image_layers,
|
||||
})
|
||||
}
|
||||
Err(oci_distribution::errors::OciDistributionError::AuthenticationFailure(message)) => {
|
||||
panic!("Container image registry authentication failure ({}). Are docker credentials set-up for current user?", &message);
|
||||
}
|
||||
Err(e) => {
|
||||
panic!(
|
||||
"Failed to pull container image manifest and config - error: {:#?}",
|
||||
&e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Docker image config to policy data.
|
||||
pub fn get_process(
|
||||
&self,
|
||||
process: &mut policy::KataProcess,
|
||||
yaml_has_command: bool,
|
||||
yaml_has_args: bool,
|
||||
) {
|
||||
debug!("Getting process field from docker config layer...");
|
||||
let docker_config = &self.config_layer.config;
|
||||
|
||||
if let Some(image_user) = &docker_config.User {
|
||||
if !image_user.is_empty() {
|
||||
debug!("Splitting Docker config user = {:?}", image_user);
|
||||
let user: Vec<&str> = image_user.split(':').collect();
|
||||
if !user.is_empty() {
|
||||
debug!("Parsing uid from user[0] = {}", &user[0]);
|
||||
match user[0].parse() {
|
||||
Ok(id) => process.User.UID = id,
|
||||
Err(e) => {
|
||||
// "image: prom/prometheus" has user = "nobody", but
|
||||
// process.User.UID is an u32 value.
|
||||
warn!(
|
||||
"Failed to parse {} as u32, using uid = 0 - error {e}",
|
||||
&user[0]
|
||||
);
|
||||
process.User.UID = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
if user.len() > 1 {
|
||||
debug!("Parsing gid from user[1] = {:?}", user[1]);
|
||||
process.User.GID = user[1].parse().unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(terminal) = docker_config.Tty {
|
||||
process.Terminal = terminal;
|
||||
} else {
|
||||
process.Terminal = false;
|
||||
}
|
||||
|
||||
for env in &docker_config.Env {
|
||||
process.Env.push(env.clone());
|
||||
}
|
||||
|
||||
let policy_args = &mut process.Args;
|
||||
debug!("Already existing policy args: {:?}", policy_args);
|
||||
|
||||
if let Some(entry_points) = &docker_config.Entrypoint {
|
||||
debug!("Image Entrypoint: {:?}", entry_points);
|
||||
if !yaml_has_command {
|
||||
debug!("Inserting Entrypoint into policy args");
|
||||
|
||||
let mut reversed_entry_points = entry_points.clone();
|
||||
reversed_entry_points.reverse();
|
||||
|
||||
for entry_point in reversed_entry_points {
|
||||
policy_args.insert(0, entry_point.clone());
|
||||
}
|
||||
} else {
|
||||
debug!("Ignoring image Entrypoint because YAML specified the container command");
|
||||
}
|
||||
} else {
|
||||
debug!("No image Entrypoint");
|
||||
}
|
||||
|
||||
debug!("Updated policy args: {:?}", policy_args);
|
||||
|
||||
if yaml_has_command {
|
||||
debug!("Ignoring image Cmd because YAML specified the container command");
|
||||
} else if yaml_has_args {
|
||||
debug!("Ignoring image Cmd because YAML specified the container args");
|
||||
} else if let Some(commands) = &docker_config.Cmd {
|
||||
debug!("Adding to policy args the image Cmd: {:?}", commands);
|
||||
|
||||
for cmd in commands {
|
||||
policy_args.push(cmd.clone());
|
||||
}
|
||||
} else {
|
||||
debug!("Image Cmd field is not present");
|
||||
}
|
||||
|
||||
debug!("Updated policy args: {:?}", policy_args);
|
||||
|
||||
if let Some(working_dir) = &docker_config.WorkingDir {
|
||||
if !working_dir.is_empty() {
|
||||
process.Cwd = working_dir.clone();
|
||||
}
|
||||
}
|
||||
|
||||
debug!("get_process succeeded.");
|
||||
}
|
||||
|
||||
pub fn get_image_layers(&self) -> Vec<ImageLayer> {
|
||||
self.image_layers.clone()
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_image_layers(
|
||||
use_cached_files: bool,
|
||||
client: &mut Client,
|
||||
reference: &Reference,
|
||||
manifest: &manifest::OciImageManifest,
|
||||
config_layer: &DockerConfigLayer,
|
||||
) -> Result<Vec<ImageLayer>> {
|
||||
let mut layer_index = 0;
|
||||
let mut layers = Vec::new();
|
||||
|
||||
for layer in &manifest.layers {
|
||||
if layer
|
||||
.media_type
|
||||
.eq(manifest::IMAGE_DOCKER_LAYER_GZIP_MEDIA_TYPE)
|
||||
{
|
||||
if layer_index < config_layer.rootfs.diff_ids.len() {
|
||||
layers.push(ImageLayer {
|
||||
diff_id: config_layer.rootfs.diff_ids[layer_index].clone(),
|
||||
verity_hash: get_verity_hash(
|
||||
use_cached_files,
|
||||
client,
|
||||
reference,
|
||||
&layer.digest,
|
||||
)
|
||||
.await?,
|
||||
});
|
||||
} else {
|
||||
return Err(anyhow!("Too many Docker gzip layers"));
|
||||
}
|
||||
|
||||
layer_index += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(layers)
|
||||
}
|
||||
|
||||
fn delete_files(decompressed_path: &Path, compressed_path: &Path, verity_path: &Path) {
|
||||
let _ = fs::remove_file(&decompressed_path);
|
||||
let _ = fs::remove_file(&compressed_path);
|
||||
let _ = fs::remove_file(&verity_path);
|
||||
}
|
||||
|
||||
async fn get_verity_hash(
|
||||
use_cached_files: bool,
|
||||
client: &mut Client,
|
||||
reference: &Reference,
|
||||
layer_digest: &str,
|
||||
) -> Result<String> {
|
||||
let base_dir = std::path::Path::new("layers_cache");
|
||||
|
||||
// Use file names supported by both Linux and Windows.
|
||||
let file_name = str::replace(&layer_digest, ":", "-");
|
||||
|
||||
let mut decompressed_path = base_dir.join(file_name);
|
||||
decompressed_path.set_extension("tar");
|
||||
|
||||
let mut compressed_path = decompressed_path.clone();
|
||||
compressed_path.set_extension("gz");
|
||||
|
||||
let mut verity_path = decompressed_path.clone();
|
||||
verity_path.set_extension("verity");
|
||||
|
||||
let mut verity_hash = "".to_string();
|
||||
let mut error_message = "".to_string();
|
||||
let mut error = false;
|
||||
|
||||
if use_cached_files && verity_path.exists() {
|
||||
info!("Using cached file {:?}", &verity_path);
|
||||
} else if let Err(e) = create_verity_hash_file(
|
||||
use_cached_files,
|
||||
client,
|
||||
reference,
|
||||
layer_digest,
|
||||
&base_dir,
|
||||
&decompressed_path,
|
||||
&compressed_path,
|
||||
&verity_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error = true;
|
||||
error_message = format!("Failed to create verity hash for {layer_digest}, error {e}");
|
||||
}
|
||||
|
||||
if !error {
|
||||
match std::fs::read_to_string(&verity_path) {
|
||||
Err(e) => {
|
||||
error = true;
|
||||
error_message = format!("Failed to read {:?}, error {e}", &verity_path);
|
||||
}
|
||||
Ok(v) => {
|
||||
verity_hash = v;
|
||||
info!("dm-verity root hash: {verity_hash}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !use_cached_files {
|
||||
let _ = std::fs::remove_dir_all(&base_dir);
|
||||
} else if error {
|
||||
delete_files(&decompressed_path, &compressed_path, &verity_path);
|
||||
}
|
||||
|
||||
if error {
|
||||
panic!("{error_message}");
|
||||
} else {
|
||||
Ok(verity_hash)
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_verity_hash_file(
|
||||
use_cached_files: bool,
|
||||
client: &mut Client,
|
||||
reference: &Reference,
|
||||
layer_digest: &str,
|
||||
base_dir: &Path,
|
||||
decompressed_path: &Path,
|
||||
compressed_path: &Path,
|
||||
verity_path: &Path,
|
||||
) -> Result<()> {
|
||||
if use_cached_files && decompressed_path.exists() {
|
||||
info!("Using cached file {:?}", &decompressed_path);
|
||||
} else {
|
||||
std::fs::create_dir_all(&base_dir)?;
|
||||
|
||||
create_decompressed_layer_file(
|
||||
use_cached_files,
|
||||
client,
|
||||
reference,
|
||||
layer_digest,
|
||||
&decompressed_path,
|
||||
&compressed_path,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
do_create_verity_hash_file(decompressed_path, verity_path)
|
||||
}
|
||||
|
||||
async fn create_decompressed_layer_file(
|
||||
use_cached_files: bool,
|
||||
client: &mut Client,
|
||||
reference: &Reference,
|
||||
layer_digest: &str,
|
||||
decompressed_path: &Path,
|
||||
compressed_path: &Path,
|
||||
) -> Result<()> {
|
||||
if use_cached_files && compressed_path.exists() {
|
||||
info!("Using cached file {:?}", &compressed_path);
|
||||
} else {
|
||||
info!("Pulling layer {layer_digest}");
|
||||
let mut file = tokio::fs::File::create(&compressed_path)
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
client
|
||||
.pull_blob(&reference, layer_digest, &mut file)
|
||||
.await
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
file.flush().await.map_err(|e| anyhow!(e))?;
|
||||
}
|
||||
|
||||
info!("Decompressing layer");
|
||||
let compressed_file = std::fs::File::open(&compressed_path).map_err(|e| anyhow!(e))?;
|
||||
let mut decompressed_file = std::fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(&decompressed_path)?;
|
||||
let mut gz_decoder = flate2::read::GzDecoder::new(compressed_file);
|
||||
std::io::copy(&mut gz_decoder, &mut decompressed_file).map_err(|e| anyhow!(e))?;
|
||||
|
||||
info!("Adding tarfs index to layer");
|
||||
decompressed_file.seek(std::io::SeekFrom::Start(0))?;
|
||||
tarindex::append_index(&mut decompressed_file).map_err(|e| anyhow!(e))?;
|
||||
decompressed_file.flush().map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_create_verity_hash_file(path: &Path, verity_path: &Path) -> Result<()> {
|
||||
info!("Calculating dm-verity root hash");
|
||||
let mut file = std::fs::File::open(path)?;
|
||||
let size = file.seek(std::io::SeekFrom::End(0))?;
|
||||
if size < 4096 {
|
||||
return Err(anyhow!("Block device {:?} is too small: {size}", &path));
|
||||
}
|
||||
|
||||
let salt = [0u8; <Sha256 as OutputSizeUser>::OutputSize::USIZE];
|
||||
let v = verity::Verity::<Sha256>::new(size, 4096, 4096, &salt, 0)?;
|
||||
let hash = verity::traverse_file(&mut file, 0, false, v, &mut verity::no_write)?;
|
||||
let result = format!("{:x}", hash);
|
||||
|
||||
let mut verity_file = std::fs::File::create(verity_path).map_err(|e| anyhow!(e))?;
|
||||
verity_file
|
||||
.write_all(result.as_bytes())
|
||||
.map_err(|e| anyhow!(e))?;
|
||||
verity_file.flush().map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_container(use_cache: bool, image: &str) -> Result<Container> {
|
||||
Container::new(use_cache, image).await
|
||||
}
|
||||
|
||||
fn build_auth(reference: &Reference) -> RegistryAuth {
|
||||
debug!("build_auth: {:?}", reference);
|
||||
|
||||
let server = reference
|
||||
.resolve_registry()
|
||||
.strip_suffix("/")
|
||||
.unwrap_or_else(|| reference.resolve_registry());
|
||||
|
||||
match docker_credential::get_credential(server) {
|
||||
Ok(DockerCredential::UsernamePassword(username, password)) => {
|
||||
debug!("build_auth: Found docker credentials");
|
||||
return RegistryAuth::Basic(username, password);
|
||||
}
|
||||
Ok(DockerCredential::IdentityToken(_)) => {
|
||||
warn!("build_auth: Cannot use contents of docker config, identity token not supported. Using anonymous access.");
|
||||
}
|
||||
Err(CredentialRetrievalError::ConfigNotFound) => {
|
||||
debug!("build_auth: Docker config not found - using anonymous access.");
|
||||
}
|
||||
Err(CredentialRetrievalError::NoCredentialConfigured) => {
|
||||
debug!("build_auth: Docker credentials not configured - using anonymous access.");
|
||||
}
|
||||
Err(CredentialRetrievalError::ConfigReadError) => {
|
||||
debug!("build_auth: Cannot read docker credentials - using anonymous access.");
|
||||
}
|
||||
Err(CredentialRetrievalError::HelperFailure { stdout, stderr }) => {
|
||||
if stdout == "credentials not found in native keychain\n" {
|
||||
// On WSL, this error is generated when credentials are not
|
||||
// available in ~/.docker/config.json.
|
||||
debug!("build_auth: Docker credentials not found - using anonymous access.");
|
||||
} else {
|
||||
warn!("build_auth: Docker credentials not found - using anonymous access. stderr = {}, stdout = {}",
|
||||
&stderr, &stdout);
|
||||
}
|
||||
}
|
||||
Err(e) => panic!("Error handling docker configuration file: {}", e),
|
||||
}
|
||||
|
||||
RegistryAuth::Anonymous
|
||||
}
|
114
src/tools/genpolicy/src/secret.rs
Normal file
114
src/tools/genpolicy/src/secret.rs
Normal file
@ -0,0 +1,114 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::obj_meta;
|
||||
use crate::pod;
|
||||
use crate::policy;
|
||||
use crate::settings;
|
||||
use crate::yaml;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use base64::{engine::general_purpose, Engine as _};
|
||||
use protocols::agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Secret.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Secret {
|
||||
#[serde(skip)]
|
||||
doc_mapping: serde_yaml::Value,
|
||||
|
||||
apiVersion: String,
|
||||
kind: String,
|
||||
metadata: obj_meta::ObjectMeta,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
data: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
immutable: Option<bool>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
impl Secret {
|
||||
pub fn get_value(&self, value_from: &pod::EnvVarSource) -> Option<String> {
|
||||
if let Some(key_ref) = &value_from.secretKeyRef {
|
||||
if let Some(name) = &key_ref.name {
|
||||
if let Some(my_name) = &self.metadata.name {
|
||||
if my_name.eq(name) {
|
||||
if let Some(data) = &self.data {
|
||||
if let Some(value) = data.get(&key_ref.key) {
|
||||
let value_bytes = general_purpose::STANDARD.decode(&value).unwrap();
|
||||
let value_string = std::str::from_utf8(&value_bytes).unwrap();
|
||||
return Some(value_string.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_value(value_from: &pod::EnvVarSource, secrets: &Vec<Secret>) -> Option<String> {
|
||||
for secret in secrets {
|
||||
if let Some(value) = secret.get_value(value_from) {
|
||||
return Some(value);
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl yaml::K8sResource for Secret {
|
||||
async fn init(&mut self, _use_cache: bool, doc_mapping: &serde_yaml::Value, _silent: bool) {
|
||||
self.doc_mapping = doc_mapping.clone();
|
||||
}
|
||||
|
||||
fn get_sandbox_name(&self) -> Option<String> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_namespace(&self) -> String {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_container_mounts_and_storages(
|
||||
&self,
|
||||
_policy_mounts: &mut Vec<policy::KataMount>,
|
||||
_storages: &mut Vec<agent::Storage>,
|
||||
_container: &pod::Container,
|
||||
_settings: &settings::Settings,
|
||||
) {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn generate_policy(&self, _agent_policy: &policy::AgentPolicy) -> String {
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
fn serialize(&mut self, _policy: &str) -> String {
|
||||
serde_yaml::to_string(&self.doc_mapping).unwrap()
|
||||
}
|
||||
|
||||
fn get_containers(&self) -> &Vec<pod::Container> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn get_annotations(&self) -> &Option<BTreeMap<String, String>> {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
|
||||
fn use_host_network(&self) -> bool {
|
||||
panic!("Unsupported");
|
||||
}
|
||||
}
|
87
src/tools/genpolicy/src/settings.rs
Normal file
87
src/tools/genpolicy/src/settings.rs
Normal file
@ -0,0 +1,87 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow OCI spec field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::policy;
|
||||
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs::File;
|
||||
use std::str;
|
||||
|
||||
/// Policy settings loaded from genpolicy-settings.json.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Settings {
|
||||
pub pause_container: policy::KataSpec,
|
||||
pub other_container: policy::KataSpec,
|
||||
pub volumes: Volumes,
|
||||
pub kata_config: KataConfig,
|
||||
pub request_defaults: policy::RequestDefaults,
|
||||
pub common: policy::CommonData,
|
||||
pub mount_destinations: Vec<String>,
|
||||
}
|
||||
|
||||
/// Volume settings loaded from genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Volumes {
|
||||
pub emptyDir: EmptyDirVolume,
|
||||
pub emptyDir_memory: EmptyDirVolume,
|
||||
pub configMap: ConfigMapVolume,
|
||||
pub confidential_configMap: ConfigMapVolume,
|
||||
}
|
||||
|
||||
/// EmptyDir volume settings loaded from genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EmptyDirVolume {
|
||||
pub mount_type: String,
|
||||
pub mount_source: String,
|
||||
pub mount_point: String,
|
||||
pub driver: String,
|
||||
pub fstype: String,
|
||||
pub options: Vec<String>,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
/// ConfigMap volume settings loaded from genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigMapVolume {
|
||||
pub mount_type: String,
|
||||
pub mount_source: String,
|
||||
pub mount_point: String,
|
||||
pub driver: String,
|
||||
pub fstype: String,
|
||||
pub options: Vec<String>,
|
||||
}
|
||||
|
||||
/// Data corresponding to the kata runtime config file data, loaded from
|
||||
/// genpolicy-settings.json.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct KataConfig {
|
||||
pub confidential_guest: bool,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub fn new(settings_file: &str) -> Self {
|
||||
debug!("Loading settings file...");
|
||||
if let Ok(file) = File::open(settings_file) {
|
||||
let settings: Self = serde_json::from_reader(file).unwrap();
|
||||
debug!("settings = {:?}", &settings);
|
||||
settings
|
||||
} else {
|
||||
panic!("Cannot open file {}. Please copy it to the current directory or specify the path to it using the -i parameter.",
|
||||
settings_file);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_container_settings(&self, is_pause_container: bool) -> &policy::KataSpec {
|
||||
if is_pause_container {
|
||||
&self.pause_container
|
||||
} else {
|
||||
&self.other_container
|
||||
}
|
||||
}
|
||||
}
|
57
src/tools/genpolicy/src/utils.rs
Normal file
57
src/tools/genpolicy/src/utils.rs
Normal file
@ -0,0 +1,57 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use log::debug;
|
||||
|
||||
/// Application configuration, derived from on command line parameters.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Config {
|
||||
pub use_cache: bool,
|
||||
|
||||
pub yaml_file: Option<String>,
|
||||
pub rules_file: String,
|
||||
pub settings_file: String,
|
||||
pub config_map_files: Option<Vec<String>>,
|
||||
|
||||
pub silent_unsupported_fields: bool,
|
||||
pub raw_out: bool,
|
||||
pub base64_out: bool,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(
|
||||
use_cache: bool,
|
||||
yaml_file: Option<String>,
|
||||
input_files_path: &str,
|
||||
settings_file_name: &str,
|
||||
config_map_files: &Vec<String>,
|
||||
silent_unsupported_fields: bool,
|
||||
raw_out: bool,
|
||||
base64_out: bool,
|
||||
) -> Self {
|
||||
let rules_file = format!("{input_files_path}/rules.rego");
|
||||
debug!("Rules file: {rules_file}");
|
||||
|
||||
let settings_file = format!("{input_files_path}/{settings_file_name}");
|
||||
debug!("Settings file: {settings_file}");
|
||||
|
||||
let cm_files = if !config_map_files.is_empty() {
|
||||
Some(config_map_files.clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
use_cache,
|
||||
yaml_file,
|
||||
rules_file,
|
||||
settings_file,
|
||||
config_map_files: cm_files,
|
||||
silent_unsupported_fields,
|
||||
raw_out,
|
||||
base64_out,
|
||||
}
|
||||
}
|
||||
}
|
225
src/tools/genpolicy/src/verity.rs
Normal file
225
src/tools/genpolicy/src/verity.rs
Normal file
@ -0,0 +1,225 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
use generic_array::{typenum::Unsigned, GenericArray};
|
||||
use sha2::Digest;
|
||||
use std::fs::File;
|
||||
use std::io::{self, Read, Seek, SeekFrom};
|
||||
use zerocopy::byteorder::{LE, U32, U64};
|
||||
use zerocopy::AsBytes;
|
||||
|
||||
#[derive(Default, zerocopy::AsBytes, zerocopy::FromBytes, zerocopy::Unaligned)]
|
||||
#[repr(C)]
|
||||
pub struct SuperBlock {
|
||||
pub data_block_size: U32<LE>,
|
||||
pub hash_block_size: U32<LE>,
|
||||
pub data_block_count: U64<LE>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Level {
|
||||
next_index: usize,
|
||||
file_offset: u64,
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Verity<T: Digest + Clone> {
|
||||
levels: Vec<Level>,
|
||||
seeded: T,
|
||||
data_block_size: usize,
|
||||
hash_block_size: usize,
|
||||
block_remaining_count: u64,
|
||||
super_block: SuperBlock,
|
||||
}
|
||||
|
||||
impl<T: Digest + Clone> Verity<T> {
|
||||
const HASH_SIZE: usize = T::OutputSize::USIZE;
|
||||
|
||||
/// Creates a new `Verity` instance.
|
||||
pub fn new(
|
||||
data_size: u64,
|
||||
data_block_size: usize,
|
||||
hash_block_size: usize,
|
||||
salt: &[u8],
|
||||
mut write_file_offset: u64,
|
||||
) -> io::Result<Self> {
|
||||
let level_count = {
|
||||
let mut max_size = data_block_size as u64;
|
||||
let mut count = 0usize;
|
||||
|
||||
while max_size < data_size {
|
||||
count += 1;
|
||||
max_size *= (hash_block_size / Self::HASH_SIZE) as u64;
|
||||
}
|
||||
count
|
||||
};
|
||||
|
||||
let mut data = Vec::new();
|
||||
data.resize(hash_block_size, 0);
|
||||
|
||||
let mut levels = Vec::new();
|
||||
levels.resize(
|
||||
level_count,
|
||||
Level {
|
||||
next_index: 0,
|
||||
file_offset: 0,
|
||||
data,
|
||||
},
|
||||
);
|
||||
|
||||
for (i, l) in levels.iter_mut().enumerate() {
|
||||
let entry_size = (data_block_size as u64)
|
||||
* ((hash_block_size / Self::HASH_SIZE) as u64).pow(level_count as u32 - i as u32);
|
||||
let count = (data_size + entry_size - 1) / entry_size;
|
||||
l.file_offset = write_file_offset;
|
||||
write_file_offset += hash_block_size as u64 * count;
|
||||
}
|
||||
|
||||
let block_count = data_size / (data_block_size as u64);
|
||||
Ok(Self {
|
||||
levels,
|
||||
seeded: T::new_with_prefix(salt),
|
||||
data_block_size,
|
||||
block_remaining_count: block_count,
|
||||
hash_block_size,
|
||||
super_block: SuperBlock {
|
||||
data_block_size: (data_block_size as u32).into(),
|
||||
hash_block_size: (hash_block_size as u32).into(),
|
||||
data_block_count: block_count.into(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/// Determines if more blocks are expected.
|
||||
///
|
||||
/// This is based on file size specified when this instance was created.
|
||||
fn more_blocks(&self) -> bool {
|
||||
self.block_remaining_count > 0
|
||||
}
|
||||
|
||||
/// Adds the given hash to the level.
|
||||
///
|
||||
/// Returns `true` is the level is now full; `false` is there is still room for more hashes.
|
||||
fn add_hash(&mut self, l: usize, hash: &[u8]) -> bool {
|
||||
let level = &mut self.levels[l];
|
||||
level.data[level.next_index * Self::HASH_SIZE..][..Self::HASH_SIZE].copy_from_slice(hash);
|
||||
level.next_index += 1;
|
||||
level.next_index >= self.hash_block_size / Self::HASH_SIZE
|
||||
}
|
||||
|
||||
/// Finalises the level despite potentially not having filled it.
|
||||
///
|
||||
/// It zeroes out the remaining bytes of the level so that its hash can be calculated
|
||||
/// consistently.
|
||||
fn finalize_level(&mut self, l: usize) {
|
||||
let level = &mut self.levels[l];
|
||||
for b in &mut level.data[level.next_index * Self::HASH_SIZE..] {
|
||||
*b = 0;
|
||||
}
|
||||
level.next_index = 0;
|
||||
}
|
||||
|
||||
fn uplevel<F>(&mut self, l: usize, reader: &mut File, writer: &mut F) -> io::Result<bool>
|
||||
where
|
||||
F: FnMut(&mut File, &[u8], u64) -> io::Result<()>,
|
||||
{
|
||||
self.finalize_level(l);
|
||||
writer(reader, &self.levels[l].data, self.levels[l].file_offset)?;
|
||||
self.levels[l].file_offset += self.hash_block_size as u64;
|
||||
let h = self.digest(&self.levels[l].data);
|
||||
Ok(self.add_hash(l - 1, h.as_slice()))
|
||||
}
|
||||
|
||||
fn digest(&self, block: &[u8]) -> GenericArray<u8, T::OutputSize> {
|
||||
let mut hasher = self.seeded.clone();
|
||||
hasher.update(block);
|
||||
hasher.finalize()
|
||||
}
|
||||
|
||||
fn add_block<F>(&mut self, b: &[u8], reader: &mut File, writer: &mut F) -> io::Result<()>
|
||||
where
|
||||
F: FnMut(&mut File, &[u8], u64) -> io::Result<()>,
|
||||
{
|
||||
if self.block_remaining_count == 0 {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"unexpected block",
|
||||
));
|
||||
}
|
||||
|
||||
self.block_remaining_count -= 1;
|
||||
|
||||
let count = self.levels.len();
|
||||
let hash = self.digest(b);
|
||||
if self.add_hash(count - 1, hash.as_slice()) {
|
||||
// Go up the levels as far as it can.
|
||||
for l in (1..count).rev() {
|
||||
if !self.uplevel(l, reader, writer)? {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn finalize(
|
||||
mut self,
|
||||
write_superblock: bool,
|
||||
reader: &mut File,
|
||||
writer: &mut impl FnMut(&mut File, &[u8], u64) -> io::Result<()>,
|
||||
) -> io::Result<GenericArray<u8, T::OutputSize>> {
|
||||
let len = self.levels.len();
|
||||
for mut l in (1..len).rev() {
|
||||
if self.levels[l].next_index != 0 {
|
||||
while l > 0 {
|
||||
self.uplevel(l, reader, writer)?;
|
||||
l -= 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.finalize_level(0);
|
||||
|
||||
writer(reader, &self.levels[0].data, self.levels[0].file_offset)?;
|
||||
self.levels[0].file_offset += self.hash_block_size as u64;
|
||||
|
||||
if write_superblock {
|
||||
writer(
|
||||
reader,
|
||||
self.super_block.as_bytes(),
|
||||
self.levels[len - 1].file_offset + 4096 - 512,
|
||||
)?;
|
||||
|
||||
// TODO: Align to the hash_block_size...
|
||||
// Align to 4096 bytes.
|
||||
writer(reader, &[0u8], self.levels[len - 1].file_offset + 4095)?;
|
||||
}
|
||||
|
||||
Ok(self.digest(&self.levels[0].data))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn traverse_file<T: Digest + Clone>(
|
||||
file: &mut File,
|
||||
mut read_offset: u64,
|
||||
write_superblock: bool,
|
||||
mut verity: Verity<T>,
|
||||
writer: &mut impl FnMut(&mut File, &[u8], u64) -> io::Result<()>,
|
||||
) -> io::Result<GenericArray<u8, T::OutputSize>> {
|
||||
let mut buf = Vec::new();
|
||||
buf.resize(verity.data_block_size, 0);
|
||||
while verity.more_blocks() {
|
||||
file.seek(SeekFrom::Start(read_offset))?;
|
||||
file.read_exact(&mut buf)?;
|
||||
verity.add_block(&buf, file, writer)?;
|
||||
read_offset += verity.data_block_size as u64;
|
||||
}
|
||||
verity.finalize(write_superblock, file, writer)
|
||||
}
|
||||
|
||||
pub fn no_write(_: &mut File, _: &[u8], _: u64) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
131
src/tools/genpolicy/src/volume.rs
Normal file
131
src/tools/genpolicy/src/volume.rs
Normal file
@ -0,0 +1,131 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::pod;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Volume {
|
||||
pub name: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub emptyDir: Option<EmptyDirVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub hostPath: Option<HostPathVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub persistentVolumeClaim: Option<PersistentVolumeClaimVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub configMap: Option<ConfigMapVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub azureFile: Option<AzureFileVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub projected: Option<ProjectedVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub secret: Option<SecretVolumeSource>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub downwardAPI: Option<DownwardAPIVolumeSource>
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct HostPathVolumeSource {
|
||||
pub path: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub r#type: Option<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct EmptyDirVolumeSource {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub medium: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub sizeLimit: Option<String>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct PersistentVolumeClaimVolumeSource {
|
||||
pub claimName: String,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ConfigMapVolumeSource {
|
||||
pub name: String,
|
||||
pub items: Option<Vec<KeyToPath>>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct KeyToPath {
|
||||
pub key: String,
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct AzureFileVolumeSource {
|
||||
pub secretName: String,
|
||||
pub shareName: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub readOnly: Option<bool>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ProjectedVolumeSource {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub defaultMode: Option<i32>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct SecretVolumeSource {
|
||||
secretName: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub defaultMode: Option<i32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Vec<KeyToPath>>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DownwardAPIVolumeSource {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub items: Option<Vec<DownwardAPIVolumeFile>>,
|
||||
// TODO: additional fields.
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Config and Storage Resources / Volume.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct DownwardAPIVolumeFile {
|
||||
pub path: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub fieldRef: Option<pod::ObjectFieldSelector>,
|
||||
}
|
242
src/tools/genpolicy/src/yaml.rs
Normal file
242
src/tools/genpolicy/src/yaml.rs
Normal file
@ -0,0 +1,242 @@
|
||||
// Copyright (c) 2023 Microsoft Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
//
|
||||
|
||||
// Allow K8s YAML field names.
|
||||
#![allow(non_snake_case)]
|
||||
|
||||
use crate::config_map;
|
||||
use crate::mount_and_storage;
|
||||
use crate::no_policy;
|
||||
use crate::pod;
|
||||
use crate::policy;
|
||||
use crate::secret;
|
||||
use crate::settings;
|
||||
use crate::volume;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use core::fmt::Debug;
|
||||
use log::debug;
|
||||
use protocols::agent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_yaml;
|
||||
use std::boxed;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fs::read_to_string;
|
||||
|
||||
/// K8s API version and resource type.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct YamlHeader {
|
||||
pub apiVersion: String,
|
||||
pub kind: String,
|
||||
}
|
||||
|
||||
/// Trait implemented by each supportes K8s resource type (e.g., Pod or Deployment).
|
||||
#[async_trait]
|
||||
pub trait K8sResource {
|
||||
async fn init(
|
||||
&mut self,
|
||||
use_cache: bool,
|
||||
doc_mapping: &serde_yaml::Value,
|
||||
silent_unsupported_fields: bool,
|
||||
);
|
||||
|
||||
fn generate_policy(&self, agent_policy: &policy::AgentPolicy) -> String;
|
||||
fn serialize(&mut self, policy: &str) -> String;
|
||||
|
||||
fn get_sandbox_name(&self) -> Option<String>;
|
||||
fn get_namespace(&self) -> String;
|
||||
|
||||
fn get_container_mounts_and_storages(
|
||||
&self,
|
||||
policy_mounts: &mut Vec<policy::KataMount>,
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
container: &pod::Container,
|
||||
settings: &settings::Settings,
|
||||
);
|
||||
|
||||
fn get_containers(&self) -> &Vec<pod::Container>;
|
||||
fn get_annotations(&self) -> &Option<BTreeMap<String, String>>;
|
||||
fn use_host_network(&self) -> bool;
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Common Definitions / LabelSelector.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct LabelSelector {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
matchLabels: Option<BTreeMap<String, String>>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
matchExpressions: Option<Vec<LabelSelectorRequirement>>,
|
||||
}
|
||||
|
||||
/// See Reference / Kubernetes API / Common Definitions / LabelSelector.
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct LabelSelectorRequirement {
|
||||
key: String,
|
||||
operator: String,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
values: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Creates one of the supported K8s objects from a YAML string.
|
||||
pub fn new_k8s_resource(
|
||||
yaml: &str,
|
||||
silent_unsupported_fields: bool,
|
||||
) -> anyhow::Result<(boxed::Box<dyn K8sResource + Sync + Send>, String)> {
|
||||
let header = get_yaml_header(yaml)?;
|
||||
let kind: &str = &header.kind;
|
||||
let d = serde_yaml::Deserializer::from_str(&yaml);
|
||||
|
||||
match kind {
|
||||
"ConfigMap" => {
|
||||
let config_map: config_map::ConfigMap = serde_ignored::deserialize(d, |path| {
|
||||
handle_unused_field(&path.to_string(), silent_unsupported_fields);
|
||||
})
|
||||
.unwrap();
|
||||
debug!("{:#?}", &config_map);
|
||||
Ok((boxed::Box::new(config_map), header.kind))
|
||||
}
|
||||
"Pod" => {
|
||||
let pod: pod::Pod = serde_ignored::deserialize(d, |path| {
|
||||
handle_unused_field(&path.to_string(), silent_unsupported_fields);
|
||||
})
|
||||
.unwrap();
|
||||
debug!("{:#?}", &pod);
|
||||
Ok((boxed::Box::new(pod), header.kind))
|
||||
}
|
||||
"Secret" => {
|
||||
let secret: secret::Secret = serde_ignored::deserialize(d, |path| {
|
||||
handle_unused_field(&path.to_string(), silent_unsupported_fields);
|
||||
})
|
||||
.unwrap();
|
||||
debug!("{:#?}", &secret);
|
||||
Ok((boxed::Box::new(secret), header.kind))
|
||||
}
|
||||
"ClusterRole"
|
||||
| "ClusterRoleBinding"
|
||||
| "LimitRange"
|
||||
| "Namespace"
|
||||
| "PersistentVolume"
|
||||
| "PersistentVolumeClaim"
|
||||
| "PriorityClass"
|
||||
| "ResourceQuota"
|
||||
| "RoleBinding"
|
||||
| "Service"
|
||||
| "ServiceAccount" => {
|
||||
let no_policy = no_policy::NoPolicyResource {
|
||||
yaml: yaml.to_string(),
|
||||
};
|
||||
debug!("{:#?}", &no_policy);
|
||||
Ok((boxed::Box::new(no_policy), header.kind))
|
||||
}
|
||||
_ => todo!("Unsupported YAML spec kind: {}", kind),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_input_yaml(yaml_file: &Option<String>) -> anyhow::Result<String> {
|
||||
let yaml_string = if let Some(yaml) = yaml_file {
|
||||
read_to_string(&yaml)?
|
||||
} else {
|
||||
std::io::read_to_string(std::io::stdin())?
|
||||
};
|
||||
|
||||
Ok(yaml_string)
|
||||
}
|
||||
|
||||
pub fn get_yaml_header(yaml: &str) -> anyhow::Result<YamlHeader> {
|
||||
return Ok(serde_yaml::from_str(yaml)?);
|
||||
}
|
||||
|
||||
pub async fn k8s_resource_init(spec: &mut pod::PodSpec, use_cache: bool) {
|
||||
for container in &mut spec.containers {
|
||||
container.init(use_cache).await;
|
||||
}
|
||||
|
||||
pod::add_pause_container(&mut spec.containers, use_cache).await;
|
||||
|
||||
if let Some(init_containers) = &spec.initContainers {
|
||||
for container in init_containers {
|
||||
let mut new_container = container.clone();
|
||||
new_container.init(use_cache).await;
|
||||
spec.containers.insert(1, new_container);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_container_mounts_and_storages(
|
||||
policy_mounts: &mut Vec<policy::KataMount>,
|
||||
storages: &mut Vec<agent::Storage>,
|
||||
container: &pod::Container,
|
||||
settings: &settings::Settings,
|
||||
volumes: &Vec<volume::Volume>,
|
||||
) {
|
||||
if let Some(volume_mounts) = &container.volumeMounts {
|
||||
for volume in volumes {
|
||||
for volume_mount in volume_mounts {
|
||||
if volume_mount.name.eq(&volume.name) {
|
||||
mount_and_storage::get_mount_and_storage(
|
||||
settings,
|
||||
policy_mounts,
|
||||
storages,
|
||||
volume,
|
||||
volume_mount,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Add the "io.katacontainers.config.agent.policy" annotation into
|
||||
/// a serde representation of a K8s resource YAML.
|
||||
pub fn add_policy_annotation(
|
||||
mut ancestor: &mut serde_yaml::Value,
|
||||
metadata_path: &str,
|
||||
policy: &str,
|
||||
) {
|
||||
let annotations_key = serde_yaml::Value::String("annotations".to_string());
|
||||
let policy_key = serde_yaml::Value::String("io.katacontainers.config.agent.policy".to_string());
|
||||
let policy_value = serde_yaml::Value::String(policy.to_string());
|
||||
|
||||
let path_components = metadata_path.split('.');
|
||||
for name in path_components {
|
||||
ancestor = ancestor.get_mut(&name).unwrap();
|
||||
}
|
||||
|
||||
if let Some(annotations) = ancestor.get_mut(&annotations_key) {
|
||||
if let Some(annotation) = annotations.get_mut(&policy_key) {
|
||||
*annotation = policy_value;
|
||||
} else if let Some(mapping_mut) = annotations.as_mapping_mut() {
|
||||
mapping_mut.insert(policy_key, policy_value);
|
||||
} else {
|
||||
let mut new_annotations = serde_yaml::Mapping::new();
|
||||
new_annotations.insert(policy_key, policy_value);
|
||||
*annotations = serde_yaml::Value::Mapping(new_annotations);
|
||||
}
|
||||
} else {
|
||||
let mut new_annotations = serde_yaml::Mapping::new();
|
||||
new_annotations.insert(policy_key, policy_value);
|
||||
ancestor
|
||||
.as_mapping_mut()
|
||||
.unwrap()
|
||||
.insert(annotations_key, serde_yaml::Value::Mapping(new_annotations));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_policy_annotation(annotations: &mut BTreeMap<String, String>) {
|
||||
annotations.remove("io.katacontainers.config.agent.policy");
|
||||
}
|
||||
|
||||
/// Report a fatal error if this app encounters an unsupported input YAML field,
|
||||
/// unless the user requested this app to ignore unsupported input fields.
|
||||
/// "Silent unsupported fields" is an expert level feature, because some of
|
||||
/// the fields ignored silently might be relevant for the output policy,
|
||||
/// with hard to predict outcomes.
|
||||
fn handle_unused_field(path: &str, silent_unsupported_fields: bool) {
|
||||
if !silent_unsupported_fields {
|
||||
panic!("Unsupported field: {}", path);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user