DEV Community

Cover image for Designing Security Workflows using Gitlab CI Templates
Denis Rendler
Denis Rendler

Posted on

Designing Security Workflows using Gitlab CI Templates

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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)