CI/CD Integration¶
Integrate cc-scan into your pipeline to gate deployments on Azure Key Vault security findings.
How It Works¶
The scanner exits with a meaningful code your pipeline can act on. When several apply, the first one in this table wins:
| Exit Code | Meaning |
|---|---|
2 | Scan failed or read nothing (sign-in error, no subscriptions, every Key Vault list failed, timeout) |
3 | No API key and no terminal to register in: set CC_SCAN_KEY |
1 | A finding at or above --fail-on (default high) |
4 | Scan finished with gaps: some subscriptions, vaults or item types could not be read, or a --subscription or --vault was not found |
0 | None of the above |
Any non-zero exit fails a pipeline step, which is what a gate should do. Treat 4 as a failure too: a scan that could not read a vault cannot tell you that vault is clean. Use --fail-on critical to gate on critical findings only, or --fail-on never to gate on scan health alone. See the Scanner reference for the full definitions and the list of security checks.
Authentication in Pipelines¶
The examples below sign in with workload identity federation: the pipeline proves who it is with a short-lived token, so no Azure secret is stored in it. The scanner then signs in one of two ways:
--auth cli(the default) uses the Azure CLI session. GitHub Actions and Azure DevOps use this: sign in withazure/loginor inside anAzureCLI@2task, then runcc-scan.azure/loginsigns in the Azure CLI only and does not setAZURE_FEDERATED_TOKEN_FILE, so keep the default--auth clithere.--auth defaultuses the Azure SDK'sDefaultAzureCredential, which readsAZURE_CLIENT_ID,AZURE_TENANT_IDandAZURE_FEDERATED_TOKEN_FILE(WorkloadIdentityCredential). GitLab uses this, because its image has no Azure CLI.
A client secret still works: set AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET and run cc-scan --auth default.
Create the Pipeline Identity¶
Create a Microsoft Entra app registration (or a user-assigned managed identity) and add a federated credential that trusts your pipeline:
- GitHub Actions: Authenticate to Azure from GitHub Actions by OpenID Connect
- Azure DevOps: an Azure Resource Manager service connection with workload identity federation
- GitLab: Connect to cloud services: Azure
Then give the identity the scanner's read-only roles: Reader on each subscription, and Key Vault Reader on vaults that use Azure RBAC (List on secrets, certificates and keys plus Get on certificates for vaults that use access policies). The commands are in the Scanner reference. Do not use Key Vault Secrets User: it can read secret values and cannot list certificates or keys.
Get Your Scanner API Key¶
Register at certifyclouds.com or run cc-scan --register locally. The key looks like cc-scan-abc1234567. Store it as a pipeline secret named CC_SCAN_KEY. In CI the scanner never asks you to register: without a key it exits 3.
GitHub Actions¶
Secrets to Configure¶
In your repository, go to Settings > Secrets and variables > Actions and add:
| Secret | Value |
|---|---|
AZURE_CLIENT_ID | Client ID of the app registration or managed identity |
AZURE_TENANT_ID | Microsoft Entra tenant ID |
AZURE_SUBSCRIPTION_ID | Any subscription the identity can read (azure/login needs one; the scanner still scans every subscription the identity can see) |
CC_SCAN_KEY | CertifyClouds scanner API key |
Workflow: Fail on Critical/High Findings¶
name: Key Vault Security Audit
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Every Monday at 06:00 UTC
permissions:
id-token: write # lets azure/login request the OIDC token
contents: read
jobs:
vault-audit:
name: Audit Key Vaults
runs-on: ubuntu-latest
steps:
- name: Sign in to Azure
uses: azure/login@v3
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Install cc-scan
run: pip install certifyclouds
- name: Run Key Vault audit
env:
CC_SCAN_KEY: ${{ secrets.CC_SCAN_KEY }}
run: |
cc-scan \
--format json \
--output results.json
- name: Upload scan results
if: always()
uses: actions/upload-artifact@v4
with:
name: vault-audit-results
path: results.json
retention-days: 30
The workflow fails when cc-scan exits non-zero: 1 for a critical or high finding, 2 for a failed scan, 3 for a missing key and 4 for a scan with gaps.
Other Options¶
Add these to the cc-scan command in the workflow above:
--securitychecks for misconfigurations only, without a full inventory.--subscription <id>scans only that subscription (repeatable).--fail-on criticalfails only on critical findings.
Soft failure mode
To audit without failing the build, capture the exit code manually:
Azure DevOps Pipelines¶
Service Connection and Variables¶
Create an Azure Resource Manager service connection that uses workload identity federation (see Create the Pipeline Identity). The pipeline refers to it by name.
In Pipelines > Library > Variable Groups, add a variable group (e.g., certifyclouds-scanner) with:
| Variable | Value | Secret |
|---|---|---|
CC_SCAN_KEY | CertifyClouds scanner API key | Yes |
Pipeline: Fail on Critical/High Findings¶
The Azure CLI sign-in exists only inside the AzureCLI@2 task, so cc-scan runs in that task's script.
trigger:
branches:
include:
- main
schedules:
- cron: '0 6 * * 1'
displayName: Weekly Monday audit
branches:
include:
- main
always: true
pool:
vmImage: ubuntu-latest
variables:
- group: certifyclouds-scanner
steps:
- task: UsePythonVersion@0
inputs:
versionSpec: '3.13'
displayName: Set up Python
- script: pip install certifyclouds
displayName: Install cc-scan
- task: AzureCLI@2
displayName: Run Key Vault audit
inputs:
azureSubscription: certifyclouds-scanner-connection # your service connection name
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
cc-scan \
--format json \
--output $(Build.ArtifactStagingDirectory)/results.json
env:
CC_SCAN_KEY: $(CC_SCAN_KEY)
- task: PublishBuildArtifacts@1
condition: always()
inputs:
pathToPublish: $(Build.ArtifactStagingDirectory)
artifactName: vault-audit-results
displayName: Publish scan results
For a security check only, add --security to the cc-scan command.
Pipeline: Soft Failure (Warn, Don't Block)¶
- task: AzureCLI@2
displayName: Run Key Vault audit (warn only)
continueOnError: true
inputs:
azureSubscription: certifyclouds-scanner-connection
scriptType: bash
scriptLocation: inlineScript
inlineScript: |
cc-scan --format json --output results.json || SCAN_EXIT=$?
echo "##vso[task.setvariable variable=SCAN_EXIT]${SCAN_EXIT:-0}"
env:
CC_SCAN_KEY: $(CC_SCAN_KEY)
- script: |
echo "Scanner exit code: $(SCAN_EXIT)"
if [ "$(SCAN_EXIT)" != "0" ]; then
echo "##vso[task.logissue type=warning]cc-scan exited $(SCAN_EXIT), review results.json"
fi
displayName: Evaluate scan results
condition: always()
GitLab CI¶
CI/CD Variables to Configure¶
Add a federated credential to the app registration for your GitLab project (see Create the Pipeline Identity). Its audience must match the aud of the job's ID token; the examples below use https://gitlab.com.
In your project, go to Settings > CI/CD > Variables and add:
| Variable | Value | Masked |
|---|---|---|
AZURE_CLIENT_ID | Client ID of the app registration | No |
AZURE_TENANT_ID | Microsoft Entra tenant ID | No |
CC_SCAN_KEY | CertifyClouds scanner API key | Yes |
Pipeline: Fail on Critical/High Findings¶
The hidden .cc-scan job writes the job's ID token to a file and points AZURE_FEDERATED_TOKEN_FILE at it. The jobs below extend it and run cc-scan --auth default.
stages:
- audit
.cc-scan:
image: python:3.11-slim
id_tokens:
AZURE_OIDC_TOKEN:
aud: https://gitlab.com
before_script:
- printf '%s' "$AZURE_OIDC_TOKEN" > /tmp/azure-oidc-token
- export AZURE_FEDERATED_TOKEN_FILE=/tmp/azure-oidc-token
- pip install certifyclouds
vault-audit:
extends: .cc-scan
stage: audit
rules:
- if: $CI_PIPELINE_SOURCE == "push"
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- cc-scan
--auth default
--format json
--output results.json
artifacts:
when: always
paths:
- results.json
expire_in: 30 days
The job fails when cc-scan exits non-zero: 1 for a critical or high finding, 2 for a failed scan, 3 for a missing key and 4 for a scan with gaps.
Pipeline: Security Findings Only¶
vault-security-check:
extends: .cc-scan
stage: audit
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
script:
- cc-scan --auth default --security
Pipeline: Scheduled Weekly Audit¶
Add a CI/CD schedule in CI/CD > Schedules (every Monday at 06:00 UTC: 0 6 * * 1), then configure a job that only runs on schedules:
weekly-vault-audit:
extends: .cc-scan
stage: audit
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
script:
- cc-scan
--auth default
--format html
--output vault-audit.html
artifacts:
when: always
paths:
- vault-audit.html
expire_in: 90 days
Pipeline: Soft Failure (Allow Failure)¶
vault-audit-warn:
extends: .cc-scan
stage: audit
allow_failure:
exit_codes: [1, 4]
script:
- cc-scan --auth default --format json --output results.json
artifacts:
when: always
paths:
- results.json
expire_in: 30 days
allow_failure: exit_codes: [1, 4] lets the pipeline continue on findings (1) or a scan with gaps (4), and still fails on a failed scan (2) or a missing key (3).
Tips¶
Pin the scanner version to avoid unexpected behaviour on upgrades:
Cache the install to speed up pipelines. For GitHub Actions:
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-certifyclouds
- run: pip install certifyclouds
Scan only expiring items if you want a lighter check:
This lists only secrets, certificates and keys that expire within 30 days or have already expired. The vault checks still run, so a vault finding at or above --fail-on still exits 1. The score always covers the whole scan.
Related¶
- Scanner CLI Reference, Full flag reference, output formats, and security check catalogue
- Scanner permissions, The read-only roles the pipeline identity needs
- Environment Setup, Environment variables reference