Secure Development Policy That Matches Real Team Workflow
For software teams that need a security policy auditors can accept and developers can actually follow. This guide shows how to turn existing engineering controls—Git hosting rules, CI checks, secrets handling, deployment gates, and incident response—into a policy grounded in what the team already does, with concrete examples you can adapt.
TL;DR — A secure development policy fails when it describes an imaginary process. Start from the controls your team already enforces in Git, CI, secrets management, code review, deployment, and logging, then write the policy as a thin layer over those controls with named owners, exact enforcement points, and evidence sources. Reading time: ~7 min
What it is and where it sits
A secure development policy is the written contract between how your team ships software and how the business proves it ships software safely. In practice, it sits above your engineering tooling but below broad corporate policy. It should map directly to real controls: branch protection, mandatory reviews, CI security scans, dependency update cadence, secret storage, release approvals, production access rules, and incident handling.
What talks to it? Not software directly, but people and systems generate evidence against it:
- Developers trigger it through pull requests, commits, local tooling, and deployment workflows.
- CI/CD enforces parts of it with non-zero exit codes.
- Repo hosting enforces branch protection and review requirements.
- Secret stores, cloud IAM, and runtime platforms enforce access boundaries.
- Auditors, customers, and internal security use it to verify that the controls exist and are followed.
What does it replace? Usually one of two bad states:
- a generic template saying things like "all code must be reviewed" with no indication of where that is enforced, or
- tribal knowledge in Slack and senior engineers' heads.
Where it lives in a typical flow: in your docs repo or policy repo, versioned like code, with links to the actual enforcement points. The policy should not be the control. The policy should point at the control.
View as text
[Developer] ---- 1. git push / open PR ----> [Git Host] [Git Host] ---- 2. trigger checks ----> [CI Pipeline] [CI Pipeline] ---- 3. pass/fail status ----> [Git Host] [Reviewer] ---- 4. approve PR ----> [Git Host] [Git Host] ---- 5. merge to main ----> [Deploy Pipeline] [Deploy Pipeline] ---- 6. deploy artifact ----> [Runtime] [Runtime] ^ | 7. logs, alerts, audit trail [Evidence Store / Docs]
Architecture context matters because a policy that says "security review is required for high-risk changes" is useless unless it also says where the risk label is applied, who can override it, what blocks the merge, and where the approval is recorded.
How it actually works
The mechanism is simple: document the real control, identify the enforcing system, define the exception path, and define the evidence. If any of those are missing, the policy is aspirational rather than operational.
One realistic end-to-end example
Assume your team already does this:
- GitHub/GitLab/Bitbucket pull requests into
main - branch protection requiring 1 review
- CI running tests, dependency scan, and secret scan
- deployments only from
main - production credentials stored outside the repo
- emergency hotfixes allowed with post-incident review
Now turn that into a secure development policy that matches reality.
- A developer opens a PR changing an API handler and a Terraform file.
- The repo host starts CI.
- CI runs unit tests,
gitleaks, and dependency audit. - Branch protection blocks merge until checks pass and a reviewer approves.
- Merge to
maintriggers deployment to staging, then production. - Production deploy uses a CI-issued identity or runtime secret, not a checked-in credential.
- If the change is an emergency fix, the on-call engineer can bypass normal timing but must open a follow-up review ticket within 24 hours.
Concretely, the policy language should name the exact controls and evidence:
- "All changes to
mainrequire a pull request and at least one approval, enforced by repository branch protection. Evidence: repository branch protection settings and PR review history." - "CI must pass unit tests and secret scanning before merge. Evidence: required status checks
testandgitleakson the PR." - "Production deployments are initiated only by the deployment pipeline from
main. Evidence: deployment job logs and release history." - "Production secrets are stored in the runtime secret manager or CI secret store, never in Git. Evidence: secret manager inventory and secret scanning results."
- "Emergency changes may bypass standard lead time only when incident severity requires it; a retrospective review issue must be created by the end of the next business day. Evidence: incident ticket and linked follow-up issue."
That wording is specific enough that an auditor can verify it and a developer can follow it.
What does failure look like in the real system? Your policy should anticipate the actual errors developers see.
A failed secret scan in CI often looks like this:
$ gitleaks detect --source . --no-git --redact
○
│╲
│ ○
○ ░
░ gitleaks
Finding: password = "p@ssw0rd-prod"
Secret: p@ssw0rd-prod
RuleID: generic-api-key
File: config/dev.env
Line: 12
9:14AM INF scanned ~124 MB in 2.1s
9:14AM WRN leaks found: 1
$ echo $?
1
A dependency audit gate might fail like this:
$ npm audit --audit-level=high
# npm audit report
lodash <=4.17.20
Severity: high
Command Injection in lodash - fix available
node_modules/lodash
1 high severity vulnerability
To address all issues, run:
npm audit fix
$ echo $?
1
Those outputs are not side details; they are the operational expression of the policy. If the policy says "dependencies are reviewed regularly" but CI never blocks on anything, the real control is weak and the policy should say so honestly.
When to use it (and when not to)
Use this approach when you need a policy that survives contact with delivery pressure, audits, and incidents.
| Scenario | Recommendation |
|---|---|
| Team already uses PRs, CI, and controlled deploys | Write the policy around those controls now. This is the sweet spot. |
| You need SOC 2, ISO 27001, customer security reviews, or procurement questionnaires | Do this. Auditors want traceable controls and evidence, not ideals. |
| Team is 2-3 people shipping prototypes with no CI and direct pushes | You probably do not need a full policy yet. Add minimal controls first: protected main branch, PRs, secret scanning. |
| Highly regulated environment with formal change advisory boards | Use this approach, but add explicit approval and segregation-of-duties sections where required. |
| Legacy monolith with manual deploys and shared prod access | Do not write an ambitious policy first. Inventory the real process, then close the biggest gaps before publishing. |
| Team wants a policy mostly for internal clarity | Still useful, but keep it short and tied to tooling. A 12-page document nobody reads is worse than a 2-page accurate one. |
You probably don't need this if your actual problem is missing controls rather than missing documentation. A policy cannot compensate for no branch protection, no access control, or no deployment logs.
Trade-offs
Every benefit costs something. If you ignore that, the policy will drift.
-
Benefit: easier audits and customer reviews
Cost: someone has to maintain evidence links, control owners, and exception records. -
Benefit: fewer security regressions through enforced checks
Cost: slower PR feedback, more CI minutes, and occasional false positives from scanners. -
Benefit: consistent release process
Cost: less flexibility for senior engineers who are used to bypassing process informally. -
Benefit: clearer accountability
Cost: named owners inherit operational burden for tuning rules, reviewing exceptions, and handling break-glass events. -
Benefit: policy matches reality, so engineers follow it
Cost: you may have to admit current controls are weaker than management assumed. -
Benefit: less vendor lock-in in the document when written generically
Cost: you still need enough implementation detail to be auditable, so the policy must reference concrete systems somewhere.
A good pattern is: vendor-neutral policy statement, followed by a control implementation register in the same repo. Example: "source changes require review" in the policy, and then a table saying "Repo host: GitHub; enforcement: branch protection on main; evidence: PR approvals and required checks." If you switch vendors later, you update the register more than the policy.
In practice
Example 1: policy-as-code-ish control register in YAML
controls:
- id: SDLC-01
title: Changes to production code require review
policy: "All changes merged to main require a pull request and at least one approval from a contributor other than the author."
enforcement:
system: "Repository branch protection"
repos: ["api", "web", "worker"]
settings:
require_pull_request: true
required_approvals: 1
dismiss_stale_reviews: true
require_status_checks: true
status_checks: ["test", "gitleaks", "dependency-audit"]
evidence:
- "Branch protection screenshot or API export"
- "PR review history"
- "CI status checks on merged PRs"
exceptions:
break_glass: "Allowed for Sev-1 incidents by on-call engineer"
follow_up: "Post-incident review ticket required within 1 business day"
owner: "Engineering Manager"
This gives you a machine-readable inventory of what the policy claims and where it is enforced. The gotcha: if this file is not reviewed when tooling changes, it becomes another stale artifact. Put it in the same repo as your engineering standards and require review from whoever owns CI or platform.
Example 2: CI gate that matches the policy
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm test
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- run: |
docker run --rm -v "$PWD:/repo" zricethezav/gitleaks:latest \
detect --source=/repo --verbose --redact
dependency-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm ci
- run: npm audit --audit-level=high
This enforces three checks that a branch protection rule can require before merge. The gotcha: secret scanning on full history can fail on old leaks unrelated to the PR. Decide whether your policy blocks on historical findings or only new findings, and configure the scanner accordingly.
Example 3: branch protection verification via API
⚠️ Changing branch protection can immediately block merges or, worse, weaken protections on your default branch. Test on a non-critical repo first and export current settings before editing.
curl -s -H "Authorization: Bearer $TOKEN" \
-H "Accept: application/vnd.github+json" \
https://api.github.com/repos/ORG/REPO/branches/main/protection | jq .
Typical output shape:
{
"required_status_checks": {
"strict": true,
"contexts": [
"test",
"gitleaks",
"dependency-audit"
]
},
"required_pull_request_reviews": {
"required_approving_review_count": 1,
"dismiss_stale_reviews": true
},
"enforce_admins": {
"enabled": true
}
}
This is useful as audit evidence and drift detection. The gotcha: API shapes differ across repo hosts, and permissions for reading protection settings may require admin scope.
Further reading
- NIST Secure Software Development Framework (SSDF)
- OWASP Software Assurance Maturity Model (SAMM)
- SLSA Specification
- The "Branch protection rule" docs for your Git hosting platform
- The "Secrets Management" and "CI/CD Security" sections of the OWASP Developer Guide
This article was written by an AI system and published pending human review. Verify anything you intend to act on.
Have a project in mind?
Get an instant AI price estimate for it, or talk directly to our team.
One email a month on what we learn building with AI