-
Notifications
You must be signed in to change notification settings - Fork 547
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 cosign verify-manifest
command
#490
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
// Copyright 2021 The Sigstore 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 cli | ||
|
||
import ( | ||
"context" | ||
"flag" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
"path/filepath" | ||
"regexp" | ||
"strings" | ||
|
||
"github.com/peterbourgon/ff/v3/ffcli" | ||
"github.com/pkg/errors" | ||
) | ||
|
||
// VerifyCommand verifies all image signatures on a supplied k8s resource | ||
type VerifyManifestCommand struct { | ||
VerifyCommand | ||
} | ||
|
||
// Verify builds and returns an ffcli command | ||
func VerifyManifest() *ffcli.Command { | ||
cmd := VerifyManifestCommand{VerifyCommand: VerifyCommand{}} | ||
flagset := flag.NewFlagSet("cosign verify-manifest", flag.ExitOnError) | ||
applyVerifyFlags(&cmd.VerifyCommand, flagset) | ||
|
||
return &ffcli.Command{ | ||
Name: "verify-manifest", | ||
ShortUsage: "cosign verify-manifest -key <key path>|<key url>|<kms uri> <path/to/manifest>", | ||
ShortHelp: "Verify all signatures of images specified in the manifest", | ||
LongHelp: `Verify all signature of images in a Kubernetes resource manifest by checking claims | ||
against the transparency log. | ||
|
||
Shell-like variables in the Dockerfile's FROM lines will be substituted with values from the OS ENV. | ||
|
||
EXAMPLES | ||
# verify cosign claims and signing certificates on images in the manifest | ||
cosign verify-manifest <path/to/my-deployment.yaml> | ||
|
||
# additionally verify specified annotations | ||
cosign verify-manifest -a key1=val1 -a key2=val2 <path/to/my-deployment.yaml> | ||
|
||
# (experimental) additionally, verify with the transparency log | ||
COSIGN_EXPERIMENTAL=1 cosign verify-dockerfile <path/to/my-deployment.yaml> | ||
|
||
# verify images with public key | ||
cosign verify-manifest -key cosign.pub <path/to/my-deployment.yaml> | ||
|
||
# verify images with public key provided by URL | ||
cosign verify-manifest -key https://host.for/<FILE> <path/to/my-deployment.yaml> | ||
|
||
# verify images with public key stored in Azure Key Vault | ||
cosign verify-manifest -key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] <path/to/my-deployment.yaml> | ||
|
||
# verify images with public key stored in AWS KMS | ||
cosign verify-manifest -key awskms://[ENDPOINT]/[ID/ALIAS/ARN] <path/to/my-deployment.yaml> | ||
|
||
# verify images with public key stored in Google Cloud KMS | ||
cosign verify-manifest -key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] <path/to/my-deployment.yaml> | ||
|
||
# verify images with public key stored in Hashicorp Vault | ||
cosign verify-manifest -key hashivault://[KEY] <path/to/my-deployment.yaml>`, | ||
|
||
FlagSet: flagset, | ||
Exec: cmd.Exec, | ||
} | ||
} | ||
|
||
// Exec runs the verification command | ||
func (c *VerifyManifestCommand) Exec(ctx context.Context, args []string) error { | ||
if len(args) != 1 { | ||
return flag.ErrHelp | ||
} | ||
|
||
manifestPath := args[0] | ||
|
||
if filepath.Ext(strings.TrimSpace(manifestPath)) != ".yaml" { | ||
return fmt.Errorf("only yaml manifests are supported at this time") | ||
} | ||
|
||
manifest, err := ioutil.ReadFile(manifestPath) | ||
if err != nil { | ||
return fmt.Errorf("could not read manifest: %v", err) | ||
} | ||
|
||
images, err := getImagesFromYamlManifest(string(manifest)) | ||
if err != nil { | ||
return fmt.Errorf("failed extracting images from manifest: %v", err) | ||
} | ||
if len(images) == 0 { | ||
return errors.New("no images found in manifest") | ||
} | ||
fmt.Fprintf(os.Stderr, "Extracted image(s): %s\n", strings.Join(images, ", ")) | ||
|
||
return c.VerifyCommand.Exec(ctx, images) | ||
} | ||
|
||
func getImagesFromYamlManifest(manifest string) ([]string, error) { | ||
var images []string | ||
re := regexp.MustCompile(`image:\s?(?P<Image>.*)\s?`) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Perhaps it could be more robust to decode the manifest and validate that the extensions are among those identified in the issue (Pod, DaemonSet...), then use the decoded object to get the image value for all the containers (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let me know if you want me to help you with this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @hectorj2f yes, this was a consideration as mentioned in #437 (comment) - I think it would be better, but as different Kinds come out, it binds us more to the K8s API for parsing unless we still just walk down the tree looking for "image" keys (but guaranteed valid YAML) which would address your other comment. This regex also doesn't account for quoted string variants, so either way this naive approach needs to change in some fashion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Agreed. There's prior art for ducktyping There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, you could use runtime.Raw and decode the object to the list of supported types here. Sure, the current implementation should not be blocked by this. Once this PR is merged, I could open a PR with my proposed changes, so you can have a look at them. |
||
for _, s := range re.FindAllStringSubmatch(manifest, -1) { | ||
images = append(images, s[1]) | ||
} | ||
return images, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
// Copyright 2021 The Sigstore 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 cli | ||
|
||
import ( | ||
"reflect" | ||
"testing" | ||
) | ||
|
||
const SingleContainerManifest = ` | ||
apiVersion: v1 | ||
kind: Pod | ||
metadata: | ||
name: single-pod | ||
spec: | ||
restartPolicy: Never | ||
containers: | ||
- name: nginx-container | ||
image: nginx:1.21.1 | ||
` | ||
|
||
const MultiContainerManifest = ` | ||
apiVersion: v1 | ||
kind: Pod | ||
metadata: | ||
name: multi-pod | ||
spec: | ||
restartPolicy: Never | ||
volumes: | ||
- name: shared-data | ||
emptyDir: {} | ||
containers: | ||
- name: nginx-container | ||
image: nginx:1.21.1 | ||
volumeMounts: | ||
- name: shared-data | ||
mountPath: /usr/share/nginx/html | ||
- name: ubuntu-container | ||
image: ubuntu:21.10 | ||
volumeMounts: | ||
- name: shared-data | ||
mountPath: /pod-data | ||
command: ["/bin/sh"] | ||
args: ["-c", "echo Hello, World > /pod-data/index.html"] | ||
` | ||
|
||
func TestGetImagesFromYamlManifest(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
fileContents string | ||
expected []string | ||
}{ | ||
{ | ||
name: "single image", | ||
fileContents: SingleContainerManifest, | ||
expected: []string{"nginx:1.21.1"}, | ||
}, | ||
{ | ||
name: "multi image", | ||
fileContents: MultiContainerManifest, | ||
expected: []string{"nginx:1.21.1", "ubuntu:21.10"}, | ||
}, | ||
{ | ||
name: "no images found", | ||
fileContents: ``, | ||
expected: nil, | ||
}, | ||
} | ||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
got, err := getImagesFromYamlManifest(tc.fileContents) | ||
if err != nil { | ||
t.Fatalf("getImagesFromYamlManifest returned error: %v", err) | ||
} | ||
if !reflect.DeepEqual(tc.expected, got) { | ||
t.Errorf("getImagesFromYamlManifest returned %v, wanted %v", got, tc.expected) | ||
} | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
apiVersion: v1 | ||
kind: Pod | ||
metadata: | ||
name: single-pod | ||
spec: | ||
restartPolicy: Never | ||
containers: | ||
- name: distroless | ||
image: gcr.io/distroless/base |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
apiVersion: v1 | ||
kind: Pod | ||
metadata: | ||
name: single-pod | ||
spec: | ||
restartPolicy: Never | ||
containers: | ||
- name: nginx-container | ||
image: nginx |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this is coming from the verify-dockerfile command description.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated the help docs to reflect.