How to improve dependency management by โshifting security leftโ and providing developers with a unified CI/CD pipeline
Developers often want to do the โrightโ thing when it comes to security, but they donโt always know what that is. In order to help developers continue to move quickly, while achieving better security outcomes, organizations are turning to devsecops.
Devsecops is the mindset shift of making all parties who are part of the application development lifecycle accountable for the security of the application, by continuously integrating security across your development process. In practice, this means shifting security reviews and testing leftโi.e., shifting from auditing or enforcing at deployment time to checking security controls earlier at build or development time.
For code your developers write, that means providing feedback on issues during the development process, so the developer doesnโt lose their flow. But for dependencies your code pulls in as part of your software supply chain, what should you do?
Letโs first define a dependency. A dependency is another binary that your software needs in order to run, specified as part of your application. Using a dependency allows you to leverage the power of open source, and to pull in code for functions that arenโt a core part of your application, or where you might not be an expert.
Dependencies often define your software supply chain. GitHubโs 2019 State of the Octoverse Report showed that on average, each repository has more than 200 dependencies. (Disclosure: I work for GitHub.) An upstream vulnerability in any one of these dependencies means youโre likely affected too.
The reality of the software supply chain is that you are dependent on code you didnโt write, yet the dependencies still require work from you for ongoing upkeep. So where should you get started in implementing security controls?
Unify your CI/CD pipelineย
Part of the goal of DevSecOps, and shifting left, is to provide not only feedback but also consistency and repeatability as part of the development environment. This isnโt unique to your supply chain, but applies to any security control.
The sooner you can unify your CI/CD pipeline, the sooner you can implement controls, allowing your security controls to shift left. You donโt want to apply the same controls multiple times in different systems. Duplicating controls doesnโt scale, spreads your (already thin) security resources even thinner, allows inconsistencies to be introduced via drift or incompatibility in systems, and worst of all, makes it more likely that something will slip through the cracks.
The precursor to shifting left and applying devsecops isnโt a security control at all. Itโs about improving developer tooling to provide a consistent way to write, build, test, and deploy code. Introducing a centralized system for any one of these can help you improve your security. Organizations will typically tackle developer tools from the last step, and work backwards to the first, adopting a consistent deployment strategy before adopting a consistent build strategy, for example. The exception to this rule is code. Even if you build locally, chances are, youโre checking your code in for posterity.
You can start applying security controls to your code even without getting all of the other steps unified. A developer-centric approach means your developers can stay in context and respond to issues as they code, not days later at deployment, or months later from a penetration test report. Building on a unified CI/CD pipeline, here are some tips on how your development team can apply DevSecOps to secure your software supply chain.
Declare dependencies in code
First things first, in order to maintain your dependenciesโfor example, applying security patchesโyou need to know what your dependencies are. Seems straightforward, right?
There are many ways to detect your dependencies, at different parts of your development process: by analyzing the dependencies declared in code (specified by a developer in a manifest file or lockfile), by tracking the dependencies pulled in as part of a build process, or by examining completed build artifacts when they enter your registry. Unfortunately, there is no perfect solution, as all methods have their challenges, but you should pick the solution that best integrates with your existing development pipeline or use multiple solutions to give you insights into dependencies at each step in your development process.
However, there are benefits to detecting dependencies in code, rather than later. Youโre shifting that dependency management step left, allowing developers to immediately perform maintenance for dependenciesโapplying updates, applying security patches, or removing unnecessary dependenciesโwithout waiting for feedback from a build or deployment step.
Even if you donโt have a centralized or consistent build pipeline, and you canโt apply a check later, detecting your dependencies in code means you can still infer this information. The main downside to detecting dependencies in code is that you might miss any artifacts pulled in later. For example, Gradle allows for dependencies to be resolved as part of a build, meaning build-time detection will contain more complete information.
To accurately detect dependencies in codeโand to more easily control what dependencies you useโyouโll want to explicitly specify them as part of your applicationโs manifest file or lockfile, rather than vendoring them into a repository (forking a copy of a dependency as part of your project, aka copy-pasting it).ย Vendoring makes sense if you have a good reason to fork the codeโfor example, to modify or limit functionality for your organizationโor to use this as a step to review dependencies (you know, actually tracking inputs from vendors). Some ecosystems also favor using vendoring.
However, if youโre planning on using the upstream version, vendoring makes updating your dependencies harder. By specifying your dependencies explicitly, itโs easier for your development team to update them, as updates require only a single line of code in a manifest, rather than re-forking and copying a whole repository. In certain ecosystems, you can use a lockfile to ensure consistency, so youโre using the same version in your development environment as you are for your production build, and review changes like any other code changes.
Standardize on โgoldenโ packagesย
You might already be familiar with the concept of โgoldenโ images, which are maintained and sanctioned by your organization, including the latest security patches. This is a common concept for containers, to provide developers with a base image on which they can build their containers, without having to worry about the underlying OS. The idea here is to only have to maintain one set of OSes, managed by a central team, that you know have been reviewed for security issues and validated in your environment. Well, why not do that for other artifacts too?ย
To supplement a unified CI/CD pipeline, you can provide a reference set of maintained artifacts and libraries. This is just a pre-emptive security control. Rather than verifying that a package is up-to-date once itโs been built, give your developers what they need as an input to their build.
For example, if multiple teams are using OpenSSL, you shouldnโt need every team to update it. If one team updates it (and there are sufficient tests in place!), then you should be able to change the default for all teams. This could be implemented by having a central internal package registry of your known good artifacts, which have already passed any security requirements, and have a clear owner responsible for updating if new versions are released.
By providing a single set of packages, youโre ensuring all teams reference these. Keep in mind, the latest you can do this is in the build system, but this could also be done earlier in code, especially if youโre using a monorepo. An added benefit of sharing common artifacts and libraries is making it easier to tell if youโre affected by a newly discovered vulnerability. If the corresponding artifact hasnโt been updated, you are! And then itโs just one change to address the issue, and for the update to flow downstream to all teams. Phew.
Automate downstream builds and deployments
To make sure that developersโ hard work pays off, their changes actually need to make it to production! In creating a unified CI/CD pipeline, you cleared a path for changes that are made to code in a development environment to propagate downstream to testing and production environments. The next step is to simplify this with automation. In an ideal world, your development team only makes changes to a development environment, with any changes to that environment automatically pushed to testing, validated, and rolled out (and back, if needed).
Rather than applying devops and devsecops by requiring your development team to learn operations tools, you simplify those tools and feedback to what these teams need to know in order to make changes where theyโre most familiar, in code. This should sound familiarโitโs whatโs happening with trends like infrastructure as code, or GitOpsโdefine things in code, and let your workflow tools handle making the actual change.
If you can automate downstream builds, testing, and deployment of your code, then your developers only need to focus on fixing code. Following devsecops principles, they donโt need to learn tooling to do validation testing, phased deployments, or whatever you might need in your environment. Crucially, for security, your development team doesnโt need to learn how to roll out a fix in order to apply a fix. Fixing a security issue in code and committing it is sufficient to ensure that it (eventually) gets fixed in production. Instead, you can focus on quickly finding and fixing bugs in code.
Creating a unified CI/CD pipeline allows you to shift security controls left, including for supply chain security. Then, to best apply devsecops principles to improve the security of your dependencies, you should ask your developers to declare your dependencies in code and in turn provide them with maintained, โgoldenโ artifacts and automated downstream actions so they can focus on code. Because this requires changes not only to security controls, but also to your developersโ experience, just using security tooling isnโt sufficient to implement devsecops. In addition to enabling platform-native dependency management features, youโll also want to take a closer look at your CI/CD pipeline and artifact management.
All together, applying devsecops allows you to gain a better understanding of whatโs in your supply chain. By using devsecops, it should be simpler to manage your dependencies, with a change to a manifest or lockfile easily updating a single artifact in use across multiple teams, and with the automation of your CI/CD pipeline ensuring that changes developers make quickly end up in production.
Maya Kaczorowski is a product manager at GitHub overseeing software supply chain security. She was previously in security and privacy at Google, focused on container security, encryption at rest, and encryption key management. Prior to Google, she was an engagement manager at McKinsey & Company, working in IT security for large enterprises. Outside of work, Maya is passionate about ice cream, puzzling, running, and reading nonfiction.


