Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
Workload Orchestration can verify that deployment artifacts are signed by a trusted key or identity before the artifacts run on your edge clusters. Verification protects against artifacts that are unsigned, modified after signing, or signed by an untrusted identity.
Workload Orchestration provides two verification layers:
| Layer | Verifies | Enforcement point |
|---|---|---|
| Layer 1 | Helm charts, remote Kubernetes YAML files, and scripts | The Workload Orchestration agent verifies each artifact before the provider applies or runs it. |
| Layer 2 | Container images | Ratify and Gatekeeper verify each image when Kubernetes admits a pod. |
You can enable either layer independently. Use both layers for end-to-end verification of deployment artifacts and the images that they reference.
Important
Keyless signatures use different formats in each layer. Layer 1 requires the new Sigstore bundle format. Ratify 1.4.x in Layer 2 requires the legacy cosign format. Use the commands shown in the relevant section.
Prerequisites
Before you begin, make sure that you have:
- An Azure Arc-enabled Kubernetes cluster with Workload Orchestration extension version 2.1.41 or later. For setup instructions, see Set up workload orchestration.
- Azure CLI with the
k8s-extensionandconnectedk8sextensions. - cosign 2.6 or later.
- Helm 3.8 or later for Helm chart and container image verification.
- Permission to create role assignments for the resources used in this article.
Define the variables
The examples in this article use PowerShell. Set the shared variables once:
$subId = "<subscription-id>"
$rg = "<resource-group>"
$location = "<azure-region>"
$clusterName = "<arc-connected-cluster-name>"
$extensionName = "<workload-orchestration-extension-name>"
$workloadOrchestrationNamespace = "<arc-connected-cluster-namespace>"
az account set --subscription $subId
$workloadOrchestrationPrincipalId = az k8s-extension show `
--cluster-name $clusterName `
--resource-group $rg `
--cluster-type connectedClusters `
--name $extensionName `
--query identity.principalId `
--output tsv
Configure Layer 1 verification
Layer 1 verifies artifacts that the helm.v3, yaml.k8s, and script providers download. Choose one trust mode for all Layer 1 artifacts:
- Certificate: Sign with an Azure Key Vault key. The extension identity verifies the signature by using Key Vault.
- Keyless: Sign through an OpenID Connect (OIDC) provider. The extension trusts the exact issuer and subject in the signing certificate.
Prepare the trust source
Create an Azure Key Vault and signing key. Key Vault names must be globally unique.
$keyVaultName = "<key-vault-name>" $keyName = "workload-signing-key" az keyvault create ` --resource-group $rg ` --name $keyVaultName ` --location $location ` --enable-rbac-authorization true $keyVaultId = az keyvault show ` --resource-group $rg ` --name $keyVaultName ` --query id ` --output tsv $keyVaultUri = az keyvault show ` --resource-group $rg ` --name $keyVaultName ` --query properties.vaultUri ` --output tsv $signedInUserId = az ad signed-in-user show --query id --output tsv az role assignment create ` --assignee-object-id $signedInUserId ` --assignee-principal-type User ` --role "Key Vault Crypto Officer" ` --scope $keyVaultId az keyvault key create ` --vault-name $keyVaultName ` --name $keyName ` --kty EC ` --curve P-256 ` --ops sign verify $keyReference = "azurekms://$keyVaultName.vault.azure.net/$keyName"Allow the Workload Orchestration extension identity to verify signatures.
az role assignment create ` --assignee-object-id $workloadOrchestrationPrincipalId ` --assignee-principal-type ServicePrincipal ` --role "Key Vault Crypto User" ` --scope $keyVaultId
Sign an artifact
Use the tab for the provider in your solution template.
Push the packaged chart to an Azure Container Registry (ACR), and grant the extension identity permission to pull it.
$containerRegistryName = "<container-registry-name>" $chartPackage = "<chart-package.tgz>" az acr create ` --resource-group $rg ` --name $containerRegistryName ` --location $location ` --sku Standard $containerRegistryId = az acr show ` --resource-group $rg ` --name $containerRegistryName ` --query id ` --output tsv $containerRegistryLoginServer = az acr show ` --resource-group $rg ` --name $containerRegistryName ` --query loginServer ` --output tsv az role assignment create ` --assignee-object-id $workloadOrchestrationPrincipalId ` --assignee-principal-type ServicePrincipal ` --role AcrPull ` --scope $containerRegistryId az acr login --name $containerRegistryName helm push $chartPackage "oci://$containerRegistryLoginServer/charts" $chartReference = "$containerRegistryLoginServer/charts/<chart-name>:<chart-version>"Sign the chart by using the trust mode you configured.
# Certificate mode cosign sign --key $keyReference --tlog-upload=false $chartReference # Keyless mode. Complete the browser sign-in when prompted. cosign sign --new-bundle-format $chartReference
Enable Layer 1 on the extension
az k8s-extension update `
--cluster-name $clusterName `
--resource-group $rg `
--cluster-type connectedClusters `
--name $extensionName `
--config "signing.enabled=true" `
--config "signing.mode=cert" `
--config "signing.trustVaultUri=$keyVaultUri" `
--config "signing.keyNames=$keyName"
Validate Layer 1
Deploy the solution through your normal Workload Orchestration workflow. Then confirm that verification ran:
kubectl logs `
--namespace $workloadOrchestrationNamespace `
deployment/symphony-api | `
Select-String -Pattern "(chart|YAML|script) signature verification (succeeded|skipped)"
The following results confirm that Layer 1 is enforcing your trust configuration:
| Test | Expected result |
|---|---|
| Deploy a correctly signed artifact. | Deployment succeeds, and the log contains signature verification succeeded. |
| Modify an artifact after signing. | Deployment fails with error 50100, signature mismatch. |
Remove the .sig or .bundle file, or use an unsigned chart. |
Deployment fails with error 50101, signature not found. |
| Sign with a different OIDC identity. | Deployment fails with error 50111, identity mismatch. |
Configure Layer 2 verification
Layer 2 uses Ratify and the Azure Policy add-on's Gatekeeper installation to verify container images at pod admission. The order in this section is required.
Caution
After you remove the default Ratify configuration objects, don't upgrade or reinstall the Ratify Helm release. The chart recreates those objects and can remove the service account annotation. If you reinstall Ratify, repeat the annotation and deletion steps before updating the Workload Orchestration extension.
Prepare the cluster and image
Confirm that OIDC issuer and workload identity are enabled on the Arc-enabled cluster and that Gatekeeper is running with external data enabled.
$oidcIssuer = az connectedk8s show ` --resource-group $rg ` --name $clusterName ` --query oidcIssuerProfile.issuerUrl ` --output tsv kubectl get deployment --namespace gatekeeper-systemIf
$oidcIssueris empty, enable the required features:az connectedk8s update ` --resource-group $rg ` --name $clusterName ` --enable-oidc-issuer ` --enable-workload-identity $oidcIssuer = az connectedk8s show ` --resource-group $rg ` --name $clusterName ` --query oidcIssuerProfile.issuerUrl ` --output tsvSet the Layer 2 variables. Use an image that's already stored in ACR, and ensure that your cluster nodes can pull it.
$containerRegistryName = "<container-registry-name>" $imageReference = "<registry-name>.azurecr.io/<repository>@sha256:<digest>" $gatekeeperNamespace = "gatekeeper-system" $ratifyServiceAccount = "ratify-admin" $ratifyIdentityName = "id-ratify" $providerName = "symphony-ratify-provider" $tenantId = az account show --query tenantId --output tsv $containerRegistryId = az acr show ` --resource-group $rg ` --name $containerRegistryName ` --query id ` --output tsv $signedInUserId = az ad signed-in-user show --query id --output tsv az role assignment create ` --assignee-object-id $signedInUserId ` --assignee-principal-type User ` --role AcrPush ` --scope $containerRegistryIdSign the image in the legacy format required by Ratify 1.4.x. Record the issuer and subject reported by cosign.
$registryToken = az acr login ` --name $containerRegistryName ` --expose-token ` --query accessToken ` --output tsv cosign sign ` --new-bundle-format=false ` --yes ` --registry-username "00000000-0000-0000-0000-000000000000" ` --registry-password $registryToken ` $imageReference $keylessIssuer = "<issuer-from-cosign-certificate>" $keylessSubject = "<subject-from-cosign-certificate>"
Install and configure Ratify
Create a user-assigned managed identity for Ratify, grant it pull access to ACR, and federate it with the Ratify service account.
az identity create ` --resource-group $rg ` --name $ratifyIdentityName ` --location $location $ratifyClientId = az identity show ` --resource-group $rg ` --name $ratifyIdentityName ` --query clientId ` --output tsv $ratifyPrincipalId = az identity show ` --resource-group $rg ` --name $ratifyIdentityName ` --query principalId ` --output tsv az role assignment create ` --assignee-object-id $ratifyPrincipalId ` --assignee-principal-type ServicePrincipal ` --role AcrPull ` --scope $containerRegistryId az identity federated-credential create ` --resource-group $rg ` --identity-name $ratifyIdentityName ` --name ratify-federated-credential ` --issuer $oidcIssuer ` --subject "system:serviceaccount:${gatekeeperNamespace}:${ratifyServiceAccount}" ` --audience api://AzureADTokenExchangeInstall Ratify with mutation disabled. The Azure Policy add-on blocks the Gatekeeper mutation objects that the chart otherwise creates.
helm repo add ratify https://ratify-project.github.io/ratify helm repo update helm install ratify ratify/ratify ` --namespace $gatekeeperNamespace ` --version 1.15.6 ` --set azureWorkloadIdentity.clientId=$ratifyClientId ` --set serviceAccount.name=$ratifyServiceAccount ` --set provider.enableMutation=false ` --wait ` --timeout 180sAnnotate the service account, restart Ratify, and remove the chart's default configuration objects. Workload Orchestration creates its own
symphony-*objects.kubectl annotate serviceaccount $ratifyServiceAccount ` --namespace $gatekeeperNamespace ` "azure.workload.identity/client-id=$ratifyClientId" ` --overwrite kubectl rollout restart deployment/ratify --namespace $gatekeeperNamespace kubectl rollout status deployment/ratify ` --namespace $gatekeeperNamespace ` --timeout 120s kubectl delete provider.externaldata.gatekeeper.sh ` ratify-provider ratify-mutation-provider ` --ignore-not-found kubectl delete store.config.ratify.deislabs.io ` store-oras ` --ignore-not-found kubectl delete verifier.config.ratify.deislabs.io ` verifier-cosign verifier-notation ` --ignore-not-found $ratifyCaBundle = kubectl get secret ratify-tls ` --namespace $gatekeeperNamespace ` --output "jsonpath={.data.ca\.crt}"
Enable Layer 2 on the extension
Update the extension only after Ratify is healthy and the default objects are removed:
az k8s-extension update `
--cluster-name $clusterName `
--resource-group $rg `
--cluster-type connectedClusters `
--name $extensionName `
--config "signing.enabled=true" `
--config "signing.mode=keyless" `
--config "signing.keylessIssuer=$keylessIssuer" `
--config "signing.keylessSubject=$keylessSubject" `
--config "signing.imageVerification.enabled=true" `
--config "signing.imageVerification.manageRatify=check" `
--config "signing.imageVerification.providerName=$providerName" `
--config "signing.imageVerification.tenantId=$tenantId" `
--config "signing.imageVerification.clientId=$ratifyClientId" `
--config "signing.imageVerification.ratifyCABundle=$ratifyCaBundle" `
--config "signing.imageVerification.policyDelivery=azurePolicy"
Confirm that the extension created the Ratify configuration:
kubectl get store.config.ratify.deislabs.io,verifier.config.ratify.deislabs.io --all-namespaces
kubectl get provider.externaldata.gatekeeper.sh $providerName
Assign the admission policy
Enabling Layer 2 creates the Ratify configuration but doesn't enforce admission. Assign the Workload Orchestration Ratify policy supplied with your release to the cluster.
Important
On a dual-registered AKS cluster, assign the policy to the AKS managed cluster resource. Assigning it to the Arc connected cluster doesn't enable enforcement.
Determine the policy scope and register the policy definition. Set
$policyFileto the policy definition supplied with your Workload Orchestration release.$arcClusterId = az connectedk8s show ` --resource-group $rg ` --name $clusterName ` --query id ` --output tsv $aksClusterId = az resource list ` --name $clusterName ` --resource-type Microsoft.ContainerService/managedClusters ` --query "[0].id" ` --output tsv $policyScope = if ($aksClusterId) { $aksClusterId } else { $arcClusterId } $policyDefinitionName = "workload-orchestration-ratify-verification" $policyFile = "<path-to-ratify-policy-definition.json>" az rest ` --method put ` --url "https://management.azure.com/subscriptions/$subId/providers/Microsoft.Authorization/policyDefinitions/${policyDefinitionName}?api-version=2023-04-01" ` --resource https://management.azure.com/ ` --headers "Content-Type=application/json" ` --body "@$policyFile" $policyDefinitionId = az policy definition show ` --name $policyDefinitionName ` --query id ` --output tsvAssign the policy in
Auditmode first. The Azure Policy add-on can take about 15 minutes to create the Gatekeeper constraint.$policyAssignmentName = "workload-orchestration-ratify-verification" $excludedNamespaces = @( "kube-system", "kube-public", "kube-node-lease", "azure-arc", "azure-arc-release", "cert-manager", $gatekeeperNamespace, $workloadOrchestrationNamespace ) $policyParameters = @{ effect = @{ value = "Audit" } providerName = @{ value = $providerName } workloadOrchestrationNamespace = @{ value = $workloadOrchestrationNamespace } gatekeeperNamespace = @{ value = $gatekeeperNamespace } excludedNamespaces = @{ value = $excludedNamespaces } } | ConvertTo-Json -Depth 10 -Compress az policy assignment create ` --name $policyAssignmentName ` --scope $policyScope ` --policy $policyDefinitionId ` --params $policyParametersAfter you confirm that signed images pass verification, change
effecttoDenyand rerun the assignment command with the same name and scope.$policyParameters = @{ effect = @{ value = "Deny" } providerName = @{ value = $providerName } workloadOrchestrationNamespace = @{ value = $workloadOrchestrationNamespace } gatekeeperNamespace = @{ value = $gatekeeperNamespace } excludedNamespaces = @{ value = $excludedNamespaces } } | ConvertTo-Json -Depth 10 -Compress az policy assignment create ` --name $policyAssignmentName ` --scope $policyScope ` --policy $policyDefinitionId ` --params $policyParameters
Validate Layer 2
Wait for the Gatekeeper constraint to show deny, and then create new pods. Gatekeeper evaluates pod creation, not pods that are already running.
kubectl get constraints --all-namespaces | Select-String ratify
$testNamespace = "artifact-verification-test"
$unsignedImage = "<registry-name>.azurecr.io/<repository>:<unsigned-tag>"
kubectl create namespace $testNamespace
kubectl delete pod signed unsigned `
--namespace $testNamespace `
--ignore-not-found
# Expected: Pod created.
kubectl run signed --image=$imageReference --namespace $testNamespace
# Expected: Gatekeeper denies admission.
kubectl run unsigned --image=$unsignedImage --namespace $testNamespace
If both pods are denied, inspect the Ratify logs:
kubectl logs deployment/ratify `
--namespace $gatekeeperNamespace `
--tail 50
- A
401 Unauthorizedor descriptor resolution error indicates an ACR identity or role assignment problem. no valid Cosign signaturesornil certificateusually indicates that the image wasn't signed with--new-bundle-format=false.
Disable verification
Disable Layer 1
az k8s-extension update `
--cluster-name $clusterName `
--resource-group $rg `
--cluster-type connectedClusters `
--name $extensionName `
--config "signing.enabled=false"
Disable Layer 2
Remove enforcement before you remove Ratify or its extension configuration. Reversing this order can cause Gatekeeper to deny every image because its verification provider is unavailable.
Delete the policy assignment.
az policy assignment delete ` --name $policyAssignmentName ` --scope $policyScopeWait until both commands return no Ratify resources. The Azure Policy add-on can take about 15 minutes to synchronize.
kubectl get constraints --all-namespaces | Select-String ratify kubectl get constrainttemplate | Select-String ratifyDisable image verification, and then optionally uninstall Ratify.
az k8s-extension update ` --cluster-name $clusterName ` --resource-group $rg ` --cluster-type connectedClusters ` --name $extensionName ` --config "signing.imageVerification.enabled=false" helm uninstall ratify --namespace $gatekeeperNamespace