> ## Documentation Index
> Fetch the complete documentation index at: https://controlplanecorporation-majid-docs-content-expansion.mintlify.site/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. Covers the two prerequisite secrets, including the bearer token guarding the public report upload endpoint.

## 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.

<Warning>
  **Upgrading an install created with `1.1.0` or earlier is a breaking change.** `postToken` is no longer a plain value; the bearer token now lives in an opaque secret you create, named by `postToken.secretName`. Put **the same token your install already uses** into that secret — a different one makes report uploads start failing with `401` while every status surface still reads healthy. See [Upgrading From 1.1.0](#upgrading-from-1-1-0).
</Warning>

<Warning>
  **Running a scan tags and reports on every image in the organization, not only images this install created.** The daemon enumerates the whole registry, writes two tags to each image it scans, and uploads a report per image to your bucket. Before pointing this at a production organization, understand that it mutates images it did not create. Narrow the blast radius by setting `schedule` to a time you choose and `rescanAfter` to `""` for a first run.
</Warning>

### 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.

### How the Two Workloads Talk

**The daemon reaches the web-server over the public internet, not over the GVC network.** The two workloads have no in-GVC path to each other, so the daemon posts each report to the web-server's public endpoint — and that endpoint gates writes on the bearer token alone.

That is why the token is a prerequisite secret rather than internal plumbing. Through `1.1.0` it shipped as the value `postToken: changeme`, which meant an internet-facing write endpoint guarded by a string published in a public repository: anyone could upload content into the user's own bucket and have it served back as HTML from their report URLs. Nothing about that credential is unreachable from outside the GVC, which is exactly the condition the bundled-credential exception depends on.

### 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 & Policies** — An identity carrying the AWS or Azure cloud-account binding for report storage, plus least-privilege policies: `reveal` on exactly the two prerequisite secrets, `manage` on images so scan tags can be written, and `pull`/`view` for the scanning service account.
* **No credential secrets** — The template creates no secrets of its own. Both the post token and the registry service account key live in secrets you create.

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

## Prerequisites

**Two secrets must exist before you install.** Secrets are org-level, so no GVC flag is involved.

### Report Post Token

The bearer token the daemon presents when uploading a report. It guards a publicly reachable write endpoint, so treat it like any other internet-facing credential.

<Steps>
  <Step title="Create the post token secret">
    An [opaque secret](/guides/create-secret/opaque) with encoding `plain` whose payload is the token:

    ```bash theme={null}
    printf '%s' "$(openssl rand -hex 32)" | \
      cpln secret create-opaque --name my-cpln-trivy-post-token --encoding plain -f -
    ```

    Set `postToken.secretName` to the name you used. Use `printf`, not `echo` — `echo` appends a newline that becomes part of the token.
  </Step>

  <Step title="Read it back later">
    `-o yaml` is required; without it the command prints the secret's metadata table rather than its payload:

    ```bash theme={null}
    cpln secret reveal my-cpln-trivy-post-token -o yaml
    ```
  </Step>
</Steps>

<Note>
  **The token is enforced, and only in the `Bearer` form.** Verified against the live public endpoint: a POST with no `Authorization` header, one with a wrong token, and one carrying the correct token *without* the `Bearer ` prefix were all refused with `401`; only `Authorization: Bearer <token>` was accepted. Report **reads** remain deliberately unauthenticated — the URL's SHA-256 hash is the only thing protecting a report.
</Note>

### Registry 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}
    printf '%s' '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>

<Warning>
  **A missing prerequisite secret wedges the install rather than failing it.** `cpln helm install` still exits 0 and reports success, the resources are created, and the workload then never starts. Because the container never ran, `cpln logs` returns **zero lines**, which reads as a broken platform rather than a missing prerequisite.

  The only diagnostic is `status.versions[].message`, which names the missing secret:

  ```bash theme={null}
  cpln workload get-deployments web-server --gvc GVC_NAME -o yaml
  ```

  ```text theme={null}
  The secret my-cpln-trivy-post-token no longer exists. Workload updates are
  paused until the secret is added or the reference to the secret removed.
  ```

  It is **`get-deployments`** — plain `cpln workload get` has no `versions` key at all. Creating the missing secret clears the wedge on its own, but slowly: recovery here was measured at **9 minutes 15 seconds**, inside the 5.5–10.5 minute range seen across the catalog, so poll rather than time-boxing it. A forced redeployment shortcuts it to roughly 90 seconds. The previously deployed revision keeps serving reports throughout.
</Warning>

### 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>

Once both secrets exist and your storage backend is ready, install the template using 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 # AWS cloud account name registered in Control Plane
    bucket: trivy-reports-bucket           # S3 bucket name
    region: us-east-1                      # AWS region
    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 the daemon presents when posting reports to the web-server.
# Name of a pre-existing CPLN opaque secret whose payload is the token (see Prerequisites).
postToken:
  secretName: my-cpln-trivy-post-token

# 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 (see Prerequisites)
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.secretName`                    | `my-cpln-trivy-post-token` | Name of an existing opaque secret whose payload is the bearer token the daemon uploads with           |
| `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                                                                           |

<Note>
  Report URLs are publicly accessible by default — they contain an unguessable SHA-256 hash, but no authentication on reads. Restrict `webServer.firewall.inboundAllowCIDR` if reports must stay private. Narrowing it also blocks the daemon, which reaches the web-server by that same public path, so add the daemon's egress range when you do.
</Note>

<Note>
  `webServer.firewall.inboundAllowCIDR` defaults to `0.0.0.0/0` by design — that is how the daemon delivers reports. The token is what keeps that endpoint from accepting anyone's uploads; see [How the Two Workloads Talk](#how-the-two-workloads-talk).
</Note>

## Upgrading From 1.1.0

Version `1.2.0` moved the report post token out of Helm values. Through `1.1.0` it shipped as `postToken: changeme` — a working, publicly documented token guarding an internet-facing write endpoint.

|                              | `1.1.0` and earlier                        | `1.2.0`                                                                          |
| ---------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------- |
| Post token                   | `postToken` — a plain string value         | `postToken.secretName` — an opaque secret you create, whose payload is the token |
| Registry service account key | `trivyAuth.secretName` prerequisite secret | Unchanged                                                                        |
| Chart-created secret         | Held the post token                        | None                                                                             |

<Warning>
  **Reuse the token your install already has.** The daemon and the web-server compare the same string, so a rotation that reaches only one of them fails every upload with `401` while both workloads still report healthy. If you generate a fresh token during the upgrade, both workloads pick it up together on the same rollout — but any token you had recorded elsewhere is then dead.
</Warning>

<Warning>
  **A `helm upgrade` that still sets `postToken` as a string is rejected before anything is applied.** A real `cpln helm upgrade` carrying the old shape failed at render, created no Helm revision, and left the running release healthy and untouched:

  ```text theme={null}
  cpln-trivy: postToken is no longer a plain value in 1.2.0. Put the token in an opaque secret
  (encoding: plain) and set postToken.secretName to its name. Reuse the SAME token your install
  already has, or the daemon's uploads start returning 401.
  ```

  Leaving `postToken.secretName` empty is refused the same way.
</Warning>

To upgrade an existing install:

<Steps>
  <Step title="Create the post token secret">
    Follow [Prerequisites](#prerequisites), putting your current token into it.
  </Step>

  <Step title="Drop the removed key from your values">
    Remove the `postToken:` string and set `postToken.secretName` instead. Leave `trivyAuth.secretName` and `serviceAccountName` exactly as they are.
  </Step>

  <Step title="Upgrade">
    Both workloads pick the token up on the rollout. Reports already stored keep their URLs.
  </Step>
</Steps>

### Rotating the Token Later

Edit the secret, then restart **both** workloads together. They must agree; a rotation reaching only one leaves uploads failing `401` with nothing else looking wrong.

## 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.

## Important Notes

* **Create both prerequisite secrets before installing.** A missing one wedges the deployment with no log output at all; [Prerequisites](#prerequisites) gives the one command that diagnoses it.
* **A scan touches every image in the organization**, not just images this install created — see the warning in [Overview](#overview).
* **Rotating the post token requires restarting both workloads together** — see [Rotating the Token Later](#rotating-the-token-later).
* **Report reads are unauthenticated.** Anyone with the URL can open a report.
* **Scans take roughly 15–20 seconds per image** — about 30 minutes for 100 images on a first run. Overlapping runs are prevented, so a long run simply delays the next scheduled one.
* **Rescans overwrite in place** — the report keeps its URL and `cpln/trivy-scan-time` is refreshed, so links saved from the console stay valid.
* **Rotate the service account key** by adding a new key, updating the opaque secret's payload, then deleting the old key. No reinstall needed.
* **The workloads are named `daemon` and `web-server` regardless of release name** — install this template only once per GVC.

## 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>
