Your version control system, like Git, is a primary vector for secret sprawl, unintentional source poisoning, and intentional source poisoning. In a shift left model, there are degrees of leftness. The most left you can get is to test all the code before the developer tries to commit anything and train them thoroughly in the best practices. But when we rely on people to remember to do things consistently and correctly, we're cutting holes in the safety net. We need mechanisms.

At Amazon they have a saying: "Good intentions don't work. Mechanisms do." Humans can feel fatigued, rushed, distracted, or otherwise encumbered, and despite all intentions to follow best practices, they don't. When you automate enforcement of best practices, you can ensure those practices are followed in a much more consistent and correct fashion.

Force code reviews by "Code Owners"

Git exists independently of GitHub, GitLab, and BitBucket. All of these businesses are providing Git as a service. Rather than categorizing them as "GaaS" (Git as a Service), let's call them GSPs (Git Service Providers). Pretty much all GSPs add functions and features you won't get from setting up your own basic Git server. But while they each add some unique value propositions, they often provide some of the same added value features as well. One of these is called "Code Owners."

Just like you would use .gitignore to specify which files should be ignored and never committed, you can add a file called CODEOWNERS to the root of a branch to specify who owns files, directories, naming patterns, etc. Those owners must review pull requests that impact the files they own before they can be merged. 

This is supported by default in GitHub, GitLab, and BitBucket Data Center. For BitBucket Cloud, there's a free plugin. Generally they're pretty similar. Owners must have write permission so they can merge the pull requests. Depending on how the Git service identifies the owners, you might use their handle in the system, their email address, or a team identifier. In general, all three services use a similar syntax for their files, but consult the documentation for each.

For example, to fully protect a GitHub repository end to end, using CODEOWNERS, they recommend adding a CODEOWNERS file to the .github directory of the main branch, using it to set ownership of all the other CODEOWNERS files in the main repo or branches. No one will be able to modify a CODEOWNERS file in a subdirectory or branch without a review except the owner of the CODEOWNERS files.

CAVEAT

This can get a little fiddly as you deal with various combinations of permissions, settings, and rules. A couple of the fiddly bits from GitHub…

  • Someone on a team that is set as a code owner, but who doesn't have write permissions, will be able to approve a pull request, but not merge it.
  • You'll need to set "Require review by Code Owners" in the branch protection rules (see next section) to limit merges to the designated code owners. Without it, the designated code owners will get notified, but anyone with write permissions will be able to merge. 

Set branch protection rules / permissions

This goes by different names at the different services. We've got GitHub branch protection, Gitlab protected branches, and Bitbucket branch permissions.

These give you a bit more expansive and varied permissions than a CODEOWNERS file does and can even impact how CODEOWNERS is enforced, but require a bit more of a learning curve, and in some cases, navigating configuration menus in the website instead of just adding files.

For example, GitHub branch protection rules are set up by going to Settings - Code & automation - Branches - Branch protection rules, then adding a rule.

The first step is to define the branches to which the rule will apply using a pattern or a name. Then, in the docs, 21 sentences following that begin with "optionally" before you get to finalizing and applying the rule.

If I've counted correctly, the fourth "optionally" covers setting "Require review by Code Owners" as mentioned in the previous section.

Three other options to keep in mind:

  • Require status checks to pass before merging: I don't just like this one because it's how you enable a GitGuardian scan on the pull request and block the merge if it fails. You can test for adherence to coding style, software content analysis (SCA) on any dependency changes, etc. And if any of them fail… the merge is blocked.
  • Allow specified actors to bypass required pull requests: Maybe you're being pressured by a hands-on CTO to use this to let them bypass a Code Owners review on their commits, because they're the CTO and they won't push bad code. Don't. Ask your manager for backup, but resist this with all your might. Even the CTO's good intentions don't work. Mechanisms do.
  • Do not allow bypassing the above settings: Even with some of the protections you can set, another person with admin privileges (or even you in a moment of weakness) can bypass them. This makes EVERYONE, no matter how good their intentions, subject to the mechanisms.

It's probably obvious to you by now that we like mechanisms.

Get fine-grained control with rulesets

Gitlab's "rulesets" are used to control a number of scans, such as SAST, SCA, and secrets scanning.

In GitHub, branch and tag behaviors can be governed with rulesets. Rulesets, however, are not limited to a single repository. They can be set at an organizational level. And all rules in effect for a branch or tag, at whatever level, are evaluated in a process called rule layering.

This is a very powerful feature, and to help you out, GitHub has its own ruleset recipes repository.

One caveat on rulesets can be found in GitHub's Enterprise Migration Guide. If you're importing commits from another GSP and some or all of those commits don't meet the requirements of rules in your organization, the commit imports will be blocked.

Next steps

Now that you've learned about these three mechanisms, start using them. Start with Code Owners and then move on to enabling it with a branch protection rule. If you're feeling adventurous, enable a GitGuardian scan on pull requests to block secrets from getting merged (and let you remediate them). And once you're a bonafide branch protecting talent, level up into rulesets.

Remember, messing up and not following best practices isn't evil. It's human. Use your skills to build mechanisms that aren't your overlords, but a backup to help you and everyone writing code for your repository catch mistakes before they become problems.