Skip to content

Commit

Permalink
[TEP-0089] SPIRE for non-falsifiable provenance.
Browse files Browse the repository at this point in the history
This PR is a part of a larger set of PRs to provide non-falsifiable
provenance through SPIRE.
In particular this PR uses the SPIRE infrastructure which has already
been merged to sign TaskRunStatus.
It also has support to verify if TaskRunStatus has been modified by
another workload between reconciles.

Update pkg/pod/pod.go

Co-authored-by: Jerop Kipruto <jerop@google.com>

Update pkg/pod/pod.go

Co-authored-by: Jerop Kipruto <jerop@google.com>

Update pkg/pod/pod.go

Co-authored-by: Jerop Kipruto <jerop@google.com>
  • Loading branch information
jagathprakash and jerop committed Mar 16, 2023
1 parent 03512d8 commit 12de7ed
Show file tree
Hide file tree
Showing 25 changed files with 2,363 additions and 64 deletions.
256 changes: 251 additions & 5 deletions docs/spire.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,58 @@ weight: 1660
-->
⚠️ This is a work in progress: SPIRE support is not yet functional

TaskRun result attestations is currently an alpha experimental feature. Currently all that is implemented is support for configuring Tekton to connect to SPIRE. See TEP-0089 for details on the overall design and feature set.
TaskRun result attestations is currently an alpha experimental feature. Currently all that is implemented is support for configuring Tekton to connect to SPIRE and enabling TaskRun to sign and verify the TaskRunStatus. See [TEP-0089](https://github.com/tektoncd/community/blob/main/teps/0089-nonfalsifiable-provenance-support.md) for details on the overall design and feature set.

This being a large feature, this will be implemented in the following phases. This document will be updated as we implement new phases.
1. Add a client for SPIRE (done).
2. Add a configMap which initializes SPIRE (in progress).
3. Modify TaskRun to sign and verify TaskRun Results using SPIRE.
4. Modify Tekton Chains to verify the TaskRun Results.
2. Add a configMap which initializes SPIRE (done).
3. Modify TaskRun to sign and verify TaskRunStatus using SPIRE (done).
4. Enabling Chains to verify the TaskRun Results.

When the TaskRun result attestations feature is [enabled](./spire.md#enabling-taskrun-result-attestations) all TaskRuns will produce a signature alongside its results, which can then be used to validate its provenance. For example, a TaskRun result that creates user-specified results `commit` and `url` would look like the following. `SVID`, `RESULT_MANIFEST`, `RESULT_MANIFEST.sig`, `commit.sig` and `url.sig` are generated attestations by the integration of SPIRE and Tekton Controller.

Parsed, the fields would be:
```
...
<truncated>
...
📝 Results
NAME VALUE
∙ RESULT_MANIFEST commit,url,SVID,commit.sig,url.sig
∙ RESULT_MANIFEST.sig MEUCIQD55MMII9SEk/esQvwNLGC43y7efNGZ+7fsTdq+9vXYFAIgNoRW7cV9WKriZkcHETIaAKqfcZVJfsKbEmaDyohDSm4=
∙ SVID -----BEGIN CERTIFICATE-----
MIICGzCCAcGgAwIBAgIQH9VkLxKkYMidPIsofckRQTAKBggqhkjOPQQDAjAeMQsw
CQYDVQQGEwJVUzEPMA0GA1UEChMGU1BJRkZFMB4XDTIyMDIxMTE2MzM1MFoXDTIy
MDIxMTE3MzQwMFowHTELMAkGA1UEBhMCVVMxDjAMBgNVBAoTBVNQSVJFMFkwEwYH
KoZIzj0CAQYIKoZIzj0DAQcDQgAEBRdg3LdxVAELeH+lq8wzdEJd4Gnt+m9G0Qhy
NyWoPmFUaj9vPpvOyRgzxChYnW0xpcDWihJBkq/EbusPvQB8CKOB4TCB3jAOBgNV
HQ8BAf8EBAMCA6gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1Ud
EwEB/wQCMAAwHQYDVR0OBBYEFID7ARM5+vwzvnLPMO7Icfnj7l7hMB8GA1UdIwQY
MBaAFES3IzpGDqgV3QcQNgX8b/MBwyAtMF8GA1UdEQRYMFaGVHNwaWZmZTovL2V4
YW1wbGUub3JnL25zL2RlZmF1bHQvdGFza3J1bi9jYWNoZS1pbWFnZS1waXBlbGlu
ZXJ1bi04ZHE5Yy1mZXRjaC1mcm9tLWdpdDAKBggqhkjOPQQDAgNIADBFAiEAi+LR
JkrZn93PZPslaFmcrQw3rVcEa4xKmPleSvQaBoACIF1QB+q1uwH6cNvWdbLK9g+W
T9Np18bK0xc6p5SuTM2C
-----END CERTIFICATE-----
∙ commit aa79de59c4bae24e32f15fda467d02ae9cd94b01
∙ commit.sig MEQCIEJHk+8B+mCFozp0F52TQ1AadlhEo1lZNOiOnb/ht71aAiBCE0otKB1R0BktlPvweFPldfZfjG0F+NUSc2gPzhErzg==
∙ url https://github.com/buildpacks/samples
∙ url.sig MEUCIF0Fuxr6lv1MmkreqDKcPH3m+eXp+gY++VcxWgGCx7T1AiEA9U/tROrKuCGfKApLq2A9EModbdoGXyQXFOpAa0aMpOg=
```

However, the verification materials are removed from the final results as part of the TaskRun status. It is stored in the termination messages (more details below):

```
$ tkn tr describe cache-image-pipelinerun-8dq9c-fetch-from-git
...
<truncated>
...
📝 Results
NAME VALUE
∙ commit aa79de59c4bae24e32f15fda467d02ae9cd94b01
∙ url https://github.com/buildpacks/samples
```

## Architecture Overview

Expand Down Expand Up @@ -64,7 +109,7 @@ When a TaskRun is created:
## Enabling TaskRun result attestations

To enable TaskRun attestations:
1. Make sure `enforce-nonfalsifiability` is set to `"spire"` in the `feature-flags` configmap, see [`install.md`](./install.md#customizing-the-pipelines-controller-behavior) for details
1. Make sure `enforce-nonfalsifiability` is set to `"spire"` in the `feature-flags` configmap, see [`additional-configs.md`](./additional-configs.md#customizing-the-pipelines-controller-behavior) for details
1. Create a SPIRE deployment containing a SPIRE server, SPIRE agents and the SPIRE CSI driver, for convenience, [this sample single cluster deployment](https://github.com/spiffe/spiffe-csi/tree/main/example/config) can be used.
1. Register the SPIRE workload entry for Tekton with the "Admin" flag, which will allow the Tekton controller to communicate with the SPIRE server to manage the TaskRun identities dynamically.
```
Expand Down Expand Up @@ -127,3 +172,204 @@ To enable TaskRun attestations:
# spire-node-alias-prefix specifies the SPIRE node alias prefix to use.
spire-node-alias-prefix: "/tekton-node/"
```
## Sample TaskRun attestation
The following example shows how this feature works:
```yaml
kind: TaskRun
apiVersion: tekton.dev/v1beta1
metadata:
name: non-falsifiable-provenance
spec:
timeout: 60s
taskSpec:
steps:
- name: non-falsifiable
image: ubuntu
script: |
#!/usr/bin/env bash
printf "%s" "hello" > "$(results.foo.path)"
printf "%s" "world" > "$(results.bar.path)"
results:
- name: foo
- name: bar
```


The termination message is:
```
message: '[{"key":"RESULT_MANIFEST","value":"foo,bar","type":1},{"key":"RESULT_MANIFEST.sig","value":"MEQCIB4grfqBkcsGuVyoQd9KUVzNZaFGN6jQOKK90p5HWHqeAiB7yZerDA+YE3Af/ALG43DQzygiBpKhTt8gzWGmpvXJFw==","type":1},{"key":"SVID","value":"-----BEGIN
CERTIFICATE-----\nMIICCjCCAbCgAwIBAgIRALH94zAZZXdtPg97O5vG5M0wCgYIKoZIzj0EAwIwHjEL\nMAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMjAzMTQxNTUzNTlaFw0y\nMjAzMTQxNjU0MDlaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG\nByqGSM49AgEGCCqGSM49AwEHA0IABPLzFTDY0RDpjKb+eZCIWgUw9DViu8/pM8q7\nHMTKCzlyGqhaU80sASZfpkZvmi72w+gLszzwVI1ZNU5e7aCzbtSjgc8wgcwwDgYD\nVR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV\nHRMBAf8EAjAAMB0GA1UdDgQWBBSsUvspy+/Dl24pA1f+JuNVJrjgmTAfBgNVHSME\nGDAWgBSOMyOHnyLLGxPSD9RRFL+Yhm/6qzBNBgNVHREERjBEhkJzcGlmZmU6Ly9l\neGFtcGxlLm9yZy9ucy9kZWZhdWx0L3Rhc2tydW4vbm9uLWZhbHNpZmlhYmxlLXBy\nb3ZlbmFuY2UwCgYIKoZIzj0EAwIDSAAwRQIhAM4/bPAH9dyhBEj3DbwtJKMyEI56\n4DVrP97ps9QYQb23AiBiXWrQkvRYl0h4CX0lveND2yfqLrGdVL405O5NzCcUrA==\n-----END
CERTIFICATE-----\n","type":1},{"key":"bar","value":"world","type":1},{"key":"bar.sig","value":"MEUCIQDOtg+aEP1FCr6/FsHX+bY1d5abSQn2kTiUMg4Uic2lVQIgTVF5bbT/O77VxESSMtQlpBreMyw2GmKX2hYJlaOEH1M=","type":1},{"key":"foo","value":"hello","type":1},{"key":"foo.sig","value":"MEQCIBr+k0i7SRSyb4h96vQE9hhxBZiZb/2PXQqReOKJDl/rAiBrjgSsalwOvN0zgQay0xQ7PRbm5YSmI8tvKseLR8Ryww==","type":1}]'
```

Parsed, the fields are:
- `RESULT_MANIFEST`: List of results that should be present, to prevent pick and choose attacks
- `RESULT_MANIFEST.sig`: The signature of the result manifest
- `SVID`: The x509 certificate that will be used to verify the signature trust chain to the authority
- `*.sig`: The signature of each individual result output
```
∙ RESULT_MANIFEST foo,bar
∙ RESULT_MANIFEST.sig MEQCIB4grfqBkcsGuVyoQd9KUVzNZaFGN6jQOKK90p5HWHqeAiB7yZerDA+YE3Af/ALG43DQzygiBpKhTt8gzWGmpvXJFw==
∙ SVID -----BEGIN CERTIFICATE-----
MIICCjCCAbCgAwIBAgIRALH94zAZZXdtPg97O5vG5M0wCgYIKoZIzj0EAwIwHjEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMjAzMTQxNTUzNTlaFw0y
MjAzMTQxNjU0MDlaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABPLzFTDY0RDpjKb+eZCIWgUw9DViu8/pM8q7
HMTKCzlyGqhaU80sASZfpkZvmi72w+gLszzwVI1ZNU5e7aCzbtSjgc8wgcwwDgYD
VR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
HRMBAf8EAjAAMB0GA1UdDgQWBBSsUvspy+/Dl24pA1f+JuNVJrjgmTAfBgNVHSME
GDAWgBSOMyOHnyLLGxPSD9RRFL+Yhm/6qzBNBgNVHREERjBEhkJzcGlmZmU6Ly9l
eGFtcGxlLm9yZy9ucy9kZWZhdWx0L3Rhc2tydW4vbm9uLWZhbHNpZmlhYmxlLXBy
b3ZlbmFuY2UwCgYIKoZIzj0EAwIDSAAwRQIhAM4/bPAH9dyhBEj3DbwtJKMyEI56
4DVrP97ps9QYQb23AiBiXWrQkvRYl0h4CX0lveND2yfqLrGdVL405O5NzCcUrA==
-----END CERTIFICATE-----
∙ bar world
∙ bar.sig MEUCIQDOtg+aEP1FCr6/FsHX+bY1d5abSQn2kTiUMg4Uic2lVQIgTVF5bbT/O77VxESSMtQlpBreMyw2GmKX2hYJlaOEH1M=
∙ foo hello
∙ foo.sig MEQCIBr+k0i7SRSyb4h96vQE9hhxBZiZb/2PXQqReOKJDl/rAiBrjgSsalwOvN0zgQay0xQ7PRbm5YSmI8tvKseLR8Ryww==
```


However, the verification materials are removed from the results as part of the TaskRun status:
```console
$ tkn tr describe non-falsifiable-provenance
Name: non-falsifiable-provenance
Namespace: default
Service Account: default
Timeout: 1m0s
Labels:
app.kubernetes.io/managed-by=tekton-pipelines

🌡️ Status

STARTED DURATION STATUS
38 seconds ago 36 seconds Succeeded

📝 Results

NAME VALUE
∙ bar world
∙ foo hello

🦶 Steps

NAME STATUS
∙ non-falsifiable Completed
```

## How is the result being verified

The signatures are being verified by the Tekton controller, the process of verification is as follows:

- Verifying the SVID
- Obtain the trust bundle from the SPIRE server
- Verify the SVID with the trust bundle
- Verify that the SVID spiffe ID is for the correct TaskRun
- Verifying the result manifest
- Verify the content of `RESULT_MANIFEST` with the field `RESULT_MANIFEST.sig` with the SVID public key
- Verify that there is a corresponding field for all items listed in `RESULT_MANIFEST` (besides SVID and `*.sig` fields)
- Verify individual result fields
- For each of the items in the results, verify its content against its associated `.sig` field


# TaskRun Status attestations

Each TaskRun status that is written by the tekton-pipelines-controller will be signed to ensure that there is no external
tampering of the TaskRun status. Upon each retrieval of the TaskRun, the tekton-pipelines-controller checks if the status is initialized,
and that the signature validates the current status.
The signature and SVID will be stored as annotations on the TaskRun Status field, and can be verified by a client.

The verification is done on every consumption of the TaskRun except when the TaskRun is uninitialized. When uninitialized, the
tekton-pipelines-controller is not influenced by fields in the status and thus will not sign incorrect reflections of the TaskRun.

The spec and TaskRun annotations/labels are not signed when there are valid interactions from other controllers or users (i.e. cancelling taskrun).
Editing the object annotations/labels or spec will not result in any unverifiable outcome of the status field.

As the TaskRun progresses, the Pipeline Controller will reconcile the TaskRun object and continually verify the current hash against the `tekton.dev/status-hash-sig` before updating the hash to match the new status and creating a new signature.

An example TaskRun annotations would be:

```console
$ tkn tr describe non-falsifiable-provenance -oyaml
apiVersion: tekton.dev/v1beta1
kind: TaskRun
metadata:
annotations:
pipeline.tekton.dev/release: 3ee99ec
creationTimestamp: "2022-03-04T19:10:46Z"
generation: 1
labels:
app.kubernetes.io/managed-by: tekton-pipelines
name: non-falsifiable-provenance
namespace: default
resourceVersion: "23088242"
uid: 548ebe99-d40b-4580-a9bc-afe80915e22e
spec:
serviceAccountName: default
taskSpec:
results:
- description: ""
name: foo
- description: ""
name: bar
steps:
- image: ubuntu
name: non-falsifiable
resources: {}
script: |
#!/usr/bin/env bash
sleep 30
printf "%s" "hello" > "$(results.foo.path)"
printf "%s" "world" > "$(results.bar.path)"
timeout: 1m0s
status:
annotations:
tekton.dev/controller-svid: |
-----BEGIN CERTIFICATE-----
MIIB7jCCAZSgAwIBAgIRAI8/08uXSn9tyv7cRN87uvgwCgYIKoZIzj0EAwIwHjEL
MAkGA1UEBhMCVVMxDzANBgNVBAoTBlNQSUZGRTAeFw0yMjAzMDQxODU0NTlaFw0y
MjAzMDQxOTU1MDlaMB0xCzAJBgNVBAYTAlVTMQ4wDAYDVQQKEwVTUElSRTBZMBMG
ByqGSM49AgEGCCqGSM49AwEHA0IABL+e9OjkMv+7XgMWYtrzq0ESzJi+znA/Pm8D
nvApAHg3/rEcNS8c5LgFFRzDfcs9fxGSSkL1JrELzoYul1Q13XejgbMwgbAwDgYD
VR0PAQH/BAQDAgOoMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNV
HRMBAf8EAjAAMB0GA1UdDgQWBBR+ma+yZfo092FKIM4F3yhEY8jgDDAfBgNVHSME
GDAWgBRKiCg5+YdTaQ+5gJmvt2QcDkQ6KjAxBgNVHREEKjAohiZzcGlmZmU6Ly9l
eGFtcGxlLm9yZy90ZWt0b24vY29udHJvbGxlcjAKBggqhkjOPQQDAgNIADBFAiEA
8xVWrQr8+i6yMLDm9IUjtvTbz9ofjSsWL6c/+rxmmRYCIBTiJ/HW7di3inSfxwqK
5DKyPrKoR8sq8Ne7flkhgbkg
-----END CERTIFICATE-----
tekton.dev/status-hash: 76692c9dcd362f8a6e4bda8ccb4c0937ad16b0d23149ae256049433192892511
tekton.dev/status-hash-sig: MEQCIFv2bW0k4g0Azx+qaeZjUulPD8Ma3uCUn0tXQuuR1FaEAiBHQwN4XobOXmC2nddYm04AZ74YubUyNl49/vnbnR/HcQ==
completionTime: "2022-03-04T19:11:22Z"
conditions:
- lastTransitionTime: "2022-03-04T19:11:22Z"
message: All Steps have completed executing
reason: Succeeded
status: "True"
type: Succeeded
- lastTransitionTime: "2022-03-04T19:11:22Z"
message: Spire verified
reason: TaskRunResultsVerified
status: "True"
type: SignedResultsVerified
podName: non-falsifiable-provenance-pod
startTime: "2022-03-04T19:10:46Z"
steps:
...
<TRUNCATED>
```

## How is the status being verified

The signature are being verified by the Tekton controller, the process of verification is as follows:

- Verify status-hash fields
- verify `tekton.dev/status-hash` content against its associated `tekton.dev/status-hash-sig` field. If status hash does
not match invalidate the `tekton.dev/verified = no` annotation will be added

## Further Details

To learn more about SPIRE attestations, check out the [TEP](https://github.com/tektoncd/community/blob/main/teps/0089-nonfalsifiable-provenance-support.md).
2 changes: 1 addition & 1 deletion examples/v1beta1/pipelineruns/4808-regression.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -92,4 +92,4 @@ spec:
name: result-test
params:
- name: RESULT_STRING_LENGTH
value: "3000"
value: "2000"
5 changes: 5 additions & 0 deletions pkg/apis/config/feature_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ func CheckAlphaOrBetaAPIFields(ctx context.Context) bool {
return cfg.FeatureFlags.EnableAPIFields == AlphaAPIFields || cfg.FeatureFlags.EnableAPIFields == BetaAPIFields
}

// IsSpireEnabled checks if non-falsifiable provenance is enforced through SPIRE
func IsSpireEnabled(ctx context.Context) bool {
return FromContextOrDefaults(ctx).FeatureFlags.EnforceNonfalsifiability == EnforceNonfalsifiabilityWithSpire
}

func setEnableAPIFields(ctx context.Context, want string) context.Context {
featureFlags, _ := NewFeatureFlagsFromMap(map[string]string{
"enable-api-fields": want,
Expand Down
22 changes: 22 additions & 0 deletions pkg/apis/config/feature_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,28 @@ func TestCheckAlphaOrBetaAPIFields(t *testing.T) {
}
}

func TestIsSpireEnabled(t *testing.T) {
ctx := context.Background()
if config.IsSpireEnabled(ctx) {
t.Errorf("IsSpireEnabled got true but expected to be false")
}
store := config.NewStore(logging.FromContext(ctx).Named("config-store"))
featureflags := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "feature-flags",
},
Data: map[string]string{
"enable-api-fields": "alpha",
"enforce-nonfalsifiability": config.EnforceNonfalsifiabilityWithSpire,
},
}
store.OnConfigChanged(featureflags)
ctx = store.ToContext(ctx)
if !config.IsSpireEnabled(ctx) {
t.Errorf("IsSpireEnabled got false but expected to be true")
}
}

func verifyConfigFileWithExpectedFeatureFlagsConfig(t *testing.T, fileName string, expectedConfig *config.FeatureFlags) {
t.Helper()
cm := test.ConfigMapFromTestFile(t, fileName)
Expand Down
38 changes: 38 additions & 0 deletions pkg/pod/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
"github.com/tektoncd/pipeline/pkg/internal/computeresources/tasklevel"
"github.com/tektoncd/pipeline/pkg/names"
"github.com/tektoncd/pipeline/pkg/spire"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
Expand Down Expand Up @@ -132,6 +133,10 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
// Secrets, along with any arguments needed by Step entrypoints to process
// those secrets.
commonExtraEntrypointArgs := []string{}
// Entrypoint arg to enable or disable spire
if config.IsSpireEnabled(ctx) {
commonExtraEntrypointArgs = append(commonExtraEntrypointArgs, "-enable_spire")
}
credEntrypointArgs, credVolumes, credVolumeMounts, err := credsInit(ctx, taskRun.Spec.ServiceAccountName, taskRun.Namespace, b.KubeClient)
if err != nil {
return nil, err
Expand Down Expand Up @@ -322,6 +327,39 @@ func (b *Builder) Build(ctx context.Context, taskRun *v1beta1.TaskRun, taskSpec
return nil, err
}

readonly := true
if config.IsSpireEnabled(ctx) {
// add SPIRE's CSI volume to the explicitly declared use volumes
volumes = append(volumes, corev1.Volume{
Name: spire.WorkloadAPI,
VolumeSource: corev1.VolumeSource{
CSI: &corev1.CSIVolumeSource{
Driver: "csi.spiffe.io",
ReadOnly: &readonly,
},
},
})

// mount SPIRE's CSI volume to each Step Container
for i := range stepContainers {
c := &stepContainers[i]
c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{
Name: spire.WorkloadAPI,
MountPath: spire.VolumeMountPath,
ReadOnly: true,
})
}
for i := range initContainers {
// mount SPIRE's CSI volume to each Init Container
c := &initContainers[i]
c.VolumeMounts = append(c.VolumeMounts, corev1.VolumeMount{
Name: spire.WorkloadAPI,
MountPath: spire.VolumeMountPath,
ReadOnly: true,
})
}
}

mergedPodContainers := stepContainers

// Merge sidecar containers with step containers.
Expand Down
Loading

0 comments on commit 12de7ed

Please sign in to comment.