From ecfc5d12b109ed24ed4435959b51d425564c1eee Mon Sep 17 00:00:00 2001 From: Sascha Grunert Date: Tue, 21 Mar 2023 12:35:24 +0100 Subject: [PATCH] Add OCI baseprofile support This allows using OCI artifacts as base profiles when the baseProfileName is prefixed with `oci://`. Signed-off-by: Sascha Grunert --- .../v1beta1/seccompprofile_types.go | 4 +- ...rofiles-operator-profile_v1_configmap.yaml | 15 ++- ...les-operator.x-k8s.io_seccompprofiles.yaml | 6 +- deploy/base-crds/crds/seccompprofile.yaml | 6 +- .../profiles/security-profiles-operator.json | 15 ++- deploy/helm/crds/crds.yaml | 6 +- deploy/helm/templates/static-resources.yaml | 15 ++- deploy/namespace-operator.yaml | 21 +++- deploy/openshift-dev.yaml | 21 +++- deploy/openshift-downstream.yaml | 21 +++- deploy/operator.yaml | 21 +++- deploy/webhook-operator.yaml | 21 +++- installation-usage.md | 118 +++++++++++++++++- internal/pkg/artifact/artifact.go | 4 +- internal/pkg/artifact/artifact_test.go | 3 +- internal/pkg/cli/puller/impl.go | 3 +- .../daemon/seccompprofile/seccompprofile.go | 55 ++++++-- internal/pkg/manager/spod/bindata/spod.go | 22 +++- test/e2e_test.go | 4 + test/tc_base_profiles_oci_test.go | 112 +++++++++++++++++ 20 files changed, 449 insertions(+), 44 deletions(-) create mode 100644 test/tc_base_profiles_oci_test.go diff --git a/api/seccompprofile/v1beta1/seccompprofile_types.go b/api/seccompprofile/v1beta1/seccompprofile_types.go index 0dbc7c047e..4ff579b1c9 100644 --- a/api/seccompprofile/v1beta1/seccompprofile_types.go +++ b/api/seccompprofile/v1beta1/seccompprofile_types.go @@ -40,7 +40,9 @@ const ExtJSON = ".json" // SeccompProfileSpec defines the desired state of SeccompProfile. type SeccompProfileSpec struct { - // name of base profile (in the same namespace) what will be unioned into this profile + // BaseProfileName is the name of base profile (in the same namespace) that + // will be unioned into this profile. Base profiles can be references as + // remote OCI artifacts as well when prefixed with `oci://`. BaseProfileName string `json:"baseProfileName,omitempty"` // Properties from containers/common/pkg/seccomp.Seccomp type diff --git a/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml b/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml index 6a7bca351e..c2cf20bcb3 100644 --- a/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml +++ b/bundle/manifests/security-profiles-operator-profile_v1_configmap.yaml @@ -3,7 +3,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -28,8 +33,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -47,6 +54,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -61,13 +69,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/bundle/manifests/security-profiles-operator.x-k8s.io_seccompprofiles.yaml b/bundle/manifests/security-profiles-operator.x-k8s.io_seccompprofiles.yaml index fad6cb9cd2..88164cdfc0 100644 --- a/bundle/manifests/security-profiles-operator.x-k8s.io_seccompprofiles.yaml +++ b/bundle/manifests/security-profiles-operator.x-k8s.io_seccompprofiles.yaml @@ -77,8 +77,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp diff --git a/deploy/base-crds/crds/seccompprofile.yaml b/deploy/base-crds/crds/seccompprofile.yaml index 64e2faa86e..f144a40836 100644 --- a/deploy/base-crds/crds/seccompprofile.yaml +++ b/deploy/base-crds/crds/seccompprofile.yaml @@ -75,8 +75,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp diff --git a/deploy/base/profiles/security-profiles-operator.json b/deploy/base/profiles/security-profiles-operator.json index 4388f1feb7..a0040ff6f1 100644 --- a/deploy/base/profiles/security-profiles-operator.json +++ b/deploy/base/profiles/security-profiles-operator.json @@ -1,6 +1,11 @@ { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -25,8 +30,10 @@ "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -44,6 +51,7 @@ "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -58,13 +66,18 @@ "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/deploy/helm/crds/crds.yaml b/deploy/helm/crds/crds.yaml index d28ceec7ca..27ae2290da 100644 --- a/deploy/helm/crds/crds.yaml +++ b/deploy/helm/crds/crds.yaml @@ -290,8 +290,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp diff --git a/deploy/helm/templates/static-resources.yaml b/deploy/helm/templates/static-resources.yaml index 986c4b54ae..1c526f809d 100644 --- a/deploy/helm/templates/static-resources.yaml +++ b/deploy/helm/templates/static-resources.yaml @@ -871,7 +871,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -896,8 +901,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -915,6 +922,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -929,13 +937,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/deploy/namespace-operator.yaml b/deploy/namespace-operator.yaml index 5f9df1d455..f9791eed8a 100644 --- a/deploy/namespace-operator.yaml +++ b/deploy/namespace-operator.yaml @@ -290,8 +290,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp @@ -2825,7 +2827,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -2850,8 +2857,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -2869,6 +2878,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -2883,13 +2893,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/deploy/openshift-dev.yaml b/deploy/openshift-dev.yaml index 8ed8bf9afa..b38c40d53a 100644 --- a/deploy/openshift-dev.yaml +++ b/deploy/openshift-dev.yaml @@ -466,8 +466,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp @@ -2807,7 +2809,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -2832,8 +2839,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -2851,6 +2860,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -2865,13 +2875,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/deploy/openshift-downstream.yaml b/deploy/openshift-downstream.yaml index 5d7b0f4ad1..a24d8169df 100644 --- a/deploy/openshift-downstream.yaml +++ b/deploy/openshift-downstream.yaml @@ -290,8 +290,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp @@ -2838,7 +2840,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -2863,8 +2870,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -2882,6 +2891,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -2896,13 +2906,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/deploy/operator.yaml b/deploy/operator.yaml index 92d0a7a573..67d62c45fe 100644 --- a/deploy/operator.yaml +++ b/deploy/operator.yaml @@ -290,8 +290,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp @@ -2825,7 +2827,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -2850,8 +2857,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -2869,6 +2878,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -2883,13 +2893,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/deploy/webhook-operator.yaml b/deploy/webhook-operator.yaml index 3d71414a3c..6f8b070d3c 100644 --- a/deploy/webhook-operator.yaml +++ b/deploy/webhook-operator.yaml @@ -466,8 +466,10 @@ spec: type: string type: array baseProfileName: - description: name of base profile (in the same namespace) what will - be unioned into this profile + description: BaseProfileName is the name of base profile (in the same + namespace) that will be unioned into this profile. Base profiles + can be references as remote OCI artifacts as well when prefixed + with `oci://`. type: string defaultAction: description: the default action for seccomp @@ -2796,7 +2798,12 @@ data: security-profiles-operator.json: | { "defaultAction": "SCMP_ACT_ERRNO", - "architectures": ["SCMP_ARCH_X86_64", "SCMP_ARCH_X86", "SCMP_ARCH_X32", "SCMP_ARCH_AARCH64"], + "architectures": [ + "SCMP_ARCH_X86_64", + "SCMP_ARCH_X86", + "SCMP_ARCH_X32", + "SCMP_ARCH_AARCH64" + ], "syscalls": [ { "names": [ @@ -2821,8 +2828,10 @@ data: "exit_group", "fchown", "fcntl", + "flock", "fstat", "fstatfs", + "fsync", "futex", "getcwd", "getdents64", @@ -2840,6 +2849,7 @@ data: "inotify_add_watch", "inotify_init1", "listen", + "lseek", "madvise", "membarrier", "mkdirat", @@ -2854,13 +2864,18 @@ data: "pipe2", "prctl", "pread64", + "prlimit64", "read", + "readlink", "readlinkat", + "renameat", + "rseq", "rt_sigaction", "rt_sigprocmask", "rt_sigreturn", "sched_getaffinity", "sched_yield", + "seccomp", "set_robust_list", "set_tid_address", "setgid", diff --git a/installation-usage.md b/installation-usage.md index de0b4cf172..22046e9ace 100644 --- a/installation-usage.md +++ b/installation-usage.md @@ -1,6 +1,7 @@ # Installation and Usage + - [Features](#features) - [Architecture](#architecture) - [Tutorials and Demos](#tutorials-and-demos) @@ -24,6 +25,7 @@ - [Create a seccomp profile](#create-a-seccomp-profile) - [Apply a seccomp profile to a pod](#apply-a-seccomp-profile-to-a-pod) - [Base syscalls for a container runtime](#base-syscalls-for-a-container-runtime) + - [OCI Artifact support for base profiles](#oci-artifact-support-for-base-profiles) - [Label namespaces for binding and recording](#label-namespaces-for-binding-and-recording) - [Bind workloads to profiles with ProfileBindings](#bind-workloads-to-profiles-with-profilebindings) - [Record profiles from workloads with ProfileRecordings](#record-profiles-from-workloads-with-profilerecordings) @@ -70,7 +72,6 @@ The feature scope of the security-profiles-operator is right now limited to: - Providing metrics endpoints - Providing a Command Line Interface `spoc` for use cases not including Kubernetes. - ## Architecture ![Architecture](doc/architecture.svg) @@ -205,11 +206,11 @@ kubectl annotate ns $spo_ns \ # Install the chart from the release URL (or a file path if desired) helm install security-profiles-operator --namespace security-profiles-operator https://github.com/kubernetes-sigs/security-profiles-operator/releases/download/v0.7.1/security-profiles-operator-0.7.1.tgz -# Or update it with +# Or update it with # helm upgrade --install security-profiles-operator --namespace security-profiles-operator https://github.com/kubernetes-sigs/security-profiles-operator/releases/download/v0.7.1/security-profiles-operator-0.7.1.tgz ``` -#### Troubleshooting and maintenance +#### Troubleshooting and maintenance These CRDs are not templated, but will be installed by default when running a helm install for the chart. There is no support at this time for upgrading or deleting CRDs using Helm. [[docs](https://helm.sh/docs/chart_best_practices/custom_resource_definitions/)] @@ -229,14 +230,13 @@ kubectl get -n $spo_ns crds --no-headers |grep security-profiles-operator |cut - # Uninstall the chart release from the namespace helm uninstall --namespace $spo_ns security-profiles-operator -# WARNING: Delete the namespace +# WARNING: Delete the namespace kubectl delete ns $spo_ns # Install it again helm upgrade --install --create-namespace --namespace $spo_ns security-profiles-operator deploy/helm/ ``` - ### Installation on AKS In case you installed SPO on an [AKS cluster](https://azure.microsoft.com/en-us/products/kubernetes-service/#overview), it is recommended to [configure webhook](#configuring-webhooks) to respect the [control-plane](https://learn.microsoft.com/en-us/azure/aks/faq#can-i-use-admission-controller-webhooks-on-aks) label as follows: @@ -525,6 +525,43 @@ If you're not using runc but the alternative the [corresponding example profile](./examples/baseprofile-crun.yaml) (tested with version 0.20.1). +#### OCI Artifact support for base profiles + +The operator supports pulling base profiles from container registries supporting +OCI artifacts, which are right now: + +- [CNCF Distribution](https://github.com/distribution/distribution) +- [Azure Container Registry](https://aka.ms/acr) +- [Amazon Elastic Container Registry](https://aws.amazon.com/ecr) +- [Google Artifact Registry](https://cloud.google.com/artifact-registry) +- [GitHub Packages container registry](https://docs.github.com/en/packages/guides/about-github-container-registry) +- [Bundle Bar](https://bundle.bar/docs/supported-clients/oras) +- [Docker Hub](https://hub.docker.com) +- [Zot Registry](https://zotregistry.io) + +To use that feature, just prefix the `baseProfileName` with `oci://`, like: + +```yaml +apiVersion: security-profiles-operator.x-k8s.io/v1beta1 +kind: SeccompProfile +metadata: + namespace: my-namespace + name: profile1 +spec: + defaultAction: SCMP_ACT_ERRNO + baseProfileName: oci://ghcr.io/security-profiles/runc:v1.1.4 +``` + +The resulting profile `profile1` will then contain all base syscalls from the +remote `runc` profile. It is also possible to reference the base profile by its +SHA256, like `oci://ghcr.io/security-profiles/runc@sha256:380…`. Please note +that all profiles must be signed using [sigstore +(cosign)](https://github.com/sigstore/cosign) signatures, otherwise the Security +Profiles Operator will reject them. + +We provide all available base profiles as part of the ["Security Profiles" +GitHub organization](https://github.com/orgs/security-profiles/packages). + ### Label namespaces for binding and recording The next two sections describe how to bind a security profile to a container @@ -1858,6 +1895,77 @@ chmod: changing permissions of '/tmp/profile-chmod.json': Operation not permitte 2023/03/10 10:25:38 Command did not exit successfully: exit status 1 ``` +### Pull security profiles from OCI registries + +The `spoc` client is able to pull security profiles from OCI artifact compatible +registries. To do that, just run `spoc pull`: + +```console +> spoc pull ghcr.io/security-profiles/runc:v1.1.4 +16:32:29.795597 Pulling profile from: ghcr.io/security-profiles/runc:v1.1.4 +16:32:29.795610 Verifying signature + +Verification for ghcr.io/security-profiles/runc:v1.1.4 -- +The following checks were performed on each of these signatures: + - Existence of the claims in the transparency log was verified offline + - The code-signing certificate was verified using trusted certificate authority certificates + +[{"critical":{"identity":{"docker-reference":"ghcr.io/security-profiles/runc"},…}}] +16:32:33.208695 Creating file store in: /tmp/pull-3199397214 +16:32:33.208713 Verifying reference: ghcr.io/security-profiles/runc:v1.1.4 +16:32:33.208718 Creating repository for ghcr.io/security-profiles/runc +16:32:33.208742 Using tag: v1.1.4 +16:32:33.208743 Copying profile from repository +16:32:34.119652 Reading profile +16:32:34.119677 Trying to unmarshal seccomp profile +16:32:34.120114 Got SeccompProfile: runc-v1.1.4 +16:32:34.120119 Saving profile in: /tmp/profile.yaml +``` + +The profile can be now found in `/tmp/profile.yaml` or the specified output file +`--output-file` / `-o`. If username and password authentication is required, +either use the `--username`, `-u` flag or export the `USERNAME` environment +variable. To set the password, export the `PASSWORD` environment variable. + +### Push security profiles to OCI registries + +The `spoc` client is also able to push security profiles from OCI artifact +compatible registries. To do that, just run `spoc push`: + +``` +> export USERNAME=my-user +> export PASSWORD=my-pass +> spoc push -f ./examples/baseprofile-crun.yaml ghcr.io/security-profiles/crun:v1.8.1 +16:35:43.899886 Pushing profile ./examples/baseprofile-crun.yaml to: ghcr.io/security-profiles/crun:v1.8.1 +16:35:43.899939 Creating file store in: /tmp/push-3618165827 +16:35:43.899947 Adding profile to store: ./examples/baseprofile-crun.yaml +16:35:43.900061 Packing files +16:35:43.900282 Verifying reference: ghcr.io/security-profiles/crun:v1.8.1 +16:35:43.900310 Using tag: v1.8.1 +16:35:43.900313 Creating repository for ghcr.io/security-profiles/crun +16:35:43.900319 Using username and password +16:35:43.900321 Copying profile to repository +16:35:46.976108 Signing container image +Generating ephemeral keys... +Retrieving signed certificate... + + Note that there may be personally identifiable information associated with this signed artifact. + This may include the email address associated with the account with which you authenticate. + This information will be used for signing this artifact and will be stored in public transparency logs and cannot be removed later. + +By typing 'y', you attest that you grant (or have permission to grant) and agree to have this information stored permanently in transparency logs. +Your browser will now be opened to: +https://oauth2.sigstore.dev/auth/auth?access_type=… +Successfully verified SCT... +tlog entry created with index: 16520520 +Pushing signature to: ghcr.io/security-profiles/crun +``` + +We can specify an username and password in the same way as for `spoc pull`. +Please also note that signing is always required for push and pull. It is +possible to add custom annotations to the security profile by using the +`--annotations` / `-a` flag multiple times in `KEY:VALUE` format. + ## Uninstalling To uninstall, remove the profiles before removing the rest of the operator: diff --git a/internal/pkg/artifact/artifact.go b/internal/pkg/artifact/artifact.go index 102b7c6626..e3a68c369c 100644 --- a/internal/pkg/artifact/artifact.go +++ b/internal/pkg/artifact/artifact.go @@ -223,8 +223,8 @@ func (a *Artifact) Push(file, to, username, password string, annotations map[str } // Pull a profile from a remote location. -func (a *Artifact) Pull(from, username, password string) (*PullResult, error) { - ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout) +func (a *Artifact) Pull(c context.Context, from, username, password string) (*PullResult, error) { + ctx, cancel := context.WithTimeout(c, defaultTimeout) defer cancel() a.logger.Info("Verifying signature") diff --git a/internal/pkg/artifact/artifact_test.go b/internal/pkg/artifact/artifact_test.go index 11900e85b0..4f2e8d1b33 100644 --- a/internal/pkg/artifact/artifact_test.go +++ b/internal/pkg/artifact/artifact_test.go @@ -17,6 +17,7 @@ limitations under the License. package artifact import ( + "context" "errors" "testing" @@ -366,7 +367,7 @@ func TestPull(t *testing.T) { sut := New(logr.Discard()) sut.impl = mock - res, err := sut.Pull("", "foo", "bar") + res, err := sut.Pull(context.Background(), "", "foo", "bar") assert(res, err) }) } diff --git a/internal/pkg/cli/puller/impl.go b/internal/pkg/cli/puller/impl.go index d9e60d0e6c..cf47691ace 100644 --- a/internal/pkg/cli/puller/impl.go +++ b/internal/pkg/cli/puller/impl.go @@ -17,6 +17,7 @@ limitations under the License. package puller import ( + "context" "os" "github.com/go-logr/logr" @@ -35,7 +36,7 @@ type impl interface { } func (*defaultImpl) Pull(from, username, password string) (*artifact.PullResult, error) { - return artifact.New(logr.New(&cli.LogSink{})).Pull(from, username, password) + return artifact.New(logr.New(&cli.LogSink{})).Pull(context.Background(), from, username, password) } func (*defaultImpl) WriteFile(name string, data []byte, perm os.FileMode) error { diff --git a/internal/pkg/daemon/seccompprofile/seccompprofile.go b/internal/pkg/daemon/seccompprofile/seccompprofile.go index f2bdc3c1f1..256a9b8c80 100644 --- a/internal/pkg/daemon/seccompprofile/seccompprofile.go +++ b/internal/pkg/daemon/seccompprofile/seccompprofile.go @@ -25,10 +25,12 @@ import ( "net/http" "os" "path" + "strings" "time" "github.com/containers/common/pkg/seccomp" "github.com/go-logr/logr" + "github.com/jellydator/ttlcache/v3" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" @@ -45,6 +47,7 @@ import ( seccompprofileapi "sigs.k8s.io/security-profiles-operator/api/seccompprofile/v1beta1" statusv1alpha1 "sigs.k8s.io/security-profiles-operator/api/secprofnodestatus/v1alpha1" spodapi "sigs.k8s.io/security-profiles-operator/api/spod/v1alpha1" + "sigs.k8s.io/security-profiles-operator/internal/pkg/artifact" "sigs.k8s.io/security-profiles-operator/internal/pkg/config" "sigs.k8s.io/security-profiles-operator/internal/pkg/controller" "sigs.k8s.io/security-profiles-operator/internal/pkg/daemon/common" @@ -81,6 +84,9 @@ const ( reasonCannotUpdateStatus string = "CannotUpdateNodeStatus" reasonProfileNotAllowed string = "ProfileNotAllowed" reasonSavedProfile string = "SavedSeccompProfile" + + defaultCacheTimeout time.Duration = 24 * time.Hour + maxCacheItems uint64 = 1000 ) // NewController returns a new empty controller instance. @@ -92,11 +98,12 @@ type saver func(string, []byte) (bool, error) // A Reconciler reconciles seccomp profiles. type Reconciler struct { - client client.Client - log logr.Logger - record record.EventRecorder - save saver - metrics *metrics.Metrics + client client.Client + log logr.Logger + record record.EventRecorder + save saver + metrics *metrics.Metrics + baseProfiles *ttlcache.Cache[string, *seccompprofileapi.SeccompProfile] } // Name returns the name of the controller. @@ -157,6 +164,10 @@ func (r *Reconciler) Setup( r.record = mgr.GetEventRecorderFor("profile") r.save = saveProfileOnDisk r.metrics = met + r.baseProfiles = ttlcache.New( + ttlcache.WithTTL[string, *seccompprofileapi.SeccompProfile](defaultCacheTimeout), + ttlcache.WithCapacity[string, *seccompprofileapi.SeccompProfile](maxCacheItems), + ) // Register the regular reconciler to manage SeccompProfiles return ctrl.NewControllerManagedBy(mgr). @@ -306,18 +317,44 @@ func (r *Reconciler) mergeBaseProfile( Flags: sp.Spec.Flags, } baseProfileName := sp.Spec.BaseProfileName + baseProfile := &seccompprofileapi.SeccompProfile{} + const ociPrefix = "oci://" + if baseProfileName == "" { + // No base profile at all op.Syscalls = sp.Spec.Syscalls return op, nil - } - baseProfile := &seccompprofileapi.SeccompProfile{} - if err := r.client.Get( - ctx, util.NamespacedName(baseProfileName, sp.GetNamespace()), baseProfile); err != nil { + } else if strings.HasPrefix(baseProfileName, ociPrefix) { + // Pull remote base profile from an OCI artifact registry + from := strings.TrimPrefix(baseProfileName, ociPrefix) + + item := r.baseProfiles.Get(from) + if item != nil { + l.Info("Using cached base profile: " + from) + baseProfile = item.Value() + } else { + l.Info("Pulling base profile: " + from) + res, err := artifact.New(l).Pull(ctx, from, "", "") + if err != nil { + return op, fmt.Errorf("retrieve base profile %s from OCI registry: %w", from, err) + } + + if res.Type() != artifact.PullResultTypeSeccompProfile { + return op, fmt.Errorf("pull result type %s is not a seccomp profile", res.Type()) + } + baseProfile = res.SeccompProfile() + r.baseProfiles.Set(from, baseProfile, ttlcache.DefaultTTL) + } + } else if err := r.client.Get( + ctx, util.NamespacedName(baseProfileName, sp.GetNamespace()), baseProfile, + ); err != nil { + // Local base profile l.Error(err, "cannot retrieve base profile "+baseProfileName) r.metrics.IncSeccompProfileError(reasonInvalidSeccompProfile) r.record.Event(sp, util.EventTypeWarning, reasonInvalidSeccompProfile, err.Error()) return op, fmt.Errorf("merging base profile: %w", err) } + op.Syscalls = util.UnionSyscalls(baseProfile.Spec.Syscalls, sp.Spec.Syscalls) return op, nil } diff --git a/internal/pkg/manager/spod/bindata/spod.go b/internal/pkg/manager/spod/bindata/spod.go index 910b6ab3f5..f1572c3c0c 100644 --- a/internal/pkg/manager/spod/bindata/spod.go +++ b/internal/pkg/manager/spod/bindata/spod.go @@ -49,6 +49,8 @@ var ( ) const ( + HomeDirectory = "/home" + TempDirectory = "/tmp" SelinuxDropDirectory = "/etc/selinux.d" SelinuxdPrivateDir = "/var/run/selinuxd" SelinuxdSocketPath = SelinuxdPrivateDir + "/selinuxd.sock" @@ -320,6 +322,14 @@ semodule -i /opt/spo-profiles/selinuxrecording.cil Name: "grpc-server-volume", MountPath: filepath.Dir(config.GRPCServerSocketMetrics), }, + { + Name: "home-volume", + MountPath: HomeDirectory, + }, + { + Name: "tmp-volume", + MountPath: TempDirectory, + }, }, SecurityContext: &corev1.SecurityContext{ AllowPrivilegeEscalation: &falsely, @@ -375,6 +385,10 @@ semodule -i /opt/spo-profiles/selinuxrecording.cil Name: config.KubeletDirEnvKey, Value: config.KubeletDir(), }, + { + Name: "HOME", + Value: HomeDirectory, + }, }, Ports: []corev1.ContainerPort{ { @@ -541,7 +555,7 @@ semodule -i /opt/spo-profiles/selinuxrecording.cil }, { Name: "tmp-volume", - MountPath: "/tmp", + MountPath: TempDirectory, }, { Name: "grpc-server-volume", @@ -769,6 +783,12 @@ semodule -i /opt/spo-profiles/selinuxrecording.cil }, }, }, + { + Name: "home-volume", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + }, }, Tolerations: []corev1.Toleration{ { diff --git a/test/e2e_test.go b/test/e2e_test.go index 526f47728d..5cfa87f2f7 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -76,6 +76,10 @@ func (e *e2e) TestSecurityProfilesOperator() { "Seccomp: Verify base profile merge", e.testCaseBaseProfile, }, + { + "Seccomp: Verify base profile merge from OCI registry", + e.testCaseBaseProfileOCI, + }, { "Seccomp: Allowed syscalls", e.testCaseAllowedSyscalls, diff --git a/test/tc_base_profiles_oci_test.go b/test/tc_base_profiles_oci_test.go new file mode 100644 index 0000000000..668e039e99 --- /dev/null +++ b/test/tc_base_profiles_oci_test.go @@ -0,0 +1,112 @@ +/* +Copyright 2023 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e_test + +import ( + "fmt" + "os" + "strings" + "time" +) + +func (e *e2e) testCaseBaseProfileOCI([]string) { + e.seccompOnlyTestCase() + + baseProfileName := "oci://ghcr.io/security-profiles/" + + if clusterType == clusterTypeVanilla && e.containerRuntime != containerRuntimeDocker { + baseProfileName += strings.ReplaceAll(baseProfileNameCrun, "-", ":") + } else { + baseProfileName += strings.ReplaceAll(baseProfileNameRunc, "-", ":") + } + + helloProfile := fmt.Sprintf(` +apiVersion: security-profiles-operator.x-k8s.io/v1beta1 +kind: SeccompProfile +metadata: + name: hello +spec: + defaultAction: SCMP_ACT_ERRNO + baseProfileName: %s + syscalls: + - action: SCMP_ACT_ALLOW + names: + - arch_prctl + - set_tid_address + - exit_group +`, baseProfileName) + + const helloPod = ` +apiVersion: v1 +kind: Pod +metadata: + name: hello +spec: + containers: + - image: quay.io/security-profiles-operator/test-hello-world:latest + name: hello + securityContext: + seccompProfile: + type: Localhost + localhostProfile: operator/%s/hello.json + restartPolicy: OnFailure +` + + e.logf("Creating hello profile") + helloProfileFile, err := os.CreateTemp("", "hello-profile*.yaml") + e.Nil(err) + defer os.Remove(helloProfileFile.Name()) + + _, err = helloProfileFile.WriteString(helloProfile) + e.Nil(err) + err = helloProfileFile.Close() + e.Nil(err) + e.kubectl("create", "-f", helloProfileFile.Name()) + defer e.kubectl("delete", "-f", helloProfileFile.Name()) + + e.logf("Waiting for profile to be reconciled") + e.waitFor("condition=ready", "sp", "hello") + + e.logf("Creating hello-world pod") + helloPodFile, err := os.CreateTemp("", "hello-pod*.yaml") + e.Nil(err) + defer os.Remove(helloPodFile.Name()) + + namespace := e.getCurrentContextNamespace(defaultNamespace) + _, err = helloPodFile.WriteString(fmt.Sprintf(helloPod, namespace)) + e.Nil(err) + err = helloPodFile.Close() + e.Nil(err) + e.kubectl("create", "-f", helloPodFile.Name()) + defer e.kubectl("delete", "pod", "hello") + + e.logf("Waiting for test pod to be initialized") + e.waitFor("condition=initialized", "pod", "hello") + + e.logf("Waiting for pod to be completed") + for i := 0; i < 20; i++ { + output := e.kubectl("get", "pod", "hello") + if strings.Contains(output, "Completed") { + break + } + time.Sleep(time.Second) + } + + e.logf("Testing that container ran successfully") + output := e.kubectl("logs", "hello") + e.Contains(output, "Hello from Docker!") +}