Chapter 34Lesson 05~330 minutes

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.

CheckpointControl standardPolicy evaluatorWaiverAudit packet

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-all sample 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.

Next lesson

GitHub Actions Importer, CI Migration, Compatibility, and Modernization: Core Concepts and Mental Model

Continue with the next lesson to build on the current concepts, evidence, security boundaries, and operational practices.

Knowledge check

Why is “all sample workflows pass” the wrong checkpoint success criterion?

The waiver approves one external action repository but the workflow still references @v5. Should it pass?

Which two predictions were required before the lab?

Why keep attempt-1 and attempt-2 policy reports separately?

A reviewer has run logs but no policy digest or exception record. What cannot they prove?

Official references and version notes

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.