The modern software development lifecycle is comprised of multiple stages, among them are installing of dependencies, building applications from source code, and running testing tools on the application - steps that can be found even in the smallest web applications. But repeating these steps after every commit can quickly become tedious and cumbersome for teams.
That is why I believe that concepts like Continuous Integration (CI) and Continuous Delivery (CD) are fundamental practices in modern software development and aim to streamline and automate the process of delivering code changes.
The CI process involves the automatic integration of code from multiple contributors and typically includes the steps for automating builds and running tests to ensure that new code changes do not break or disrupt the existing codebase. Continuous Delivery extends this automation to ensure that the code changes are automatically prepared and ready to be deployed to a production environment. This means that at any given time, the software is in a deployable state, allowing teams to release new changes to customers quickly and safely. Together, CI/CD enables more efficient and reliable software development cycles, promoting rapid releases while maintaining high quality and stability.
In recent years, as the focus on cybersecurity has grown, a new approach known as DevSecOps has gained momentum. This methodology builds on the CI/CD pipelines workflows by incorporating security tools that conduct Static Application Security Testing (SAST) and Dynamic Application Security Testing (DAST). This integration offers rapid feedback, enabling teams to identify and address known vulnerabilities before they are merged into the codebase.
Overview of GitLab CI/CD
GitLab, an open-source DevOps platform, has introduced the DevOps practices through a human-friendly language known as YAML. By utilizing predefined and intuitive tags, we can design complex workflows for our projects. However, I've often discover that as a project grows, so does the complexity of its GitLab pipeline YAML file. Managing a single file becomes challenging, and modifications frequently resulted in conflicts.
To tackle this issue, I leveraged the capabilities of the YAML language, such as aliases and anchors, to reduce code redundancy. Additionally, I've sometimes moved instructions from GitLab's CI/CD configuration file to scripted commands or created custom Docker images which contained the necessary tools allowing me to reduce often repeatable tasks. One other effective method was to create Gitlab reusable pipeline templates and afterwards just include them in my projects.
Creating Pipeline Templates for Security Tools
All the code I am using in this guide implementation can be found in my public repository: https://github.com/httpsec-eu/gitlab-security-templates
Pre-requisites:
To perform the steps describe in the guide you will need access to a Gitlab instance to create two repositories: one repository will be used for authoring the templates and the second repository will be used to showcase the usage of the templates. If you don't have an on-premise Gitlab instance you can easily create a free account on Gitlab.com at: https://gitlab.com/users/sign_in
Additionally, you will need a Docker runner registered to the repository with the code in order to run the demo pipeline.
Configuring the templates repository
A GitLab template is a YAML document that may define an entire pipeline or specific tasks to be integrated into additional pipelines, with the distinction largely lying in the content of the template itself. In this guide, I'll demonstrate how to create a pipeline designed to be added to existing pipelines, introducing a security tool named Syft which generates a project's components list, also known as an SBOM.
When setting up a template repository, I often use the following organizational structure:
├── docs
│ └── sbom.md
├── templates
│ └── sbom.yml
├── LICENSE.md
└── README.md
This format helps me keep the documentation and any relevant information quickly available which improves the usability and user experience.
After creating the folder structure, let's add the job code. Below is the code I am using for this guide - which can also be found in templates-repo/template/sbom.yml
folder in the repo mentioned previously.
# templates/sbom.yml
spec:
inputs:
stage:
default: security-dependency-sbom
description: The stage where the job will run
docker-image:
default: denis.rendler/dependency-scan:1.0
description: The Docker image used to run the job
path:
default: ./
description: The path, file or docker image to scan
report-type:
default: cyclonedx-json
description: The type of report to build
options:
- cyclonedx-json
- spdx-json
report-file:
default: dependency-sbom.json
description: The file name for the report
report-expire:
default: "1 year"
description: The amount of time Gitlab should keep the report
options:
- "30 days"
- "90 days"
- "1 year"
---
"Build $[[ inputs.report-type ]] SBOM: $[[ inputs.path ]]":
stage: $[[ inputs.stage ]]
image: $[[ inputs.docker-image ]]
script:
- syft scan $[[ inputs.path ]] -o $[[ inputs.report-type]]=$[[ inputs.report-file ]]
tags:
- infosec
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event" # Add the job to merge request pipelines if there's an open merge request.
- if: $CI_OPEN_MERGE_REQUESTS # Don't add it to a *branch* pipeline if it's already in a merge request pipeline.
when: never
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
when: always
variables:
GIT_DEPTH: "50"
artifacts:
name: "sbom-$[[ inputs.report-type ]]-$CI_COMMIT_SHORT_SHA"
paths:
- $[[ inputs.report-file ]]
expire_in: $[[ inputs.report-expire ]]
reports:
dependency_scanning: $[[ inputs.report-file ]]
allow_failure: false
Given that the template is designed for use across various projects, it's essential to allow for the modification of certain job values to facilitate an easy integration into existing pipelines. Thus, I am making use of Gitlab's spec:inputs
feature to configure the behavior of the job when it is added to the pipeline. We can also configure default values that help incorporate the job immediately without additional configuration.
While I could also use environment variables, I found that the side effects can lead to a lot of confusion and increase in debug time. For example, if I were to set the path
input as an environment variable, I would have to rename it to avoid overriding the value of the Linux environment variable called PATH
.
After adding the template, and documenting its use cases and usage using a Markdown file in the docs/
folder, I like to create a new Git tag. Using Git tags I can make changes to the template without fear that I will break my colleague's pipelines, while at the same time providing them with the option to update at their own pace.
Based on my experience, this workflow significantly increases the adoption of security tools, since teams do not fear that their release processes will be affected. This is particularly due to the fast feedback loop on known vulnerabilities, allowing teams to address these issues prior to the code being merged.
Using the templates
To use the template, GitLab provides a YAML keyword: include
- which merges external files with the current pipeline YAML.
In the second repository, create a file named .gitlab-ci.yml
and position it in the root directory.
Add the code provided below at the top of the file and update the stage
input with the name of the stage where you want the job to run:
include:
- project: 'denis.rendler/ci-templates'
file: '/templates/sbom.yml'
ref: 0.1
inputs:
stage: testing
The required keywords are: project
, file
and ref
.
The project
keyword specifies the project from which the template is imported. The file
keyword determines the import's path and file, which is relative to the repository. And the ref
keyword directs GitLab to utilize the tag 0.1
.
Alternatively, a branch name or commit SHA can be used, though this approach is generally recommended for development phases or when testing the template.
Final thoughts
Building custom Gitlab's CI/CD templates along with custom Docker images we can easily design and implement cybersecurity workflows that can be reused to secure many company projects.
Leveraging a SemVer versioning system the templates we design can be integrated into the projects at their own pace, or updated to new versions without fear of breaking project pipelines.
Taking advantage of the smaller Docker images and securing access only to private repositories, topics I discussed in my previous articles, I believe we can achieve a more robust infrastructure against supply-chain attacks.
Top comments (0)