Checkpoint Lab — Enterprise Policies, Allowed Actions, Runner Governance, and Auditability
Write and test a fictional enterprise Actions control standard, exercise compliant and noncompliant workflows, grant one time-bounded waiver, and assemble audit-ready evidence.
Learning objectives
- Publish a fictional, versioned Actions control standard.
- Predict and verify ALLOW/DENY outcomes for five sample workflows.
- Preserve a first denial packet and add one exact, expiring waiver.
- Define the audit evidence required to review policy and execution.
- Bridge governance controls into the CI-migration discipline of Chapter 35.
1. Checkpoint scenario and success criteria
You are the platform-governance engineer for the fictional company
AtlasWorks. Your assignment is to publish a versioned
Actions control standard, evaluate five sample workflows,
intentionally preserve one denied case, approve one narrow seven-day
exception and produce an evidence packet that another reviewer can
audit without access to your memory.
Success is not “all samples pass.” A correct control system must allow compliant work, deny unsafe work, explain each decision, preserve the original denial, and expire exceptional authority. The entire mandatory lab runs locally; Enterprise policy, runner groups and audit APIs are faithfully represented as configuration/evidence fixtures.
2. Preflight, assumptions and predictions
- Use a new temporary directory and synthetic files only.
- Python 3.11+ is the only required runtime.
- No GitHub organization/enterprise settings are changed.
- No real token, secret, self-hosted runner or cloud target is used.
- Platform assumptions were checked on 2026-09-10; REST examples elsewhere in the chapter use API version 2026-03-10.
Before running the evaluator, write two predictions.
Prediction A: a workflow using actions/checkout at the
exact approved SHA with contents: read should be
allowed. Prediction B: a workflow using a mutable unapproved action
and the privileged runner group from an unapproved workflow should
be denied for multiple independent reasons. Later, compare
prediction to evidence rather than grading yourself from memory.
3. Write the control standard
Save this as policy/control-standard.json. The standard
intentionally resembles policy concepts instead of GitHub’s raw API
schema; it is a portable teaching contract that can be reviewed and
versioned in Git.
{
"standard_id": "AW-ACTIONS-2026-09-v1",
"owner": "AtlasWorks Platform Security",
"scope": "organization",
"inheritance": "repository may narrow; repository may not loosen organization or enterprise deny",
"actions": {
"mode": "selected",
"require_full_sha": true,
"allowed_repositories": [
"actions/checkout",
"actions/upload-artifact",
"atlasworks/platform-ci"
]
},
"token": {
"default": "restricted",
"forbid_write_all": true,
"require_explicit_job_permissions": true
},
"forks": {
"send_write_token": false,
"send_secrets": false
},
"runners": {
"prod-deploy": {
"repositories": ["payments"],
"workflows": ["deploy.yml"],
"allow_public_repositories": false
}
},
"exceptions": {
"must_expire": true,
"maximum_days": 14,
"require_risk_owner": true,
"require_compensating_controls": true
},
"audit": {
"preserve_policy_digest": true,
"preserve_decision_report": true,
"external_retention_required_for_long_term_review": true
}
}
mkdir -p gha-governance-checkpoint/{policy,workflows,exceptions,evidence}
cd gha-governance-checkpoint
# Save the JSON above as policy/control-standard.json
sha256sum policy/control-standard.json | tee evidence/policy.sha256
4. Create five sample workflows and predict their outcomes
The fifth sample represents
payments/.github/workflows/deploy.yml; the filenames
below are prefixed only so the local lab can keep the cases
together. Write your expected ALLOW/DENY result before evaluating.
name: safe-ci
on: [push]
permissions: {}
jobs:
test:
permissions:
contents: read
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: echo test
name: mutable-external
on: [push]
permissions: {}
jobs:
test:
runs-on: ubuntu-24.04
steps:
- uses: vendor/security-scan@v5
name: broad-token
on: [push]
permissions: write-all
jobs:
publish:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
name: wrong-runner
on: [pull_request]
permissions: {}
jobs:
test:
runs-on:
group: prod-deploy
steps:
- run: echo unsafe-boundary
name: approved-deploy
on:
workflow_dispatch:
permissions: {}
jobs:
deploy:
runs-on:
group: prod-deploy
permissions:
contents: read
steps:
- run: echo synthetic-deployment
Expected reasoning: sample 1 is eligible; sample 2 fails allowlist
and immutable-reference rules; sample 3 violates token policy;
sample 4 violates the runner trust/workflow boundary; sample 5 is
eligible only when evaluated as the fictional
payments repository and
deploy.yml workflow.
5. Evaluate and preserve the first denial packet
Save the sample YAML files, then run the evaluator with error handling that preserves its non-zero exit. The failed control test is expected and should remain in the evidence packet.
from pathlib import Path
import json, re, hashlib, sys
P=json.loads(Path("policy/control-standard.json").read_text())
SHA=re.compile(r"^[0-9a-f]{40}$",re.I)
def evaluate(path, repo="web-api", logical_name=None, exceptions=()):
text=path.read_text()
name=logical_name or path.name
f=[]
if "permissions: write-all" in text:
f.append({"control":"token.forbid_write_all","reason":"write-all requested"})
for raw in text.splitlines():
line=raw.strip()
if not line.startswith("- uses:"): continue
ref=line.split("uses:",1)[1].strip().split()[0]
if ref.startswith("./") or ref.startswith("$/"): continue
if "@" not in ref:
f.append({"control":"actions.reference","reason":f"missing ref: {ref}"}); continue
action, rev=ref.rsplit("@",1)
allowed=action in P["actions"]["allowed_repositories"]
waived=any(x.get("repository")==repo and x.get("action")==action and x.get("active") for x in exceptions)
if not allowed and not waived:
f.append({"control":"actions.allowlist","reason":f"not allowed: {action}"})
if P["actions"]["require_full_sha"] and not SHA.fullmatch(rev):
f.append({"control":"actions.require_full_sha","reason":f"mutable/non-SHA ref: {ref}"})
if "group: prod-deploy" in text:
if repo != "payments": f.append({"control":"runners.repository","reason":"prod-deploy restricted to payments"})
if name != "deploy.yml": f.append({"control":"runners.workflow","reason":"prod-deploy restricted to deploy.yml"})
if "pull_request" in text: f.append({"control":"runners.trust","reason":"PR code cannot target prod-deploy"})
return {"file":str(path),"repository":repo,"logical_workflow":name,"decision":"DENY" if f else "ALLOW","findings":f}
cases=[]
for path in sorted(Path("workflows").glob("*.yml")):
if path.name=="05-approved-deploy.yml":
cases.append(evaluate(path,"payments","deploy.yml"))
else:
cases.append(evaluate(path))
print(json.dumps(cases,indent=2))
sys.exit(2 if any(c["decision"]=="DENY" for c in cases) else 0)
# Save the evaluator above as evaluate.py, then:
set +e
python evaluate.py > evidence/attempt1-policy-decisions.json
rc=$?
set -e
printf 'policy_evaluator_exit=%s
' "$rc" | tee evidence/attempt1.exit
sha256sum workflows/*.yml > evidence/workflow-inputs.sha256
cat evidence/attempt1-policy-decisions.json
Verify that each denial names a precise control. If the evaluator merely says “noncompliant,” the evidence is too weak for remediation. A good governance tool separates allowlist, SHA, token and runner failures so the least destructive fix is obvious.
6. Document one justified time-bounded exception
Assume the migration team cannot immediately replace
vendor/security-scan, but security has reviewed commit
0123456789abcdef0123456789abcdef01234567. Create a
seven-day waiver for that exact action/SHA and only the
web-api migration workflow. The original
@v5 sample remains denied; the waiver does not
legitimize a mutable tag.
{
"exception_id": "AW-EX-2026-0910-01",
"repository": "web-api",
"workflow": "migration.yml",
"action": "vendor/security-scan",
"approved_sha": "0123456789abcdef0123456789abcdef01234567",
"active": true,
"risk_owner": "web-api-owner",
"approved_by": "platform-security-reviewer",
"starts_at": "2026-09-10T00:00:00Z",
"expires_at": "2026-09-17T00:00:00Z",
"compensating_controls": [
"full commit SHA",
"permissions: contents:read only",
"GitHub-hosted runner",
"no secrets"
],
"removal_condition": "migration completed and platform-native scanner enabled"
}
A mature implementation validates current time, exact workflow path and exact SHA automatically. The checkpoint keeps that validation conceptual so the focus remains governance. The important property is that the waiver cannot widen itself to all releases or all repositories.
7. Retest the corrected/waived path without erasing attempt 1
Create a new migration sample that uses the approved full SHA and
restrictive permissions, then extend the evaluator to load the
waiver. Save its output as attempt2; never overwrite
attempt1. The resulting history proves enforcement
first rejected unsafe input and later accepted a narrower,
authorized state.
name: migration
on:
workflow_dispatch:
permissions: {}
jobs:
scan:
permissions:
contents: read
runs-on: ubuntu-24.04
steps:
- uses: vendor/security-scan@0123456789abcdef0123456789abcdef01234567
If the exception expires before the migration ends, the expected state is denial. Renewal requires a new approval record. This is how a time boundary becomes enforceable governance rather than documentation.
8. Define the audit evidence required for review
| Evidence object | Minimum fields | Question answered |
|---|---|---|
| Policy snapshot | Standard ID, owner, scope, SHA-256, effective date | Which control revision governed the workflow? |
| Workflow sample | Repository/path, source SHA or file digest, action refs, permissions, runner target | What execution intent was evaluated? |
| Decision report | ALLOW/DENY, control IDs, reasons, evaluator version | Why was execution accepted or rejected? |
| Exception | ID, exact scope/action/SHA, owner, approver, start/expiry, compensating controls | Who authorized temporary divergence and until when? |
| GitHub run evidence when used | Run ID, attempt, event/ref/SHA, job/step conclusions, runner identity | What actually executed? |
| Administrative audit when available | Actor, action, scope, timestamp, before/after or correlated policy change | Who changed the control plane? |
| Assumptions/limitations | Plan, visibility, API version, simulated features, unavailable evidence | What does the packet not prove? |
9. Map the local standard to real GitHub controls
The local fixture deliberately maps to current GitHub control families. An enterprise/organization can limit allowed actions and reusable workflows, and can require full action SHAs. Organization/repository Actions settings define token defaults and private-fork behavior subject to higher-level policy. Runner groups restrict access to self-hosted/larger runner pools. Ruleset workflows and environments can add merge/deployment governance where available. Audit logs and Enterprise Cloud streaming/API capabilities provide administrative evidence.
Do not assume every plan exposes every control. The mandatory lab is plan-neutral precisely because governance concepts should be learnable before an organization purchases enterprise features. In production, record the exact plan, visibility and admin role next to each enforced control.
10. Cleanup and rollback
The mandatory lab created only local files. Preserve the evidence packet if you want to review it, then delete the disposable directory. If you performed optional read-only GitHub inspection, there is no configuration to roll back. This checkpoint intentionally avoids live enterprise writes so cleanup cannot accidentally widen or narrow real policy.
cd ..
# Optional: archive evidence first.
tar -czf gha-governance-checkpoint-evidence.tgz gha-governance-checkpoint/evidence gha-governance-checkpoint/policy gha-governance-checkpoint/exceptions
# Delete only after reviewing the path carefully.
rm -rf gha-governance-checkpoint
11. Verification checklist
- Policy standard has an owner, scope, inheritance statement and immutable digest.
- Compliant checkout example uses the exact approved commit SHA.
- Mutable/unapproved external action is denied and attempt-1 evidence remains preserved.
-
Broad
write-allsample is denied independently of action policy. - Untrusted/wrong workflow cannot target the fictional privileged runner group.
- Approved deployment sample is tied to the exact fictional repository/workflow boundary.
- Waiver names one action, one approved SHA, one workflow/repository, an owner, expiry and compensating controls.
- Evidence packet explains simulated versus real GitHub state and records current API/platform assumptions.
12. What Chapter 34 adds—and the bridge to Chapter 35
This chapter completes the governance layer around the technical mechanisms learned earlier in the course. Secure Actions operation is now not just a collection of good workflow files; it is a versioned control system with enforceable executable-dependency policy, token and fork boundaries, runner entitlements, rules, auditable exceptions and retained evidence.
Chapter 35 applies the same discipline to CI migration. GitHub Actions Importer can translate configuration, but a safe migration must preserve behavioral invariants across triggers, secrets, runners, artifacts, deployments and governance. Generated YAML is only the beginning of equivalence testing.
Knowledge check
Why is “all sample workflows pass” the wrong checkpoint success criterion?
A governance system must also prove that unsafe samples are rejected for the expected reasons. Denial is a successful control outcome when the input violates policy.
The waiver approves one external action repository but the
workflow still references @v5. Should it
pass?
No. The waiver approves the reviewed full SHA; immutable-reference enforcement should still reject the mutable tag.
Which two predictions were required before the lab?
That an approved exact-SHA least-privilege CI workflow would be allowed, and that a mutable/unapproved action plus unauthorized privileged runner use would be denied for independent controls.
Why keep attempt-1 and attempt-2 policy reports separately?
They prove the governance transition and preserve first-failure evidence instead of rewriting the historical decision after a correction.
A reviewer has run logs but no policy digest or exception record. What cannot they prove?
They cannot reliably prove which control revision authorized the run or whether temporary divergence was properly approved and bounded.
Official references and version notes
- Enforcing policies for GitHub Actions in your enterprise — Current enterprise policy options, selected-action rules and full-SHA enforcement.
- Managing GitHub Actions settings for a repository — Repository-level action policy, token defaults and fork settings, including inheritance limits.
- Managing access to self-hosted runners using groups — Runner-group organization/repository/workflow access and public-repository warnings.
- Reviewing the audit log for your organization — Audit-log search/export/API boundaries and retention guidance.
- Secure use reference — Least privilege, untrusted input, third-party action and runner security guidance.
- REST API endpoints for GitHub Actions permissions — Current read/write API surface for organization/repository Actions permission settings.
- Streaming the audit log for your enterprise — Enterprise Cloud audit streaming, retention and at-least-once delivery behavior.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.