docs/docs/ocp/concepts.md
Bundles are the primary packaging and distribution unit in OCP. Each bundle contains Rego policies, data files, and is intended to be consumed by any number of OPA instances. The OCP configuration for the bundle specifies a set of requirements that list the sources (Rego, data, etc.) to include in the bundle.
OCP builds OPA Bundles and pushes them to external object storage systems (e.g., S3, GCS, Azure Cloud Storage, File System). OPA instances are configured to download bundles directly from these storage systems. See the OPA Configuration documentation for more information how to configure authentication and bundle downloads for different cloud providers
In OCP, bundles must not require multiple sources with overlapping packages. When OCP builds bundles it checks that no two (distinct) sources being included in a bundle contain packages that are the same or prefix each other. This rule is applied transitively to all sources included in the bundle. If two sources contain overlapping packages OCP will report a build error:
requirement "lib1" contains conflicting package x.y.z
- package x.y from "system"
In this example:
If you are interested in seeing this restriction relaxed please leave a comment on Issue #30 including any details you can share about your use case.
object_storage:
Configure the storage backend (S3, GCS, Azure Cloud Storage, or filesystem, etc.) for bundle distribution. OCP will write bundles to the object storage backend and the bundles will be served from there.
bundles/prod-app.tar.gzmy-prod-bucketprod/bundle.tar.gzlabels:
Add metadata to bundles to describe environment, team, system-type, etc. Labels are used by Stacks (see below) for bundle selection and policy composition.requirements:
Specify policies or data (from Sources) that must be included in the bundle. Requirements can include optional path and prefix settings to rewrite package names and data paths.excluded_files: (optional)
A list of files to be excluded from the bundle during build for example any hidden filesbundles:
prod-app:
object_storage:
filesystem:
path: bundles/prod-app.tar.gz
labels:
environment: prod
team: payments
requirements:
- source: app-policy
bundles:
prod-app:
object_storage:
aws:
bucket: my-prod-bucket
key: prod/bundle.tar.gz
url: https://s3.amazonaws.com
region: us-east-1
credentials: s3-prod-creds
bundles:
prod-app:
object_storage:
gcp:
project: my-gcp-project
bucket: policy-bundles
object: bundles/my-app/bundle.tar.gz
credentials: gcp-service-account
bundles:
prod-app:
object_storage:
azure:
account_url: https://mystorageaccount.blob.core.windows.net
container: policy-bundles
key: bundles/my-app/bundle.tar.gz
credentials: azure-credentials
Sources define how OCP pulls Rego and data from external systems, local files, or built-in libraries to compose and build bundles.
https://github.com/example/app-policy.gitrefs/head/maind4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3policies/authz.*/*Example:
Configuration sourcing policy from git (app-policy), data from file (global-data)
and additional data pulled via HTTP from Amazon S3 (s3-data). Information about used credentials is available in Secrets section.
sources:
app-policy:
git:
repo: https://github.com/example/app-policy.git
reference: refs/heads/main
excluded_files:
- .*/*
credentials: github-token
global-data:
paths:
- global/common.json
s3-data:
datasources:
- name: s3-datasource
type: http
path: data/from/s3
config:
url: https://my-bucket.s3.my-region.amazonaws.com/s3-data.json
credentials: aws_auth
When including sources in bundles, stacks, or as requirement of other sources, you can use path and prefix settings to rewrite Rego package names and data paths. This allows you to mount sources into different namespaces to avoid conflicts, import external policies and data under controlled prefixes, and select only specific subtrees of data or policies from a source.
Each source requirement can specify:
data to include (default: data, meaning everything)data, meaning no change)Basic path and prefix usage:
bundles:
my-app:
requirements:
- source: library-policies
path: library
prefix: imported.lib.v1
This configuration selects everything under data.library from the library-policies source, rewrites package names from data.library.authz to data.imported.lib.v1.authz, moves data from data.library to data.imported.lib.v1, and adjusts all references accordingly.
Mounting everything with a prefix:
requirements:
- source: external-policies
prefix: external.policies
This mounts all content from external-policies under the data.external.policies namespace.
Selecting a specific subtree:
requirements:
- source: shared-utils
path: utils.validation
prefix: app.validation
This takes only the data.utils.validation subtree and mounts it at data.app.validation.
data. prefix can be omitted for convenience: path: library is equivalent to path: data.librarypath/prefix pair is allowed per source requirementStacks enforce that certain policies are distributed to OPAs managed by OCP. When OCP builds bundles it identifies the applicable stacks (via Selectors) and then adds the required sources (declared via requirements) to the bundle. Consider using stacks if:
Let's look at an example:
Stacks provide a convenient and scalable way of enforcing this policy. Instead of manually modifying the policy for each microservice or requiring that each team write policies that call into a common library, you can define this policy once and configure a stack to inject it into the bundles for each microservice.
Because Stacks inherently involve multiple policy decisions, conflicts can arise. See the Conflict Resolution section for more information.
When OCP builds a bundle it includes all of the sources from all stacks that apply. A stack applies if both:
The selector and exclude selector are evaluated the same way. A selector matches if:
OR
AND EITHER
OR
A selector value matches the label value if:
If a stack policy and a bundle policy generate different decisions we refer to this as a conflict. Similarly, when multiple stacks are included in a bundle they may also generate conflicting decisions. Before returning the final decision to the application, the overall policy should resolve any potential conflicts by combining the different decisions. Below we provide examples of how to implement common conflict resolution patterns for different use cases. In general, conflict resolution involves:
The following example shows how to implement a common pattern where:
To illustrate this pattern we will use a simple example with two bundle policies and a stack policy. The bundle policies allow access to microservice APIs (for a "petshop" service and a "notifications" service) and the stack policy will deny access based on a blocklist. Finally, there is an entrypoint policy that composes the bundle and stack policies to produce the final decision.
The petshop service will define a policy that allows:
package service
import rego.v1
allow if {
input.action == "view_pets"
}
allow if {
input.action == "update_pets"
input.principal.is_employee
}
The notifications service will define a policy that allows customers to subscribe to newsletters:
package service
import rego.v1
allow if {
input.action == "subscribe_to_newsletter"
input.principal.is_customer
}
The stack policy will deny users that are contained in the blocklist datasource.
package globalsecurity
import rego.v1
deny if {
input.principal.username in data.blocklist
}
Finally, the entrypoint policy will combine the service and stack policy to produce the final decision:
package main
import rego.v1
main if {
data.service.allow
not data.mandatory.globalsecurity.deny
}
The configuration below illustrates how the bundles, sources, and stacks are tied together:
bundles:
petshop-svc:
labels:
environment: prod
requirements:
- source: petshop-svc
notifications-svc:
labels:
environment: prod
requirements:
- source: notifications-svc
stacks:
mandatory:
selector:
environment: [prod]
requirements:
- source: main
automount: false
- source: globalsecurity
sources:
petshop-svc: ...
notifications-svc: ...
globalsecurity: ...
main: ...
The following example shows how to implement a common pattern where:
To illustrate this pattern we will use a simple example with a single bundle policy and two stack policies. The final decision will be generated by the entrypoint policy by unioning the bundle and stack decisions. For this example, we will assume that application querying OPA is a job running in a CI/CD pipeline that provides a set of build artifacts to deploy.
The bundle policy will deny deployments that contain artifacts that do not contain a "qa" attestation.
package pipeline
import rego.v1
deny contains msg if {
some artifact in input.artifacts
"qa" in artifact.attestations
msg := sprintf("deployment contains untested artifact: %v", [artifact.name])
}
The first stack policy will block deployments that do not contain an SBOM:
package pipelines.stacks.sbom
import rego.v1
deny contains "deployments must contain sbom" if {
not input.sbom
}
The second stack policy will block deployments if an artifact has critical CVEs:
package pipelines.stacks.cves
import rego.v1
deny contains msg if {
some artifact in input.artifacts
some cve in data.cves[artifact.sha]
cve.level == "critical"
msg := sprintf("artifact contains critical cve: %v", [cve.id])
}
The entrypoint policy will union all of the deny reasons to produce the final set. Since stacks are added to bundles dynamically at build-time, the entrypoint policy iterates over the stacks namespace. Only applicable stacks will be present in the bundle.
package pipelines
import rego.v1
deny contains msg if {
some msg in data.pipeline.deny
}
deny contains msg if {
some stackname
some msg in data.pipelines.stacks[stackname].deny
}
The configuration below illustrates how the bundles, sources, and stacks are tied together:
bundles:
pipeline-a1234:
labels:
environment: prod
type: pipeline
requirements:
- source: pipeline-a1234
options:
no_default_stack_mount: true
stacks:
sbom:
selector:
environment: [prod]
type: [pipeline]
requirements:
- source: sbom
cves:
selector:
environment: [prod]
type: [pipeline]
requirements:
- source: cves
pipelines:
selector:
type: [pipeline]
requirements:
- source: pipelines
sources:
pipeline-a1234: ...
sbom: ...
cves: ...
pipelines: ...
Goal: Secrets enable OCP to securely communicate with external systems (object storage, Git, datasources, etc.) without hardcoding credentials in configuration files.
Example:
secrets:
s3-prod-creds:
type: aws_auth
access_key_id: ${S3_ACCESS_KEY_ID}
secret_access_key: ${S3_SECRET_ACCESS_KEY}
github-token:
type: basic_auth
username: ${GITHUB_USERNAME}
password: ${GITHUB_TOKEN}
OCP configuration files can be organized as a single file or split across multiple files and directories. For small or simple deployments, a single configuration file may be sufficient and easier to manage. When defining an "application," it is common practice to group related bundles and sources together in the same configuration file. This approach keeps the application's policy logic and its data sources tightly coupled, making updates and reviews straightforward.
Best practices suggest keeping secrets and environment-specific overrides in separate files or directories, while grouping each application's bundles and sources together. Use lexical naming and directory structure to avoid conflicts. For collaborative environments, version control each file and use directory-based organization to support team workflows and automated deployment pipelines. Choose the level of granularity that matches your operational complexity—favor modularity for larger teams and environments, but keep things simple for smaller setups.
When you execute OCP commands you specify the path to configuration files or directories with -c/–config. The flag can point at individual files or directories. If a directory is provided, OCP will load the contents of the directory and all subdirectories (recursively) and merge them.
By default, OCP will merge object keys and override <GlossaryTooltip term="scalar-values">scalar values</GlossaryTooltip>. Files are loaded in lexical order and the last file to set a scalar or list value wins. If the –merge-conflict-fail argument is specified, then scalar and list values are never overridden and an error will be returned if two files set the same field to a different value.