Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add OCI artifact platform support #1658

Merged
merged 1 commit into from
May 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions cmd/spoc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,10 @@ func main() {
Action: push,
ArgsUsage: "FILE",
Flags: []cli.Flag{
&cli.StringFlag{
Name: pusher.FlagProfile,
&cli.StringSliceFlag{
Name: pusher.FlagProfiles,
Aliases: []string{"f"},
Usage: "the profile to be used",
Usage: "the profiles to be used",
DefaultText: pusher.DefaultInputFile,
TakesFile: true,
},
Expand All @@ -126,6 +126,11 @@ func main() {
spocli.EnvKeyPassword,
),
},
&cli.StringSliceFlag{
Name: pusher.FlagPlatforms,
Aliases: []string{"p"},
Usage: "the platforms to be used in format: os[/arch][/variant][:os_version]",
},
},
},
&cli.Command{
Expand All @@ -151,6 +156,11 @@ func main() {
spocli.EnvKeyPassword,
),
},
&cli.StringFlag{
Name: puller.FlagPlatform,
Aliases: []string{"p"},
Usage: "the platform to be used in format: os[/arch][/variant][:os_version]",
},
},
},
)
Expand Down
115 changes: 113 additions & 2 deletions installation-usage.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# Installation and Usage

<!-- toc -->

- [Features](#features)
- [Architecture](#architecture)
- [Tutorials and Demos](#tutorials-and-demos)
Expand Down Expand Up @@ -59,6 +58,7 @@
- [Run commands with seccomp profiles](#run-commands-with-seccomp-profiles)
- [Pull security profiles from OCI registries](#pull-security-profiles-from-oci-registries)
- [Push security profiles to OCI registries](#push-security-profiles-to-oci-registries)
- [Using multiple platforms](#using-multiple-platforms)
- [Uninstalling](#uninstalling)
<!-- /toc -->

Expand Down Expand Up @@ -558,7 +558,10 @@ 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.
signatures, otherwise the Security Profiles Operator will reject them. The OCI
artifact profiles also support different architectures, where the operator
always tries to select the correct one via `runtime.GOOS`/`runtime.GOARCH` but
also allows to fallback to a default profile.

The operator internally caches pulled artifacts up to 24 hours for 1000
profiles, means that they will be refreshed after that time period, if the stack
Expand Down Expand Up @@ -1986,6 +1989,114 @@ 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.

### Using multiple platforms

`spoc push` supports specifying the target platforms for the profiles to be
pushed. This can be done by using the `--platforms` / `-p` together with the
`--profiles` / `-p` flag. For example, to push two profiles into one artifact:

```
> spoc push -f ./profile-amd64.yaml -p linux/amd64 -f ./profile-arm64.yaml -p linux/arm64 ghcr.io/security-profiles/test:latest
10:59:17.887884 Pushing profiles to: ghcr.io/security-profiles/test:latest
10:59:17.887970 Creating file store in: /tmp/push-2265359353
10:59:17.887989 Adding 2 profiles
10:59:17.887995 Adding profile ./profile-arm64.yaml for platform linux/arm64 to store
10:59:17.888193 Adding profile ./profile-amd64.yaml for platform linux/amd64 to store
10:59:17.888240 Packing files
Pushing signature to: ghcr.io/security-profiles/test
```

The pushed artifact now contains both profiles, separated by their platform:

```
> skopeo inspect --raw docker://ghcr.io/security-profiles/test:latest | jq .
{
"schemaVersion": 2,
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"config": {
"mediaType": "application/vnd.unknown.config.v1+json",
"digest": "sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a",
"size": 2
},
"layers": [
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:6ddecdf312758a19ec788c3984418541274b3c9daf2b10f687d847bc283b391b",
"size": 1167,
"annotations": {
"org.opencontainers.image.title": "profile-linux-arm64.yaml"
},
"platform": {
"architecture": "arm64",
"os": "linux"
}
},
{
"mediaType": "application/vnd.oci.image.layer.v1.tar",
"digest": "sha256:6ddecdf312758a19ec788c3984418541274b3c9daf2b10f687d847bc283b391b",
"size": 1167,
"annotations": {
"org.opencontainers.image.title": "profile-linux-amd64.yaml"
},
"platform": {
"architecture": "amd64",
"os": "linux"
}
}
],
"annotations": {
"org.opencontainers.image.created": "2023-04-28T08:59:17Z"
}
}
```

There are a few fallback scenarios included in the CLI:

- If neither a platform nor a input file is specified, then `spoc` will fallback
to the default profile (`/tmp/profile.yaml`) and platform
(`runtime.GOOS`/`runtime.GOARCH`).
- If only one platform is specified, then `spoc` will apply it and use the
default profile.
- If only one input file is specified, then `spoc` will apply it and use the
default platform.
- If multiple platforms and input files are provided, then `spoc` requires them
to match their occurrences. Platforms have to be unique as well.

The Security Profiles Operator will try to pull the correct profile by using
`runtime.GOOS`/`runtime.GOARCH`, but also falls back to the default profile
(without any platform specified), if it exists. `spoc pull` behaves in the same
way, for example if a profile does not support any platform:

```
> spoc pull ghcr.io/security-profiles/runc:v1.1.6
11:07:14.788840 Pulling profile from: ghcr.io/security-profiles/runc:v1.1.6
11:07:14.788852 Verifying signature
11:07:17.559037 Copying profile from repository
11:07:18.359152 Trying to read profile: profile-linux-amd64.yaml
11:07:18.359209 Trying to read profile: profile.yaml
11:07:18.359224 Trying to unmarshal seccomp profile
11:07:18.359728 Got SeccompProfile: runc-v1.1.6
11:07:18.359732 Saving profile in: /tmp/profile.yaml
```

We can see from the logs that `spoc` tries to read `profile-linux-amd64.yaml`,
and if that does not work it falls back to `profile.yaml`. We can also directly
specify which platform to pull:

```
> spoc pull -p linux/arm64 ghcr.io/security-profiles/test:latest
11:08:53.355689 Pulling profile from: ghcr.io/security-profiles/test:latest
11:08:53.355724 Verifying signature
11:08:56.229418 Copying profile from repository
11:08:57.311964 Trying to read profile: profile-linux-arm64.yaml
11:08:57.311981 Trying to unmarshal seccomp profile
11:08:57.312473 Got SeccompProfile: crun-v1.8.4
11:08:57.312476 Saving profile in: /tmp/profile.yaml
```

## Uninstalling

To uninstall, remove the profiles before removing the rest of the operator:
Expand Down
112 changes: 93 additions & 19 deletions internal/pkg/artifact/artifact.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"context"
"fmt"
"path/filepath"
"strings"

"github.com/go-logr/logr"
v1 "github.com/opencontainers/image-spec/specs-go/v1"
Expand Down Expand Up @@ -86,7 +87,11 @@ func New(logger logr.Logger) *Artifact {
}

// Push a profile to a remote location.
func (a *Artifact) Push(file, to, username, password string, annotations map[string]string) error {
func (a *Artifact) Push(
files map[*v1.Platform]string,
to, username, password string,
annotations map[string]string,
) error {
dir, err := a.MkdirTemp("", "push-")
if err != nil {
return fmt.Errorf("create temp dir: %w", err)
Expand All @@ -111,21 +116,31 @@ func (a *Artifact) Push(file, to, username, password string, annotations map[str
ctx, cancel := context.WithTimeout(context.Background(), defaultTimeout)
defer cancel()

a.logger.Info("Adding profile to store: " + file)
absPath, err := a.FilepathAbs(file)
if err != nil {
return fmt.Errorf("get absoluate file path: %w", err)
}
fileDescriptor, err := a.StoreAdd(
ctx, store, defaultProfileYAML, "", absPath,
)
if err != nil {
return fmt.Errorf("add profile to store: %w", err)
}
for k, v := range annotations {
fileDescriptor.Annotations[k] = v
fileDescriptors := []v1.Descriptor{}
a.logger.Info("Adding " + fmt.Sprint(len(files)) + " profiles")
for platform, file := range files {
a.logger.Info(
"Adding profile " + file +
" for platform " +
platformToString(platform) +
" to store",
)
absPath, err := a.FilepathAbs(file)
if err != nil {
return fmt.Errorf("get absolute file path: %w", err)
}
fileDescriptor, err := a.StoreAdd(
ctx, store, profileName(platform), "", absPath,
)
if err != nil {
return fmt.Errorf("add profile to store: %w", err)
}
for k, v := range annotations {
fileDescriptor.Annotations[k] = v
}
fileDescriptor.Platform = platform
fileDescriptors = append(fileDescriptors, fileDescriptor)
}
fileDescriptors := []v1.Descriptor{fileDescriptor}

a.logger.Info("Packing files")
manifestDescriptor, err := a.Pack(
Expand Down Expand Up @@ -176,7 +191,7 @@ func (a *Artifact) Push(file, to, username, password string, annotations map[str
return fmt.Errorf("copy to repository: %w", err)
}

a.logger.Info("Signing container image")
a.logger.Info("Signing OCI artifact")
o := &options.SignOptions{
Upload: true,
TlogUpload: true,
Expand Down Expand Up @@ -223,7 +238,11 @@ func (a *Artifact) Push(file, to, username, password string, annotations map[str
}

// Pull a profile from a remote location.
func (a *Artifact) Pull(c context.Context, from, username, password string) (*PullResult, error) {
func (a *Artifact) Pull(
c context.Context,
from, username, password string,
platform *v1.Platform,
) (*PullResult, error) {
ctx, cancel := context.WithTimeout(c, defaultTimeout)
defer cancel()

Expand Down Expand Up @@ -296,8 +315,15 @@ func (a *Artifact) Pull(c context.Context, from, username, password string) (*Pu
return nil, fmt.Errorf("copy from repository: %w", err)
}

a.logger.Info("Reading profile")
content, err := a.ReadFile(filepath.Join(dir, defaultProfileYAML))
// Allow a fallback to defaultProfileYAML if no platform is available.
content := []byte{}
for _, name := range []string{profileName(platform), defaultProfileYAML} {
a.logger.Info("Trying to read profile: " + name)
content, err = a.ReadFile(filepath.Join(dir, name))
if err == nil {
break
}
}
if err != nil {
return nil, fmt.Errorf("read profile: %w", err)
}
Expand Down Expand Up @@ -338,3 +364,51 @@ func (a *Artifact) Pull(c context.Context, from, username, password string) (*Pu
a.logger.Info("No profile found")
return nil, fmt.Errorf("%w: last err: %w", ErrDecodeYAML, err)
}

// profileName returns the name for the profile based on the platform.
func profileName(platform *v1.Platform) string {
name := strings.Builder{}
name.WriteString("profile")

if platform != nil {
for _, part := range []string{
platform.OS,
platform.Architecture,
platform.Variant,
platform.OSVersion,
} {
if part != "" {
name.WriteRune('-')
name.WriteString(part)
}
}
}

name.WriteString(".yaml")
return name.String()
}

// platformToString returns a string for the provided platform.
func platformToString(platform *v1.Platform) string {
name := strings.Builder{}

for i, part := range []string{
platform.OS,
platform.Architecture,
platform.Variant,
} {
if part != "" {
if i > 0 {
name.WriteRune('/')
}
name.WriteString(part)
}
}

if platform.OSVersion != "" {
name.WriteRune(':')
name.WriteString(platform.OSVersion)
}

return name.String()
}
Loading