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, registryghcr.io/netshop-lab, labelapp.kubernetes.io/part-of: netshop. CRD-backed kinds (kyverno.io,external-secrets.io,bitnami.com/SealedSecret) are validated withkubeconform -ignore-missing-schemas.
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
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.
-
CI gate (
ci-supply-chain.yml): every image is scanned withaquasecurity/trivy-action;severity: HIGH,CRITICAL+exit-code: 1fails 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, emittingVulnerabilityReport/ConfigAuditReport/ExposedSecretReportCRs you can alert on -- catching CVEs disclosed after deploy. The ConfigMap scopes it tonetshopand the same HIGH/CRITICAL severities as the CI gate.kubectl get vulnerabilityreports -n netshop -o wide
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>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.
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): aSecretStore(AWS Secrets Manager via IRSA -- swap the provider for Vault / GCP / Azure) plus anExternalSecretthat pulls the credentials and projects them into an identically-namednetshop-dbSecret. Git stores a reference; the secret lives in the external store and rotates on arefreshInterval. -
Sealed Secrets (
secrets/sealed-secret.yaml): aSealedSecretwhose values are encrypted with the in-cluster controller's public key -- safe to commit. The controller unseals it intonetshop-db. Git stores the (encrypted) secret itself; no external store needed. The header documents thekubesealcommand 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 |
# 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.
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