diff --git a/go.sum b/go.sum index 82868db21d..d82b6e102d 100644 --- a/go.sum +++ b/go.sum @@ -437,7 +437,9 @@ github.com/coreos/etcd v3.3.17+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk= github.com/coreos/go-iptables v0.4.5/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= github.com/coreos/go-iptables v0.5.0/go.mod h1:/mVI274lEDI2ns62jHCDnCyBF9Iwsmekav8Dbxlm1MU= +github.com/coreos/go-oidc v2.1.0+incompatible h1:sdJrfw8akMnCuUlaZU3tE/uYXFgfqom8DBE9so9EBsM= github.com/coreos/go-oidc v2.1.0+incompatible/go.mod h1:CgnwVTmzoESiwO9qyAFEMiHoZ1nMCKZlZ9V6mm3/LKc= +github.com/coreos/go-oidc/v3 v3.0.0 h1:/mAA0XMgYJw2Uqm7WKGCsKnjitE/+A0FFbOmiRJm7LQ= github.com/coreos/go-oidc/v3 v3.0.0/go.mod h1:rEJ/idjfUyfkBit1eI1fvyr+64/g9dcKpAm8MJMesvo= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= @@ -1527,6 +1529,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs= github.com/segmentio/ksuid v1.0.3/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= +github.com/segmentio/ksuid v1.0.4 h1:sBo2BdShXjmcugAMwjugoGUdUV0pcxY5mW4xKRn3v4c= github.com/segmentio/ksuid v1.0.4/go.mod h1:/XUiZBD3kVx5SmUOl55voK5yeAbBNNIed+2O73XgrPE= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -1538,6 +1541,7 @@ github.com/shirou/gopsutil/v3 v3.21.4/go.mod h1:ghfMypLDrFSWN2c9cDYFLHyynQ+QUht0 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sigstore/cosign v1.2.1 h1:xuEhyQ3gE/jzVch1d9WQ4dg9ZbI6x2FSGq78MZm5wX4= github.com/sigstore/cosign v1.2.1/go.mod h1:HjFCBUZd/q5IXxSVf2Qazr0IFSlxqSKv4Y79XoA5to8= +github.com/sigstore/fulcio v0.1.2-0.20210831152525-42f7422734bb h1:smRYK5Ii+6MzPPz6yisB65v2Pam5oHPOTLDlxyM3qYY= github.com/sigstore/fulcio v0.1.2-0.20210831152525-42f7422734bb/go.mod h1:LznI5ABAkquvZrJ1PQaGCgspMfw2CB6ODBCQyhU3Q0w= github.com/sigstore/rekor v0.3.0 h1:OBEvo/Rv8NKKtiWq0WRHgXFpVPe1fGiqz93dfBh/Myo= github.com/sigstore/rekor v0.3.0/go.mod h1:cL9B3+/gp3BG+/bhkSHBA3MQZMten5xM6BhJYd5b5zU= @@ -1555,6 +1559,7 @@ github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic github.com/sirupsen/logrus v1.8.0/go.mod h1:4GuYW9TZmE769R5STWrRakJc4UqQ3+QQ95fyz7ENv1A= github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA= github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM= @@ -2504,6 +2509,7 @@ gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/square/go-jose.v2 v2.2.2/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.3.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= +gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI= gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= gopkg.in/src-d/go-git-fixtures.v3 v3.5.0/go.mod h1:dLBcvytrw/TYZsNTWCnkNF2DSIlzWYqTe3rJR56Ac7g= diff --git a/pkg/cosign/cosign.go b/pkg/cosign/cosign.go index 4b466461e2..6bf1d54426 100644 --- a/pkg/cosign/cosign.go +++ b/pkg/cosign/cosign.go @@ -8,6 +8,7 @@ import ( "fmt" "github.com/in-toto/in-toto-golang/in_toto" "github.com/kyverno/kyverno/pkg/engine/common" + "github.com/sigstore/cosign/cmd/cosign/cli/fulcio" "github.com/sigstore/cosign/pkg/cosign/attestation" "github.com/sigstore/sigstore/pkg/signature/dsse" "strings" @@ -54,7 +55,7 @@ func VerifySignature(imageRef string, key []byte, repository string, log logr.Lo } cosignOpts := &cosign.CheckOpts{ - //RootCerts: fulcio.GetRoots(), + RootCerts: fulcio.GetRoots(), Annotations: map[string]interface{}{}, SigVerifier: pubKey, RegistryClientOpts: []remote.Option{ @@ -81,13 +82,13 @@ func VerifySignature(imageRef string, key []byte, repository string, log logr.Lo if err != nil { msg := err.Error() logger.Info("image verification failed", "error", msg) - if strings.Contains(msg, "NAME_UNKNOWN: repository name not known to registry") { + if strings.Contains(msg, "MANIFEST_UNKNOWN: manifest unknown") { return "", fmt.Errorf("signature not found") } else if strings.Contains(msg, "no matching signatures") { - return "", fmt.Errorf("invalid signature") + return "", fmt.Errorf("signature mismatch") } - return "", errors.Wrap(err, "failed to verify image") + return "", err } digest, err = extractDigest(imageRef, verified, log) @@ -125,7 +126,13 @@ func FetchAttestations(imageRef string, key []byte, repository string) ([]map[st verified, err := client.Verify(context.Background(), ref, cosignOpts) if err != nil { - return nil, errors.Wrap(err, "failed to verify image attestations") + msg := err.Error() + logger.Info("failed to fetch attestations", "error", msg) + if strings.Contains(msg, "MANIFEST_UNKNOWN: manifest unknown") { + return nil, fmt.Errorf("not found") + } + + return nil, err } inTotoStatements, err := decodeStatements(verified) diff --git a/pkg/engine/imageVerify.go b/pkg/engine/imageVerify.go index b678dc333b..fa563777b9 100644 --- a/pkg/engine/imageVerify.go +++ b/pkg/engine/imageVerify.go @@ -57,14 +57,25 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe policyContext.JSONContext.Restore() + if err := LoadContext(logger, rule.Context, policyContext.ResourceCache, policyContext, rule.Name); err != nil { + appendError(resp, rule, fmt.Sprintf("failed to load context: %s", err.Error()), response.RuleStatusError) + continue + } + + ruleCopy, err := substituteVariables(rule, policyContext.JSONContext, logger) + if err != nil { + appendError(resp, rule, fmt.Sprintf("failed to substitute variables: %s", err.Error()), response.RuleStatusError) + continue + } + iv := &imageVerifier{ logger: logger, policyContext: policyContext, - rule: rule, + rule: ruleCopy, resp: resp, } - for _, imageVerify := range rule.VerifyImages { + for _, imageVerify := range ruleCopy.VerifyImages { iv.verify(imageVerify, images.Containers) iv.verify(imageVerify, images.InitContainers) } @@ -73,6 +84,34 @@ func VerifyAndPatchImages(policyContext *PolicyContext) (resp *response.EngineRe return } +func appendError(resp *response.EngineResponse, rule *v1.Rule, msg string, status response.RuleStatus) { + rr := ruleResponse(rule, utils.ImageVerify, msg, status) + resp.PolicyResponse.Rules = append(resp.PolicyResponse.Rules, *rr) + incrementErrorCount(resp) +} + +func substituteVariables(rule *v1.Rule, ctx context.EvalInterface, logger logr.Logger) (*v1.Rule, error) { + + // remove attestations as variables are not substituted in them + ruleCopy := rule.DeepCopy() + for _, iv := range ruleCopy.VerifyImages { + iv.Attestations = nil + } + + var err error + *ruleCopy, err = variables.SubstituteAllInRule(logger, ctx, *ruleCopy) + if err != nil { + return nil, err + } + + // replace attestations + for i := range rule.VerifyImages { + ruleCopy.VerifyImages[i].Attestations = rule.VerifyImages[i].Attestations + } + + return ruleCopy, nil +} + type imageVerifier struct { logger logr.Logger policyContext *PolicyContext diff --git a/pkg/engine/jsonContext.go b/pkg/engine/jsonContext.go index 59c9b70605..474dfdd6ce 100644 --- a/pkg/engine/jsonContext.go +++ b/pkg/engine/jsonContext.go @@ -36,8 +36,8 @@ func LoadContext(logger logr.Logger, contextEntries []kyverno.ContextEntry, resC if trimmedTypedValue := strings.Trim(value, "\n"); strings.Contains(trimmedTypedValue, "\n") { tmp := map[string]interface{}{key: value} tmp = parseMultilineBlockBody(tmp) - new_val, _ := json.Marshal(tmp[key]) - value = string(new_val) + newVal, _ := json.Marshal(tmp[key]) + value = string(newVal) } jsonData := pkgcommon.VariableToJSON(key, value) @@ -238,15 +238,16 @@ func fetchConfigMap(logger logr.Logger, entry kyverno.ContextEntry, lister dynam return data, nil } -// parseMultilineBlockBody recursively iterates through a map and updates its values in the following way -// whenever it encounters a string value containing "\n", -// it converts it into a []string by splitting it by "\n" +// parseMultilineBlockBody recursively iterates through a map and updates its values to a list of strings +// if it encounters a string value containing newline delimiters "\n" and not in PEM format. This is done to +// allow specifying a list with newlines. Since PEM format keys can also contain newlines, an additional check +// is performed to skip splitting those into an array. func parseMultilineBlockBody(m map[string]interface{}) map[string]interface{} { for k, v := range m { switch typedValue := v.(type) { case string: trimmedTypedValue := strings.Trim(typedValue, "\n") - if strings.Contains(trimmedTypedValue, "\n") { + if !pemFormat(trimmedTypedValue) && strings.Contains(trimmedTypedValue, "\n") { m[k] = strings.Split(trimmedTypedValue, "\n") } else { m[k] = trimmedTypedValue // trimming a str if it has trailing newline characters @@ -257,3 +258,8 @@ func parseMultilineBlockBody(m map[string]interface{}) map[string]interface{} { } return m } + +// check for PEM header found in certs and public keys +func pemFormat(s string) bool { + return strings.Contains(s, "-----BEGIN") +} diff --git a/pkg/engine/jsonContext_test.go b/pkg/engine/jsonContext_test.go index 0bb4c65455..950229cd59 100644 --- a/pkg/engine/jsonContext_test.go +++ b/pkg/engine/jsonContext_test.go @@ -51,6 +51,13 @@ func Test_parseMultilineBlockBody(t *testing.T) { expectedMultilineBlockRaw: []byte(`{"key1":"value1","key2":"[\"cluster-admin\", \"cluster-operator\", \"tenant-admin\"]"}`), expectedErr: false, }, + { + multilineBlockRaw: []byte(`{ + "key1": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHMmDjK65krAyDaGaeyWNzgvIu155\nJI50B2vezCw8+3CVeE0lJTL5dbL3OP98Za0oAEBJcOxky8Riy/XcmfKZbw==\n-----END PUBLIC KEY-----" + }`), + expectedMultilineBlockRaw: []byte(`{"key1":"-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEHMmDjK65krAyDaGaeyWNzgvIu155\nJI50B2vezCw8+3CVeE0lJTL5dbL3OP98Za0oAEBJcOxky8Riy/XcmfKZbw==\n-----END PUBLIC KEY-----"}`), + expectedErr: false, + }, } for _, tc := range tcs { diff --git a/pkg/kyverno/common/common.go b/pkg/kyverno/common/common.go index b8474e17a3..def5a92238 100644 --- a/pkg/kyverno/common/common.go +++ b/pkg/kyverno/common/common.go @@ -173,7 +173,7 @@ func PolicyHasVariables(policy v1.ClusterPolicy) [][]string { } // for now forbidden sections are match, exclude and -func ruleForbiddenSectionsHaveVariables(rule v1.Rule) error { +func ruleForbiddenSectionsHaveVariables(rule *v1.Rule) error { var err error err = JSONPatchPathHasVariables(rule.Mutation.PatchesJSON6902) @@ -236,9 +236,15 @@ func objectHasVariables(object interface{}) error { // PolicyHasNonAllowedVariables - checks for unexpected variables in the policy func PolicyHasNonAllowedVariables(policy v1.ClusterPolicy) error { - for _, rule := range policy.Spec.Rules { - var err error + for _, r := range policy.Spec.Rules { + rule := r.DeepCopy() + // do not validate attestation variables as they are based on external data + for _, vi := range rule.VerifyImages { + vi.Attestations = nil + } + + var err error ruleJSON, err := json.Marshal(rule) if err != nil { return err @@ -251,9 +257,9 @@ func PolicyHasNonAllowedVariables(policy v1.ClusterPolicy) error { matchesAll := RegexVariables.FindAllStringSubmatch(string(ruleJSON), -1) matchesAllowed := AllowedVariables.FindAllStringSubmatch(string(ruleJSON), -1) - if (len(matchesAll) > len(matchesAllowed)) && len(rule.Context) == 0 { - return fmt.Errorf("Rule \"%s\" has forbidden variables. Allowed variables are: {{request.*}}, {{serviceAccountName}}, {{serviceAccountNamespace}}, {{@}} and ones defined by the context", rule.Name) + allowed := "{{request.*}}, {{element.*}}, {{serviceAccountName}}, {{serviceAccountNamespace}}, {{@}}, and context variables" + return fmt.Errorf("rule \"%s\" has forbidden variables. Allowed variables are: %s", rule.Name, allowed) } }