Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 

README.md

NetShop Supply-Chain Security

This directory hardens the software supply chain for NetShop: everything between "developer pushes code" and "a pod runs in the cluster". The goal is that only images we built, scanned, and cryptographically signed can run, that they carry a Software Bill of Materials, that the namespace enforces security best practices, and that no plaintext secret ever lands in git.

It pairs with the GitOps unit: gitops/applications/addon-kyverno.yaml installs the policy engine and gitops/applications/addon-security-policies.yaml deploys the policies here, so these controls are reconciled declaratively like everything else.

Conventions: namespace netshop, registry ghcr.io/netshop-lab, label app.kubernetes.io/part-of: netshop. CRD-backed kinds (kyverno.io, external-secrets.io, bitnami.com/SealedSecret) are validated with kubeconform -ignore-missing-schemas.


Layout

security/
├── ci-supply-chain.yml          # GitHub Actions: build -> Trivy -> SBOM -> cosign sign
├── signing/
│   ├── verify-image-policy.yaml # Kyverno: verify cosign sigs, BLOCK unsigned images
│   └── cosign-commands.sh       # producer-side cosign sign/verify reference
├── scanning/
│   ├── generate-sbom.sh         # syft -> SPDX/CycloneDX SBOM per image
│   └── trivy-operator.yaml      # continuous in-cluster CVE scanning config
├── policies/                    # policy-as-code (Kyverno)
│   ├── require-pod-security.yaml        # runAsNonRoot, RO rootfs, drop ALL caps, no privesc
│   ├── disallow-latest-tag.yaml         # no :latest / untagged images
│   ├── require-resource-limits.yaml     # CPU+memory requests AND limits
│   ├── require-partof-label.yaml        # app.kubernetes.io/part-of=netshop
│   └── restrict-image-registry.yaml     # (namespaced Policy) trusted registries only
└── secrets/
    ├── external-secret.yaml     # External Secrets Operator: SecretStore + ExternalSecret
    └── sealed-secret.yaml       # Bitnami Sealed Secrets: git-safe encrypted Secret

1. Image signing & verification (cosign + Kyverno)

Producer side (signing/cosign-commands.sh, run by CI): sign each image by its immutable digest using keyless cosign -- ephemeral keys plus the GitHub Actions OIDC identity, with the signature + certificate + inclusion proof written to the public Rekor transparency log. Nothing secret to store.

cosign sign --yes ghcr.io/netshop-lab/web@sha256:<digest>
cosign verify \
  --certificate-identity-regexp '^https://github.com/dwin-gharibi/netshop/.github/workflows/ci-supply-chain.yml@.*$' \
  --certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
  ghcr.io/netshop-lab/web@sha256:<digest>

Cluster side (signing/verify-image-policy.yaml): a Kyverno ClusterPolicy with a verifyImages rule that requires a valid signature on every ghcr.io/netshop-lab/* image in the netshop namespace and, with validationFailureAction: Enforce, rejects unsigned images at admission -- they are never scheduled. mutateDigest: true rewrites the tag to the verified digest so the artifact that runs is exactly the one that was verified (no tag-to-digest swap window). A key-based variant is included commented-out.


2. Vulnerability scanning (Trivy)

  • CI gate (ci-supply-chain.yml): every image is scanned with aquasecurity/trivy-action; severity: HIGH,CRITICAL + exit-code: 1 fails the build on serious, fixable CVEs. Results are also uploaded as SARIF to the GitHub Security tab, and the rendered Helm chart/manifests get an IaC (scan-type: config) misconfiguration scan.

    - uses: aquasecurity/trivy-action@0.24.0
      with:
        image-ref: ghcr.io/netshop-lab/web@sha256:<digest>
        severity: HIGH,CRITICAL
        ignore-unfixed: true
        exit-code: "1"
  • Runtime (scanning/trivy-operator.yaml): CI only sees CVEs known at build time. The Trivy Operator runs inside the cluster and continuously re-scans running workloads, emitting VulnerabilityReport / ConfigAuditReport / ExposedSecretReport CRs you can alert on -- catching CVEs disclosed after deploy. The ConfigMap scopes it to netshop and the same HIGH/CRITICAL severities as the CI gate.

    kubectl get vulnerabilityreports -n netshop -o wide

3. SBOM (syft)

scanning/generate-sbom.sh runs syft to emit an SPDX (and CycloneDX) SBOM per image -- the ingredient list that lets you answer "are we exposed to CVE-XXXX?" in seconds. CI generates it, uploads it as an artifact, and attaches it as a signed cosign attestation so the cluster can verify provenance/SBOM as well as the image:

syft ghcr.io/netshop-lab/web:1.0.0 -o spdx-json=web.spdx.json
cosign attest --yes --type spdxjson --predicate web.spdx.json \
  ghcr.io/netshop-lab/web@sha256:<digest>

4. Policy-as-code (Kyverno)

The policies/ directory enforces best practices on the netshop namespace. All start at validationFailureAction: Enforce (reject on violation); in a brownfield cluster, set Audit first, fix offenders, then flip to Enforce.

Policy file Kind Enforces
require-pod-security.yaml ClusterPolicy runAsNonRoot, readOnlyRootFilesystem, capabilities.drop: [ALL], no privilege escalation
disallow-latest-tag.yaml ClusterPolicy reject :latest / untagged images (reproducible, verifiable deploys)
require-resource-limits.yaml ClusterPolicy CPU + memory requests and limits on every container
require-partof-label.yaml ClusterPolicy app.kubernetes.io/part-of: netshop on pods + controllers
restrict-image-registry.yaml Policy (namespaced) images only from ghcr.io/netshop-lab + approved upstream bases

The last one is a namespaced Policy (the rest are cluster-wide ClusterPolicy) to demonstrate the split: use a Policy when a rule should be scoped to and owned by a single team's namespace. These intentionally mirror the hardening the Helm chart already applies -- the policies make it non-bypassable for any future or out-of-band manifest.


5. Secrets (no plaintext in git)

The chart ships a demo Secret (helm/netshop/templates/secret.yaml, netshop-db, password netshop-demo-pw). Two production-grade replacements are provided; pick one and gate the chart's Secret off so a single controller owns netshop-db.

  • External Secrets Operator (secrets/external-secret.yaml): a SecretStore (AWS Secrets Manager via IRSA -- swap the provider for Vault / GCP / Azure) plus an ExternalSecret that pulls the credentials and projects them into an identically-named netshop-db Secret. Git stores a reference; the secret lives in the external store and rotates on a refreshInterval.

  • Sealed Secrets (secrets/sealed-secret.yaml): a SealedSecret whose values are encrypted with the in-cluster controller's public key -- safe to commit. The controller unseals it into netshop-db. Git stores the (encrypted) secret itself; no external store needed. The header documents the kubeseal command to (re)generate the ciphertext against your cluster (the committed values are placeholders).

Sealed Secrets External Secrets
Where the secret lives encrypted, in git external store (Vault/cloud)
Rotation re-seal + commit automatic on refreshInterval
External dependency controller only operator + secret manager
Best when small teams, fully GitOps central secret mgmt, frequent rotation

Validation

# Every YAML well-formed + schema-valid (CRDs skipped via -ignore-missing-schemas):
for f in $(find security -name '*.yaml' -o -name '*.yml' | grep -v ci-supply-chain); do
  python3 -c "import yaml; list(yaml.safe_load_all(open('$f')))"
  kubeconform -strict -ignore-missing-schemas -kubernetes-version 1.30.0 "$f"
done

# The workflow file is valid YAML too:
python3 -c "import yaml; list(yaml.safe_load_all(open('security/ci-supply-chain.yml')))"

-ignore-missing-schemas is required because ClusterPolicy/Policy (kyverno.io), SecretStore/ExternalSecret (external-secrets.io) and SealedSecret (bitnami.com) are CRDs not present in the upstream Kubernetes OpenAPI bundle.


How it fits together (end to end)

 developer push
      │
      ▼
 ci-supply-chain.yml ──► build ──► Trivy scan (gate HIGH/CRITICAL)
      │                                   │
      │                                   ├─► syft SBOM ──► cosign attest (signed)
      │                                   └─► cosign sign (keyless, Rekor)
      ▼
 image @sha256 (signed, scanned, SBOM) pushed to ghcr.io/netshop-lab
      │
      ▼   (GitOps: Argo CD syncs helm/netshop)
 admission to `netshop` namespace
      │
      ├─► verify-image-policy.yaml ─► reject if unsigned / wrong identity
      ├─► require-pod-security / resource-limits / part-of / no-latest / registry
      └─► secrets sourced via External Secrets OR Sealed Secrets (never plaintext)
      │
      ▼
 running pod  ──►  Trivy Operator continuously re-scans for new CVEs