1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-15 17:51:20 +00:00
kyverno/pkg/engine/k8smanifest.go
Charles-Edouard Brétéché fb94f6ea75
refactor: introduce engine api package (#6154)
* refactor: introduce engine api package

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

* status

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>

---------

Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
2023-01-30 11:41:09 +00:00

417 lines
14 KiB
Go

package engine
import (
"context"
"crypto/rand"
"crypto/x509"
_ "embed"
"encoding/json"
"fmt"
"math"
"math/big"
"os"
"strconv"
"strings"
"github.com/ghodss/yaml"
"github.com/go-logr/logr"
kyvernov1 "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/auth"
"github.com/kyverno/kyverno/pkg/clients/dclient"
"github.com/kyverno/kyverno/pkg/config"
engineapi "github.com/kyverno/kyverno/pkg/engine/api"
"github.com/pkg/errors"
"github.com/sigstore/k8s-manifest-sigstore/pkg/k8smanifest"
"go.uber.org/multierr"
admissionv1 "k8s.io/api/admission/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
const (
DefaultAnnotationKeyDomain = "cosign.sigstore.dev"
CosignEnvVariable = "COSIGN_EXPERIMENTAL"
)
//go:embed resources/default-config.yaml
var defaultConfigBytes []byte
func processYAMLValidationRule(log logr.Logger, ctx *PolicyContext, rule *kyvernov1.Rule) *engineapi.RuleResponse {
if isDeleteRequest(ctx) {
return nil
}
ruleResp := handleVerifyManifest(ctx, rule, log)
return ruleResp
}
func handleVerifyManifest(ctx *PolicyContext, rule *kyvernov1.Rule, logger logr.Logger) *engineapi.RuleResponse {
verified, reason, err := verifyManifest(ctx, *rule.Validation.Manifests, logger)
if err != nil {
logger.V(3).Info("verifyManifest return err", "error", err.Error())
return ruleError(rule, engineapi.Validation, "error occurred during manifest verification", err)
}
logger.V(3).Info("verifyManifest result", "verified", strconv.FormatBool(verified), "reason", reason)
if !verified {
return ruleResponse(*rule, engineapi.Validation, reason, engineapi.RuleStatusFail)
}
return ruleResponse(*rule, engineapi.Validation, reason, engineapi.RuleStatusPass)
}
func verifyManifest(policyContext *PolicyContext, verifyRule kyvernov1.Manifests, logger logr.Logger) (bool, string, error) {
// load AdmissionRequest
request, err := policyContext.jsonContext.Query("request")
if err != nil {
return false, "", errors.Wrapf(err, "failed to get a request from policyContext")
}
reqByte, _ := json.Marshal(request)
var adreq *admissionv1.AdmissionRequest
err = json.Unmarshal(reqByte, &adreq)
if err != nil {
return false, "", errors.Wrapf(err, "failed to unmarshal a request from requestByte")
}
// unmarshal admission request object
var resource unstructured.Unstructured
objectBytes := adreq.Object.Raw
err = json.Unmarshal(objectBytes, &resource)
if err != nil {
return false, "", errors.Wrapf(err, "failed to Unmarshal a requested object")
}
logger.V(4).Info("verifying manifest", "namespace", adreq.Namespace, "kind", adreq.Kind.Kind,
"name", adreq.Name, "username", adreq.UserInfo.Username)
// allow dryrun request
if adreq.DryRun != nil && *adreq.DryRun {
return true, "allowed because of DryRun request", nil
}
// prepare verifyResource option
vo := &k8smanifest.VerifyResourceOption{}
// adding default ignoreFields from
// github.com/sigstore/k8s-manifest-sigstore/blob/main/pkg/k8smanifest/resources/default-config.yaml
vo = k8smanifest.AddDefaultConfig(vo)
// adding default ignoreFields from pkg/engine/resources/default-config.yaml
vo = addDefaultConfig(vo)
// adding ignoreFields from Policy
for _, i := range verifyRule.IgnoreFields {
converted := k8smanifest.ObjectFieldBinding(i)
vo.IgnoreFields = append(vo.IgnoreFields, converted)
}
// dryrun setting
vo.DisableDryRun = !verifyRule.DryRunOption.Enable
if verifyRule.DryRunOption.Namespace != "" {
vo.DryRunNamespace = verifyRule.DryRunOption.Namespace
} else {
vo.DryRunNamespace = config.KyvernoDryRunNamespace()
}
if !vo.DisableDryRun {
// check if kyverno can 'create' dryrun resource
ok, err := checkDryRunPermission(policyContext.client, adreq.Kind.Kind, vo.DryRunNamespace)
if err != nil {
logger.V(1).Info("failed to check permissions to 'create' resource. disabled DryRun option.", "dryrun namespace", vo.DryRunNamespace, "kind", adreq.Kind.Kind, "error", err.Error())
vo.DisableDryRun = true
}
if !ok {
logger.V(1).Info("kyverno does not have permissions to 'create' resource. disabled DryRun option.", "dryrun namespace", vo.DryRunNamespace, "kind", adreq.Kind.Kind)
vo.DisableDryRun = true
}
// check if kyverno namespace is not used for dryrun
ok = checkDryRunNamespace(vo.DryRunNamespace)
if !ok {
logger.V(1).Info("an inappropriate dryrun namespace is set; set a namespace other than kyverno.", "dryrun namespace", vo.DryRunNamespace)
vo.DisableDryRun = true
}
}
// can be overridden per Attestor
if verifyRule.Repository != "" {
vo.ResourceBundleRef = verifyRule.Repository
}
// signature annotation
// set default annotation domain
if verifyRule.AnnotationDomain != "" && verifyRule.AnnotationDomain != DefaultAnnotationKeyDomain {
vo.AnnotationConfig.AnnotationKeyDomain = verifyRule.AnnotationDomain
}
// signature verification by each attestor
verifiedMsgs := []string{}
for i, attestorSet := range verifyRule.Attestors {
path := fmt.Sprintf(".attestors[%d]", i)
verified, reason, err := verifyManifestAttestorSet(resource, attestorSet, vo, path, string(adreq.UID), logger)
if err != nil {
return verified, reason, err
}
if !verified {
return verified, reason, err
} else {
verifiedMsgs = append(verifiedMsgs, reason)
}
}
msg := fmt.Sprintf("verified manifest signatures; %s", strings.Join(verifiedMsgs, ","))
return true, msg, nil
}
func verifyManifestAttestorSet(resource unstructured.Unstructured, attestorSet kyvernov1.AttestorSet, vo *k8smanifest.VerifyResourceOption, path string, uid string, logger logr.Logger) (bool, string, error) {
verifiedCount := 0
attestorSet = expandStaticKeys(attestorSet)
requiredCount := getRequiredCount(attestorSet)
errorList := []error{}
verifiedMessageList := []string{}
failedMessageList := []string{}
for i, a := range attestorSet.Entries {
var entryError error
var verified bool
var reason string
attestorPath := fmt.Sprintf("%s.entries[%d]", path, i)
if a.Attestor != nil {
nestedAttestorSet, err := kyvernov1.AttestorSetUnmarshal(a.Attestor)
if err != nil {
entryError = errors.Wrapf(err, "failed to unmarshal nested attestor %s", attestorPath)
} else {
attestorPath += ".attestor"
verified, reason, err = verifyManifestAttestorSet(resource, *nestedAttestorSet, vo, attestorPath, uid, logger)
if err != nil {
entryError = errors.Wrapf(err, "failed to verify signature; %s", attestorPath)
}
}
} else {
verified, reason, entryError = k8sVerifyResource(resource, a, vo, attestorPath, uid, i, logger)
}
if entryError != nil {
errorList = append(errorList, entryError)
} else if verified {
// verification success.
verifiedCount++
verifiedMessageList = append(verifiedMessageList, reason)
} else {
failedMessageList = append(failedMessageList, reason)
}
if verifiedCount >= requiredCount {
logger.V(2).Info("manifest verification succeeded", "verifiedCount", verifiedCount, "requiredCount", requiredCount)
reason := fmt.Sprintf("manifest verification succeeded; verifiedCount %d; requiredCount %d; message %s",
verifiedCount, requiredCount, strings.Join(verifiedMessageList, ","))
return true, reason, nil
}
}
if len(errorList) != 0 {
err := multierr.Combine(errorList...)
logger.V(2).Info("manifest verification failed", "verifiedCount", verifiedCount, "requiredCount",
requiredCount, "errors", errorList)
return false, "", err
}
reason := fmt.Sprintf("manifest verification failed; verifiedCount %d; requiredCount %d; message %s",
verifiedCount, requiredCount, strings.Join(failedMessageList, ","))
logger.V(2).Info("manifest verification failed", "verifiedCount", verifiedCount, "requiredCount",
requiredCount, "reason", failedMessageList)
return false, reason, nil
}
func k8sVerifyResource(resource unstructured.Unstructured, a kyvernov1.Attestor, vo *k8smanifest.VerifyResourceOption, attestorPath, uid string, i int, logger logr.Logger) (bool, string, error) {
// check annotations
if a.Annotations != nil {
mnfstAnnotations := resource.GetAnnotations()
err := checkManifestAnnotations(mnfstAnnotations, a.Annotations)
if err != nil {
return false, "", err
}
}
// build verify option
vo, subPath, envVariables, err := buildVerifyResourceOptionsAndPath(a, vo, uid, i)
// unset env variables after verification
defer cleanEnvVariables(envVariables)
if err != nil {
logger.V(4).Info("failed to build verify option", err.Error())
return false, "", errors.Wrapf(err, attestorPath+subPath)
}
logger.V(4).Info("verifying resource by k8s-manifest-sigstore")
result, err := k8smanifest.VerifyResource(resource, vo)
if err != nil {
logger.V(4).Info("verifyResoource return err", err.Error())
if k8smanifest.IsSignatureNotFoundError(err) {
// no signature found
failReason := fmt.Sprintf("%s: %s", attestorPath+subPath, err.Error())
return false, failReason, nil
} else if k8smanifest.IsMessageNotFoundError(err) {
// no signature and message found
failReason := fmt.Sprintf("%s: %s", attestorPath+subPath, err.Error())
return false, failReason, nil
} else {
return false, "", errors.Wrapf(err, attestorPath+subPath)
}
} else {
resBytes, _ := json.Marshal(result)
logger.V(4).Info("verify result", string(resBytes))
if result.Verified {
// verification success.
reason := fmt.Sprintf("singed by a valid signer: %s", result.Signer)
return true, reason, nil
} else {
failReason := fmt.Sprintf("%s: %s", attestorPath+subPath, "failed to verify signature.")
if result.Diff != nil && result.Diff.Size() > 0 {
failReason = fmt.Sprintf("%s: failed to verify signature. diff found; %s", attestorPath+subPath, result.Diff.String())
} else if result.Signer != "" {
failReason = fmt.Sprintf("%s: no signer matches with this resource. signed by %s", attestorPath+subPath, result.Signer)
}
return false, failReason, nil
}
}
}
func buildVerifyResourceOptionsAndPath(a kyvernov1.Attestor, vo *k8smanifest.VerifyResourceOption, uid string, i int) (*k8smanifest.VerifyResourceOption, string, []string, error) {
subPath := ""
var entryError error
envVariables := []string{}
if a.Keys != nil {
subPath = subPath + ".keys"
Key := a.Keys.PublicKeys
if strings.HasPrefix(Key, "-----BEGIN PUBLIC KEY-----") || strings.HasPrefix(Key, "-----BEGIN PGP PUBLIC KEY BLOCK-----") {
// prepare env variable for pubkey
// it consists of admission request ID, key index and random num
n, _ := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64)))
pubkeyEnv := fmt.Sprintf("_PK_%s_%d_%d", uid, i, n)
err := os.Setenv(pubkeyEnv, Key)
envVariables = append(envVariables, pubkeyEnv)
if err != nil {
entryError = errors.Wrapf(err, "failed to set env variable; %s", pubkeyEnv)
} else {
keyPath := fmt.Sprintf("env://%s", pubkeyEnv)
vo.KeyPath = keyPath
}
} else {
// this supports Kubernetes secrets and kms
vo.KeyPath = Key
}
if a.Keys.Rekor != nil {
vo.RekorURL = a.Keys.Rekor.URL
}
} else if a.Certificates != nil {
subPath = subPath + ".certificates"
if a.Certificates.Certificate != "" {
Cert := a.Certificates.Certificate
n, _ := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64)))
certEnv := fmt.Sprintf("_CERT_%s_%d_%d", uid, i, n)
err := os.Setenv(certEnv, Cert)
envVariables = append(envVariables, certEnv)
if err != nil {
entryError = errors.Wrapf(err, "failed to set env variable; %s", certEnv)
} else {
certPath := fmt.Sprintf("env://%s", certEnv)
vo.Certificate = certPath
}
}
if a.Certificates.CertificateChain != "" {
CertChain := a.Certificates.CertificateChain
n, _ := rand.Int(rand.Reader, big.NewInt(int64(math.MaxInt64)))
certChainEnv := fmt.Sprintf("_CC_%s_%d_%d", uid, i, n)
err := os.Setenv(certChainEnv, CertChain)
envVariables = append(envVariables, certChainEnv)
if err != nil {
entryError = errors.Wrapf(err, "failed to set env variable; %s", certChainEnv)
} else {
certChainPath := fmt.Sprintf("env://%s", certChainEnv)
vo.CertificateChain = certChainPath
}
}
if a.Certificates.Rekor != nil {
vo.RekorURL = a.Keys.Rekor.URL
}
} else if a.Keyless != nil {
subPath = subPath + ".keyless"
_ = os.Setenv(CosignEnvVariable, "1")
envVariables = append(envVariables, CosignEnvVariable)
if a.Keyless.Rekor != nil {
vo.RekorURL = a.Keyless.Rekor.URL
}
if a.Keyless.Roots != "" {
Roots := a.Keyless.Roots
cp, err := loadCertPool([]byte(Roots))
if err != nil {
entryError = errors.Wrap(err, "failed to load Root certificates")
} else {
vo.RootCerts = cp
}
}
Issuer := a.Keyless.Issuer
vo.OIDCIssuer = Issuer
Subject := a.Keyless.Subject
vo.Signers = k8smanifest.SignerList{Subject}
}
if a.Repository != "" {
vo.ResourceBundleRef = a.Repository
}
return vo, subPath, envVariables, entryError
}
func cleanEnvVariables(envVariables []string) {
for _, ev := range envVariables {
os.Unsetenv(ev)
}
}
func addConfig(vo, defaultConfig *k8smanifest.VerifyResourceOption) *k8smanifest.VerifyResourceOption {
if vo == nil {
return nil
}
ignoreFields := []k8smanifest.ObjectFieldBinding(vo.IgnoreFields)
ignoreFields = append(ignoreFields, []k8smanifest.ObjectFieldBinding(defaultConfig.IgnoreFields)...)
vo.IgnoreFields = ignoreFields
return vo
}
func loadDefaultConfig() *k8smanifest.VerifyResourceOption {
var defaultConfig *k8smanifest.VerifyResourceOption
err := yaml.Unmarshal(defaultConfigBytes, &defaultConfig)
if err != nil {
return nil
}
return defaultConfig
}
func addDefaultConfig(vo *k8smanifest.VerifyResourceOption) *k8smanifest.VerifyResourceOption {
dvo := loadDefaultConfig()
return addConfig(vo, dvo)
}
func loadCertPool(roots []byte) (*x509.CertPool, error) {
cp := x509.NewCertPool()
if !cp.AppendCertsFromPEM(roots) {
return nil, fmt.Errorf("error creating root cert pool")
}
return cp, nil
}
func checkManifestAnnotations(mnfstAnnotations map[string]string, annotations map[string]string) error {
for key, val := range annotations {
if val != mnfstAnnotations[key] {
return fmt.Errorf("annotations mismatch: %s does not match expected value %s for key %s",
mnfstAnnotations[key], val, key)
}
}
return nil
}
func checkDryRunPermission(dclient dclient.Interface, kind, namespace string) (bool, error) {
canI := auth.NewCanI(dclient.Discovery(), dclient.GetKubeClient().AuthorizationV1().SelfSubjectAccessReviews(), kind, namespace, "create", "")
ok, err := canI.RunAccessCheck(context.TODO())
if err != nil {
return false, err
}
return ok, nil
}
func checkDryRunNamespace(namespace string) bool {
// should not use kyverno namespace for dryrun
if namespace != config.KyvernoNamespace() {
return true
} else {
return false
}
}