Skip to content

CODEOWNERS for Infrastructure Repositories

Lesson 5 of 8Intermediate13 min readGit for DevOps & Infrastructure · DevOps RepositoriesVerified: GitHub CODEOWNERS and repository rulesets documentation, September 2026

An infrastructure pull request needs a reviewer who would recognise a bad plan. CODEOWNERS is how the right person gets asked.

It is also, on its own, a suggestion. The most common misconfiguration in this area is adding the file, watching reviewers get requested, and reasonably concluding that a control is in place when none is.

CODEOWNERS is metadata. It maps paths to owners and causes review requests to be routed. On its own it blocks nothing — a pull request can be approved by anybody and merged.

A ruleset or branch protection rule is enforcement. “Require review from Code Owners” is a separate setting, and without it the file is a courtesy.

The pairing:

CODEOWNERS says who owns what.

A ruleset requires their approval and defines how many.

Branch protection or the ruleset prevents direct pushes, so the path cannot be bypassed.

Check the bypass list. A rule with administrators on it describes an intention.

The paths where a wrong change is expensive, in rough order of consequence.

.github/workflows/ — what runs, with what permissions, holding which secrets. A change here can grant a workflow the ability to publish, deploy or read every secret in the repository. It is the highest-consequence path in any infrastructure repository and the one most often left unowned.

Production environment paths — whatever your layout calls them.

Networking — security groups, firewall rules, VPC and subnet configuration, load balancer rules. A wrong change here is an exposure rather than an outage, which makes it worse.

Identity and access — IAM policies, roles, Kubernetes RBAC, service accounts. Four lines can grant everything, and an over-broad grant looks structurally identical to a correct one. This is the category where the reviewer’s expertise matters most, because there is nothing in the diff to alarm somebody who does not read policy documents for a living.

Data infrastructure — databases, storage, anything whose destruction is unrecoverable.

Shared modules — a change reaches every consumer.

Policy — the rules everything else is checked against.

Cluster-scoped Kubernetes resources — CRDs, ClusterRoles, admission webhooks. No namespace boundary to contain a mistake.

Secrets configuration — encrypted files, external secret definitions, and the mechanisms that supply them.

# Default owner — the platform team sees anything not otherwise owned.
* @example-org/platform
# Automation: highest consequence, strictest ownership.
/.github/ @example-org/platform-leads
/.github/workflows/ @example-org/platform-leads
/.github/CODEOWNERS @example-org/platform-leads
# Policy — the rules everything else is checked against.
/policies/ @example-org/platform-leads @example-org/security
# Shared modules — a change reaches every consumer.
/infrastructure/modules/ @example-org/platform
# Production.
/infrastructure/environments/production/ @example-org/platform-leads
/clusters/production/ @example-org/platform-leads
# Cross-cutting concerns, wherever they appear.
**/network* @example-org/network
**/*iam* @example-org/security
**/*rbac* @example-org/security
**/*firewall* @example-org/network
# Data infrastructure.
**/database* @example-org/data @example-org/platform
**/*rds* @example-org/data
# Secrets.
**/vault* @example-org/security
**/*secret* @example-org/security
# Applications belong to their teams.
/applications/api/ @example-org/api-team
/applications/web/ @example-org/web-team
# Cluster-scoped Kubernetes resources: no namespace boundary.
/platform/cluster-resources/ @example-org/platform-leads

Later patterns win. General rules first, specific overrides after. A file ordered the other way silently applies the wrong owner.

Only the last matching pattern applies. A file matched by both **/network* and /applications/api/ gets whichever appears later — not both. This surprises people who expect the rules to combine, and it is the commonest source of “why was the network team not asked”.

Every path should match something. The * default guarantees that.

Teams, not individuals. A team survives somebody leaving; a username does not, and a CODEOWNERS naming a departed engineer routes to nobody.

The **/network* style rules are worth explaining because they are the difference between ownership that works and ownership that only covers where you remembered to look.

Path-based ownership assumes structure. If networking always lives in infrastructure/network/, a directory rule covers it.

Pattern-based ownership covers the cases you did not anticipate. A security group defined in an application’s directory, an IAM policy in a module, an RBAC binding in a service’s manifests. Those are exactly the ones that slip through a directory-only file.

The trade-off is false positives. **/*secret* matches a file about secret scanning configuration, and the security team is asked about something they do not need to see. That is a cost worth paying — an unnecessary review request is an annoyance, and a missed one is an incident.

Keep the patterns few and obvious. Five cross-cutting patterns that everybody understands beats twenty nobody can predict the effect of.

Test them. Open a pull request touching a file you expect a pattern to match and confirm the routing. A pattern that does not fire is invisible.

Different paths warrant different requirements, and rulesets scoped to paths are the mechanism.

PathApprovalsCode owner required
Development environment1No
Application configuration1Team owner
Shared modules1Yes
Production2Yes
.github/workflows/2Yes
IAM, RBAC, networking2Yes, the specialist
Policy2Yes

Two approvals for production is a common default and is not automatic — it costs time on every change, and for a small team where everybody knows the estate it may be ceremony. Decide it rather than copying it.

Dismiss stale approvals on new commits. Critical for infrastructure: an approval given against one plan should not carry to a different one.

Require branches to be up to date. Forces a re-plan against the base that will actually be merged.

Worth being explicit, because “requires two approvals” gets treated as a counting exercise.

The code owner should be able to recognise a bad change. Somebody who reads IAM policies for the IAM path; somebody who has operated the network for the network path. An approval from a person who cannot evaluate the change is a delay rather than a control.

They should read the plan or the rendered diff, not the source diff. That is where the effect is.

They own the standard, not just the review. A code owner who keeps correcting the same thing should turn it into a policy, a lint rule or a template.

They are accountable for what merges. That is what ownership means, and a repository where the code owner list is aspirational rather than actual has a gap between who is asked and who is responsible.

Owners who are not asked. A path with no matching rule, or a later pattern overriding an earlier one unintentionally. Test the routing.

Owners who are always asked. A team on every pull request stops reading them. If a team is the code owner for everything, they are the code owner for nothing.

A single person as owner. They go on holiday and the repository stops. Teams, always.

A team with one active member. Nominally a team, practically a person.

Stale entries. A team that was reorganised, a user who left. Review the file when the organisation changes, and a routing to a nonexistent team fails silently.

Owners who rubber-stamp. The hardest to detect and the most damaging. A code owner approving without reading has converted a control into a delay, and every metric still looks healthy.

Bypass permissions. Administrators who can merge without review. Occasionally necessary, and worth being a short and reviewed list.

Self-approval. Check whether an owner can approve their own pull request. In a small team this may be unavoidable and should be a deliberate decision.

CODEOWNERS rots faster than most configuration because it references organisational structure.

Review it when teams change. A reorganisation invalidates entries silently.

Check for nonexistent teams and users. A rule naming something that no longer exists routes to nobody, and GitHub does not loudly complain.

Check for paths with no owner beyond the default. If the platform team is the fallback for a directory that a product team actually owns, the routing is wrong.

Watch the review latency per owner. A team taking four days to review is a bottleneck, and the answer is usually a wider owning team rather than removing the requirement.

Put the file under its own ownership. /.github/CODEOWNERS @platform-leads means changing who owns what is itself reviewed — which prevents somebody quietly making themselves the owner of a path, and prevents a well-meaning cleanup removing a rule that mattered.

CODEOWNERS is per repository, which leaves a gap at organisation scale.

Each repository needs its own file. A polyrepo with twelve infrastructure repositories needs twelve, and they drift.

Organisation-level rulesets, where the platform supports them, apply rules across repositories without touching each one. That covers the enforcement half — required reviews, required checks — and not the routing half, because routing depends on paths that differ per repository.

A default owner in every repository is the minimum: whoever is accountable when nobody else is. A repository with no CODEOWNERS at all is a repository where any collaborator’s approval suffices.

Templates put the file in new repositories and do nothing for existing ones.

A scheduled check reporting which repositories lack a CODEOWNERS, or name a nonexistent team, is the practical answer. It changes nothing and it makes the gap visible, which is the prerequisite for closing it.

Watch for repositories nobody owns. These accumulate in a polyrepo, they still deploy things, and their CODEOWNERS routes to a team that was dissolved two reorganisations ago. Finding them is a one-off exercise worth doing.

Being precise, because CODEOWNERS is sometimes treated as a complete governance answer.

It does not control read access. Everybody with repository access sees everything in it. Ownership is about who approves, not who sees — which is the argument for separate repositories where read separation is a requirement.

It does not apply to anything outside pull requests. A direct push bypasses it entirely, which is why “no direct pushes” is the rule underneath everything else.

It does not apply to changes made outside the repository. A console edit, a kubectl apply, an engineer running Terraform from their laptop. Those are drift, and no repository control reaches them.

It does not make the reviewer competent. Routing to a team that cannot evaluate the change produces an approval, not a review.

It does not survive its own removal. A pull request deleting a CODEOWNERS rule is reviewed by whoever owned it at that moment — which is why the file should own itself, and why that entry is not a formality.

It does not scale down. In a three-person team where everybody owns everything, CODEOWNERS is overhead pretending to be governance. Adopt it when the second team appears.

The file without the ruleset. Routing, not enforcement.

Assuming rules combine. Only the last matching pattern applies.

Ordering specific before general. The general rule wins and overrides everything.

Individuals rather than teams. Breaks when they leave or go on holiday.

.github/ unowned. The highest-consequence path in the repository.

A team that owns everything. They stop reading.

Never testing the routing. A pattern that does not fire is invisible.

Stale entries after a reorganisation. Routing to nobody, silently.

A long bypass list. The rule describes an intention.

Not owning CODEOWNERS itself. Ownership can be changed without review.

A repository with no ownership, and a decision to add some.

  1. Write the ownership map first, on paper. Who is accountable for which parts of the estate? This is an organisational question and it is the input to everything else.

  2. Start with a default. * @platform and nothing else. That alone means somebody is always asked, which is more than most repositories have.

  3. Add the highest-consequence paths. .github/, production, policy. Three rules.

  4. Enable code-owner review as a required check. Do this before adding more rules — a thorough file with no enforcement teaches the wrong lesson.

  5. Add team ownership for application paths, so teams review their own.

  6. Add cross-cutting patterns for networking, identity and secrets. Test each by opening a pull request that should match it.

  7. Add the self-ownership rule for CODEOWNERS.

  8. Review the routing after a month. Who was asked, who was not, and what took too long.

Step 4 before step 5 is the ordering that matters. Teams sometimes build an elaborate file over several weeks and enable enforcement at the end, during which the file has been decorative and nobody noticed.

Expect step 1 to be the hard part. In many organisations the ownership map does not exist, and writing it down surfaces disagreements. Those disagreements are real and the file cannot resolve them — it can only record whatever answer the organisation reaches.

A change to CODEOWNERS is a governance change, and it deserves the treatment.

Adding yourself as an owner is worth a second look. Sometimes correct, sometimes somebody unblocking themselves.

Removing an owner needs a reason. A path that had a security team owner and now does not is a change to what gets scrutinised.

Broadening a pattern means more requests, which sounds harmless and can produce the always-asked failure mode.

Narrowing a pattern means fewer, which is where things slip through.

Reordering rules changes the effective ownership of whatever the moved rule matched, and the diff makes this genuinely hard to see. A reordering pull request deserves somebody working out which paths changed owner.

A rule added during an incident should be revisited afterwards. Emergency changes to governance are exactly the ones that outlive their justification.

The general standard: the file records who is accountable for what. Changing it changes accountability, and that is not a change to slip into a pull request about something else.

Ownership is easy to configure and easy to have configured badly for a year without noticing.

Review latency per owning team. A team taking four days is a bottleneck, and the fix is usually a wider owning team rather than removing the requirement.

Requests per team. A team on most pull requests has effectively been made a rubber stamp; a team on none is either not needed or not being routed correctly.

Paths falling to the default owner. If the platform team is being asked about a product team’s configuration, the routing does not match the ownership map.

Approvals with no comments, consistently, on a high-consequence path. Not proof of rubber-stamping and worth a conversation.

Incidents traced to a change that went through the process. The most informative signal. Was the right owner asked? Did they have what they needed to evaluate it? If the plan was buried in a CI log, that is a surfacing problem rather than an ownership one.

Bypasses used. Every merge that skipped required review, and why. A short list with explanations is fine; a long one means the requirement is being routed around.

What not to measure: the number of rules in the file. A long CODEOWNERS is a repository whose structure does not match its ownership map, not a well-governed one.

CODEOWNERS answers “who should look at this?”. A ruleset answers “must they?”. You need both, and the file is worth nothing without the setting.

The deeper point: routing is only useful if the person routed to can evaluate the change. An infrastructure pull request needs a reviewer who would recognise a bad plan, and building that list is an organisational exercise that the file merely records.

  • CODEOWNERS routes; a ruleset requiring code owner review enforces
  • Only the last matching pattern applies — rules do not combine
  • .github/workflows/ is the highest-consequence path and the most often unowned
  • Pattern rules like **/*iam* catch the cases a directory structure did not anticipate
  • Own teams, never individuals; a team with one active member is a person
  • Dismiss stale approvals — an approval for one plan should not carry to another
  • A code owner who cannot evaluate the change is a delay rather than a control
  • Put CODEOWNERS under its own ownership

Use a disposable repository with directories mirroring an infrastructure layout.

  1. Add a CODEOWNERS with a * default and a specific rule for /environments/production/. Open a pull request touching production. Predict: who is requested?

  2. Add a general rule after the production rule. Open the same pull request. Predict: did the routing change?

  3. Add **/*iam* and create applications/api/iam-policy.tf. Predict: is the security team requested?

  4. Add a rule for /applications/api/ after the IAM pattern. Open the same pull request. Predict: are both requested, or only one?

  5. Merge a change with only a non-owner approval. Predict: does it merge? Then enable “Require review from Code Owners” and try again.

  6. Name a nonexistent team as an owner. Open a pull request touching that path. Predict: what happens?

  7. Add /.github/CODEOWNERS to the file itself and try to change ownership of another path.

  8. Delete the repository.

Lab: ship an infrastructure change through a pull requestPractise the review discipline that makes an infrastructure change reviewable before it is applied.

The GitOps and infrastructure repository templates are in the Professional Toolkit.