The most surprising thing about integrating Trivy with Flux for GitOps is that you can actually prevent bad deployments from ever reaching your cluster, not just detect them after the fact.

Let’s see it in action. Imagine we have a Kubernetes manifest for a deployment that includes a container image with a known critical vulnerability.

# deployment-vulnerable.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
      - name: my-app-container
        image: docker.io/library/nginx:1.21.0 # This version has known vulnerabilities
        ports:
        - containerPort: 80

Normally, Flux would happily pull this manifest from Git and create the Deployment resource in your cluster. Then, a Kubernetes admission controller or a post-sync hook might catch the vulnerability, but the image would have already been pulled and potentially scheduled.

Here’s how we can use Trivy and Flux to stop this before it happens. We’ll set up a Git repository that Flux monitors. Inside this repository, we’ll have our application manifests and a separate configuration for Trivy.

First, you need a Flux GitRepository source:

# git-repo.yaml
apiVersion: source.toolkit.fluxcd.io/v1beta2
kind: GitRepository
metadata:
  name: my-app-repo
spec:
  interval: 1m
  ref:
    branch: main
  url: https://github.com/your-username/your-gitops-repo

And a Flux Kustomization that points to your application manifests:

# kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: my-app-kustomization
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: my-app-repo
  path: ./apps
  prune: true

Now, the magic: we’ll introduce a Trivy scan as a pre-sync hook. Flux can run custom commands before it applies manifests. We’ll leverage this. The core idea is to have a container that runs Trivy, checks the images specified in the manifests that Flux is about to apply, and exits with a non-zero status code if vulnerabilities are found.

Here’s a conceptual Kustomization that includes the Trivy scanner. This isn’t a direct Flux feature for pre-sync hooks in the traditional sense of modifying the Kustomization itself to run a scanner before applying. Instead, we integrate Trivy into the GitOps repository and let Flux pull the scanning configuration. A more robust pattern often involves using a CI pipeline that runs Trivy and only commits clean manifests, or using an admission controller. However, to illustrate the "scan before sync" idea within Flux’s declarative model, we can imagine a scenario where Flux itself triggers a scan after fetching but before applying.

A more direct way Flux enables this is by not applying manifests if a preceding step fails. We can orchestrate this using Helm charts or Kustomizations that have dependencies.

Let’s reframe this to a more practical Flux pattern: using a separate Kustomization that depends on the GitRepository and runs a scan as its primary action, and then have your application Kustomization depend on that.

Here’s a Kustomization for the Trivy scanner itself:

# trivy-scanner-kustomization.yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: trivy-scanner
spec:
  interval: 1m # Scan frequently
  sourceRef:
    kind: GitRepository
    name: my-app-repo # Pointing to the same repo
  path: ./scanners/trivy # Directory containing scanner config
  dependsOn:
    - name: my-app-repo # Ensure we have the repo content first

Inside the ./scanners/trivy directory in your Git repo, you’d have a kustomization.yaml that defines a Pod or Job that runs Trivy. This Pod/Job would read the manifests from the ./apps directory and scan them.

Example of ./scanners/trivy/kustomization.yaml:

# ./scanners/trivy/kustomization.yaml
apiVersion: v1
kind: Pod
metadata:
  name: trivy-scanner-pod
spec:
  containers:
  - name: trivy
    image: aquasec/trivy:latest
    command: ["/bin/sh", "-c"]
    args:
      - |
        set -e
        # Trivy needs to read the manifests. This is the tricky part.
        # A common pattern is to mount a volume with the git repo content.
        # For simplicity here, we'll assume the manifests are accessible.
        # In a real setup, you might mount a ConfigMap or use a sidecar.

        # Example: Scan all images in deployments in the 'apps' directory
        echo "Scanning images in ./apps..."
        trivy fs --severity HIGH,CRITICAL --exit-code 1 ./apps
        echo "Scan complete. No critical or high vulnerabilities found."
  restartPolicy: Never # This is a one-off scan

Crucially, the trivy fs --severity HIGH,CRITICAL --exit-code 1 ./apps command is the heart of it.

  • trivy fs ./apps: Tells Trivy to scan filesystems, specifically the ./apps directory in this case, looking for image references.
  • --severity HIGH,CRITICAL: Filters the scan to only report on vulnerabilities of these severity levels.
  • --exit-code 1: This is key. If Trivy finds any vulnerabilities matching the severity criteria, it will exit with a non-zero status code (1).

Now, to make the application deployment depend on the scanner succeeding:

# my-app-kustomization.yaml (updated)
apiVersion: kustomize.toolkit.fluxcd.io/v1beta2
kind: Kustomization
metadata:
  name: my-app-kustomization
spec:
  interval: 5m
  sourceRef:
    kind: GitRepository
    name: my-app-repo
  path: ./apps
  prune: true
  dependsOn:
    - name: trivy-scanner # This Kustomization depends on the scanner finishing successfully

When Flux reconciles trivy-scanner, it will create the Pod. If the Trivy scan finds critical or high vulnerabilities in the ./apps directory, the Pod will exit with status code 1. Flux, seeing a non-zero exit code from a Pod it managed (or if the Pod fails to complete successfully for any reason), will mark the trivy-scanner Kustomization as failed. Because my-app-kustomization has dependsOn: [trivy-scanner], it will not proceed to apply the application manifests. Your GitOps pipeline effectively halts at the scan.

The specific lever you control here is the dependsOn field in Flux’s Kustomization resource. By making your application deployment dependent on a Kustomization that runs a scanner (which exits non-zero on failure), you create a declarative gate. The scanner Pod effectively acts as a gatekeeper.

The one thing most people don’t realize is that the Pod created by the scanner Kustomization doesn’t need to be a Kubernetes Job. A simple Pod with restartPolicy: Never is sufficient because Flux tracks the Pod’s completion status. If it completes successfully (exit code 0), the dependency is met. If it fails (non-zero exit code), the dependency is broken, and subsequent Kustomizations are not reconciled.

If everything is fixed, the next error you’ll hit is likely related to network policies or RBAC preventing your application from starting correctly.

Want structured learning?

Take the full Trivy course →