Skip to content

Verify before you push

The first transfer copied the file without looking at it. Anything you transfer, you also distribute, so in this step the workflow checks the file before pushing it: it compares a checksum and scans it for viruses. If a check fails, the file never reaches the destination. All files are in examples/walk-through/02-verify.

This step builds on the resources of the first transfer. The Secret, the Endpoints and the ArtifactType stay as they are, only the WorkflowTemplate changes and a new Order asks for the checksum.

WorkflowTemplate

The new blob-transfer replaces the one from the first transfer. Instead of a single script, it runs four steps one after another: pull, checksum, virus-scan and push.

apiVersion: argoproj.io/v1alpha1
kind: WorkflowTemplate
metadata:
  name: blob-transfer # same name as in step 1: this replaces the template
spec:
  serviceAccountName: arc-workflow
  # ARC passes these parameters to every workflow it creates. All values are strings.
  arguments:
    parameters:
      # from the source Endpoint
      - name: srcType
      - name: srcRemoteURL
      - name: srcSecret # "true" if the Endpoint has a secretRef, otherwise "false"
      # from the destination Endpoint
      - name: dstType
      - name: dstRemoteURL
      - name: dstSecret
      # from the artifact spec in the Order: spec.key -> specKey, ...
      - name: specKey
      - name: specOverride
      - name: specSha256 # no default: orders without a checksum fail
      # Also passed by ARC, but not used by this template:
      # - name: cron # "true" if the artifact is scheduled by a cron, otherwise "false"
      # - name: <name> # every parameter of the ArtifactType, overriding the ones above
  # Steps run in pods of their own, so they share files through a volume. Argo creates
  # this PersistentVolumeClaim for every workflow and deletes it when the workflow is done.
  # The size is fixed here, examples/blob sizes the volume from the blob instead.
  volumeClaimTemplates:
    - metadata:
        name: work
      spec:
        accessModes: [ReadWriteOnce]
        resources:
          requests:
            storage: 1Gi
  entrypoint: transfer
  templates:
    # Every step runs after the previous one succeeded. If a check fails, the
    # workflow stops and the blob never reaches the destination.
    - name: transfer
      steps:
        - - name: pull
            template: pull
        - - name: checksum
            template: checksum
        - - name: virus-scan
            template: virus-scan
        - - name: push
            template: push

    - name: pull
      script:
        image: amazon/aws-cli:2.37.9
        command: [bash]
        volumeMounts:
          - name: work
            mountPath: /work
        env: # parameters as environment variables, see step 1
          - name: SPEC_KEY
            value: "{{workflow.parameters.specKey}}"
          - name: SRC_REMOTE_URL
            value: "{{workflow.parameters.srcRemoteURL}}"
        source: |
          set -euo pipefail
          echo "Pulling ${SRC_REMOTE_URL}/${SPEC_KEY}"
          curl -fsSL -o /work/blob "${SRC_REMOTE_URL}/${SPEC_KEY}"

    - name: checksum
      script:
        image: amazon/aws-cli:2.37.9
        command: [bash]
        volumeMounts:
          - name: work
            mountPath: /work
            readOnly: true # checks must not change what they check
        env:
          - name: SPEC_SHA256
            value: "{{workflow.parameters.specSha256}}"
        source: |
          set -euo pipefail
          echo "${SPEC_SHA256}  /work/blob" | sha256sum --check

    - name: virus-scan
      script:
        image: clamav/clamav-debian:1.5.4 # ships with a virus database
        command: [bash]
        volumeMounts:
          - name: work
            mountPath: /work
            readOnly: true
        source: |
          set -euo pipefail
          clamscan /work/blob

    # More checks go here as further steps, e.g. creating an SBOM or verifying a signature.

    - name: push
      script:
        image: amazon/aws-cli:2.37.9
        command: [bash]
        volumeMounts:
          - name: work
            mountPath: /work
          - name: dst-secret-vol
            mountPath: /secrets/dst
        env:
          - name: SPEC_OVERRIDE
            value: "{{workflow.parameters.specOverride}}"
          - name: DST_REMOTE_URL
            value: "{{workflow.parameters.dstRemoteURL}}"
        source: |
          set -euo pipefail
          export AWS_ACCESS_KEY_ID=$(cat /secrets/dst/AWS_ACCESS_KEY_ID)
          export AWS_SECRET_ACCESS_KEY=$(cat /secrets/dst/AWS_SECRET_ACCESS_KEY)
          echo "Pushing to ${DST_REMOTE_URL}/${SPEC_OVERRIDE}"
          aws --endpoint-url "${DST_REMOTE_URL}" \
            s3 cp --no-progress /work/blob "s3://${SPEC_OVERRIDE}"
          echo "Done."
kubectl apply -f examples/walk-through/02-verify/1-workflow-template.yaml

A few things are new compared to the first transfer:

  • Steps. Each step runs in a pod of its own, and a step only starts when the previous one succeeded. That is what makes the checks a gate: a failing check stops the workflow before push. Steps in the same list item run in parallel, see Steps.
  • A shared volume. Since the steps run in different pods, they pass the file through a volume. Argo creates the PersistentVolumeClaim from volumeClaimTemplates for every workflow and deletes it afterwards, see Volumes. The checks mount it read-only. Artifacts are the alternative, but need an artifact repository.
  • A checksum from the Order. The expected SHA-256 is part of the artifact spec and reaches the workflow as specSha256. The parameter has no default, so from now on every blob order without a sha256 fails. This includes first-transfer, if you force a reconciliation of it.
  • A virus scan. The ClamAV image ships with a virus database, clamscan exits with an error if it finds something.

More checks fit in as further steps, e.g. creating an SBOM or verifying a signature. The Blob example also sizes the volume from the file instead of using a fixed size.

Order

The Order adds the checksum to the artifact spec. In practice, it comes from the publisher of the file, e.g. a SHA256SUMS file next to a release, not from whoever writes the Order. Quote it: YAML reads a checksum that consists only of digits as a number.

apiVersion: arc.opendefense.cloud/v1alpha1
kind: Order
metadata:
  name: verified-transfer
  labels:
    walk-through: verify # copied to the ArtifactWorkflows and Workflows of this Order
spec:
  defaults:
    srcRef:
      name: github-raw
    dstRef:
      name: seaweedfs
  artifacts:
    - type: blob
      spec:
        key: opendefensecloud/artifact-conduit/refs/tags/v0.3.0/examples/blob/file.txt
        override: test/walk-through/verified.txt
        # quoted, YAML would read a checksum of only digits as a number
        sha256: "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" # -> specSha256
kubectl apply -f examples/walk-through/02-verify/2-order.yaml

The Order has a label. ARC copies the labels of an Order to its ArtifactWorkflows and Argo Workflows, which makes it easy to find them among the workflows of the first transfer.

Watch it run

Wait for the workflow to finish and read its logs:

export WORKFLOW="$(kubectl get workflows -l walk-through=verify -o jsonpath='{.items[0].metadata.name}')"
kubectl wait workflow "${WORKFLOW}" --for jsonpath='{.status.phase}'=Succeeded --timeout 5m
kubectl logs -l workflows.argoproj.io/workflow="${WORKFLOW}" -c main --prefix --tail -1

The logs of all steps are printed, but not necessarily in the order the steps ran. As in the first transfer, lines with argo=true come from the Argo executor.

argo watch @latest
argo logs @latest

The virus scan takes a moment, clamscan loads its database first. Check that the file arrived in the bucket:

kubectl run s3-check --rm -it --restart Never --image amazon/aws-cli:2.37.9 \
  --env AWS_ACCESS_KEY_ID=admin --env AWS_SECRET_ACCESS_KEY=adminadmin -- \
  --endpoint-url http://dst.seaweedfs:8333 s3 cp s3://test/walk-through/verified.txt -

Watch it fail

Now order the same file with a checksum that does not match, as if the file had been tampered with:

kubectl apply -f - <<EOF
apiVersion: arc.opendefense.cloud/v1alpha1
kind: Order
metadata:
  name: tampered-transfer
  labels:
    walk-through: tampered
spec:
  defaults:
    srcRef:
      name: github-raw
    dstRef:
      name: seaweedfs
  artifacts:
    - type: blob
      spec:
        key: opendefensecloud/artifact-conduit/refs/tags/v0.3.0/examples/blob/file.txt
        override: test/walk-through/tampered.txt
        sha256: "0000000000000000000000000000000000000000000000000000000000000000"
EOF

The checksum step fails, and so does the workflow:

export WORKFLOW="$(kubectl get workflows -l walk-through=tampered -o jsonpath='{.items[0].metadata.name}')"
kubectl wait workflow "${WORKFLOW}" --for jsonpath='{.status.phase}'=Failed --timeout 5m
kubectl logs -l workflows.argoproj.io/workflow="${WORKFLOW}" -c main --prefix --tail -1
argo watch @latest
argo logs @latest

The Order reports the failure as well, including the logs of the failed step:

kubectl get orders.arc.opendefense.cloud tampered-transfer -o jsonpath='{.status.artifactWorkflows}'

push never ran, so there is no tampered.txt in the bucket. This command fails with a 404:

kubectl run s3-check --rm -it --restart Never --image amazon/aws-cli:2.37.9 \
  --env AWS_ACCESS_KEY_ID=admin --env AWS_SECRET_ACCESS_KEY=adminadmin -- \
  --endpoint-url http://dst.seaweedfs:8333 s3 cp s3://test/walk-through/tampered.txt -

ARC keeps failed workflows for troubleshooting and does not retry them on its own. Deleting the Order also deletes its workflows:

kubectl delete orders.arc.opendefense.cloud tampered-transfer

Next: pull from more than one kind of source.