> ## Documentation Index
> Fetch the complete documentation index at: https://docs.controlplane.com/llms.txt
> Use this file to discover all available pages before exploring further.

# CPLN Trivy

> Automated vulnerability scanning for images stored in a Control Plane image registry. Scans each image using Trivy, stores HTML reports in S3 or Azure File Share, and tags images with a direct link to their report.

## Overview

CPLN Trivy automates vulnerability scanning for every image in your Control Plane image registry. A scheduled daemon queries the registry for unscanned images, runs Trivy against each one, and stores an HTML report in S3 or an Azure File Share. After each scan, the image is tagged with a direct link to its report — visible in the Control Plane console.

### Architecture

* **daemon** (cron workload) — Runs on a configurable schedule, queries the registry for images that do not yet have a `cpln/trivy-scan` tag, and orchestrates scanning. Includes a **trivy-api** sidecar that wraps the Trivy CLI and returns HTML vulnerability reports.
* **web-server** (serverless workload) — Receives scan reports from the daemon, stores them in the configured storage backend, and serves them publicly via URL.

After each scan, the daemon writes two tags to the image:

| Tag                    | Value                                |
| ---------------------- | ------------------------------------ |
| `cpln/trivy-scan`      | URL to the HTML vulnerability report |
| `cpln/trivy-scan-time` | Timestamp of the scan                |

Each run scans images that do not yet have a `cpln/trivy-scan` tag. When `rescanAfter` is set (default `7d`), images whose last scan is older than that window are scanned again and their report is refreshed in place at the same URL. Setting `rescanAfter` to `""` disables rescanning — then re-scanning an image requires removing its `cpln/trivy-scan` tag first.

### What Gets Created

* **Cron Daemon Workload** — Trivy daemon with trivy-api sidecar, runs on a cron schedule.
* **Serverless Web-Server Workload** — Report storage and serving, autoscales from 1–3 replicas.
* **Identity & Policy** — Identity bound to each workload with access to the configured storage cloud account, plus `reveal` access to the referenced credentials secret.
* **Secret** — CPLN secret storing the shared bearer token between the daemon and web-server. The service account key itself lives in an opaque secret you create beforehand (see Prerequisites).

<Note>
  This template does not create a GVC. You must deploy it into an existing GVC.
</Note>

## Prerequisites

### Service Account

Trivy authenticates against the Control Plane image registry using a service account key stored in an **opaque** secret.

<Steps>
  <Step title="Create or select a service account">
    Create a Control Plane service account (or use an existing one). Set `serviceAccountName` in `values.yaml` to its name — the template grants it image `pull` and `view` permissions automatically.
  </Step>

  <Step title="Generate a key">
    Generate a key for the service account and copy the key value. It cannot be retrieved later.
  </Step>

  <Step title="Store the key in an opaque secret">
    ```bash theme={null}
    echo -n "your-service-account-key" | cpln secret create-opaque --name trivy-credentials --encoding plain -f -
    ```
  </Step>

  <Step title="Reference the secret in values.yaml">
    The template grants the workload identity `reveal` access automatically:

    ```yaml theme={null}
    trivyAuth:
      secretName: trivy-credentials
    ```
  </Step>
</Steps>

### Storage

Choose a storage backend — either AWS S3 or Azure File Share. Set `storage.type` to the appropriate value and configure only that section.

<Tabs>
  <Tab title="AWS S3">
    <Steps>
      <Step title="Create an S3 bucket">
        Create an S3 bucket in your AWS account to store scan reports. Set `storage.s3.bucket` and `storage.s3.region`.
      </Step>

      <Step title="Register a Cloud Account">
        If you do not have one, [create an AWS Cloud Account](https://docs.controlplane.com/guides/create-cloud-account) in Control Plane. Set `storage.s3.cloudAccountName` to its name.
      </Step>

      <Step title="Create an IAM policy">
        Create an IAM policy scoped to your bucket (replace `YOUR_BUCKET_NAME`) and set `storage.s3.policyName` to its name:

        ```json theme={null}
        {
            "Version": "2012-10-17",
            "Statement": [
                {
                    "Effect": "Allow",
                    "Action": [
                        "s3:GetObject",
                        "s3:PutObject",
                        "s3:DeleteObject",
                        "s3:ListBucket"
                    ],
                    "Resource": [
                        "arn:aws:s3:::YOUR_BUCKET_NAME",
                        "arn:aws:s3:::YOUR_BUCKET_NAME/*"
                    ]
                }
            ]
        }
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Azure File Share">
    <Steps>
      <Step title="Create a storage account and file share">
        Create an Azure storage account and file share. Set `storage.azureFileshare.accountName` and `storage.azureFileshare.fileShare`.
      </Step>

      <Step title="Register a Cloud Account">
        If you do not have one, [create an Azure Cloud Account](https://docs.controlplane.com/guides/create-cloud-account) in Control Plane. Set `storage.azureFileshare.cloudAccountName` to its name.
      </Step>

      <Step title="Set the scope">
        Set `storage.azureFileshare.scope` to the full Azure resource scope for role assignment:
        `/subscriptions/<id>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<name>`
      </Step>
    </Steps>
  </Tab>
</Tabs>

To install, follow the instructions for your preferred method:

<CardGroup cols={2}>
  <Card title="UI" href="/template-catalog/install-manage/ui" icon="laptop">
    Browse, install, and manage templates visually
  </Card>

  <Card title="CLI" href="/template-catalog/install-manage/cli" icon="terminal">
    Manage templates from your terminal
  </Card>

  <Card title="Terraform" href="/template-catalog/install-manage/terraform" icon={<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"><g fill-rule="evenodd"><path d="M77.941 44.5v36.836L46.324 62.918V26.082zm0 0" fill="#5c4ee5"/><path d="M81.41 81.336l31.633-18.418V26.082L81.41 44.5zm0 0" fill="#4040b2"/><path d="M11.242 42.36L42.86 60.776V23.941L11.242 5.523zm0 0M77.941 85.375L46.324 66.957v36.82l31.617 18.418zm0 0" fill="#5c4ee5"/></g></svg>}>
    Declare templates in your Terraform configurations
  </Card>

  <Card
    title="Pulumi"
    href="/template-catalog/install-manage/pulumi"
    icon={<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" id="Pulumi-Icon--Streamline-Svg-Logos" height="24" width="24">
    <desc>
        Pulumi Icon Streamline Icon: https://streamlinehq.com
    </desc>
    <path fill="#f26e7e" d="M4.683025 13.3318c0.869125 -0.5018 0.870575 -2.1264 0.003225 -3.62865s-2.27504 -2.313275 -3.1441725 -1.811475C0.672945 8.3935 0.6715 10.0181 1.53885 11.52035c0.86735 1.502275 2.27505 2.313275 3.144175 1.81145Zm0.0052 3.2167c0.86735 1.502275 0.865925 3.126875 -0.003225 3.628675 -0.86915 0.5018 -2.2768275 -0.309225 -3.144175 -1.81145 -0.8673525 -1.50225 -0.8659075 -3.126875 0.003225 -3.628675 0.8691325 -0.5018 2.276825 0.309225 3.144175 1.81145Zm5.922875 3.4243c0.86735 1.50225 0.8659 3.126775 -0.003225 3.62875 -0.869125 0.501775 -2.27685 -0.309325 -3.1442 -1.81155 -0.867325 -1.50225 -0.865875 -3.12685 0.00325 -3.628675 0.869125 -0.5018 2.276825 0.309225 3.144175 1.811475Zm-0.001925 -6.845275c0.86735 1.50225 0.8659 3.12685 -0.003225 3.628675 -0.869125 0.5018 -2.276825 -0.309225 -3.144175 -1.811475 -0.86735 -1.50225 -0.8659 -3.12685 0.003225 -3.62865 0.869125 -0.501825 2.276825 0.3092 3.144175 1.81145Z" stroke-width="0.25"></path>
    <path fill="#8a3391" d="M22.45775 11.524125c0.86725 -1.502225 0.865925 -3.12685 -0.003225 -3.62865 -0.869125 -0.501825 -2.276825 0.3092 -3.144175 1.811475 -0.86735 1.50225 -0.8659 3.126825 0.003225 3.62865 0.869125 0.501825 2.276825 -0.3092 3.144175 -1.811475Zm0.000175 3.2151c0.869075 0.5018 0.870625 2.1264 0.003225 3.62865 -0.86735 1.50225 -2.27505 2.313275 -3.144175 1.81145 -0.869125 -0.5018 -0.870575 -2.126425 -0.003225 -3.62865 0.86735 -1.50225 2.27505 -2.313275 3.144175 -1.81145ZM16.536225 18.157875c0.86915 0.501825 0.8706 2.126425 0.00325 3.628675 -0.86735 1.502125 -2.275075 2.313225 -3.1442 1.81145 -0.869125 -0.50175 -0.870575 -2.126425 -0.003225 -3.62865 0.867375 -1.502275 2.27505 -2.3133 3.144175 -1.811475Zm-0.003325 -6.843775c0.869125 0.5018 0.870575 2.126425 0.003225 3.628675s-2.27505 2.313275 -3.1442 1.811475c-0.869125 -0.501825 -0.870575 -2.126425 -0.003225 -3.628675 0.86735 -1.502275 2.27505 -2.313275 3.1442 -1.811475Z" stroke-width="0.25"></path>
    <path fill="#f7bf2a" d="M15.138225 2.06721c0 1.003615 -1.40625 1.817215 -3.14095 1.817215 -1.7347 0 -3.14095 -0.8136 -3.14095 -1.817215C8.856325 1.06359 10.262575 0.25 11.997275 0.25c1.7347 0 3.14095 0.81359 3.14095 1.81721ZM9.2166 5.482375c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172s1.40625 -1.817225 3.14095 -1.817225c1.7347 0 3.14095 0.8136 3.14095 1.817225Zm8.71005 1.8172c1.7347 0 3.14095 -0.813575 3.14095 -1.8172s-1.40625 -1.817225 -3.14095 -1.817225c-1.7347 0 -3.14095 0.8136 -3.14095 1.817225s1.40625 1.8172 3.14095 1.8172Zm-2.788425 1.605625c0 1.003625 -1.40625 1.8172 -3.14095 1.8172 -1.7347 0 -3.14095 -0.813575 -3.14095 -1.8172 0 -1.0036 1.40625 -1.8172 3.14095 -1.8172 1.7347 0 3.14095 0.8136 3.14095 1.8172Z" stroke-width="0.25"></path>
    </svg>}
  >
    Declare templates in your Pulumi programs
  </Card>
</CardGroup>

## Configuration

The default `values.yaml` for this template:

```yaml theme={null}
# Storage backend for vulnerability reports. Options: "s3" or "azureFileshare"
storage:
  type: s3

  s3:
    cloudAccountName: my-aws-cloud-account
    bucket: trivy-reports-bucket
    region: us-east-1
    policyName: my-trivy-s3-policy  # IAM policy scoped to the bucket (see Prerequisites)

  azureFileshare:
    cloudAccountName: my-azure-cloud-account
    accountName: mystorageaccount
    fileShare: trivy-reports
    scope: "" # /subscriptions/<id>/resourceGroups/<rg>/providers/Microsoft.Storage/storageAccounts/<name>

# Bearer token shared between the daemon and web-server. Change before deploying to production.
postToken: changeme

# Authentication for Trivy to pull images from the Control Plane image registry.
# Name of a pre-existing CPLN opaque secret whose payload is the service account key (see Prerequisites).
trivyAuth:
  secretName: trivy-credentials

# Control Plane service account that Trivy uses to pull images
serviceAccountName: trivy-service-account

# Cron schedule for the scanning daemon
schedule: "*/59 * * * *"

# Rescan images whose last scan is older than this (e.g. "7d", "24h"). Empty = scan once only.
rescanAfter: 7d

daemon:
  image: ghcr.io/controlplane-com/cpln-trivy-daemon:1.2.0
  resources:
    cpu: 1
    memory: 1Gi
  firewall:
    outboundAllowCIDR:
      - 0.0.0.0/0

trivyApi:
  image: ghcr.io/controlplane-com/cpln-trivy-trivy-api:1.2.0
  resources:
    cpu: 2
    memory: 4Gi

webServer:
  image: ghcr.io/controlplane-com/cpln-trivy-web-server:1.2.0
  resources:
    cpu: 150m
    memory: 128Mi
  autoscaling:
    minScale: 1
    maxScale: 3
  firewall:
    inboundAllowCIDR:
      - 0.0.0.0/0
    outboundAllowCIDR:
      - 0.0.0.0/0
```

### Configuration Reference

| Parameter                                 | Default                 | Description                                                                                           |
| ----------------------------------------- | ----------------------- | ----------------------------------------------------------------------------------------------------- |
| `storage.type`                            | `s3`                    | Storage backend for reports. Options: `s3`, `azureFileshare`                                          |
| `storage.s3.cloudAccountName`             | —                       | AWS Cloud Account name registered in Control Plane                                                    |
| `storage.s3.bucket`                       | —                       | S3 bucket name                                                                                        |
| `storage.s3.region`                       | —                       | AWS region (e.g. `us-east-1`)                                                                         |
| `storage.s3.policyName`                   | —                       | Name of the IAM policy scoped to the bucket                                                           |
| `storage.azureFileshare.cloudAccountName` | —                       | Azure Cloud Account name registered in Control Plane                                                  |
| `storage.azureFileshare.accountName`      | —                       | Azure storage account name                                                                            |
| `storage.azureFileshare.fileShare`        | —                       | Azure file share name                                                                                 |
| `storage.azureFileshare.scope`            | —                       | Full Azure resource scope for role assignment                                                         |
| `postToken`                               | `changeme`              | Shared bearer token between daemon and web-server                                                     |
| `trivyAuth.secretName`                    | `trivy-credentials`     | Name of an existing opaque secret whose payload is the service account key                            |
| `serviceAccountName`                      | `trivy-service-account` | Service account Trivy uses to pull images                                                             |
| `schedule`                                | `*/59 * * * *`          | Cron schedule for the scanning daemon                                                                 |
| `rescanAfter`                             | `7d`                    | Rescan images whose last scan is older than this (`<N>d` or `<N>h`). Empty string disables rescanning |
| `daemon.resources.cpu`                    | `1`                     | CPU for the daemon container                                                                          |
| `daemon.resources.memory`                 | `1Gi`                   | Memory for the daemon container                                                                       |
| `trivyApi.resources.cpu`                  | `2`                     | CPU for the trivy-api sidecar                                                                         |
| `trivyApi.resources.memory`               | `4Gi`                   | Memory for the trivy-api sidecar                                                                      |
| `webServer.autoscaling.minScale`          | `1`                     | Minimum web-server replicas                                                                           |
| `webServer.autoscaling.maxScale`          | `3`                     | Maximum web-server replicas                                                                           |

<Warning>
  Change `postToken` from its default `changeme` value before any production deployment. This token authenticates report submissions from the daemon to the web-server.
</Warning>

<Note>
  Report URLs are publicly accessible by default — they contain an unguessable SHA-256 hash, but no authentication. Restrict `webServer.firewall.inboundAllowCIDR` if reports must stay private.
</Note>

## Viewing Reports

Once the daemon has run, navigate to any scanned image in the Control Plane console. The `cpln/trivy-scan` tag on the image contains a direct URL to the HTML vulnerability report. Opening that URL serves the report from the web-server.

To list all scanned images via CLI:

```bash theme={null}
cpln image query --tag cpln/trivy-scan --max -1 -o json | jq '.items[].name'
```

## Maintenance

### Periodic Re-Scans

With `rescanAfter` set (default `7d`), re-scans happen automatically: any image whose last scan is older than the window is scanned again on the next daemon run, and its report is refreshed at the same URL. Adjust the window (e.g. `24h` for daily) or set it to `""` to scan each image only once.

### Force an Immediate Re-Scan

To re-scan an image before its `rescanAfter` window elapses, remove its scan tags:

```bash theme={null}
cpln image tag my-image:latest --remove cpln/trivy-scan --remove cpln/trivy-scan-time
```

To reset all scan tags and trigger a full re-scan on the next daemon run:

```bash theme={null}
cpln image query --tag cpln/trivy-scan --max -1 -o json | jq -r '.items[].name' | \
  xargs -I{} cpln image tag {} --remove cpln/trivy-scan --remove cpln/trivy-scan-time
```

The daemon will pick the images up on its next scheduled run.

## External References

<CardGroup cols={2}>
  <Card title="Trivy Documentation" href="https://trivy.dev/latest/docs/" icon="shield-halved">
    Official Trivy vulnerability scanner documentation
  </Card>

  <Card title="Create a Cloud Account" href="https://docs.controlplane.com/guides/create-cloud-account" icon="cloud">
    Set up AWS or Azure cloud accounts for storage access
  </Card>
</CardGroup>
