# CL-0019: Image Tag Without Digest

**Severity:** MEDIUM

**References:**
- [OWASP Docker Security Rule #13](https://cheatsheetseries.owasp.org/cheatsheets/Docker_Security_Cheat_Sheet.html#rule-13-enhance-supply-chain-security)

## What it detects

Services using a version tag (e.g., `nginx:1.25.3`) without a digest pin (`@sha256:...`). This rule does not overlap with CL-0004:

| Image reference | CL-0004 | CL-0019 |
|----------------|---------|---------|
| `nginx` | fires | — |
| `nginx:latest` | fires | — |
| `nginx:1.25.3` | — | fires |
| `nginx:1.25.3@sha256:...` | — | — |

Build-only services (no `image:` key) are skipped.

## Why it matters

Docker tags are mutable pointers. A registry operator, compromised CI pipeline, or supply chain attack can overwrite `nginx:1.25.3` with a different image. Without a digest pin, `docker compose pull` will silently fetch the replacement. You have no cryptographic guarantee that the image you pull today is the same one you tested last week.

## Fix

Add a digest pin. Get the current digest without pulling the full image:

```bash
# Preferred: doesn't require a full pull, returns the OCI index digest
crane digest nginx:1.25.3
docker buildx imagetools inspect nginx:1.25.3 --format '{{ .Manifest.Digest }}'

# Or after pulling
docker pull nginx:1.25.3 && \
  docker inspect --format='{{index .RepoDigests 0}}' nginx:1.25.3
```

Apply it:

```yaml
# Before
services:
  app:
    image: nginx:1.25.3

# After
services:
  app:
    image: nginx:1.25.3@sha256:6a5af...
```

### Pin the index digest, not a per-arch digest

For multi-arch images (most official images on Docker Hub), there are two kinds of digests:

- **OCI index / manifest list digest** — points to a manifest that lists per-architecture manifests. This is what `docker pull` resolves through.
- **Per-arch manifest digest** — points to a single architecture (e.g. `linux/amd64`).

Pin the **index** digest. Pinning a per-arch digest will break ARM64 (or AMD64) users who pull on the other architecture. `crane digest` and `docker buildx imagetools inspect` return the index digest by default; `docker inspect` after a pull returns whichever architecture you happened to pull. Verify with `docker manifest inspect <image>@<digest>` — the result should be a `manifest.list.v2+json` (index), not a single `manifest.v2+json`.

Use [Dependabot](https://docs.github.com/en/code-security/dependabot) or [Renovate](https://docs.renovatebot.com/docker/) to automatically update digest pins when new images are published under the same tag.

## See also

- [CL-0004](CL-0004.md) — image not pinned to a tag (the weaker prerequisite to digest pinning)
