Skip to content

Commit

Permalink
add attestation to the build reconciler
Browse files Browse the repository at this point in the history
- the controller is now aware of its own service account name, this is
  to fetch all the secrets associated with it (currently for attestation
  only, but can be expanded to cosign later)

- the feature flag for enabling slsa is marked experimental since the
  RFC hasn't been merged in yet

Signed-off-by: Bohan Chen <[email protected]>
  • Loading branch information
chenbh committed Dec 15, 2023
1 parent 042c6b2 commit 755cdc1
Show file tree
Hide file tree
Showing 10 changed files with 1,089 additions and 82 deletions.
23 changes: 21 additions & 2 deletions cmd/controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import (
"github.com/pivotal/kpack/pkg/reconciler/sourceresolver"
"github.com/pivotal/kpack/pkg/registry"
"github.com/pivotal/kpack/pkg/secret"
"github.com/pivotal/kpack/pkg/slsa"
)

const (
Expand All @@ -85,11 +86,14 @@ func main() {
flag.StringVar(&images.CompletionWindowsImage, "completion-windows-image", os.Getenv("COMPLETION_WINDOWS_IMAGE"), "The image used to finish a build on windows")
flag.StringVar(&images.BuildWaiterImage, "build-waiter-image", os.Getenv("BUILD_WAITER_IMAGE"), "The image used to initialize a build")

flag.StringVar(&cfg.SystemNamespace, "system-namespace", os.Getenv("SYSTEM_NAMESPACE"), "Namespace for the the controller, this will be used to lookup secrets for image signing and attestation.")
flag.StringVar(&cfg.SystemServiceAccount, "system-service-account", os.Getenv("SYSTEM_SERVICE_ACCOUNT"), "Service account for the the controller, this will be used to lookup secrets for image signing and attestation.")
flag.BoolVar(&cfg.EnablePriorityClasses, "enable-priority-classes", flaghelpers.GetEnvBool("ENABLE_PRIORITY_CLASSES", false), "if set to true, enables different pod priority classes for normal builds and automated builds")
flag.StringVar(&cfg.MaximumPlatformApiVersion, "maximum-platform-api-version", os.Getenv("MAXIMUM_PLATFORM_API_VERSION"), "The maximum allowed platform api version a build can utilize")
flag.BoolVar(&cfg.SshTrustUnknownHosts, "insecure-ssh-trust-unknown-hosts", flaghelpers.GetEnvBool("INSECURE_SSH_TRUST_UNKNOWN_HOSTS", true), "if set to true, automatically trust unknown hosts when using git ssh source")

flag.BoolVar(&featureFlags.InjectedSidecarSupport, "injected-sidecar-support", flaghelpers.GetEnvBool("INJECTED_SIDECAR_SUPPORT", false), "if set to true, all builds will execute in standard containers instead of init containers to support injected sidecars")
flag.BoolVar(&featureFlags.GenerateSlsaAttestation, "experimental-generate-slsa-attestation", flaghelpers.GetEnvBool("EXPERIMENTAL_GENERATE_SLSA_ATTESTATION", false), "if set to true, SLSA attestations will be generated for each build")

flag.Parse()

Expand Down Expand Up @@ -205,9 +209,24 @@ func main() {
K8sClient: k8sClient,
}

secretFetcher := &secret.Fetcher{Client: k8sClient}
slsaAttester := slsa.Attester{
Version: cmd.Identifer,

buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, featureFlags.InjectedSidecarSupport)
LifecycleProvider: lifecycleProvider,
ImageReader: slsa.NewImageReader(&registry.Client{}),

Images: images,
Features: featureFlags,
Config: cfg,
}

secretFetcher := &secret.Fetcher{
Client: k8sClient,
SystemNamespace: cfg.SystemNamespace,
SystemServiceAccountName: cfg.SystemServiceAccount,
}

buildController := build.NewController(ctx, options, k8sClient, buildInformer, podInformer, metadataRetriever, buildpodGenerator, podProgressLogger, keychainFactory, &slsaAttester, secretFetcher, featureFlags)
imageController := image.NewController(ctx, options, k8sClient, imageInformer, buildInformer, duckBuilderInformer, sourceResolverInformer, pvcInformer, cfg.EnablePriorityClasses)
sourceResolverController := sourceresolver.NewController(ctx, options, sourceResolverInformer, gitResolver, blobResolver, registryResolver)
builderController, builderResync := builder.NewController(ctx, options, builderInformer, builderCreator, keychainFactory, clusterStoreInformer, buildpackInformer, clusterBuildpackInformer, clusterStackInformer, secretFetcher)
Expand Down
4 changes: 4 additions & 0 deletions config/controller.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ spec:
value: "false"
- name: INJECTED_SIDECAR_SUPPORT
value: "false"
- name: EXPERIMENTAL_GENERATE_SLSA_ATTESTATION
value: "false"
- name: INSECURE_SSH_TRUST_UNKNOWN_HOSTS
value: "true"
- name: CONFIG_LOGGING_NAME
Expand All @@ -110,6 +112,8 @@ spec:
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: SYSTEM_SERVICE_ACCOUNT
value: controller
- name: BUILD_INIT_IMAGE
valueFrom:
configMapKeyRef:
Expand Down
27 changes: 14 additions & 13 deletions pkg/apis/build/v1alpha2/build_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ type BuildSpec struct {
Cosign *CosignConfig `json:"cosign,omitempty"`
DefaultProcess string `json:"defaultProcess,omitempty"`
// +listType
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
Affinity *corev1.Affinity `json:"affinity,omitempty"`
RuntimeClassName *string `json:"runtimeClassName,omitempty"`
SchedulerName string `json:"schedulerName,omitempty"`
PriorityClassName string `json:"priorityClassName,omitempty"`
CreationTime string `json:"creationTime,omitempty"`
Tolerations []corev1.Toleration `json:"tolerations,omitempty"`
NodeSelector map[string]string `json:"nodeSelector,omitempty"`
Affinity *corev1.Affinity `json:"affinity,omitempty"`
RuntimeClassName *string `json:"runtimeClassName,omitempty"`
SchedulerName string `json:"schedulerName,omitempty"`
PriorityClassName string `json:"priorityClassName,omitempty"`
CreationTime string `json:"creationTime,omitempty"`
}

func (bs *BuildSpec) RegistryCacheTag() string {
Expand Down Expand Up @@ -129,12 +129,13 @@ type BuildStack struct {

// +k8s:openapi-gen=true
type BuildStatus struct {
corev1alpha1.Status `json:",inline"`
BuildMetadata corev1alpha1.BuildpackMetadataList `json:"buildMetadata,omitempty"`
Stack corev1alpha1.BuildStack `json:"stack,omitempty"`
LatestImage string `json:"latestImage,omitempty"`
LatestCacheImage string `json:"latestCacheImage,omitempty"`
PodName string `json:"podName,omitempty"`
corev1alpha1.Status `json:",inline"`
BuildMetadata corev1alpha1.BuildpackMetadataList `json:"buildMetadata,omitempty"`
Stack corev1alpha1.BuildStack `json:"stack,omitempty"`
LatestImage string `json:"latestImage,omitempty"`
LatestCacheImage string `json:"latestCacheImage,omitempty"`
LatestAttestationImage string `json:"latestAttestationImage,omitempty"`
PodName string `json:"podName,omitempty"`
// +listType
StepStates []corev1.ContainerState `json:"stepStates,omitempty"`
// +listType
Expand Down
6 changes: 5 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ package config
import "github.com/pivotal/kpack/pkg/apis/build/v1alpha2"

type Config struct {
SystemNamespace string `json:"systemNamespace"`
SystemServiceAccount string `json:"systemServiceAccount"`

EnablePriorityClasses bool `json:"enablePriorityClasses"`
MaximumPlatformApiVersion string `json:"maximumPlatformApiVersion"`
SshTrustUnknownHosts bool `json:"sshTrustUnknownHosts"`
}

type FeatureFlags struct {
InjectedSidecarSupport bool `json:"injectedSidecarSupport"`
InjectedSidecarSupport bool `json:"injectedSidecarSupport"`
GenerateSlsaAttestation bool `json:"generateSlsaAttestation"`
}

type Images struct {
Expand Down
207 changes: 175 additions & 32 deletions pkg/reconciler/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,11 @@ package build
import (
"context"
"encoding/json"
"fmt"

"github.com/google/go-containerregistry/pkg/authn"
buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2"
corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1"
"github.com/pivotal/kpack/pkg/buildchange"
"github.com/pivotal/kpack/pkg/buildpod"
"github.com/pivotal/kpack/pkg/client/clientset/versioned"
buildinformers "github.com/pivotal/kpack/pkg/client/informers/externalversions/build/v1alpha2"
buildlisters "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha2"
"github.com/pivotal/kpack/pkg/cnb"
"github.com/pivotal/kpack/pkg/reconciler"
"github.com/pivotal/kpack/pkg/registry"
ggcrv1 "github.com/google/go-containerregistry/pkg/v1"
intoto "github.com/in-toto/in-toto-golang/in_toto"
"github.com/pkg/errors"
"go.uber.org/zap"
corev1 "k8s.io/api/core/v1"
Expand All @@ -27,6 +21,20 @@ import (
"k8s.io/client-go/tools/cache"
"knative.dev/pkg/controller"
"knative.dev/pkg/logging/logkey"

buildapi "github.com/pivotal/kpack/pkg/apis/build/v1alpha2"
corev1alpha1 "github.com/pivotal/kpack/pkg/apis/core/v1alpha1"
"github.com/pivotal/kpack/pkg/buildchange"
"github.com/pivotal/kpack/pkg/buildpod"
"github.com/pivotal/kpack/pkg/client/clientset/versioned"
buildinformers "github.com/pivotal/kpack/pkg/client/informers/externalversions/build/v1alpha2"
buildlisters "github.com/pivotal/kpack/pkg/client/listers/build/v1alpha2"
"github.com/pivotal/kpack/pkg/cnb"
"github.com/pivotal/kpack/pkg/config"
"github.com/pivotal/kpack/pkg/reconciler"
"github.com/pivotal/kpack/pkg/registry"
"github.com/pivotal/kpack/pkg/secret"
"github.com/pivotal/kpack/pkg/slsa"
)

const (
Expand All @@ -49,17 +57,41 @@ type PodProgressLogger interface {
GetTerminationMessage(pod *corev1.Pod, s *corev1.ContainerStatus) (string, error)
}

func NewController(ctx context.Context, opt reconciler.Options, k8sClient k8sclient.Interface, informer buildinformers.BuildInformer, podInformer corev1Informers.PodInformer, metadataRetriever MetadataRetriever, podGenerator PodGenerator, podProgressLogger *buildchange.ProgressLogger, keychainFactory registry.KeychainFactory, injectedSidecarSupport bool) *controller.Impl {
//go:generate counterfeiter . SLSAAttester
type SLSAAttester interface {
GenerateStatement(build *buildapi.Build, buildMetadata *cnb.BuildMetadata, pod *corev1.Pod, builderAndAppKeychain authn.Keychain, builderID slsa.BuilderID, depFns ...slsa.BuilderDependencyFn) (intoto.Statement, error)
Sign(ctx context.Context, stmt intoto.Statement, signers ...slsa.Signer) ([]byte, error)
Write(ctx context.Context, digestStr string, payload []byte, keychain authn.Keychain) (ggcrv1.Image, string, error)
}

//go:generate counterfeiter . SecretFetcher
type SecretFetcher interface {
SecretsForServiceAccount(ctx context.Context, serviceAccount, namespace string) ([]*corev1.Secret, error)
SecretsForSystemServiceAccount(context.Context) ([]*corev1.Secret, error)
}

func NewController(
ctx context.Context, opt reconciler.Options, k8sClient k8sclient.Interface,
informer buildinformers.BuildInformer, podInformer corev1Informers.PodInformer,
metadataRetriever MetadataRetriever,
podGenerator PodGenerator, podProgressLogger *buildchange.ProgressLogger,
keychainFactory registry.KeychainFactory,
attester SLSAAttester,
secretFetcher SecretFetcher,
featureFlags config.FeatureFlags,
) *controller.Impl {
c := &Reconciler{
Client: opt.Client,
K8sClient: k8sClient,
MetadataRetriever: metadataRetriever,
Lister: informer.Lister(),
PodLister: podInformer.Lister(),
PodGenerator: podGenerator,
PodProgressLogger: podProgressLogger,
KeychainFactory: keychainFactory,
InjectedSidecarSupport: injectedSidecarSupport,
Client: opt.Client,
K8sClient: k8sClient,
MetadataRetriever: metadataRetriever,
Lister: informer.Lister(),
PodLister: podInformer.Lister(),
PodGenerator: podGenerator,
PodProgressLogger: podProgressLogger,
KeychainFactory: keychainFactory,
Attester: attester,
SecretFetcher: secretFetcher,
FeatureFlags: featureFlags,
}

logger := opt.Logger.With(
Expand All @@ -79,15 +111,17 @@ func NewController(ctx context.Context, opt reconciler.Options, k8sClient k8scli
}

type Reconciler struct {
Client versioned.Interface
KeychainFactory registry.KeychainFactory
Lister buildlisters.BuildLister
MetadataRetriever MetadataRetriever
K8sClient k8sclient.Interface
PodLister v1Listers.PodLister
PodGenerator PodGenerator
PodProgressLogger PodProgressLogger
InjectedSidecarSupport bool
Client versioned.Interface
KeychainFactory registry.KeychainFactory
Lister buildlisters.BuildLister
MetadataRetriever MetadataRetriever
K8sClient k8sclient.Interface
PodLister v1Listers.PodLister
PodGenerator PodGenerator
PodProgressLogger PodProgressLogger
Attester SLSAAttester
SecretFetcher SecretFetcher
FeatureFlags config.FeatureFlags
}

func (c *Reconciler) Reconcile(ctx context.Context, key string) error {
Expand Down Expand Up @@ -128,7 +162,7 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error
return controller.NewPermanentError(err)
}

if c.InjectedSidecarSupport {
if c.FeatureFlags.InjectedSidecarSupport {
pod, err = c.setBuildReady(ctx, pod)
if err != nil {
return err
Expand All @@ -154,7 +188,7 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error
})

if err != nil {
return errors.Wrap(err, "unable to create app image keychain")
return fmt.Errorf("unable to create app image keychain: %v", err)
}

buildMetadata, err = c.MetadataRetriever.GetBuildMetadata(build.Tag(), cacheTag, keychain)
Expand All @@ -164,12 +198,22 @@ func (c *Reconciler) reconcile(ctx context.Context, build *buildapi.Build) error
} else {
buildMetadata, err = c.buildMetadataFromBuildPod(pod)
if err != nil {
return errors.Wrap(err, "failed to get build metadata from build pod")
return fmt.Errorf("failed to get build metadata from build pod: %v", err)
}
}

var attestDigest string
if c.FeatureFlags.GenerateSlsaAttestation {
attestDigest, err = c.attestBuild(ctx, build, buildMetadata, pod)
if err != nil {
return fmt.Errorf("attesting build: %v", err)
}
}

build.Status.BuildMetadata = buildMetadata.BuildpackMetadata
build.Status.LatestImage = buildMetadata.LatestImage
build.Status.LatestCacheImage = buildMetadata.LatestCacheImage
build.Status.LatestAttestationImage = attestDigest
build.Status.Stack.RunImage = buildMetadata.StackRunImage
build.Status.Stack.ID = buildMetadata.StackID
}
Expand Down Expand Up @@ -358,6 +402,105 @@ func (c *Reconciler) buildMetadataFromBuildPod(pod *corev1.Pod) (*cnb.BuildMetad
return nil, errors.New(buildapi.CompletionContainerName + " container not found")
}

func (c *Reconciler) attestBuild(ctx context.Context, build *buildapi.Build, buildMetadata *cnb.BuildMetadata, pod *corev1.Pod) (string, error) {
keychain, err := c.KeychainFactory.KeychainForSecretRef(ctx, registry.SecretRef{
ServiceAccount: build.Spec.ServiceAccountName,
Namespace: build.Namespace,
ImagePullSecrets: build.Spec.Builder.ImagePullSecrets,
})
if err != nil {
return "", err
}

controllerSecrets, err := c.SecretFetcher.SecretsForSystemServiceAccount(ctx)
if err != nil {
return "", fmt.Errorf("getting controller secrets: %v", err)
}

buildSecrets, err := c.SecretFetcher.SecretsForServiceAccount(ctx, build.ServiceAccount(), build.Namespace)
if err != nil {
return "", fmt.Errorf("getting service account secrets: %v", err)
}

secrets := append(controllerSecrets, buildSecrets...)
signingKeys, err := secret.FilterAndExtractSLSASecrets(secrets)
if err != nil {
return "", fmt.Errorf("parsing slsa secrets: %v", err)
}

signers := make([]slsa.Signer, len(signingKeys))
for i, key := range signingKeys {
var s slsa.Signer
switch key.Type {
case secret.CosignKeyType:
s, err = slsa.NewCosignSigner(key.Key, key.Password, key.SecretName)
case secret.PKCS8KeyType:
s, err = slsa.NewPKCS8Signer(key.Key, key.SecretName)
}
if err != nil {
return "", fmt.Errorf("creating signer: %v", err)
}
signers[i] = s
}

buildId := slsa.UnsignedBuildID
if len(signers) > 0 {
buildId = slsa.SignedBuildID
}

deps, err := c.attestBuildDeps(ctx, build, pod, secrets)
if err != nil {
return "", fmt.Errorf("gathering build deps: %v", err)
}

statement, err := c.Attester.GenerateStatement(build, buildMetadata, pod, keychain, buildId, deps...)
if err != nil {
return "", fmt.Errorf("generating statement: %v", err)
}

payload, err := c.Attester.Sign(ctx, statement, signers...)
if err != nil {
return "", fmt.Errorf("signing statement: %v", err)
}

_, digest, err := c.Attester.Write(ctx, buildMetadata.LatestImage, payload, keychain)
if err != nil {
return "", fmt.Errorf("writting attestation: %v", err)
}

return digest, nil
}

func (c *Reconciler) attestBuildDeps(ctx context.Context, build *buildapi.Build, pod *corev1.Pod, secrets []*corev1.Secret) ([]slsa.BuilderDependencyFn, error) {
ns, err := c.K8sClient.CoreV1().Namespaces().Get(ctx, build.Namespace, metav1.GetOptions{})
if err != nil {
return nil, err
}

sa, err := c.K8sClient.CoreV1().ServiceAccounts(build.Namespace).Get(ctx, build.ServiceAccount(), metav1.GetOptions{})
if err != nil {
return nil, err
}

deps := []slsa.BuilderDependencyFn{
slsa.WithVersionedObject(ns),
slsa.WithVersionedObject(build),
slsa.WithVersionedObject(pod),
slsa.WithVersionedObject(sa),
}

attestSecrets := make([]slsa.K8sObject, len(secrets))
for i, v := range secrets {
attestSecrets[i] = slsa.K8sObject(v)
}

if len(attestSecrets) != 0 {
deps = append(deps, slsa.WithVersionedObjects(attestSecrets))
}

return deps, nil
}

func contains(arr []string, s string) bool {
for _, item := range arr {
if s == item {
Expand Down
Loading

0 comments on commit 755cdc1

Please sign in to comment.