diff --git a/.github/workflows/publish-image.yml b/.github/workflows/publish-image.yml index 998e734a3..147520aa5 100644 --- a/.github/workflows/publish-image.yml +++ b/.github/workflows/publish-image.yml @@ -18,6 +18,7 @@ jobs: permissions: contents: read packages: write + id-token: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 @@ -57,3 +58,11 @@ jobs: cache-from: type=gha provenance: true sbom: true + id: push + + # Keyless signature tied to this workflow's OIDC identity. The Kyverno + # policy in deploy/aks/policies only admits images signed by it. + - uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Sign the pushed image + run: cosign sign --yes "${IMAGE}@${{ steps.push.outputs.digest }}" diff --git a/deploy/aks/README.md b/deploy/aks/README.md index be3b149dc..c91d2f45d 100644 --- a/deploy/aks/README.md +++ b/deploy/aks/README.md @@ -10,7 +10,8 @@ Git holds the desired state: - `k8s/base` and `k8s/overlays/demo`: a one-replica StatefulSet on a 1Gi volume, a Service, and the image tag. - `argocd/application.yaml`: the Argo CD Application that syncs the demo overlay from `master`. -- `.github/workflows/publish-image.yml`: builds, scans and pushes `ghcr.io/sharedcode/joltrin:sha-`. +- `.github/workflows/publish-image.yml`: builds, scans, pushes `ghcr.io/sharedcode/joltrin:sha-` and signs it with keyless cosign. +- `argocd/kyverno.yaml` and `policies/verify-joltrin-image.yaml`: Kyverno, plus a policy that rejects any pod in `joltrin-demo` whose image was not signed by `publish-image` on `master`. The cluster and Argo CD come from [gitops-aks-demo](https://github.com/gerardrecinto/gitops-aks-demo) (`make up`, about $0.13 an hour, `make down` removes it). @@ -18,9 +19,11 @@ The cluster and Argo CD come from [gitops-aks-demo](https://github.com/gerardrec 1. Run the `publish-image` workflow. The repository is public and the image holds only binaries built from it, with no secrets, so the GHCR package can be made public once, which lets the cluster pull without a pull secret. 2. Put the new tag in `k8s/overlays/demo/kustomization.yaml` through a pull request. -3. From gitops-aks-demo, `make up`, then apply the Application: +3. From gitops-aks-demo, `make up`, then apply the Applications. Kyverno goes first so the policy CRD exists: ``` +kubectl apply -f deploy/aks/argocd/kyverno.yaml +kubectl apply -f deploy/aks/argocd/policies.yaml kubectl apply -f deploy/aks/argocd/application.yaml kubectl -n argocd get applications kubectl -n joltrin-demo port-forward svc/joltrin 8080:80 @@ -28,4 +31,6 @@ kubectl -n joltrin-demo port-forward svc/joltrin 8080:80 4. When done, `make down` in gitops-aks-demo. The volume is deleted with the resource group. +The policy is in Enforce mode, so an image that was not signed by `publish-image` (including any tag built before signing was added) is refused at admission. The tag in the overlay must come from a run of the updated workflow. + One replica is deliberate. The B-Tree engine has no guarantee of safe multi-process writes to one data path, so scaling out needs the Redis-backed lock cache first. diff --git a/deploy/aks/argocd/kyverno.yaml b/deploy/aks/argocd/kyverno.yaml new file mode 100644 index 000000000..a914d70d7 --- /dev/null +++ b/deploy/aks/argocd/kyverno.yaml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: kyverno + namespace: argocd +spec: + project: default + source: + repoURL: https://kyverno.github.io/kyverno/ + chart: kyverno + targetRevision: 3.9.2 + destination: + server: https://kubernetes.default.svc + namespace: kyverno + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + - CreateNamespace=true + - ServerSideApply=true diff --git a/deploy/aks/argocd/policies.yaml b/deploy/aks/argocd/policies.yaml new file mode 100644 index 000000000..b0dc8ceee --- /dev/null +++ b/deploy/aks/argocd/policies.yaml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: joltrin-policies + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/SharedCode/joltrin.git + targetRevision: master + path: deploy/aks/policies + destination: + server: https://kubernetes.default.svc + namespace: kyverno + syncPolicy: + automated: + prune: true + selfHeal: true + syncOptions: + # The ClusterPolicy CRD only exists once the kyverno app has synced. + - SkipDryRunOnMissingResource=true diff --git a/deploy/aks/policies/verify-joltrin-image.yaml b/deploy/aks/policies/verify-joltrin-image.yaml new file mode 100644 index 000000000..cd5760237 --- /dev/null +++ b/deploy/aks/policies/verify-joltrin-image.yaml @@ -0,0 +1,29 @@ +apiVersion: kyverno.io/v1 +kind: ClusterPolicy +metadata: + name: verify-joltrin-image +spec: + validationFailureAction: Enforce + webhookTimeoutSeconds: 30 + rules: + - name: signed-by-publish-image + match: + any: + - resources: + kinds: + - Pod + namespaces: + - joltrin-demo + verifyImages: + - imageReferences: + - ghcr.io/sharedcode/joltrin* + mutateDigest: true + verifyDigest: true + required: true + attestors: + - entries: + - keyless: + issuer: https://token.actions.githubusercontent.com + subject: https://github.com/SharedCode/joltrin/.github/workflows/publish-image.yml@refs/heads/master + rekor: + url: https://rekor.sigstore.dev