This blog is designed to provide developers with a practical, straightforward guide to secure their open source (OS) projects. We’ll cover the most important aspects of securing OS projects, and provide practical guidelines using GitHub as the source control management system (SCM), and GitHub Actions as the CI/CD solution. At the end of this blog, we will provide reading material and resources to help you learn more about securing OS projects.
How to secure your SCM
SSO or 2FA
Account takeovers are one of the common techniques used by attackers to gain initial access to organizations or individual accounts. Enabling “Two-Factor Authentication” for an individual account or for your organization can significantly reduce the risk of unfortunate events by adding an extra layer of security. If your company uses a centrally managed identity provider, it is recommended to configure single sign-on (SSO) to authenticate users and make account management easier. This allows users to use a single set of credentials to access multiple applications, which can simplify the process of logging in and reduce the risk of unauthorized access. It can also improve the security of your organization by centralizing the management of user identities and authentication.
Account takeovers are one of the common techniques used by attackers to gain initial access to organizations or individual accounts. The attackers can then leverage that account to leak source code or modify the codebase to influence the production environment, similarly to what recently happened with the LastPass incident. The guiding principle of permissions is “least privilege” meaning we should make sure that entities have the least privileges required to perform their actions.
Applying the principle of least privilege in GitHub means setting the organization base permissions to “None” or at most to “Read”. You should also try to minimize the number of your organization’s admins.
For repositories, it’s important to minimize the number of admins and limit user permissions to only what’s necessary, for example – that users that don’t write code wouldn’t have write access.
Branch protection rules
Branch protection rules are one of the main controls that allow you to protect your source code against unauthorized changes. The general guideline is to protect all branches that are used to deploy to sensitive environments. We recommend configuring the following GitHub branch protection rules:
- Require a pull request before merging:
- Required reviewers: One reviewer is acceptable, but having two reviewers is preferable as it reduces the potential for bypassing the single approval.
- Require review from code owners
- Dismiss stale pull request approvals when new commits are pushed
- Require approval of the most recent push from someone other than the last pusher
- Require status checks to pass before merging
- Prevent force push (default when a protection rule is created)
- Prevent branch deletion (default when a protection rule is created)
In addition it is recommended to configure the “Do not allow bypassing the above settings”, preventing administrators with high privileges from making mistakes.
If you’re using webhooks and the service you’re using supports token authentication, configure your webhooks to use a secret. This way you can make sure the webhooks you receive arrive from your repositories and not from a malicious source. Also make sure your webhooks use HTTPS.
How to secure your code
Secrets like API keys or private keys can sometimes be committed accidentally or unknowingly to the codebase. These secrets can then be found and exploited by malicious actors. As a best practice it’s recommended to regularly scan your code, including past commits, for secrets. If any secrets are found, revoke them immediately and issue new ones. If you’re overwhelmed by the variety of secret scanning tools available, a good tool to start with would be Gitleaks. To prevent a leakage of secrets, it’s best to scan on any push of code and when the CI pipeline is triggered.
If your project depends on other projects then it is probable that at least one of these projects has a security vulnerability. Your project may be at risk if you use the vulnerable code. That is why it’s important to routinely monitor your project for new vulnerabilities in its dependencies, even if there are no new commits.
Static application security testing (SAST)
Humans write code -> humans make mistakes -> mistakes cause security vulnerabilities -> vulnerabilities cause damage. To break this cycle we suggest that you scan your code for vulnerabilities to reduce the likelihood of a major vulnerability in your code. If you want to choose a good SAST we strongly recommend finding one that doesn’t spam you with false positives.
Dependency confusion prevention
Dependency confusion is an attack that exploits default behaviors of package managers to cause a client to download a malicious package. A common scenario would be that a user tries to install an internal package that is found in the company’s internal package repository but the package manager CLI will try first to fetch it from its default source, for example https://pypi.org/ instead of an internal instance of PyPI. This behavior can be exploited by registering the internal package name in the public registry hoping someone would accidentally download and run the malicious package. If your project uses private packages as dependencies you need to make sure that their names are registered by you on the public registry of your package manager (for example PyPI or NpmJS) in order to prevent this attack.
Another scenario worth mentioning is when a public project uses internal dependencies (stored in private repositories). In this scenario attackers can discover the name of the internal dependency and try to execute a dependency confusion attack by registering a public package with the same name.
How to secure your build
Pipeline-based access controls (PBAC) and credential hygiene
Insufficient PBAC (one of the “OWASP Top 10 CI/CD security risks”) can allow attackers to abuse permissions granted to the pipeline to move laterally within or outside the CI/CD system.
Credential hygiene is the principle of minimizing access to credentials throughout the entire pipeline.
To mitigate risks related to PBAC and credential hygiene you should:
- Minimize the build runner permissions (for example, an AWS EC2 instance that is assigned with an instance role with high privileges to other cloud resources).
- Consider using short lived credentials such as OIDC to access cloud resources, where possible.
- Minimize permissions of tokens used during the build process:
- See here for how to minimize GitHub’s “GITHUB_TOKEN” permissions.
- GitHub also offers fine grained tokens to minimize permissions granted by personal access tokens.
- Lastly, make sure that your deploy keys don’t have ‘write’ permissions because an attacker that gets access to your CI will be able to use that key to push malicious code to your codebase.
- Filter ingress and egress network access for build runners.
For more information you can read a blog we wrote about optimizing CI/CD credential hygiene.
Locking dependencies results in installing specific versions of a dependency after verifying their hash according to the hashes and versions that were used during their development. Hash verification is an important step to achieve reproducible builds, ensuring that each installation is exactly the same. This will prevent installation of compromised packages, unless of course you manually update a compromised package.
Note that there’s a caveat to locking dependencies: you won’t automatically get the latest updates, which could expose you to future vulnerabilities. To mitigate this, define a continuous process to update dependencies versions.
Another suggestion is to lock the GitHub Actions you use by specifying a hash instead of a version. Although this does not refer directly to locking software dependencies, this suggestion is relevant as it helps ensuring reproducible builds.
Prevent untrusted input
Don’t use inputs from externally controlled sources in your builds. For example, consider the following code snippe:
This snippet uses an issue title as an input to the pipeline. The problem is that the issue title is evaluated and executed as a bash command which can be exploited to inject commands to the pipeline.
If you can, abstain from using any inputs in your pipeline. See here for more on untrusted input issues.
How to secure your artifacts
Signing your released artifacts allows your users to verify that the artifact was produced by you and wasn’t modified by a malicious actor. Although this is not true 100% of the time, it does make attackers’ life harder by adding another defense mechanism that they need to bypass in order to compromise a project.
Generally speaking, there are three types of artifacts that should be signed:
- Code packages: Each programming language’s package manager has a different way to sign packages, or doesn’t have a way at all, so you’ll need to refer to the relevant documentation
- Docker images: As of writing this blog there are two main ways to sign images: Docker Content Trust (only relevant for DockerHub) and Sigstore’s cosign (which can be used in any container registry)
This blog covers the most important controls that you should implement to protect your OS repositories, but of course there’s more. If you‘d like to dive deeper into the rabbit hole, check out the following links:
- Security Scorecards – Security health metrics for Open Source
- OWASP Cheat Sheet Series – A concise collection of high value information on specific application security topics.
- SLSA – Safeguarding artifact integrity across any software supply chain
- OWASP Application Security Verification Standard.
- CI/CD Goat – A deliberately vulnerable CI/CD environment. Learn CI/CD security through multiple challenges.
- ClusterFuzzLite – A continuous fuzzing solution that runs as part of Continuous Integration (CI) workflows to find vulnerabilities.
- Infrastructure as code analysis.