Automation

Azure Container Registry: migrate Docker Content Trust to Notation without opening the delivery chain

A production runbook for inventorying Docker Content Trust, dual-signing with Notation, validating digests, and cutting over CI/CD gates with an explicit rollback.

18 Sept 2026 azureacrdocker-content-trustnotationnotary-projectcontainer-securitysupply-chaindevopsautomationguardrailsrunbookrollbackproduction

A pipeline can reject unsigned images for years and lose that control during an otherwise legitimate migration. Docker Content Trust can no longer be enabled on new Azure Container Registry instances, or registries that did not previously use it, after 31 May 2026. Its complete removal is scheduled for 31 March 2028. Replacing DCT with Notation is therefore not a CLI upgrade. It changes the contract between build, registry and deployment.

The running case is a platform that publishes several images to ACR. Production promotions currently verify DCT-signed tags. The team needs to move to Notation signatures attached to OCI digests without creating a window in which an unverified image can reach AKS or another runtime. The runbook must end with an explicit decision: cut over, extend dual verification, or restore the last known gate.

Freeze the migration contract

Pause policy changes first, not builds. Describe what the current control actually protects: registries, repositories, promotion tags, signing identities, consumers and failure behavior.

yaml acr-signature-migration-contract.yml
migration: dct-to-notation
registry: acrprodweu.azurecr.io
repositories:
- payments/api
- payments/worker

current_gate:
mechanism: docker-content-trust
protected_tags: [release, stable]
failure_mode: block

target_gate:
mechanism: notation
reference: immutable-digest
trust_policy_version: prod-images-v1
failure_mode: block

cutover_requires:
- same_digest_verified_by_both_paths
- approved_publisher_identity
- negative_test_rejected
- rollback_pipeline_available

A tag-only inventory is not enough. A tag can move between the DCT observation and the Notation signature. The reconciliation key must be the full sha256 digest, together with the build run that produced it and the environment that consumes it.

Inventory before signing

Start with registries and pipelines, then work down to images. Identify registries where DCT is enabled, jobs that set DOCKER_CONTENT_TRUST, historical signer roles, protected tags and deployments that verify only a tag.

bash 01-inventory-current-trust.sh
ACR="acrprodweu"
REPO="payments/api"
TAG="release"

az acr config content-trust show --registry "$ACR" --output json

az acr manifest list-metadata --registry "$ACR" --name "$REPO" --orderby time_desc --top 20 --output table

docker trust inspect --pretty "$ACR.azurecr.io/$REPO:$TAG"

Keep a matrix per repository: observed tag, resolved digest, DCT presence, Notation presence, expected identity, last successful verification and consumer. Do not treat a failed docker trust inspect as proof of an unsigned image until authentication, registry and exact tag have been confirmed.

Use this step to remove a common ambiguity. ACR stores the artifact, but trust policy is enforced by clients, pipelines or admission controls. Enabling a registry capability never proved that every consumer blocked unapproved images.

Build Notation trust separately

Do not mechanically translate old DCT roles. Establish a trust store, a policy scoped to the registry and repository, and a publishing identity distinct from the deployment identity. The policy must name authorized identities and reject out-of-scope repositories.

json trustpolicy.json
{
"version": "1.0",
"trustPolicies": [
  {
    "name": "payments-production",
    "registryScopes": [
      "acrprodweu.azurecr.io/payments/api",
      "acrprodweu.azurecr.io/payments/worker"
    ],
    "signatureVerification": { "level": "strict" },
    "trustStores": ["ca:payments-release"],
    "trustedIdentities": ["x509.subject:<approved-publisher-subject>"]
  }
]
}

Import the policy on an isolated validation runner. Version its contents, certificate bundle, and Notation and plug-in versions. A local policy edited by hand is not a reproducible gate.

bash 02-load-notation-policy.sh
notation policy import trustpolicy.json --force
notation policy show
notation cert ls

Dual-sign the same digest

Select a previously published digest that is still deployable and representative. Do not rebuild the image for the migration. A new build would change the object and make the controls impossible to compare. Add a Notation signature to the existing digest with the approved key, then inventory its OCI referrers.

bash 03-sign-existing-digest.sh
IMAGE="acrprodweu.azurecr.io/payments/api@sha256:<candidate-digest>"
KEY_ID="https://kv-signing-prod.vault.azure.net/keys/container-signing/<version>"

notation sign --signature-format cose --id "$KEY_ID" --plugin azure-kv "$IMAGE"

notation ls "$IMAGE"
notation verify "$IMAGE"

Dual signing is not intended to preserve two systems forever. It creates a comparison period in which the old gate remains authoritative and Notation produces a shadow decision. For each promotion, log the digest, policy scope, recognized identity, DCT result, Notation result, tool versions and pipeline run.

Test rejection before cutover

A positive test proves only the nominal path. Build at least four cases: a correctly signed digest, a digest without a Notation signature, a valid signature from an unapproved identity, and a tag that moved after verification.

text notation-migration-evaluation.txt
Case 1 - approved digest
DCT: pass
Notation: pass
Expected: candidate remains eligible

Case 2 - unsigned digest
DCT: not sufficient for target state
Notation: fail
Expected: promotion blocked

Case 3 - unapproved publisher
Signature cryptographically valid
Trust policy: fail
Expected: promotion blocked

Case 4 - tag moved after verification
Verified digest differs from deployment digest
Expected: promotion blocked

Test verifier failure too. If Key Vault, the trust store, ACR or the plug-in is unavailable, the pipeline must distinguish a rejected artifact from an unavailable verifier while blocking promotion in both cases. The operator message and escalation path must not suggest disabling the control to get the release moving.

Cut over one repository at a time

Do not replace the global gate in one change. Start with a lower-risk repository, continue signing releases with both mechanisms, then make Notation authoritative only for that scope. Deploy by digest and compare the verified digest with the image actually observed in the runtime.

The gate can be promoted after several representative runs agree, negative tests are blocked, and no official consumer depends exclusively on DCT. The inventory must cover recovery jobs, approved manual deployments and secondary clusters. The main pipeline is not always the only route to production.

Do not delete DCT keys, metadata or stages during the first cutover. Remove their authority to approve a promotion, but retain the evidence required to explain older releases for the defined audit period.

Decide cutover, extension or rollback

text dct-notation-cutover-decision.txt
CUT OVER TO NOTATION
Every consumer deploys by digest.
The expected publisher passes the scoped policy.
Unsigned, wrong-publisher and moved-tag cases are blocked.
The pipeline rollback has been tested.

EXTEND DUAL VERIFICATION
Decisions agree but one DCT consumer remains active.
Notation stays shadow and no global exception is created.
The remaining consumer has an owner and exit date.

ROLL BACK THE GATE
Policy targets the wrong scope, the trust store is not reproducible,
or the verifier produces inconsistent decisions.
Restore the previous pipeline and policy versions.
Keep the candidate digest out of production and fix outside the incident.

STOP
A digest other than the verified digest reaches the runtime.
An unsigned image passes promotion.
The old control was removed before the new control was validated.

Rollback applies to the pipeline version, trust policy and authority decision. It does not mean re-enabling DCT on a registry that can no longer enable it, or temporarily accepting unverified images. Until the new gate is reliable, retain the last already verified image that can be deployed by digest.

Conclusion

A DCT-to-Notation migration succeeds when it preserves the delivery contract, not when a signing command exits successfully. Inventory consumers, reconcile both controls against one immutable digest, test refus, then cut over repository by repository.

The final outcome must remain operational: adopt Notation after positive and negative evidence, keep bounded dual verification for one identified consumer, or restore the last known gate without opening a production exception.