SECURITY / CI/CD & supply chain · 25–35 min per platform

Secure CI/CD with GitHub Actions or Azure DevOps

Choose GitHub Actions or Azure DevOps. Review pipeline permissions, protect credentials, and put approval boundaries around delivery.

Before you start

Environment: A disposable GitHub repository with Actions, or an Azure DevOps Services project with YAML Pipelines. Azure examples use a Microsoft-hosted Ubuntu agent; Azure DevOps Server features can differ. Hosted runs need your own account, permissions, and available runner capacity.

Choose the platform you use; completing both is optional. Edit .github/workflows/security-lab.yml for GitHub or azure-pipelines.yml for Azure DevOps. Use synthetic data, keep deployments disabled, and review the existing pipeline before merging any fragment. The site does not connect to either platform.

Commands run in your own terminal. This website cannot inspect or secure your environment.

GitHub Actions / Workflow fragment · merge at the top level

1. Make permissions and trust boundaries explicit

A workflow processing a contribution should not automatically receive release credentials.

Example configuration · review before applying
permissions:
  contents: read

What to check

Check the token permissions shown in the job setup logs. Grant additional rights only in the job that needs them. This setting does not constrain a separate PAT or cloud credential.

Try it deliberately

Use pull_request for untrusted contribution checks. Never combine privileged pull_request_target or workflow_run execution with unchecked contributor code or artifacts. Put deployment behind a reviewed environment and use short-lived OIDC credentials with restrictive trust conditions when supported.

Recovery & cleanup

Keep the previous workflow in Git. If a job fails because a permission is missing, identify the required API operation rather than switching to write-all.

Official guidance: GitHub Actions secure use ↗

GitHub Actions / Job step · Bash runner

2. Treat event text as data

Issue titles and branch names are attacker-controlled input. Interpolating them directly into a shell script can turn text into commands.

Example configuration · review before applying
- name: Show the proposed title safely
  env:
    PR_TITLE: ${{ github.event.pull_request.title }}
  run: printf "%s\n" "$PR_TITLE"

What to check

Quotes and shell-looking characters in a synthetic pull-request title print as text. Check the rendered workflow and logs without including real secrets.

Try it deliberately

Pass untrusted text through an environment variable and quote it; do not insert event expressions directly into run scripts. Review third-party actions, pin them to verified full commit SHAs, and schedule reviewed updates. A digest or SHA identifies content; it does not prove that content is trustworthy.

Recovery & cleanup

If a secret entered logs or Git history, revoke and rotate it at its issuer first, then investigate and clean retained copies. Deleting the visible line alone does not revoke access.

Official guidance: GitHub script injection guidance ↗ · GitHub action pinning and secrets ↗

Azure DevOps / Workflow fragment · Azure Pipelines steps

3. Scope repository access to the build identity

The job token and repository permissions determine how far a pipeline can reach.

Example configuration · review before applying
steps:
- checkout: self
  clean: true
  persistCredentials: false
- bash: git rev-parse HEAD
  displayName: Record the checked-out commit

What to check

The log identifies the expected commit. persistCredentials: false avoids leaving the checkout token in Git configuration; it does not reduce the job token’s server-side permissions.

Try it deliberately

In Project Settings → Pipelines → Settings, enable project-scoped job authorization and repository protection for YAML pipelines. Grant the project build identity only required repository permissions, explicitly authorize additional repositories, and test an unauthorized repository is denied. Review organization-level settings too.

Recovery & cleanup

Record previous settings and ACLs before changing them. Fix a missing permission on the specific repository rather than restoring collection-wide access. Use a disposable project to test the boundary.

Official guidance: Microsoft: secure repository access ↗

Azure DevOps / Job step · Azure Pipelines Ubuntu agent

4. Map secrets explicitly without printing them

A masked log is not a guarantee that a secret cannot leak through a script, argument, or artifact.

Example configuration · review before applying
- bash: |
    case "$DEMO_API_TOKEN" in
      ""|'$(DemoApiToken)')
        echo "Demo token is missing or unresolved" >&2
        exit 1
        ;;
    esac
    echo "Demo token is available; value is not printed"
  displayName: Check a synthetic secret is mapped
  env:
    DEMO_API_TOKEN: $(DemoApiToken)

What to check

Create DemoApiToken as a secret pipeline variable using a synthetic value first. The step succeeds without printing the value. It checks availability, not whether a real API would accept a credential.

Try it deliberately

Prefer a service connection or managed identity where possible. If a secret is required, restrict variable-group access to the intended pipelines, pass it through the step environment, and keep it out of command arguments and artifacts. Do not enable secrets for untrusted fork builds.

Recovery & cleanup

Remove the synthetic variable after practice. For a real exposure, revoke or rotate at the issuer and investigate retained logs and artifacts; hiding the text afterward does not invalidate the credential.

Official guidance: Microsoft: secrets in Azure Pipelines ↗

Azure DevOps / Job step · AzureCLI@2, inspection only

5. Use a narrowly scoped federated service connection

A short-lived federated login avoids keeping a long-lived Azure client secret in the pipeline.

Example configuration · review before applying
- task: AzureCLI@2
  displayName: Inspect the practice Azure identity
  inputs:
    azureSubscription: opsrung-lab-azure
    scriptType: bash
    scriptLocation: inlineScript
    inlineScript: |
      az account show --query "{subscription:id,tenant:tenantId}" --output json

What to check

Before running, create an Azure Resource Manager service connection named opsrung-lab-azure using workload identity federation. Its identity needs only the permissions for this practice task. The output should identify the intended subscription and tenant without exposing a token.

Try it deliberately

Authorize only this pipeline rather than all pipelines. Assign the narrowest Azure role and scope needed; confirm an unrelated resource is inaccessible. Use the current service-connection wizard and generated federation settings instead of copying issuer or subject strings from an old tutorial. Azure and project permissions are required.

Recovery & cleanup

Disable pipeline access to the practice connection and remove lab-only Azure role assignments after testing. Preserve an approved connection while investigating a mismatch; do not replace federation with a broad secret-based credential as a shortcut.

Official guidance: Microsoft: workload identity service connections ↗

Azure DevOps / Workflow fragment · no-op deployment job

6. Put approval checks on the protected resource

A resource owner should control release approval independently of a contributor editing YAML.

Example configuration · review before applying
jobs:
- deployment: ReviewBoundary
  displayName: Test the approval boundary without deploying
  environment: opsrung-review-lab
  pool:
    vmImage: ubuntu-latest
  strategy:
    runOnce:
      deploy:
        steps:
        - checkout: none
        - bash: echo "Approval passed; no application was deployed"

What to check

Create opsrung-review-lab under Pipelines → Environments, restrict pipeline access, and add an approval under Approvals and checks before running. The stage should wait; rejecting the approval prevents the step from running. Without configured checks, the environment name alone does not enforce approval.

Try it deliberately

Have a separate authorized reviewer approve the synthetic run. Test rejection as well as acceptance. Add appropriate branch control or required-template checks on protected resources and restrict who can administer or bypass them. These checks are managed in the resource UI, not declared by this YAML.

Recovery & cleanup

Cancel pending practice runs and remove only the lab environment/checks you created. Keep real release checks in place when repairing a blocked deployment.

Official guidance: Microsoft: approvals and resource checks ↗

Both platforms / Review · no deployment command

7. Keep an evidence trail for the release

Passing checks is more useful when you can connect them to the exact artifact that was deployed.

Bash · review before running
# In your repository, record the reviewed commit
git rev-parse HEAD
# For an existing local release file, replace the filename
sha256sum release.tar.gz

What to check

Record the commit, pipeline run, artifact digest, dependency inventory, and vulnerability review together. In Azure Pipelines, match the run commit to Build.SourceVersion; in GitHub, confirm the workflow SHA, including whether a PR merge commit was built. A checksum alone is not proof of origin.

Try it deliberately

Gate merges on relevant tests and security findings, assign time-bound owners for exceptions, and use protected deployment environments. Generate an SBOM and provenance with a maintained tool suited to the build; verify attestations at consumption. Test restoring the last known working release.

Recovery & cleanup

Keep the previous artifact and its configuration. Revert through the reviewed release process; database migrations and revoked credentials may need separate recovery steps.

Official guidance: GitHub artifact attestations ↗

Reviewed 31 October 2026. Adapt to your installed versions and environment; these guides cover selected controls, not a compliance certification.