Getting Started with Secure CI/CD: Essential Practices for Beginners

I think it’s time to focus on a few key practices for beginners to implement secure CI/CD. I’ve been building the idea on the idea of a CI/CD pipeline in a bunch of the below posts:

All of these articles are a scattering of topics based upon some of the daily randomness that I’ve faced in my work life. After looking at all of these articles, I realized that they are a good basis for building out a secure CI/CD pipeline but the steps aren’t connected very well.

I also think that it’s going to take quite a few articles to put all of this together but let’s start with the absolute base, the GitHub repository!

Starting at Step 0 – Creating the GitHub Repository

I’m going to be cheating here a little bit because I already have a repository started called blog_files. This repository is where I host some of the raw YAML/JSON files and more for this blog. As I already have this blog created, I won’t cover the actual steps to create a GitHub Repository.

Adding a README.md File to the Repository

This step is useful to provide users of the repository the purpose of the repository and possibly other useful details. In order to get the ball rolling, I’ve added a simple README.md file to my repo.

Adding a .gitignore File to the Repository

This step is important to help prevent sensitive files from being committed to the repository. We are trying to prevent various secure items from being added to the repository so we use the .gitignore file to prevent this. This file goes at the root of the repository and tells git to ignore files matching certain patterns in commits. I’m planning to have this repository host Terraform and Python files so I’ve used this .gitignore file. Your file could vary depending upon what you put into your repository.

Securing the GitHub Repository

With the repository created, you’ll want to configure some basic things to help secure the repository. For our purposes, we’re going to consider the main branch as production. Anything committed to the main branch, will be promoted to our production environment.

You will also need to be the repository owner or at least someone that has more access than collaborator to be able to make these changes.

Branch Protection Rules

As main is considered production, we’ll want to make sure we provide the most protection on this branch. We want to prevent changes that could contain errors or vulnerabilities from being promoted to our production environment.

Open your repository in a web browser and navigate to the Settings page. If you do not see the Settings menu item, then you do not have the access required. Navigate to the Branches page:

On the Branches page, click on Add branch protection rule. On this page, I decided to enable a number of options as shown below

I’m planning to protect my main branch so that will be used for my Branch name pattern. You could decide to protect all of your branches if you like. I’m keeping it quite simple here. I then enabled the following items:

  • Require a pull request before merging
    • Require approvals : Require 1 approval
    • Dismiss stale pull request approvals when new commits are pushed
    • Require review from Code Owners
    • Require approval of the most recent reviewable push
  • Require status checks to pass before merging
    • Require branches to be up to date before merging
  • Require conversation resolution before merging
  • Require linear history
  • Require deployments to succeed before merging

After selecting these items, click on Create. You can also read more about branch protections rules here.

Code Security and Analysis

Enabling and Configuring Dependabot

From there, we’ll want to enable some of the built in security scanning functions available from GitHub. On the Settings page, go to Code security and analysis. Under Dependabot, we’ll want to enable everything. I created a simple weekly update schedule for Dependabot updates using the below sample .github/dependabot.yml file:

# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "" # See documentation for possible values
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"

Enabling and Configuring CodeQL

I’m planning to make this repository host a simple Python application so I want to enable CodeQL scanning for Python. I’ve also made this codeql.yml file available in the repository. The only change that I made to the default configuration was to comment our the push and pull_request. I’ve changed these from main to ** so that all pushes and pull requests will be scanned. I don’t anticipate many changes to this repository so this is pretty safe.

# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"

on:
  push:
    branches: [ "**" ]
  pull_request:
    branches: [ "**" ]
  schedule:
    - cron: '45 7 * * 0'

jobs:
  analyze:
    name: Analyze
    # Runner size impacts CodeQL analysis time. To learn more, please see:
    #   - https://gh.io/recommended-hardware-resources-for-running-codeql
    #   - https://gh.io/supported-runners-and-hardware-resources
    #   - https://gh.io/using-larger-runners
    # Consider using larger runners for possible analysis time improvements.
    runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
    timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
    permissions:
      # required for all workflows
      security-events: write

      # only required for workflows in private repositories
      actions: read
      contents: read

    strategy:
      fail-fast: false
      matrix:
        language: [ 'python' ]
        # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ]
        # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both
        # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
        # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support

    steps:
    - name: Checkout repository
      uses: actions/checkout@v4

    # Initializes the CodeQL tools for scanning.
    - name: Initialize CodeQL
      uses: github/codeql-action/init@v3
      with:
        languages: ${{ matrix.language }}
        # If you wish to specify custom queries, you can do so here or in a config file.
        # By default, queries listed here will override any specified in a config file.
        # Prefix the list here with "+" to use these queries and those in the config file.

        # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
        # queries: security-extended,security-and-quality


    # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
    # If this step fails, then you should remove it and run the build manually (see below)
    - name: Autobuild
      uses: github/codeql-action/autobuild@v3

    # ℹ️ Command-line programs to run using the OS shell.
    # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun

    #   If the Autobuild fails above, remove it and uncomment the following three lines.
    #   modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.

    # - run: |
    #     echo "Run, Build Application using script"
    #     ./location_of_script_within_repo/buildscript.sh

    - name: Perform CodeQL Analysis
      uses: github/codeql-action/analyze@v3
      with:
        category: "/language:${{matrix.language}}"

Secret scanning

We also want to have Github scanning for possible secrets and tokens in our repository.

In order to prevent secrets from showing up in the repository, I’ve enabled the Push protection. This should help prevent commits into branches that contain secrets.

Declaring Success With a Secure Repository

At this point, we have a very basic repository configured with some basic security controls. This repository is going to lay the ground work for my future posts. As this series progresses, I plan to introduce more features and security measures. Some of these features are simply building upon my previous posts while others will include some additional features that I haven’t yet covered.

Stay tuned as we develop a secure pipeline together! If you would like to continue this journey, then checkout Build Secure Python Pipelines: Adding Tests and Hooks in Action.