diff --git a/charts/registry-scanner/Chart.yaml b/charts/registry-scanner/Chart.yaml
index 51a0178fb..cccb3948e 100644
--- a/charts/registry-scanner/Chart.yaml
+++ b/charts/registry-scanner/Chart.yaml
@@ -1,10 +1,10 @@
apiVersion: v2
-name: registry-scanner
+appVersion: 0.12.1
description: Sysdig Registry Scanner
-type: application
home: https://www.sysdig.com/
icon: https://avatars.githubusercontent.com/u/5068817?s=200&v=4
-version: 1.11.1
-appVersion: 0.12.1
maintainers:
- - name: sysdiglabs
+- name: sysdiglabs
+name: registry-scanner
+type: application
+version: 1.12.0
diff --git a/charts/registry-scanner/README.md b/charts/registry-scanner/README.md
index c8c4d1040..86e4fa0fc 100644
--- a/charts/registry-scanner/README.md
+++ b/charts/registry-scanner/README.md
@@ -65,6 +65,7 @@ The following table lists the configurable parameters of the Sysdig Registry Sca
| config.registryUser | The username for registry authentication. | "" |
| config.registryPassword | The password for registry authentication. | "" |
| config.registryType | **required**
The registry Type. Supported types: artifactory, ecr, icr, acr, quay, harbor, gar, gcr, nexus, ocp and dockerv2. | "" |
+| config.acr_workloadidentity | Use an Azure AD access token from the init container (IMDS) instead of `registryPassword` for ACR authentication. Applies only when `registryType` is `acr`. `registryUser` is still required and `registryPassword` is ignored. | false |
| config.registryAccountId | The account ID. Applicable only for ICR registry type. | "" |
| config.icrIamApi | The ICR IAM API. Applicable only for ICR registry type. | "" |
| config.icrIamApiSkipTLS | Ignore TLS certificate for IAM API. Applicable only for ICR registry type. | false |
diff --git a/charts/registry-scanner/templates/_helpers.tpl b/charts/registry-scanner/templates/_helpers.tpl
index de551ea03..f8a12e876 100644
--- a/charts/registry-scanner/templates/_helpers.tpl
+++ b/charts/registry-scanner/templates/_helpers.tpl
@@ -130,6 +130,16 @@ Allow overriding registry and repository for air-gapped environments
{{- end -}}
{{- end -}}
+{{/*
+Init container image for AAD token fetcher (ACR workload identity)
+*/}}
+{{- define "registry-scanner.initContainerImage" -}}
+{{- $initRegistry := .Values.image.initContainerRegistry | default .Values.image.registry | default "quay.io" -}}
+{{- $initRepository := .Values.image.initContainerRepository | default "library/curl" -}}
+{{- $initTag := .Values.image.initContainerTag | default "7.85.0" -}}
+{{- printf "%s/%s:%s" $initRegistry $initRepository $initTag -}}
+{{- end -}}
+
{{- define "registry-scanner.rawPullSecretList" -}}
{{- range .Values.imagePullSecrets }}{{- if . }}{{- .name}},{{- end}}{{- end}}
{{- end -}}
diff --git a/charts/registry-scanner/templates/_job.tpl b/charts/registry-scanner/templates/_job.tpl
index 949620879..3e0adafb4 100644
--- a/charts/registry-scanner/templates/_job.tpl
+++ b/charts/registry-scanner/templates/_job.tpl
@@ -9,6 +9,9 @@
labels:
{{- include "registry-scanner.labels" . | nindent 12 }}
{{- include "registry-scanner.customLabels" . | nindent 12 }}
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ azure.workload.identity/use: "true"
+ {{- end }}
{{- with .Values.podAnnotations }}
annotations:
{{- toYaml . | nindent 12 }}
@@ -21,12 +24,94 @@
serviceAccountName: {{ include "registry-scanner.serviceAccountName" . }}
securityContext:
{{- toYaml .Values.podSecurityContext | nindent 12 }}
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ initContainers:
+ - name: aad-token-fetcher
+ image: {{ include "registry-scanner.initContainerImage" . }}
+ imagePullPolicy: {{ .Values.image.initContainerPullPolicy | default .Values.image.pullPolicy }}
+ command:
+ - /bin/sh
+ - -c
+ - |
+ # Obtain an ACR refresh token as the *federated* managed identity (the UAMI in the
+ # service account's azure.workload.identity/client-id annotation), NOT the node/kubelet
+ # identity. The azure-workload-identity webhook injects AZURE_CLIENT_ID / AZURE_TENANT_ID /
+ # AZURE_FEDERATED_TOKEN_FILE because the pod carries azure.workload.identity/use=true.
+ REGISTRY_URL="{{ .Values.config.registryURL }}"
+ : "${AZURE_CLIENT_ID:?workload identity not injected (AZURE_CLIENT_ID unset) - check the SA azure.workload.identity/client-id annotation and the azure.workload.identity/use pod label}"
+ : "${AZURE_TENANT_ID:?workload identity not injected (AZURE_TENANT_ID unset)}"
+ : "${AZURE_FEDERATED_TOKEN_FILE:?workload identity not injected (AZURE_FEDERATED_TOKEN_FILE unset)}"
+ AUTHORITY_HOST="${AZURE_AUTHORITY_HOST:-https://login.microsoftonline.com/}"
+
+ fetch_token() {
+ FED_TOKEN="$(cat "$AZURE_FEDERATED_TOKEN_FILE")"
+ # 1) Federated client-assertion exchange -> AAD access token. ACR with
+ # azureAdAuthenticationAsArmPolicy enabled expects the ARM audience; a token for
+ # the ACR-specific audience yields a refresh token that lacks registry:catalog:*.
+ AAD_TOKEN="$(curl -s -X POST \
+ "${AUTHORITY_HOST}${AZURE_TENANT_ID}/oauth2/v2.0/token" \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode "client_id=${AZURE_CLIENT_ID}" \
+ --data-urlencode 'grant_type=client_credentials' \
+ --data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
+ --data-urlencode "client_assertion=${FED_TOKEN}" \
+ --data-urlencode 'scope=https://management.azure.com/.default' \
+ | grep -o '"access_token":"[^"]*' | cut -d'"' -f4)"
+ [ -n "$AAD_TOKEN" ] || return 1
+ # 2) Exchange the AAD token for an ACR refresh token (the docker password used with the
+ # null-GUID username the chart sets as registryUser).
+ ACR_TOKEN="$(curl -s -X POST \
+ "https://${REGISTRY_URL}/oauth2/exchange" \
+ -H 'Content-Type: application/x-www-form-urlencoded' \
+ --data-urlencode 'grant_type=access_token' \
+ --data-urlencode "service=${REGISTRY_URL}" \
+ --data-urlencode "tenant=${AZURE_TENANT_ID}" \
+ --data-urlencode "access_token=${AAD_TOKEN}" \
+ | grep -o '"refresh_token":"[^"]*' | cut -d'"' -f4)"
+ [ -n "$ACR_TOKEN" ] || return 1
+ printf '%s' "$ACR_TOKEN" > /aad-token/token
+ }
+
+ MAX_RETRIES=5
+ RETRY_COUNT=0
+ while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
+ if fetch_token; then
+ echo "ACR refresh token obtained via workload identity"
+ exit 0
+ fi
+ RETRY_COUNT=$((RETRY_COUNT + 1))
+ [ $RETRY_COUNT -lt $MAX_RETRIES ] && { echo "Retrying ACR token fetch ($RETRY_COUNT/$MAX_RETRIES)..."; sleep 2; }
+ done
+ echo "Failed to obtain ACR token via workload identity after $MAX_RETRIES attempts" >&2
+ exit 1
+ volumeMounts:
+ - name: aad-token
+ mountPath: /aad-token
+ securityContext:
+ allowPrivilegeEscalation: false
+ capabilities:
+ drop:
+ - ALL
+ readOnlyRootFilesystem: true
+ runAsNonRoot: true
+ runAsUser: 65534
+ runAsGroup: 65534
+ seccompProfile:
+ type: RuntimeDefault
+ {{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:
{{- toYaml .Values.securityContext | nindent 14 }}
image: {{ include "registry-scanner.image" . }}
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ # The scanner binary ignores REGISTRYSCANNER_REGISTRY_PASSWORD_FILE; load the WIF-minted ACR
+ # token that the init container wrote into the password env, then exec the scanner.
+ command: ["/bin/sh", "-c", "export REGISTRYSCANNER_REGISTRY_PASSWORD=\"$(cat /aad-token/token)\"; exec /registry-scanner \"$@\"", "registry-scanner"]
+ args: ["--scan_runner=new-vm-scanner-k8s-job"]
+ {{- else }}
args: [ "--scan_runner=new-vm-scanner-k8s-job"]
+ {{- end }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
resources:
{{- toYaml .Values.resources | nindent 14 }}
@@ -34,6 +119,10 @@
- name: config-volume
mountPath: /config.yaml
subPath: config.yaml
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ - name: aad-token
+ mountPath: /aad-token
+ {{- end }}
{{- if .Values.reportToPersistentVolumeClaim }}
- name: report-storage
mountPath: "/output"
@@ -120,6 +209,9 @@
key: registryUser
{{- if ne .Values.config.registryType "ocp" }}
- name: REGISTRYSCANNER_REGISTRY_PASSWORD
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ value: ""
+ {{- else }}
valueFrom:
secretKeyRef:
{{- if not .Values.existingSecretName }}
@@ -128,6 +220,11 @@
name: {{ .Values.existingSecretName }}
{{- end }}
key: registryPassword
+ {{- end }}
+ {{- end }}
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ - name: REGISTRYSCANNER_REGISTRY_PASSWORD_FILE
+ value: /aad-token/token
{{- end }}
{{- end }}
{{ if .Values.config.parallelGoRoutines }}
@@ -160,6 +257,10 @@
- name: config-volume
configMap:
name: {{ include "registry-scanner.fullname" . }}
+ {{- if and (eq .Values.config.registryType "acr") .Values.config.acr_workloadidentity }}
+ - name: aad-token
+ emptyDir: {}
+ {{- end }}
{{- if .Values.ssl.ca.certs }}
- name: ca-certs
projected:
diff --git a/charts/registry-scanner/templates/secret.yaml b/charts/registry-scanner/templates/secret.yaml
index 952e189d9..02710ec31 100644
--- a/charts/registry-scanner/templates/secret.yaml
+++ b/charts/registry-scanner/templates/secret.yaml
@@ -20,9 +20,10 @@ data:
registryPassword: {{ required "A valid .Values.config.registryPassword is required" .Values.config.registryPassword | b64enc | quote }}
{{- else if eq .Values.config.registryType "ocp" }}
registryUser: {{ "serviceaccount"| b64enc | quote }}
+ {{- else if not (and .Values.config.acr_workloadidentity (or (eq .Values.config.registryType "acr") (eq .Values.config.registryType ""))) }}
+ registryPassword: {{ required "A valid .Values.config.registryPassword is required" .Values.config.registryPassword | b64enc | quote }}
{{- else }}
registryUser: {{ required "A valid .Values.config.registryUser is required" .Values.config.registryUser | b64enc | quote }}
- registryPassword: {{ required "A valid .Values.config.registryPassword is required" .Values.config.registryPassword | b64enc | quote }}
{{- end }}
{{- end }}
{{- range $index, $cert := .Values.ssl.ca.certs }}
diff --git a/charts/registry-scanner/tests/job_test.yaml b/charts/registry-scanner/tests/job_test.yaml
index 9eaf5a0d3..cb50ccca3 100644
--- a/charts/registry-scanner/tests/job_test.yaml
+++ b/charts/registry-scanner/tests/job_test.yaml
@@ -176,3 +176,77 @@ tests:
secretKeyRef:
name: RELEASE-NAME-registry-scanner
key: aws_region
+
+ - it: wires ACR workload identity token fetcher and password file env vars
+ set:
+ scanOnStart.enabled: true
+ config.registryType: "acr"
+ config.registryUser: "dummy"
+ config.acr_workloadidentity: true
+ asserts:
+ - contains:
+ path: spec.template.spec.initContainers
+ content:
+ name: aad-token-fetcher
+ - contains:
+ path: spec.template.spec.volumes
+ content:
+ name: aad-token
+ emptyDir: {}
+ - contains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: aad-token
+ mountPath: /aad-token
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: REGISTRYSCANNER_REGISTRY_PASSWORD
+ value: ""
+ - contains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: REGISTRYSCANNER_REGISTRY_PASSWORD_FILE
+ value: /aad-token/token
+
+ - it: does not wire ACR workload identity token fetcher when flag is disabled
+ set:
+ scanOnStart.enabled: true
+ config.registryType: "acr"
+ config.registryUser: "dummy"
+ config.registryPassword: "dummy"
+ config.acr_workloadidentity: false
+ asserts:
+ - isNull:
+ path: spec.template.spec.initContainers
+ - notContains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: REGISTRYSCANNER_REGISTRY_PASSWORD_FILE
+ value: /aad-token/token
+
+ - it: does not wire ACR workload identity token fetcher for non-ACR registries
+ set:
+ scanOnStart.enabled: true
+ config.registryType: "quay"
+ config.registryUser: "dummy"
+ config.registryPassword: "dummy"
+ config.acr_workloadidentity: true
+ asserts:
+ - isNull:
+ path: spec.template.spec.initContainers
+ - notContains:
+ path: spec.template.spec.volumes
+ content:
+ name: aad-token
+ emptyDir: {}
+ - notContains:
+ path: spec.template.spec.containers[0].volumeMounts
+ content:
+ name: aad-token
+ mountPath: /aad-token
+ - notContains:
+ path: spec.template.spec.containers[0].env
+ content:
+ name: REGISTRYSCANNER_REGISTRY_PASSWORD_FILE
+ value: /aad-token/token
diff --git a/charts/registry-scanner/tests/secret_test.yaml b/charts/registry-scanner/tests/secret_test.yaml
new file mode 100644
index 000000000..21b00f9d5
--- /dev/null
+++ b/charts/registry-scanner/tests/secret_test.yaml
@@ -0,0 +1,42 @@
+suite: Registry Scanner - Secret
+templates:
+ - templates/secret.yaml
+tests:
+ - it: should set registryPassword for ACR when workload identity is disabled
+ set:
+ config.secureAPIToken: dummy
+ config.registryType: acr
+ config.registryPassword: dummy
+ config.acr_workloadidentity: false
+ asserts:
+ - equal:
+ path: data.registryPassword
+ value: ZHVtbXk=
+ - isNull:
+ path: data.registryUser
+
+ - it: should set registryUser and omit registryPassword for ACR when workload identity is enabled
+ set:
+ config.secureAPIToken: dummy
+ config.registryType: acr
+ config.registryUser: dummy
+ config.acr_workloadidentity: true
+ asserts:
+ - equal:
+ path: data.registryUser
+ value: ZHVtbXk=
+ - isNull:
+ path: data.registryPassword
+
+ - it: should still set registryPassword for non-ACR registries even when workload identity is enabled
+ set:
+ config.secureAPIToken: dummy
+ config.registryType: quay
+ config.registryPassword: dummy
+ config.acr_workloadidentity: true
+ asserts:
+ - equal:
+ path: data.registryPassword
+ value: ZHVtbXk=
+ - isNull:
+ path: data.registryUser
diff --git a/charts/registry-scanner/values.yaml b/charts/registry-scanner/values.yaml
index 12512dd05..b46b30d99 100644
--- a/charts/registry-scanner/values.yaml
+++ b/charts/registry-scanner/values.yaml
@@ -30,6 +30,12 @@ config:
registryPassword: ""
# **required**
The registry Type. Supported types: artifactory, ecr, icr, acr, quay, harbor, gar, gcr, nexus, ocp and dockerv2.
registryType: ""
+ # Authenticate to ACR as the federated managed identity (Azure Workload Identity) via an init
+ # container, instead of registryPassword. The init container exchanges the projected federated
+ # token for an ACR refresh token. Requires the service account to be annotated with
+ # azure.workload.identity/client-id; the chart adds the azure.workload.identity/use pod label.
+ # Applies only when registryType is 'acr'. registryUser is still required (null GUID); registryPassword is ignored.
+ acr_workloadidentity: false
# The account ID. Applicable only for ICR registry type.
registryAccountId: ""
# The ICR IAM API. Applicable only for ICR registry type.
@@ -180,6 +186,14 @@ image:
pullPolicy: Always
# Whether or not to use a Federal Information Processing Standard (FIPS) compliant image.
fips: false
+ # Init container image registry for AAD token fetcher (when registryType is acr). Defaults to the main image registry.
+ initContainerRegistry: ""
+ # Init container image repository for AAD token fetcher. Defaults to curel/curl on the main registry.
+ initContainerRepository: "curl/curl"
+ # Init container image tag. Defaults to 8.21.0
+ initContainerTag: "8.21.0"
+ # Init container pull policy. Defaults to the main pullPolicy.
+ initContainerPullPolicy: ""
serviceAccount:
# Specifies whether a service account should be created.