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.
permissions:
contents: readWhat 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.
- 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.
steps:
- checkout: self
clean: true
persistCredentials: false
- bash: git rev-parse HEAD
displayName: Record the checked-out commitWhat 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.
- 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.
- 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 jsonWhat 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.
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.
# In your repository, record the reviewed commit
git rev-parse HEAD
# For an existing local release file, replace the filename
sha256sum release.tar.gzWhat 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.