Harnessing Policy as Code for Embedding Security Controls in CICD Pipeline

Harnessing Policy as Code for Embedding Security Controls in CICD Pipeline

Introduction

The rapid software development lifecycle presents challenges in verifying the security requirements for code changes. While DevSecOps practices strive to integrate security assurance into the development lifecycle, ensuring compliance with security requirements remains challenging, particularly within large organisations. Whereas manual verification of these requirements was feasible (albeit costly) when software releases were infrequent, it has become increasingly impractical to conduct manual checks for frequently released applications without compromising security or incurring high costs. Integrating security requirement checks into the CI/CD pipeline streamlines verification efforts, reducing costs and enabling security engineers to focus on more critical concerns.

In this article, I'll explain the benefit of having a policy enforcement system component in the pipeline and propose a design. I'll also show how the design can be implemented using GitHub Actions and Open Policy Agent.

What is a Security Policy?

Every organisation has a list of security requirements to be met before an application can be deployed to the production environment. These might come from regulatory requirements, the business's need to manage risk within an acceptable threshold or both.

While the term "policy" often refers to high-level statements and "requirement" is a more detailed condition that should be met, we're using them interchangeably in this document. In fact, in most cases, it is a "requirement" that can be translated to code, not a policy!

In this context, a security policy is a set of rules that governs secure application development. It can vary from basic rules (fail the build if SCA is disabled) to more complicated rules (do not deploy to test environment if particular vulnerabilities exist in an internet-facing application or if encryption is not enabled for object storage). It is worth noting that policies might be technical (how specific features should be configured. e.g. the patterns to capture secrets in the code) or process-related (e.g. who is allowed to approve a code review)

The Benefits of Policy Enforcement in the Pipeline

While traditionally, many of those requirements have been checked manually before the deployment, automating the policy evaluation and enforcing the policies in the pipeline (for the applicable ones) can have enormous benefits for an organisation, such as

  • Consistency: Automated enforcement ensures that security policies are applied uniformly across all environments, reducing the risk of human error.
  • Early Response: Policies can catch security issues early in development, minimising the cost and effort required to fix vulnerabilities.
  • Speed: Automating policy enforcement allows security checks to be conducted at the same pace as development and deployment, maintaining the pipeline's agility.
  • Compliance: This ensures that the development process adheres to industry regulations and standards, which are crucial for audibility and legal compliance.
  • Transparency and Accountability: This provides clear visibility into security practices and compliance, making it easier to identify and address policy violations.

Dynamic Policy Enforcement System

The primary objective is to develop a component to enforce a set of defined policies in the CICD pipeline.

While the CICD pipeline needs to be capable of early detection of vulnerabilities throughout the development lifecycle, integrating security policies within the deployment pipeline and automating the decision-making process regarding the progress of the code in the deployment pipeline can further enhance risk management.

The below design allows the pipeline to define security policies consistently and enforce them in appropriate places throughout the development pipeline. While for more straightforward policies, PEP relies on a single source of data (e.g., pipeline config to check whether a particular security gate is present), for more complicated policies (e.g., blocking the pipeline if particular vulnerabilities are found in a business-critical application), inputs from other sources, including business-related data and scanning tool results, are needed.

Policy Enforcement Components

Implementing the segregation of PAP, PDP & PEP results in heightened security assurance and facilitates the allocation of distinct responsibilities among relevant stakeholders. This approach is remarkably adaptable when deployed within large corporate structures.

  • Policy Administration Point (PAP)

This component is responsible for creating, managing, and administering policies. It enables management of the policy's lifecycle (creation, modification, deletion, etc.) through an interface.

  • Policy Decision Point (PDP)

This rule engine evaluates requests against defined policies (through PAP) and returns an evaluation outcome.

  • Policy Enforcement Point (PEP)

This component enforces the policy decisions made by the PDP. The enforcement will take the form of an action defined in the policy. It is worth mentioning that the PEP decides the execution action based on the PDP's evaluation. The action that PEP takes for every scenario will not be defined in PDP.

  • Policy Store

The policy store is a repository or the database where the actual policies are stored. It ensures policies are stored securely and can be retrieved accurately, allowing PDP to fetch policies when needed for evaluation.

Combining the above four components forms a comprehensive control mechanism for enforcing security policies. This control mechanism ensures that policies are defined, decisions are made based on these policies, and the decisions are enforced consistently across the system.

  • Environment variables

The use of business-related data is imperative for enhancing the decision-making process. Depending solely on scanning results often proves inadequate for PDP to make well-informed decisions. Supplemental data (e.g., the business criticality of the application and regulatory requirements) is essential for enabling informed decision-making.

Policy Enforcement System in CI Pipeline

The diagram displays the incorporation of the policy enforcement system within the CI (Continuous Integration) pipeline. It is important to note that while the visuals depict the CI pipeline, a similar pattern can be equally applicable to CD (Continuous Deployment)

Policy Enforcement in the CI System

In this design, the enforcement system will be integrated into the CI pipeline to ensure the defined policies are applied consistently to all the code changes.

In the next section, I'll show a sample implementation of the above design.


An Implementation

In this section, I'll walk through an implementation of the above design using GitHub Actions and Open Policy Agent. The example will clarify the components I defined in the earlier section.

An Implementation of Policy Enforcement using GHA & OPA

In this implementation, a Pull Request (a request to integrate the code in a feature branch into the main branch) triggers the workflow that performs policy evaluation (and enforcement). The following sections explain the technical details of PDP, PEP, and the policy store. Although the workflow is triggered at PR, it can configured to run at any other point in the pipeline (e.g. before pushing the build to the binary repository)

PEP Implementation

GitHub Actions can be used as the workflow orchestrator and the enforcement point.

Policy checks are triggered automatically as a GitHub-required workflow (on pull requests to designated branches). Enrolment and configuration of the checks can be managed by organisation-level custom properties (a tag that can be assigned to each repo in GitHub)

Once a pull request has been raised, the workflow will be triggered using GitHub Actions. The workflow leverages composite actions to install OPA CLI, load policy bundles from the policy repository, fetch the required data and evaluate the policy.

Policies can be applied as 'bundles', a collection of policies from the same domain (e.g. IaC resources). This approach significantly reduces coupling between policies and the OPA runtime, which promotes cohesion between policies and data).

PDP Implementation

OPA (Open Policy Agent) is a general-purpose policy evaluation engine. Policies are created using Rego, a technology-agnostic and declarative Domain-Specific Language (DSL). Policy enforcement is separated from decisions. OPA will return the policy evaluation outcome. The GitHub Action workflow determines what to do with the evaluation outcome.

OPA can be run separately within the organisation's infrastructure (e.g., k8s) or as a CLI within a GitHub Runner. The decision to select the deployment model depends on many factors, including (but not limited to) the need for scalability and cost.

Policy Store Implementation

The policies can be stored in any database. Depending on the organisation's IT estate, specific technology can be used. Storing policies in a separate code repository, like application (or infrastructure) code, has certain benefits.

The following outlines a plan to store security policies and workflows in GitHub repositories. This approach benefits from using a version control system, facilitates segregation, and allows different teams within an enterprise to address specific concerns such as defining security policies, configuring the DevSecOps toolchain, and utilising the pipeline.

Storage of workflow and security policies in the GitHub repository

The delineation of domains within the policy store facilitates the targeted application of policies to pertinent repositories as required. For instance, infrastructure policies may pertain exclusively to repositories hosting infrastructure code rather than those containing application code. This approach can be expanded beyond security to encompass the storage and application of other policy types, such as operational and resilience policies.

In the following sections, I'll present examples of the workflow and security policies that can be written in YAML and Rego, respectively. Depending on the technical skills of security admins, a separate user interface (PAP) can be introduced to facilitate their work.


Environment Variables

Environment variables are pieces of metadata that are essential for evaluating some security policies. They can also inform policy enforcement about whether to fail the pipeline in case of violation or whether to proceed and report. GitHub custom properties are key-value pairs that can be used to pass environment variables to PEP and PDP.


Policy Implementation

This section shows some examples of the policies that can be written in OPA language.

  • A policy that checks for baseline security steps in the CI pipeline (from the SAST domain)

package pipeline

import future.keywords


# A list of required security checks
baseline_security_steps = {"sonarqube" , "codeql_analysis" , "snyk"}

# Deny pipelines that are missing required steps
deny[msg] {
	steps = input.jobs[_].steps

	actions := {a | a:=steps[_].uses}

	not object.subset(actions, baseline_security_steps)

	msg := sprintf("The configured pipeline doesn't contain all the required security steps '%s'", [baseline_security_steps])
}
        

  • A policy that checks for encryption configuration on an AWS S3 bucket. (from IaC domain)

package terraform.s3

import rego.v1

import input as tfplan

allowed_sse_algorithms := ["aws:kms"]

# Check if server-side encryption is enabled
deny contains msg if {
	buckets := s3_buckets[_]
	count(buckets.change.after.server_side_encryption_configuration) == 0
	msg := sprintf(
		"%s: doesn't have server-side encryption enabled",
		[buckets.name],
	)
}


# Check if SSE-KMS is used and not the default config.
deny contains msg if {
	buckets := s3_buckets[_]
	sse_configuration := buckets.change.after.server_side_encryption_configuration[_]
	apply_sse_by_default := sse_configuration.rule[_].apply_server_side_encryption_by_default[_]
	not apply_sse_by_default.sse_algorithm in allowed_sse_algorithms
	msg := sprintf(
		"%s: expected sse_algorithm to be one of %v",
		[buckets.name, allowed_sse_algorithms],
	)
}

s3_buckets contains r if {
	r := tfplan.resource_changes[_]
	r.type == "aws_s3_bucket"
}
        

  • A policy that checks for "critical" vulnerabilities in the scan results returned by the SCA tool. (from the SAST domain)

package scans

import future.keywords.if

deny_list := fill_defaults([
  {
    "severity": {"value": critical, "operator": "=="}
  }
])

deny[msg] {
  item =  deny_list_violations[i][j]
  issue := item.issue
  violation = item.violation
  msg := sprintf("Vulnerability ['%s'] matches the following item found on the deny list '%s'", [issue.id, violation])
}

deny_list_violations[violations] {
    issue := input.data[i]
    violations := [x |
        x := {
            "issue": {"id": issue.id},
            "violation": remove_null(deny_list[k])
        };
        deny_compare(issue, deny_list[k])
        count(x.violation) > 0
    ]
    count(violations) > 0
}

deny_compare(issue, rule) := true if {
  compare(issue.attributes.effective_severity_level, rule.severity.operator, rule.severity.value)
}

remove_null(obj) := filtered {
  filtered := {x | x := obj[_]; x.value != null}
}

compare(a, "==", b) := a == b

fill_defaults(obj) := list {
    defaults := {
        "id": {"value": null, "operator":  null},
        "severity": {"value": null, "operator":  null}
    }
    list :=  [x | x := object.union(defaults, obj[_])]
}        

Below is a CI pipeline configuration using GitHub Actions, which runs unit tests and security scans and builds a Maven project.

name: "CI pipeline"

jobs:
  build_test_scan_publish:
    runs-on: ${{ fromJSON(inputs.runner_group_labels) }}
    steps:
      - name: build
        id: build
        run: |
          mvn install -DskipTests=true 

      - name: unit_test
        id: unit_test
        run: |
          mvn test

      - name: sonarqube_scan
        id: sonarqube_scan
        uses: workflow-repo/.github/actions/sonarqube@main
        with:
          path_to_source: "java/src"
          app_name: "sample-pipeline-rhel"
          sonar_token: "SONAR_TOKEN"

      - name: codeql_analysis
        id: codeql_analysis
        uses: workflow-repo/.github/actions/codeql_analysis@main
        with:
          codeql_language: ${{ inputs.codeql_language }}
          debug_steps: ${{ inputs.debug_steps }}
          secrets: ${{ toJSON(secrets) }}
        env: ${{ vars }}

      - name: publish
        id: publish
        run: |
          echo "Publishing build artefacts ..."        

The security policy workflow described below utilises all the components introduced above. The steps defined in the workflow are to install the OPA CLI, check if the policies apply to the repo (using?GitHub custom properties), evaluate the repositories against policies, and take action (report or break mode) based on the evaluation's outcome.

Incorporating more complex rules can enrich the logic in the PEP. Removing the logic from the workflow file (e.g. moving it to the policy file or possibly to a separate config file) further enhances the implementation's maintainability.

name: "Security  Checks"

    steps:
    - name: Default Configuration
      id: set_opa_env_vars_default
      uses: gh-actions/.github/actions/set_opa_default_env@main
      with:
        debug_steps: false

    - name: Install OPA Runtime
      id: setup_opa
      uses: gh-actions/.github/actions/setup-opa@main
      with:
        version: ${{ env.opa_cli_version }}
        debug_steps: ${{ env.debug_steps }}

    - name: Get Repository Metadata
      id: get_opa_custom_properties
      uses: gh-actions/.github/actions/get_opa_custom_properties@main
      with:
        debug_steps: ${{ env.debug_steps }}
        github_token: ${{ secrets.GITHUB_TOKEN }}

    - name: Configure Compliance Domains
      id: set_opa_compliance_domains
      run: |
        SAST_DOMAIN=$(echo "${{ toJSON(steps.get_opa_custom_properties.outputs.opa_compliance_domains) }}" | jq -r '. | any(. == "sast")')
        IAC_DOMAIN=$(echo "${{ toJSON(steps.get_opa_custom_properties.outputs.opa_compliance_domains) }}" | jq -r '. | any(. == "iac")')
        echo "SAST_DOMAIN = $SAST_DOMAIN"
        echo "IAC_DOMAIN = $IAC_DOMAIN"
        echo "SAST_DOMAIN=$SAST_DOMAIN" >> $GITHUB_OUTPUT
        echo "IAC_DOMAIN=$IAC_DOMAIN" >> $GITHUB_OUTPUT
        echo "SAST_DOMAIN=$SAST_DOMAIN" >> $GITHUB_ENV
        echo "IAC_DOMAIN=$IAC_DOMAIN" >> $GITHUB_ENV
      shell: bash

    - name: Policy Decision Point SAST
      id: call_security_checks_sast
      if: ${{ steps.set_opa_compliance_domains.outputs.SAST_DOMAIN == 'true' }}
      uses: gh-actions/.github/actions/security_checks_sast@main
      with:
        policy_domain: "sast"
        debug_steps: ${{ env.debug_steps }}

    - name: Policy Decision Point IAC
      id: call_ccompliance_checks_iac
      if: ${{ steps.set_opa_compliance_domains.outputs.IAC_DOMAIN == 'true' }}
      uses: gh-actions/.github/actions/compliance_checks_iac@main
      with:
        policy_domain: "iac"
        debug_steps: ${{ env.debug_steps }}

  
    - name: Calculate Violations
      id: calculate_total_violations_count
      run: |
        if [[ "${{ env.SAST_DOMAIN }}" == "true" ]]; then
          SAST_VIOLATIONS=${{ env.sast_compliance_violations_count }}
        else
          SAST_VIOLATIONS=0
        fi
        if [[ "${{ env.IAC_DOMAIN }}" == "true" ]]; then
          IAC_VIOLATIONS=${{ env.iac_compliance_violations_count }}
        else
          IAC_VIOLATIONS=0
        fi
        echo "sast_compliance_violations_count = $SAST_VIOLATIONS"
        echo "iac_compliance_violations_count = $IAC_VIOLATIONS"
     
        TOTAL_VIOLATIONS=$(($SAST_VIOLATIONS + $IAC_VIOLATIONS))
        echo "TOTAL_VIOLATIONS = $TOTAL_VIOLATIONS"
        echo "TOTAL_VIOLATIONS=$TOTAL_VIOLATIONS" >> $GITHUB_ENV
      shell: bash

    - name: Policy Enforcement
      id: sast_open_alert_violations_pep
      if: ${{ env.TOTAL_VIOLATIONS > 0 }}
      run: |
        echo -e "Policy Enforcement Point (PEP) will be evaluated for ALL domains that are configures for compliance checks\n"
        if [[ "${{ env.sast_compliance_violations_count }}" -gt 0 ]]; then
          OPA_COMPLIANCE_POLICY_ACTION_SAST="${{ steps.get_opa_custom_properties.outputs.opa_compliance_policy_action_sast }}"
          echo -e "${{ env.sast_compliance_violations_count }} SAST policy violations were found\n"
          if [[ "$OPA_COMPLIANCE_POLICY_ACTION_SAST" == "break" ]]; then
            echo -e "SAST compliance checks are configured for ENFORCE MODE ($OPA_COMPLIANCE_POLICY_ACTION_SAST) - The workflow will terminate with FAIL status\n"
            exit 1
          else
            echo -e "SAST compliance checks are configured for REPORT MODE ($OPA_COMPLIANCE_POLICY_ACTION_SAST) - Policy violations will NOT be ENFORCD\n"
          fi
        fi
        if [[ "${{ env.iac_compliance_violations_count }}" -gt 0 ]]; then
          OPA_COMPLIANCE_POLICY_ACTION_IAC="${{ steps.get_opa_custom_properties.outputs.opa_compliance_policy_action_iac }}"
          echo -e "${{ env.iac_compliance_violations_count }} IAC policy violations were found\n"
          if [[ "$OPA_COMPLIANCE_POLICY_ACTION_IAC" == "break" ]]; then
            echo -e "IAC compliance checks are configured for ENFORCE MODE ($OPA_COMPLIANCE_POLICY_ACTION_IAC) - The workflow will terminate with FAIL status\n"
            exit 1
          else
            echo -e "IAC compliance checks are configured for REPORT MODE ($OPA_COMPLIANCE_POLICY_ACTION_IAC) - Policy violations will NOT be ENFORCD\n"
          fi
        fi
               

This workflow can be tailored as a mandatory step across the GitHub organisation (by utilising Rulesets) to establish an effective control measure. This customisation allows for consistent evaluation and enforcement of defined security policies at specific checkpoints, such as during pull requests or before publishing build artefacts to the binary repository.

Conclusion

The following article presents a design for defining, evaluating, and enforcing security policies throughout the software development lifecycle. Additionally, a sample implementation leveraging GitHub Actions and OPA is demonstrated. Although the implementation is centred on GitHub Actions, it is adaptable for integration with other CI tools.

This approach is scalable to encompass a range of policies beyond security and can be applied to any policy translated to the code!

Matt Swann

Cyber Security Engineer | MIET

6 个月

It's been a great project- working to implement this architecture in a logical and extensible way, considering the multiple silo's that have to divide responsibility to build, maintain and enforce such a system. Embedding security controls into the CI/CD pipeline means more vulnerabilities are caught early in the development lifecycle. The architecture's driven ruleset, fulfilled through OPA’s Policy as Code approach, helps us embed security early, reducing the cost of fixing vulnerabilities later. One interesting thing to note is that, by decoupling policy from application logic we can adapt security rules quickly, which is key to maintaining agility and responding to evolving threats, and it's extensibility means we can apply these policies across multiple systems while maintaining the ability to check and enforce compliance.

Ambrose Andongabo (CISSP)

Senior Information Security Consultant - Whitbread

7 个月

Great job.

Rohit Raj, CISSP

VP Cloud Security

7 个月

Great article ??

Fraser Goffin

Stepping back from commercial engagements to focus on open source projects.

7 个月

Thanks for the shout-out Mehran Koushkebaghi

Roya Derakhshan

Lecturer at University College London

7 个月

Well done Mehran!

要查看或添加评论,请登录

Mehran Koushkebaghi的更多文章

社区洞察

其他会员也浏览了