Platform Engineering for Security Guardrails
Every page in this course's Pipeline Security and Cloud & Infrastructure groups assumes someone has to actually wire a scanner into a pipeline, write a policy, and decide what blocks a merge. This page is about who does that wiring once, for everyone, instead of every team doing it — badly, inconsistently, or not at all — on their own. An internal developer platform (IDP) turns a security requirement from a line item on a reviewer's checklist into a property of the template a service is born from: the scanning is already in the pipeline before the first commit, the policy is enforced by the cluster and the CI system themselves rather than by a person remembering to ask for it, and a security team of a handful of engineers can hold the line for a thousand services instead of a dozen. That's not a smaller version of manual review. It's a different shape of solving the same problem, and this page is about exactly how that shape works, where it still needs a human, and where it quietly breaks if nobody maintains it.
Imagine a school where a hall monitor has to personally check every student's backpack every morning to make sure nobody snuck in something they shouldn't have. With ten kids that works fine. With ten thousand kids, the monitor becomes the reason nobody gets to class on time, and eventually people just start sneaking past when the line gets too long. Now imagine the school does something different: it designs the backpacks so the dangerous stuff physically doesn't fit in them anymore. Nobody has to remember a rule, because there's no version of the backpack where the rule doesn't already apply. A platform's "golden path" is that backpack — security gets built into the thing every student already starts with, instead of being one very tired monitor's job to catch on the way in.
The bottleneck a paved road is built to remove
☺ Like you're 10: A team of five people can carefully check five requests a day. It cannot carefully check five hundred, no matter how hard they try.
Start with the arithmetic, because the rest of this page only makes sense once the arithmetic is uncomfortable. A security team's ability to review a pipeline by hand is bounded by its headcount — a small, senior, and expensive group, almost always smaller than the engineering organization it serves. Industry surveys on this ratio move around by methodology and year (treat any specific figure you read as directional, not a constant), but the shape is consistent everywhere it's measured: one security engineer for every few dozen to a few hundred developers is typical, and it does not improve on its own as an organization grows — headcount for a security team grows roughly linearly with budget, while the number of repositories, pipelines, and services a fast-moving engineering org produces tends to grow faster than that, because writing a new service is cheap and reviewing one thoroughly isn't.
Run that ratio through a manual-review model and the failure mode is exactly the one What is DevSecOps? already named for a single late-stage gate — except now it's not one queue before release, it's N queues, one per pipeline, each waiting on the same fixed pool of reviewers. Three things happen, in order, as N grows past what the team can hold in its head: review latency stretches from hours to days to "whenever someone gets to it," teams start routing around the queue because a blocked deploy is a business cost nobody wants to absorb repeatedly, and the security team — unable to review everything carefully — starts reviewing everything superficially instead, which is a worse outcome than reviewing a smaller set of things well. None of this is a staffing failure. It's what happens when a linear-cost process meets a workload that isn't linear.
The platform-engineering answer isn't "hire more reviewers" — that's fighting the growth curve with a resource that scales slower than the thing it's trying to keep up with. It's removing the review from the steady-state path entirely: build the security requirement into the thing every new service starts from, and enforce the requirements that can't be pre-baked at a layer every service passes through automatically, rather than at a gate a person has to staff. Secure SDLC gates & the DevSecOps maturity model covers where those gates physically live in a single pipeline; this page is about not making a human staff each one.
Golden-path templates: security pre-wired into the scaffold
☺ Like you're 10: The safest way to build something is to start from a copy of something that was already built safely.
A golden path — the term is closely associated with how Netflix and Spotify have publicly described their own internal platforms, and it's worth reading their own writing rather than treating any single blog post as the canonical definition — is a supported, opinionated, pre-approved way to build a specific kind of thing: a Node service, a batch job, a React frontend. It's not the only way; a team that has a genuinely good reason can still go off-road. It is, deliberately, the easiest way, and easiest wins by default because most engineers most of the time just want to ship the feature they're actually being asked to build.
Backstage, the CNCF project originated at Spotify, is the most widely adopted open-source implementation of this pattern, and its Software Templates feature (the "scaffolder") is the concrete mechanism: a template describes a set of parameters, a skeleton of files to generate, and a sequence of actions to run against them — publish the result to a Git provider, register it in the software catalog, wire up CI. Commercial platform-orchestration products like Humanitec and Port implement the same idea with different mechanics underneath. What matters for this page isn't which product a platform team picks — it's what gets baked into the template before a developer ever types a line of application code.
# A trimmed Backstage Software Template — the security-relevant parts.
# Real Backstage scaffolder actions: fetch:template, publish:github, catalog:register.
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: golden-path-node-service
title: Node.js Service (Golden Path)
description: >
A production-ready Node service. SAST, SCA, secrets scanning, and a
signed, non-root container are already wired in before your first commit.
spec:
owner: platform-team
type: service
parameters:
- title: Basics
required: [name]
properties:
name: { type: string, title: Service name }
steps:
- id: fetch
name: Fetch golden-path skeleton
action: fetch:template
input:
url: ./skeletons/node-service # contains .semgrep.yml, .gitleaks.toml,
values: { name: '${{ parameters.name }}' } # a distroless non-root Dockerfile, and CI already wired to all three
- id: publish
name: Publish to GitHub
action: publish:github
input:
repoUrl: 'github.com?owner=acme-corp&repo=${{ parameters.name }}'
requiredApprovingReviewCount: 1
requiredStatusCheckContexts: # branch protection, set by the template — not a per-team decision
- semgrep-sast
- trivy-image-scan
- gitleaks-secrets
- id: register
name: Register in the software catalog
action: catalog:register
input:
repoContentsUrl: '${{ steps.publish.output.repoContentsUrl }}'
catalogInfoPath: /catalog-info.yamlRead that template again with an eye specifically for what the developer running it never had to decide. They didn't choose whether SAST runs — the skeleton already has a Semgrep config and a CI job wired to it. They didn't choose whether the base image runs as root — the Dockerfile in the skeleton already doesn't. They didn't set up branch protection — the publish:github step does it as a side effect of creating the repository, before the developer's first pull request even exists. Compare this to a security team's usual position: reviewing a pipeline that some other team already built, after the fact, and negotiating retroactively for changes the team now has to go find time for. The golden path collapses that negotiation into a decision the platform team made once, upstream, for every future instance.
Policy enforced at the platform layer, not per repo
☺ Like you're 10: A rule that lives in two hundred different notebooks gets copied wrong in at least a few of them. A rule that lives in one place and applies to everyone can't drift.
A golden path handles the moment a service is born. It says nothing about what happens six months later, when someone edits the Dockerfile the template generated and quietly drops the non-root user, or forks the CI config and deletes the SCA step because it was "slowing down the pipeline." A template is a starting point, not a leash — which is exactly why the second half of a platform's job is enforcing the requirement continuously, at a layer the individual repository doesn't control, rather than trusting that everyone keeps what the template gave them.
Cluster-wide admission control
The clearest version of this lives in Kubernetes. Instead of each team's pipeline being trusted to have run a container scan and to have signed the image, the cluster itself refuses to run anything that can't prove it, via a Kyverno ClusterPolicy or an OPA Gatekeeper ConstraintTemplate installed once by the platform team and applied to every namespace by default:
# One cluster-wide policy — not two hundred per-team ones — enforced
# by the admission controller itself, using the same keyless-signing
# identity model covered in Zero Trust for Pipelines.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce # not Audit — this actually blocks the request
background: false
rules:
- name: verify-cosign-signature
match:
any:
- resources: { kinds: [Pod] }
verifyImages:
- imageReferences: ["registry.acme.internal/*"]
attestors:
- entries:
- keyless:
subject: "https://github.com/acme-corp/*/.github/workflows/*"
issuer: "https://token.actions.githubusercontent.com"
- name: require-non-root
match:
any:
- resources: { kinds: [Pod] }
validate:
message: "containers must set runAsNonRoot: true"
pattern:
spec:
=(securityContext):
runAsNonRoot: trueNo individual team wrote this policy, and — this is the point — no individual team can quietly stop enforcing it either. It doesn't matter whether the Pod came from the golden-path pipeline, a hand-rolled one, or a `kubectl apply` someone ran directly from their laptop; the same two rules run against all of them, at the one chokepoint every workload has to pass through to actually run. See Kyverno and Kubernetes Security Deep Dive for the full admission-control mechanics this leans on, and Sigstore & cosign for what "keyless" signing is actually verifying.
Infrastructure-as-code, the same way
The identical pattern applies one layer down, to Terraform. Instead of every team independently deciding to run Checkov or tfsec in their own pipeline — which means every team can also independently decide not to — a platform team publishes a small set of pre-approved, pre-hardened Terraform modules through an internal module registry (an S3 bucket, Terraform Cloud's private registry, or a plain Git-tag-based source), and pairs it with a policy check run centrally, using OPA & Conftest against every plan before it can apply, regardless of which repository the plan came from. A team that consumes module "vpc" from the registry inherits flow logs, encryption, and least-privilege security groups by construction. A team that writes raw aws_vpc resources by hand still hits the same Conftest policy at plan time and gets the same rejection either way. See Infrastructure as Code Hardening for the scanning mechanics themselves.
| Per-repo enforcement | Platform-layer enforcement | |
|---|---|---|
| Who decides the rule applies | Each team, independently, for their own pipeline | The platform team, once, for every consumer |
| Consistency across 200 services | As good as the least careful team's copy-paste | Identical by construction — one policy source, no drift |
| Cost of a rule change | Open 200 pull requests, chase 200 reviews | Edit one ClusterPolicy or Conftest rule; every consumer gets it next admission/plan |
| Can a team quietly opt out | Yes — delete the step, nobody notices until an incident | No — the check runs at a chokepoint the team's own repo doesn't control |
| What breaks if the rule is wrong | One team's pipeline | Every pipeline or workload that hits it, at once |
That last row is the trade a platform team is making, and it's worth stating plainly: centralizing enforcement centralizes the blast radius of getting the enforcement wrong. A cluster-wide admission policy with validationFailureAction: Enforce and a bug in its match rule doesn't fail one team's deploy — it can fail every deploy, cluster-wide, at once, the exact same way a misconfigured Kubernetes admission webhook with failurePolicy: Fail can take down Pod creation for the whole cluster. Roll new platform-layer policies out in Audit mode first, watch the violation reports, and only flip to enforcing once you've confirmed the policy catches what it should and nothing it shouldn't. A platform that becomes a single point of outage is not a smaller version of the manual-review bottleneck it replaced — it's a different, sharper failure mode, and it deserves the same caution you'd give any other change to a production control plane.
The gauntlet: what makes bypass structurally hard, not just discouraged
☺ Like you're 10: A "please don't" sign is not the same thing as a locked door. A platform aims to be the locked door.
Put the golden path and the platform policy layer together and something changes qualitatively, not just quantitatively: bypassing security stops being a matter of skipping a step someone asked you to do, and becomes a matter of getting past a checkpoint you structurally can't route around. A developer who deletes the SAST job from their CI config hasn't actually removed SAST from the picture — they've just made sure their own copy of it doesn't run, while the org-level required status check (GitHub repository rulesets, or GitLab's compliance-framework pipelines — both platforms have moved this feature around their settings more than once, so verify the current path in their own docs) still refuses to let the pull request merge without it. A developer who hand-builds an unsigned image and tries to run it in the cluster hasn't skipped image scanning — they've just discovered that the cluster won't schedule what they built, because the admission controller from the previous section checks every Pod, not just the ones that came through the golden-path pipeline.
On a throwaway kind or minikube cluster, install Kyverno and apply the require-non-root rule from the policy above. Then run kubectl run bad --image=nginx with no security context and watch the admission request get rejected — read the error message Kyverno returns, it names the exact rule and field. Now try the same thing after adding --overrides='{"spec":{"securityContext":{"runAsNonRoot":true}}}' and watch it succeed. Nobody reviewed either kubectl command by hand; the cluster itself did the reviewing, at the one point every Pod — golden-path or hand-rolled — has to pass through.
Guardrails, not walls: exceptions and the escape hatch
☺ Like you're 10: A guardrail on a mountain road bends the car back onto the road. A wall just stops it. A good platform is a guardrail, not a wall.
A platform that has no way to say "not yet, and here's why" stops being a paved road and becomes an obstacle teams learn to route around entirely — which recreates the exact shadow-IT problem the review-queue bottleneck already produces, just one layer deeper. The legacy billing service that's three years from a rewrite genuinely can't meet the non-root requirement this quarter. The team migrating off a vendor's SDK genuinely needs a wider egress allowlist for six weeks. A platform that treats every one of these as a fight to be won rather than a documented exception to be granted will get the exception anyway — just undocumented, worked around at 11pm by someone who's now the only person who knows it exists.
The fix isn't fewer guardrails. It's making the exception itself a first-class, auditable object instead of an ad hoc favor — the same "as code, in version control, reviewable" discipline What is DevSecOps? applies to the rules themselves, applied now to the departures from them. Gatekeeper's match.excludedNamespaces and Kyverno's PolicyException resource both do this natively:
# The exception is a tracked, reviewable Kubernetes object — not a Slack
# message nobody can find again six months later.
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireNonRoot
metadata:
name: require-non-root
spec:
match:
# Waiver tracked in PLAT-4471 — legacy image can't drop root until the
# Q1 rewrite lands. Revisit this line at that ticket's due date, not later.
excludedNamespaces: ["legacy-billing"]Two properties separate this from a quiet workaround. First, it's visible — anyone auditing cluster policy sees exactly which namespace is exempt and, if the team writing it follows the convention, exactly why and until when. DefectDojo and Compliance as Code at Scale cover how an org tracks these waivers alongside vulnerability findings so an auditor sees one consistent picture instead of policy state in one tool and exception state nowhere at all. Second, it's scoped — the exemption covers one namespace and one rule, not "security policy off" for the whole cluster, the way disabling the admission controller entirely to unblock one team would.
An exception without an expiry date is not an exception — it's a permanent, silent hole that happens to have a ticket number attached. The legacy-billing waiver above is worth exactly as much as the process that revisits it; a platform team that grants exceptions and never re-reviews them ends up, a few years later, with half the fleet quietly opted out of the rule the other half enforces, and nobody remembers why. Whatever mechanism grants the exception should also flag it when it goes stale — a scheduled report of every open waiver past its review date, fed into the same dashboard vulnerability management & triage already uses for aging findings.
Why self-service scales further than a review queue ever could
☺ Like you're 10: Checking one drawing takes the same five minutes whether it's the first one you've checked today or the five-hundredth. Printing a stencil takes five minutes once, and after that, tracing it takes no time at all.
Go back to the arithmetic from the first section and rerun it with the platform model instead of the review-queue model. The marginal cost of the hundred-and-first service scaffolded from the golden path is close to zero — the security requirements were already encoded in the template and the policy layer before that service's first commit existed; nobody has to spend reviewer-hours on it specifically. The marginal cost of the hundred-and-first service under manual review is the same as the first one's: a person, reading a diff, on a clock. One model's cost curve is flat once the fixed investment in the template and the policy is made. The other's is linear forever. That's not an incremental improvement — it's the difference between a process that can keep up with organizational growth and one that mathematically cannot, no matter how good the reviewers are.
Matthew Skelton and Manuel Pais's Team Topologies (IT Revolution, 2019) gives this a name worth borrowing: a platform's job is reducing a consuming team's extraneous cognitive load — the effort spent on things that aren't the actual problem the team is trying to solve. A developer who has to research which SAST tool to configure, write the CI YAML for it, figure out what a non-root Dockerfile even looks like, and then defend all of it in a security review is spending real attention on work that isn't "build the checkout feature." A developer who inherits all of that, correctly configured, the moment they run the golden-path template is spending that same attention on the feature instead. This reframing has picked up a name in platform-engineering circles worth knowing even though its exact origin is harder to pin to one source than "paved road" is: teams increasingly describe it as shifting security down into the platform, as a complement to shifting it left in the individual pipeline — left moves a check earlier in one team's timeline; down moves the responsibility for it out of that team's timeline entirely and into infrastructure they don't have to think about.
Manual review scales with headcount. A golden path plus a platform policy layer scales with engineering effort spent once, on the template and the policy, rather than with reviewer-hours spent repeatedly, per service. That's the entire argument for self-service secure-by-default in one sentence — everything else on this page is the mechanics of making it actually true instead of aspirational.
What changes for the security team — and what doesn't
☺ Like you're 10: The hall monitor's job doesn't disappear when the backpacks get redesigned. It becomes designing backpacks, and checking the new kid nobody's met before.
None of this eliminates security expertise from the picture — it redirects it. The engineers who used to spend their week reviewing individual pull requests now spend it curating the golden path itself: deciding what the non-root Dockerfile actually looks like, choosing which SAST rules are worth the false-positive cost of enforcing org-wide, writing and testing the ClusterPolicy before it ever flips to Enforce. That's harder work, not easier — a mistake in a golden path or a platform policy affects every consumer at once, which is exactly the blast-radius trade-off flagged earlier in this page, and it demands the kind of systems thinking a one-off pull-request review never required.
Two things specifically don't move onto the platform, because they can't. Threat modeling for a genuinely novel architecture — a new data flow, a new trust boundary, a new third-party integration — still needs a human who understands the specific design, the same way threat modeling and secure by design & threat intelligence describe; a golden path can encode "here is how services normally talk to each other," but it can't reason about a design nobody has built yet. And exceptions — the previous section's whole subject — still need a human to weigh a genuine business trade-off, because a policy engine can enforce a rule perfectly and still can't decide whether breaking it, this once, for this reason, is the right call. The platform doesn't replace judgment. It removes the need to apply that judgment identically, by hand, two hundred times over.
DSOMM and the broader field of maturity models give a platform team a way to measure whether any of this is actually working, beyond "it feels like fewer fires": the percentage of services running on a current golden path rather than a stale or hand-rolled one, the number of active policy exceptions and how many are past their review date, and mean-time-to-remediate on the findings that do slip through, tracked the same way vulnerability management & triage tracks any other backlog. Those numbers are the platform's own report card — and, same as the golden path itself, they only mean something if someone keeps maintaining what they're measuring. DevSecOps anti-patterns and best practices & the operating model both return to this exact tension: a platform is a force multiplier for the team that keeps investing in it, and a false sense of security for the one that ships it once and walks away.
Professor Owl: Two hundred services, six security engineers. If every pipeline needs a human to bless it before it ships, that math was broken before we even started.
Benny the Beaver: So build it into the road. I scaffold a new service from the golden path, and the scanning, the signing, the non-root Dockerfile — it's already there. I didn't have to ask for any of it.
Timmy the Turtle: I used to be the gate everyone waited in line for. Now the gate's built into the template itself — I still check everything, I just don't have to be a person standing there doing it by hand on every pipeline.
Recon the Robot: And I don't review Benny's cluster once and trust it forever. Every admission request gets checked against the same policy, every single time, whether Benny remembers I exist or not.
Rocky the Raccoon: Cute. What happens when I skip the golden path entirely and hand-roll my own pipeline from scratch?
Recon the Robot: Then you hit me at the cluster boundary anyway. The template is the easy way in, Rocky — it was never the only way past me.
Foxy: Fine, but what about the team that genuinely needs an exception — the legacy billing service nobody's rewritten yet?
Professor Owl: Then the exception is code too — scoped to one namespace, tied to a ticket, and it shows up in Nutty's audit trail like everything else. A guardrail bends for a documented reason. It doesn't just quietly stop existing.
1. Explain the arithmetic problem with a manual-review security model as an engineering org grows, and why hiring more reviewers doesn't actually fix it. 2. What specifically does a golden-path template like a Backstage Software Template pre-wire, and why does that reduce a security team's workload more than reviewing the same repository after the fact would? 3. Give one concrete example each of policy enforced per-repo versus policy enforced at the platform layer, and explain the trade-off the platform-layer version makes in exchange for consistency. 4. Why is a policy exception without an expiry date effectively the same as no policy at all for the namespace it covers? 5. In one sentence, why does self-service secure-by-default scale further than a review queue, even with the same number of security engineers on staff? 6. Name two things a platform explicitly does not replace, and explain why each still needs a human.
Check your answers
- A security team's review capacity grows roughly linearly with headcount, which is bounded by budget, while the number of pipelines and repositories a growing engineering org produces tends to grow faster than that. Hiring more reviewers still leaves you fighting a workload that scales faster than the resource meant to keep up with it — it delays the bottleneck, it doesn't remove it.
- It pre-wires SAST/SCA/secrets-scanning configuration, branch protection with required status checks, and a hardened (e.g. non-root, distroless) container definition — all before the developer's first commit. It reduces workload more than after-the-fact review because the requirement is now a property every instance of the template starts with, rather than something a reviewer has to check and then negotiate for, repository by repository.
- Per-repo example: each team independently running Checkov in their own CI. Platform-layer example: a Kyverno
ClusterPolicythat verifies image signatures and pod security context at admission for every namespace, regardless of which pipeline produced the workload. The trade-off: the platform-layer version can't be silently disabled by one team, but a bug or an overly broad rule in it affects every consumer at once — the same blast-radius risk as a misconfigured Kubernetes admission webhook. - Because nothing forces anyone to revisit it — the excluded namespace or the disabled rule stays exempt indefinitely, with no mechanism that distinguishes "still a valid, reviewed reason" from "someone forgot this existed three years ago." An expiring, tracked waiver at least forces a periodic decision; an open-ended one is a permanent gap wearing a ticket number.
- Because its marginal cost per additional service is close to zero — the security requirements were encoded once, in the template and the policy layer — while manual review's marginal cost per additional service is the same reviewer-hours the first one cost, forever.
- Threat modeling for a genuinely novel architecture, because a template can only encode patterns that already exist, not reason about a design nobody has built yet — and granting policy exceptions, because weighing a real business trade-off against a security requirement is a judgment call a policy engine can enforce consistently but can't actually make.