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
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.
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.
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.
This rule engine evaluates requests against defined policies (through PAP) and returns an evaluation outcome.
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.
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.
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)
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.
领英推荐
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.
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.
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])
}
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"
}
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!
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.
Senior Information Security Consultant - Whitbread
7 个月Great job.
VP Cloud Security
7 个月Great article ??
Stepping back from commercial engagements to focus on open source projects.
7 个月Thanks for the shout-out Mehran Koushkebaghi
Lecturer at University College London
7 个月Well done Mehran!