Production Capstone: Build and Govern a Secure GitLab Software Delivery Platform: Security, Governance, and Reliability Validation
Validate the platform as an interacting control system, prove commit-to-artifact traceability, test tier-sensitive governance assumptions, and map every requirement to independent evidence and an accountable owner.
Learning objectives
- Validate access, merge, pipeline, runner, variable, environment, registry, scanner, API, webhook, and release controls as interacting boundaries.
- Prove commit-to-artifact-to-release traceability with independent checks rather than trusting pipeline success alone.
- Distinguish GitLab Free controls from Premium/Ultimate enforcement and from Self-Managed/Dedicated responsibilities.
- Evaluate reliability and cost behavior including redundant pipelines, artifact/cache retention, runner capacity, retries, rate limits, registry cleanup, and external dependencies.
- Produce an evidence matrix mapping every invariant to a GitLab resource, verification method, failure signal, and accountable remediation owner.
1. Validate control interactions, not isolated checkboxes
Most production failures happen between controls. A protected branch can still be undermined by an overly permissive matching rule. A protected variable can still be exposed to trusted-branch code running on an unsafe persistent runner. A security scanner can run successfully but cover the wrong source or produce evidence that no policy consumes. A release can exist while pointing to the wrong artifact.
| Boundary pair | Dangerous assumption | Validation question |
|---|---|---|
| Branch protection + approvals | “Protected means reviewed.” | Can a user push directly? Is review actually required or merely requested? Which tier enforces it? |
| Pipeline source + variables | “Masked/protected means safe.” | Which pipeline sources can reach the variable, and can untrusted code execute in those pipelines? |
| Runner + job token | “Ephemeral token means low risk.” | Can the runner or job access another project, network, registry, or host data beyond intent? |
| Artifact + release tag | “Same version label means same bytes.” | Does the release manifest contain the digest of the exact built artifact from the tagged commit? |
| Scanner + policy | “Scanner ran, so merge is safe.” | What was scanned, what report was produced, and what actually blocks/records a risky result? |
| Webhook + API | “Signed event means authorized action.” | Did the receiver validate scope and perform an idempotent, least-privilege mutation? |
2. Prove the release tuple independently
The core traceability tuple is
tag → commit SHA → pipeline/source → artifact digest → release
record. Each link should be independently observable. If any link
disagrees, stop promotion until the discrepancy is explained.
from pathlib import Path
import json, hashlib, subprocess
root=Path('gitlab-ch34-capstone'); ev=root/'evidence'
artifact=json.loads((ev/'artifact_manifest.json').read_text())
release=json.loads((ev/'release_manifest.json').read_text())
actual=hashlib.sha256((root/artifact['file']).read_bytes()).hexdigest()
checks={
'artifact_digest_matches_bytes': actual == artifact['sha256'],
'release_digest_matches_artifact': release['artifact_sha256'] == artifact['sha256'],
'security_report_hash_matches': release['security_report_sha256'] == (ev/'security_report.sha256').read_text().strip(),
}
try:
tag_sha=subprocess.check_output(['git','-C',str(root),'rev-list','-n','1','v0.1.0-capstone'],text=True).strip()
checks['tag_resolves_to_release_commit']= release['commit_sha'] in {'LOCAL_COMMIT_UNKNOWN',tag_sha} or release['commit_sha']==tag_sha
except Exception:
checks['tag_resolves_to_release_commit']='TAG_NOT_AVAILABLE_IN_LOCAL_FALLBACK'
print(json.dumps(checks,indent=2))
if any(v is False for v in checks.values()): raise SystemExit('traceability validation failed')
A GitLab pipeline ID alone does not prove that the retained artifact came from that pipeline. The digest binds the bytes. A Git tag alone does not bind an external deployment target to those bytes. The deployment record or manifest must carry the immutable identity forward.
3. Access and merge-governance validation
Review the effective member roles at the project and inherited group levels. The target invariant is least privilege: normal application delivery should not require Owner, and elevated roles should be deliberate. Then inspect branch rules. GitLab’s current protection-rule semantics matter: when multiple rules match, the most permissive rule can determine push/merge behavior, while Code Owner approval uses a different, restrictive combination rule where that paid feature is enabled.
ACCESS VALIDATION RECORD
Project: asterline-lab/delivery-platform/app
Expected Developer capability: branch/MR/pipeline work
Expected Maintainer capability: repository/project configuration and release governance
Owner required for routine delivery: NO
Inherited memberships reviewed: YES
main direct push allowed: NO
release tag creation: restricted to intended release role
Required approvals enforced by GitLab: NO on Free path / verify on paid path
CODEOWNERS enforcement: NO on Free path / verify on paid path
Evidence IDs: <member snapshot>, <branch rule>, <MR>, <tag rule>
4. Pipeline, variable, job-token, and runner validation
Review four separate dimensions. First, pipeline source: does the
configuration run the intended jobs for merge-request,
default-branch, tag, scheduled, or triggered pipelines? Second,
variables: are sensitive values protected, hidden/masked where
appropriate, and scoped only to eligible pipelines/environments?
Third, CI_JOB_TOKEN: is cross-project access
constrained to the documented allowlist/resource model? Fourth,
runner trust: could repository-controlled code reach a persistent
host, sibling project, Docker socket, or production network?
| Check | Pass condition | Failure signal |
|---|---|---|
| Pipeline source | Only documented sources create release-capable jobs | unexpected pipeline type contains release/deploy job |
| Protected variable | Unavailable to unprotected refs/untrusted source | secret-dependent job runs from untrusted ref |
| Job token scope | Only intended GitLab resources accessible | cross-project API/registry access succeeds outside allowlist intent |
| Runner executor | Appropriate isolation; no privileged shared host for untrusted code | host mount/Docker socket/shell executor exposes cross-job state |
| Runner selection | Sensitive jobs require specific protected/trusted runner path | release job lands on generic runner |
5. Validate security evidence without overclaiming coverage
For the local fixture, inspect the report hash and scope. For GitLab-native SAST or pipeline secret detection, capture analyzer job, report artifact, commit SHA, and coverage assumptions. On Free/Premium, downloadable report artifacts are useful evidence even when Ultimate-only vulnerability-management dashboards or approval policies are unavailable.
from pathlib import Path
import json, hashlib
p=Path('gitlab-ch34-capstone/evidence/security_report.json')
report=json.loads(p.read_text())
print('scanner =',report['scanner'])
print('scope =',report['scope'])
print('findings=',len(report['findings']))
print('sha256 =',hashlib.sha256(p.read_bytes()).hexdigest())
print('ASSERTION: this report covers only its declared scope; it is not proof of vulnerability absence')
6. Registry and release controls: mutable names versus immutable identity
A package version, container tag, or release name is useful for humans but may be mutable or re-creatable depending on the registry and policy. Use a digest/checksum for exact identity. If a container image is used, record the OCI manifest digest. If using the GitLab Container Registry, cleanup and protection rules affect who can mutate or delete tags/repositories; cleanup is destructive and should never be the only retention mechanism for release evidence.
| Object | Human label | Immutable/stronger identity | Retention question |
|---|---|---|---|
| Job artifact | job name / filename | checksum + job/pipeline/commit IDs | Does expiry remove evidence needed for audit/recovery? |
| Generic package | package name + version | package file checksum | Can the version be overwritten or deleted? |
| Container image | tag such as v0.1.0 |
OCI digest | Can mutable tag movement hide a provenance change? |
| GitLab Release | release/tag name | tag commit SHA + evidence + linked artifact digest | Will deleting tag/release break required evidence? |
7. Validate tier/offering assumptions explicitly
| Control | Current baseline | Capstone treatment |
|---|---|---|
| Protected branches | Free/Premium/Ultimate; all offerings | Use on mandatory path |
| MR approvals | optional approvals on Free; required rules on Premium/Ultimate | Record process approval on Free; optional enforced rule on paid tier |
| Code Owners | Premium/Ultimate | File documents intent on Free; enforcement optional |
| Protected environments / deployment approvals | Premium/Ultimate | Use approval fixture on Free; optional live enforcement |
| Basic SAST / downloadable report | Free/Premium/Ultimate | Optional live scanner or local report fixture |
| Pipeline secret-detection report artifact | Free/Premium/Ultimate with supported runner | Optional live scanner or fixture |
| Security policies / dependency scanning | Ultimate | Policy design and evidence fixture only unless licensed |
| Releases / release evidence | Free/Premium/Ultimate | Use disposable release path |
| Self-Managed backup/restore | Self-Managed | Fixture/disposable instance only |
| Duo/Agent Platform | feature/add-on/credits/status dependent | No mandatory AI; preserve governance constraints from Chapter 33 |
8. Test the major architectural tradeoffs
| Choice | Benefit | Cost/risk | Decision rule |
|---|---|---|---|
| Central governance | consistent minimum controls, easier audit | can slow teams; policy mistakes have broad blast radius | centralize invariants, allow team-local implementation where risk permits |
| Team autonomy | fast local decisions | drift and uneven evidence | grant autonomy inside protected platform boundaries |
| GitLab-hosted runner | lower operations burden | quota/cost and shared execution model constraints | use for low-trust general work when acceptable |
| Self-managed runner | custom network/tooling | operator owns isolation, patching, capacity, credential exposure | use only with an explicit trust class and lifecycle |
| Deep paid security enforcement | strong centralized guardrails | license cost + configuration complexity | use where risk/compliance justifies it; preserve scanner evidence on Free |
| Minimal pipeline | fast/cheap | less evidence and coverage | remove redundant work, not essential validation |
| Long retention | better historical evidence | storage cost and sensitive-data footprint | retain only what policy/recovery requires |
9. Reliability and cost review
Efficiency is a reliability property when it reduces queueing and resource exhaustion. Inspect duplicate branch/MR pipelines, unnecessary retries, artifact/cache retention, runner concurrency, serial deployment needs, API pagination/rate limits, registry growth, and Self-Managed dependencies. Do not “optimize” by deleting the evidence required to investigate failure.
RELIABILITY / COST REVIEW
Pipelines
[ ] workflow:rules prevents accidental duplicate sources
[ ] interruptible only where cancellation is safe
[ ] retry used only for transient/idempotent failures
Runners
[ ] capacity and trust class documented
[ ] no privileged persistent runner for untrusted code
Artifacts / cache
[ ] artifact retention matches evidence need
[ ] cache is acceleration, not release evidence
API / hooks
[ ] pagination and rate limits handled
[ ] webhook duplicate delivery is idempotent
Registry
[ ] retention/cleanup preserves release identities
Self-Managed
[ ] DB, Gitaly, object storage, registry, Sidekiq, network/TLS dependencies mapped
10. Build the evidence matrix
from pathlib import Path
import csv
root=Path('gitlab-ch34-capstone'); ev=root/'evidence'
rows=[
['least_privilege','group/project membership','membership snapshot','unexpected inherited elevated role','group owner'],
['reviewed_default_branch','branch rule + MR','direct-push test + MR record','direct push succeeds / review absent','maintainer'],
['pipeline_identity','.gitlab-ci.yml + pipeline','source + commit SHA','release job on unintended source','platform'],
['runner_isolation','runner config/metadata','trust-class review','sensitive job on unsafe runner','runner operator'],
['artifact_identity','artifact manifest','independent SHA-256','checksum mismatch','release owner'],
['release_traceability','tag/release/manifest','tag->commit->digest check','tuple disagreement','release owner'],
['security_evidence','scanner/report','report hash + scope','missing/stale/out-of-scope report','security'],
['webhook_integrity','webhook receiver','signature + scope + duplicate test','invalid/stale/duplicate event causes action','integration owner'],
['recovery','backup/runbook','restore drill evidence','backup exists but restore fails','Self-Managed operator'],
]
with (ev/'evidence_matrix.csv').open('w',newline='') as f:
w=csv.writer(f); w.writerow(['invariant','resource','verification','failure_signal','owner']); w.writerows(rows)
print((ev/'evidence_matrix.csv').read_text())
Knowledge check
Why can two branch protection rules create a surprising result?
Multiple matching rules have defined precedence/composition behavior; for many push/merge settings the effective rule can be more permissive than expected. Validate the effective behavior, not only the presence of one strict-looking rule.
What is the strongest evidence that a released tarball is the one produced by the reviewed commit?
A chain that binds the reviewed commit to the pipeline/job and records a checksum of the exact artifact bytes, with the release record carrying that checksum or immutable package/image identity.
Why is an empty SAST report insufficient to declare the application secure?
Scanner scope, language support, configuration, analyzer version, runtime behavior, dependency risk, secrets, infrastructure, and many other classes of weakness may be outside that report.
What should an evidence matrix include beyond “pass/fail”?
The requirement/invariant, enforcing resource or setting, independent verification method, failure signal, and accountable remediation owner.
When should a cache be used as release evidence?
Never. Cache is an acceleration mechanism with different retention and poisoning semantics; release evidence should use explicit artifacts/packages/images plus immutable identities.
11. Lesson summary and bridge
- GitLab controls are validated at their interactions: source, authorization, runner, credential, artifact, policy, and evidence boundaries.
- Traceability requires an immutable identity chain rather than confidence in labels or green pipeline status.
- Tier-gated controls are explicitly separated from Free-path process simulations.
- Reliability and cost reviews optimize duplicate work, retention, capacity, and retries without deleting necessary evidence.
- The evidence matrix turns architecture requirements into operational accountability.
Next, you will deliberately break four parts of the platform, preserve evidence, diagnose root cause, apply the least destructive correction, and prove recovery.
Primary sources and version notes
These lessons were finalized against current official GitLab documentation on 2026-08-22 with GitLab 19.3 as the reference release. Availability can vary by GitLab.com, Self-Managed, or Dedicated offering; Free, Premium, or Ultimate tier; namespace settings; administrator policy; runner type; and feature status. Re-check current documentation before applying a production design. Current protection-rule behavior and paid approval/environment-policy boundaries are especially important in this lesson because a visual “protected” or “approved” state can still be misinterpreted if overlapping rules or tier limitations are ignored.
- GitLab 19.3 release
- GitLab roles and permissions
- Protected branches
- Protection rules and permissions
- Merge requests
- Merge request approvals
- Code Owners
- CI/CD pipelines
- CI/CD variables
- CI_JOB_TOKEN
- Runner security
- Job artifacts
- Caching in GitLab CI/CD
- Protected environments
- Deployment approvals
- Container Registry
- Protected container repositories
- Releases
- Release evidence
- SAST
- Pipeline secret detection
- Security policies
- REST API
- Webhooks
- GitLab agent for Kubernetes
- Audit events
- Health check endpoints
- Back up GitLab
- Restore GitLab
- GitLab Duo Agent Platform
- Agent tool governance
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.