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.