Skip to content

Repository files navigation

secure-software-supply-chain

A Kubernetes operator that manages a full software supply chain pipeline from two CRs. Built with Kubebuilder, powered by Tekton, secured by Sigstore.

"Treat your pipeline as infrastructure, not a script."


Overview

secure-software-supply-chain is part of the BlanketOps platform engineering project. It implements a supply chain security pipeline as a Kubernetes controller — declarative, auditable, and self-managing.

A SupplyChain CR defines the pipeline for a repository. An ImageBuild CR is auto-created on every GitHub push via a Tekton EventListener — no manual triggering required. The controller drives a Tekton PipelineRun through build, scan, sign, attest, and publish — all reconciled automatically.

GitHub Push
    └── EventListener (Tekton Triggers)
            └── creates ──► ImageBuild CR
                                └── owns ──► Tekton PipelineRun
                                                 ├── git-clone             (source fetch)
                                                 ├── authentication-fulcio (OIDC warm-up)
                                                 ├── sonarqube-scanner     (quality gate)
                                                 ├── build-image-buildah   (OCI image build)
                                                 ├── push-image-docker     (registry push)
                                                 ├── vulnerability-scan-trivy (CVE scan)
                                                 ├── sign-image-cosign     (keyless signing)
                                                 ├── attest-image-rekor-fulcio (provenance)
                                                 └── publish-metadata-grafeas  (metadata)

Architecture

Supply Chain Architecture

The pipeline is driven end-to-end by two CRs and a set of Tekton Tasks. The controller reconciles the full lifecycle — from webhook registration to signed image attestation.


CRs

SupplyChain — the pipeline definition for a repository. One per repo. The controller enforces this at reconcile time. It owns and reconciles:

  • Custom Tekton Tasks
  • TriggerBinding, TriggerTemplate, EventListener (GitHub webhook automation)
  • Ingress for the EventListener (host sourced from spec.webhookHost)
  • The supply-chain-runner ServiceAccount

GitHubWebhook — manages GitHub webhook registration. Automatically registers the webhook URL with GitHub using a GitHub App installation token. Idempotent — safe to apply on every reconcile.

ImageBuild — a single pipeline execution. Auto-created by the EventListener on every GitHub push. Owns the Tekton PipelineRun and tracks per-step status.

ImageSignature — the cryptographic audit record. Created before the PipelineRun with Phase=Pending, updated to Phase=Signed on success. Carries the Fulcio cert reference, principal identity, and Rekor log index.

ImageBuildResult — the durable execution record. Survives PipelineRun pruning. Captures full pipeline step results: git provenance, image digest, Trivy scan summary, SonarQube gate status, Grafeas occurrence.


Trigger Layer

The SupplyChain controller automatically provisions the full Tekton Triggers stack per chain:

  • TriggerBinding — extracts git-repo-url, git-revision, git-commit-sha, short-sha, repo-full-name from the GitHub push payload
  • TriggerTemplate — creates an ImageBuild CR with extracted params. Name is deterministic: <supplychain>-<branch>-<full-sha> — idempotent on replay
  • EventListener — shared across all SupplyChains in the namespace, exposed via nginx Ingress
  • Ingress — routes spec.webhookHost → EventListener. Host updated automatically when webhookHost changes

Mediator

The ImageBuildReconciler uses a mediator pattern to sequence prerequisites before pipeline construction.

Gate 1 — Prerequisites

  • Git SSH ExternalSecret reconciliation
  • Registry ExternalSecret reconciliation (Buildah + Tekton Chains split credentials)
  • SonarQube ExternalSecret reconciliation
  • Convergence wait — all secrets must materialise before proceeding

Gate 2 — Signing Context (three-proof authorization)

Before Fulcio is called, three SubjectAccessReviews are performed against the supply-chain-runner ServiceAccount. All three must pass:

Proof Resource Verb Meaning
ScopeProof supplychains get SA can see the chain it claims to execute against
IntentProof imagebuilds create SA is authorized to initiate a build
OutputProof imagesignatures create SA is authorized to produce signing records

All three proofs are embedded in the attestation predicate that Fulcio signs over. This makes the ephemeral cert meaningful — it signs over a complete, API-server-verified authorization story, not just an identity claim.

After all three SARs pass, a short-lived OIDC token is minted from the ServiceAccount and exchanged with Fulcio for an ephemeral signing certificate. The principal and cert PEM are stored on ImageBuild.Status for the terminal block to read back after the PipelineRun completes.

Gate 3 — PipelineRun

Builds and creates the Tekton PipelineRun with the signing context injected. The ImageSignature CR is created at Phase=Pending before the PipelineRun starts.


Terminal Actions

When a PipelineRun reaches a terminal state (Succeeded or Failed), the reconciler runs three best-effort actions:

  1. Recorder — creates or updates an ImageBuildResult CR with the full pipeline step results
  2. Signature — marks the ImageSignature as Signed (with digest) or Failed
  3. Pruner — deletes old PipelineRuns beyond the retention window (keeps last 3 succeeded, 1 failed)

Install

1. Install dependencies

supplychain install

This applies all platform dependencies in order:

  • MetalLB (LoadBalancer support for kind)
  • Tekton Pipelines, Triggers, Interceptors, Chains, Dashboard, Tasks, Results
  • Sigstore (Fulcio, Rekor)
  • Grafeas
  • NGINX Ingress Controller
  • SonarQube

2. Bootstrap SonarQube

After supplychain install completes and SonarQube is ready:

supplychain init-sonarqube --new-password <your-password>

This automatically:

  • Waits for SonarQube to be ready
  • Changes the default admin password
  • Generates a supply-chain user token
  • Patches the ClusterSecretStore with the token at /supplychain/sonarqube/token

3. Deploy the operator

# Build and load into kind
docker build -t blanketops/supply-chain-controller:latest .
kind load docker-image blanketops/supply-chain-controller:latest --name blanketops

# Install CRDs
make install

# Apply RBAC
kubectl apply -f config/rbac/signing_role.yaml
kubectl apply -f config/rbac/eventlistener_role.yaml

# Deploy
make deploy IMG=blanketops/supply-chain-controller:latest

4. Apply samples

kubectl apply -k config/samples

This applies:

  • ClusterSecretStore (ESO fake provider with credentials)
  • SupplyChain CR
  • GitHubWebhook CR

Webhook Setup (Tailscale Funnel)

The GitHubWebhook controller auto-registers the webhook with GitHub. The hookURL must be publicly reachable. We use Tailscale Funnel to expose the in-cluster EventListener without a cloud load balancer.

Setup

# Expose the nginx ingress via Tailscale Funnel
tailscale serve --bg --https=443 http://<metallb-ingress-ip>
tailscale funnel --bg 443

Set spec.webhookHost in the SupplyChain CR to your Tailscale hostname:

spec:
  webhookHost: your-machine.tailf8145.ts.net

The controller automatically updates the EventListener Ingress host and the GitHubWebhook CR uses the same URL for webhook registration.

Persistence (systemd)

To survive reboots, create a systemd service that bridges the MetalLB IP to localhost:

sudo tee /etc/systemd/system/kind-ingress-bridge.service <<EOF
[Unit]
Description=Bridge localhost to kind ingress-nginx
After=network.target

[Service]
ExecStart=/usr/bin/socat TCP-LISTEN:8888,fork,reuseaddr TCP:<metallb-ip>:80
Restart=always

[Install]
WantedBy=multi-user.target
EOF

sudo systemctl enable --now kind-ingress-bridge
tailscale funnel --bg 8888

Usage

SupplyChain CR

apiVersion: supplychain.blanketops.dev/v1alpha1
kind: SupplyChain
metadata:
  name: for-kaniko-app
  namespace: default
spec:
  repository: ntlaletsi70/for-kaniko-app
  serviceAccountName: supply-chain-runner
  webhookHost: your-machine.tailf8145.ts.net
  image:
    registry: docker.io
    name: nkanyezisolutions/for-kaniko-app
    tagStrategy: git-sha
    cloneSecretRef: github-ssh-credentials
    registrySecretRef: registry-credentials
  steps:
    trivy: true
    sign: true
    attest: true
    sonarQube:
      serverURL: http://sonarqube-sonarqube.default.svc.cluster.local:9000
      tokenSecretRef: sonarqube-token
      projectKey: ntlaletsi70_for-kaniko-app
    grafeas:
      serverURL: http://grafeas.grafeas.svc.cluster.local:8080
  signing:
    fulcioURL: http://fulcio-server.fulcio-system.svc.cluster.local
    rekorURL: http://rekor-server.rekor-system.svc.cluster.local

GitHubWebhook CR

apiVersion: supplychain.blanketops.dev/v1alpha1
kind: GitHubWebhook
metadata:
  name: for-kaniko-app-webhook
  namespace: default
spec:
  repository: ntlaletsi70/for-kaniko-app
  supplyChainRef: for-kaniko-app
  hookURL: https://your-machine.tailf8145.ts.net
  secretRef: github-app-credentials

Push a commit — builds fire automatically

kubectl get imagebuilds -n default -w

Phase transitions: PendingRunningSucceeded

Manually trigger a build

apiVersion: supplychain.blanketops.dev/v1alpha1
kind: ImageBuild
metadata:
  name: for-kaniko-app-manual-001
  namespace: default
spec:
  supplyChainRef:
    name: for-kaniko-app
  gitRef:
    url: git@github.com:ntlaletsi70/for-kaniko-app.git
    revision: main
  imageTag: manual-001

Inspect results

# Build status
kubectl get imagebuilds -n default
kubectl describe imagebuild <name>

# Signing audit record
kubectl get imagesignatures -n default

# Durable build result (survives PipelineRun pruning)
kubectl get imagebuildresults -n default

# PipelineRun logs
kubectl get pipelineruns -n default
tkn pipelinerun logs <name> -f

Verify the signed image

cosign verify \
  --certificate-identity-regexp=".*" \
  --certificate-oidc-issuer="https://kubernetes.default.svc.cluster.local" \
  docker.io/nkanyezisolutions/for-kaniko-app:<sha>

CLI commands

# Install all dependencies
supplychain install

# Bootstrap SonarQube (run once after install)
supplychain init-sonarqube --new-password <password>

# Check dependency status
supplychain status

# Remove all dependencies
supplychain uninstall

# Open supply chain dashboard
supplychain observe

# Open RBAC audit dashboard
supplychain observe rbac

Uninstall

make undeploy
make uninstall
supplychain uninstall

API Group

supplychain.blanketops.dev/v1alpha1

Resources: SupplyChain, GitHubWebhook, ImageBuild, ImageSignature, ImageBuildResult


Part of BlanketOps

This operator is one component of the BlanketOps platform:


Acknowledgements


Demo

Demo Description
Full Pipeline GitHub push → ImageBuild → PipelineRun → Signed image
Setup Fresh cluster setup: install deps, apply SupplyChain CR
Webhook Automation GitHubWebhook CR: auto-register → push → pipeline fires
Signing Verification Verify signed image with cosign + Rekor transparency log

See demo/ for scripts and tape files.# secure-software-supplychain

About

A Kubernetes operator that manages a full software supply chain pipeline from two CRs. Built with Kubebuilder, powered by Tekton, secured by Sigstore.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages