# Using GitOps to manage Custom Assembly resources

URL: https://deploy-preview-3742--ornate-narwhal-088216.netlify.app/chainguard/chainguard-images/features/ca-docs/custom-assembly-gitops.md
Last Modified: August 3, 2026
Tags: Chainguard Containers, Procedural, Custom Assembly

How to use GitOps to manage Custom Assembly resources.

Chainguard&rsquo;s Custom Assembly is a tool that lets customers create customized container images with extra packages and annotations added. This enables customers to reduce their risk exposure by creating container images that are tailored to their internal organization and application requirements while still having few-to-zero CVEs. It can be managed in the Chainguard Console, with chainctl, with the API, or from a CI/CD pipeline.
This guide shows how to use Chainguard Custom Assembly as code from a CI/CD pipeline, storing your configuration in Git and using automation to apply changes and trigger builds. The examples in this guide focus on GitHub Actions, and are adapted from Chainguard&rsquo;s custom-assembly-as-code demo repository.
NOTE: chainctl is an API client that handles common tasks like authentication and applying configuration files. You can manage Custom Assembly interactively using chainctl. Running chainctl non-interactively is a common pattern for implementing GitOps workflows.
Prerequisites Before getting started, you need the following:
A Chainguard organization with access to Custom Assembly, as well as permission to manage Custom Assembly for your organization A CI/CD platform in place. This guide uses GitHub Actions as an example Custom Assembly builds need no GitHub credentials beyond the token actions/checkout uses by default, so the example workflow in this guide does not authenticate to the GitHub API. A Git repository to host your apko configuration files A configured assumable identity for your CI workload If you have not yet set up CI identities, refer to Chainguard&rsquo;s tutorials for creating and assuming identities. The full IDs for your image-syncer and custom-image-builder identities, named catalog_syncer and apko_builder in older organizations Understanding apko overlay files Custom Assembly uses apko overlay YAML files to customize images. You can use them to define changes such as additional packages to install, environment variables, and annotations.
This example overlay file shows the configuration options available for customizing Chainguard images:
contents: packages: - curl - jq environment: APP_ENV: production LOG_LEVEL: info annotations: org.opencontainers.image.title: &#34;Python App with Tools&#34; org.opencontainers.image.description: &#34;Custom Python image with curl and jq&#34; accounts: run-as: &#34;appuser&#34; users: - username: &#34;appuser&#34; uid: 65532 gid: 65532 homedir: &#34;/home/appuser&#34; groups: - groupname: &#34;appgroup&#34; gid: 65532 members: - &#34;appuser&#34; certificates: additional: - name: &#34;certificate name&#34; content: | -----BEGIN CERTIFICATE----- ... -----END CERTIFICATE----- Repository structure Chainguard recommends organizing your configuration YAML files in a dedicated directory, as in the following example repository structure:
&lt;github-repository&gt;/ ├── .github/ │ └── workflows/ │ └── build-custom-images.yaml ├── ca-images-iac/ │ ├── python-app.yaml │ ├── nginx-custom.yaml │ └── node-api.yaml └── README.mdIn this example, the ca-images-iac/ directory contains the apko overlay files, while the workflow file defines how and when builds are triggered.
Step 1: Create an assumable identity First, create an identity that your CI/CD platform can assume. The process varies by platform; the following example uses GitHub Actions.
chainctl iam identities create github-actions-identity \ --description=&#34;GitHub Actions identity for Custom Assembly&#34; \ --identity-issuer=https://token.actions.githubusercontent.com \ --subject-pattern=&#34;.*&#34; \ --claim-pattern=repository:&lt;github-organization&gt;/&lt;github-repository&gt; \ --claim-pattern=&#39;event_name:^(push|workflow_dispatch)$&#39;Replace &lt;github-organization&gt;/&lt;github-repository&gt; with the repository that holds your workflow. This creates an identity that GitHub Actions workflows in that repository can assume, whether triggered by a push or started manually from the Actions tab.
Claim values are patterns, not literal strings, so ^(push|workflow_dispatch)$ matches either event. Match both: the workflow later in this guide triggers on push and workflow_dispatch, and the testing steps start a run manually. An identity pinned to event_name:push alone rejects manual runs with token has invalid &quot;event_name&quot;: workflow_dispatch. Be sure to quote the pattern so your shell does not interpret the | as a pipe.
Repeat --claim-pattern once per claim, as shown in this example. Passing several claim:pattern pairs as a single comma-separated value does not create separate claims — chainctl treats the entire string as one pattern, matching a repository claim whose literal value is &lt;github-organization&gt;/&lt;github-repository&gt;,event_name:push. This fails silently, producing an identity that no workflow can assume.
This example matches on the repository and event_name claims rather than the sub claim, so it is unaffected by GitHub&rsquo;s immutable subject claims, which change only the sub claim. The repository claim carries the repository name, and names can be reassigned. For stronger protection against namespace reuse, pin the identity to the repository&rsquo;s numeric ID by adding --claim-pattern=repository_id:&lt;github-repository-id&gt;.
Step 2: Grant permissions The identity needs permission to build Custom Assembly images. You can create a least-privilege custom role that contains the repo.update and repo.create permissions, then grant the necessary permission using chainctl.
After creating the custom role, set an environment variable named IDENTITY_ID to the UIDP of the github-actions-identity identity you just created:
IDENTITY_ID=$(chainctl iam identities list -o json | jq -r &#39;.items[] | select(.name==&#34;github-actions-identity&#34;) | .id&#39;)Then use this variable to create a role binding that grants the custom role to the identity:
chainctl iam role-bindings create \ --identity=$IDENTITY_ID \ --role=&lt;custom-role&gt; \ --parent=&lt;chainguard-org&gt;Be sure to replace &lt;custom-role&gt; with the name of the custom role you created and &lt;chainguard-org&gt; with the name of your Chainguard organization.
Step 3: Note your identity ID You&rsquo;ll need your identity ID for your CI/CD workflow configuration. Save it for use in the next section:
chainctl iam identities list -o table Trigger builds with chainctl in CI/CD workflows Regardless of which CI/CD platform you use, you trigger Custom Assembly builds with the same chainctl images repos build apply command:
chainctl images repos build apply --file ca-images-iac/custom-jre.yaml \ --parent &lt;chainguard-org&gt; \ --repo &lt;image-name&gt; \ --yesThis command follows the example repo structure that appears earlier on this page, where ca-images-iac is the directory that contains the apko overlay files.
This command:
Reads your apko overlay configuration from the YAML file Applies it to build a custom image Pushes the result to your Chainguard registry Skips the interactive confirmation when you pass --yes, making it suitable for automated workflows GitHub Actions example This section provides a complete example for automating Custom Assembly builds with GitHub Actions.
Create .github/workflows/build-custom-images.yaml in your repository. This example is based on Chainguard&rsquo;s custom-assembly-as-code demo:
# Trigger builds automatically when the specified file changes. Only runs on pushes to the main branch. Use a wildcard to trigger on any file in a specified directory. name: build on: push: branches: [main] paths: - &#39;ca-images-iac/custom-jre.yaml&#39; workflow_dispatch: # Images are signed by either the image-syncer or custom-image-builder identity in # your organization. Find these values under &#34;Assumed Identities&#34; in your # organization settings. They are defined here, at workflow level, so every step can # read them. env: CUSTOM_IMAGE: &#34;cgr.dev/&lt;chainguard-org&gt;/&lt;image-name&gt;&#34; IMAGE_SYNCER: &#34;&lt;chainguard-org-id&gt;/&lt;image-syncer-id&gt;&#34; CUSTOM_IMAGE_BUILDER: &#34;&lt;chainguard-org-id&gt;/&lt;custom-image-builder-id&gt;&#34; # Top-level permissions follow the principle of least privilege. Job-level permissions grant only what&#39;s needed. permissions: {} jobs: build-custom-image-as-code: runs-on: ubuntu-latest permissions: actions: read contents: read id-token: write steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@20cf305ff2072d973412fa9b1e3a4f227bda3c76 # v2.14.0 with: egress-policy: audit # Nothing after this step uses git or the GitHub API, so there is no reason to # leave the checkout token behind in .git/config. - name: Checkout repository uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 with: ref: main persist-credentials: false - name: Setup Go environment uses: actions/setup-go@41dfa10bad2bb2ae585af6ee5bb4d7d973ad74ed # v5.1.0 with: cache: false # Pin Crane to a release rather than @latest so a run cannot pick up an # unreviewed version. Check for newer releases periodically. - name: Install Crane run: go install github.com/google/go-containerregistry/cmd/crane@v0.21.9 - name: Install Cosign uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0 # Authenticate to Chainguard using assumable identity - uses: chainguard-dev/setup-chainctl@8d93dcbef466d3cf3533f67084f52eb74ef9d262 # v0.2.4 with: identity: &#34;&lt;chainguard-org-id&gt;/&lt;chainguard-identity-id&gt;&#34; - name: &#39;Auth to Registry&#39; run: | chainctl auth configure-docker chainctl auth status # Verify existing image signature before rebuilding. - name: Verify signature &amp;&amp; pull existing image id: cosign-verify continue-on-error: false run: | cosign verify \ --certificate-oidc-issuer=https://issuer.enforce.dev \ --certificate-identity-regexp=&#34;https://issuer.enforce.dev/(${IMAGE_SYNCER}|${CUSTOM_IMAGE_BUILDER})&#34; \ &#34;$CUSTOM_IMAGE:latest&#34; | jq # Extract and display packages from the SBOM attestation. - name: Print created time and list packages id: crane-config continue-on-error: false run: | echo &#34;Created time: $(crane config &#34;$CUSTOM_IMAGE:latest&#34; | jq -r .created)&#34; crane manifest &#34;$CUSTOM_IMAGE:latest&#34; | jq -r &#39;.manifests[] | select(.platform.architecture==&#34;amd64&#34;) | .digest&#39; | xargs -I {} cosign verify-attestation --type=spdx \ --certificate-oidc-issuer=https://issuer.enforce.dev \ --certificate-identity-regexp=&#34;https://issuer.enforce.dev/(${IMAGE_SYNCER}|${CUSTOM_IMAGE_BUILDER})&#34; \ &#34;$CUSTOM_IMAGE@{}&#34; 2&gt; /dev/null | jq -r .payload | base64 -d | jq &#39;.predicate&#39; | jq &#39;.packages[] | select(.externalRefs[]?.referenceCategory == &#34;PACKAGE_MANAGER&#34;) | .externalRefs[] | select(.referenceCategory == &#34;PACKAGE_MANAGER&#34;) | .referenceLocator&#39; # Apply the apko configuration file to trigger the build. The --yes flag skips the confirmation prompt. - name: Trigger custom build id: start-custom-build continue-on-error: false run: | chainctl images repos build apply -f ca-images-iac/custom-jre.yaml \ --parent &lt;chainguard-org&gt; --repo &lt;image-name&gt; --yes Extending the workflow with GitHub API access Some extensions to this workflow do need GitHub credentials, such as committing an updated overlay file, opening a pull request that reports which CVEs a rebuild fixed, or commenting build results on an existing pull request.
Rather than storing a long-lived Personal Access Token, add an Octo STS step. Octo STS exchanges the workflow&rsquo;s OIDC token for a GitHub token that is scoped to the permissions you declare and expires with the run:
- uses: octo-sts/action@6177b4481c00308b3839969c3eca88c96a91775f # v1.0.0 id: octo-sts with: scope: &lt;github-organization&gt;/&lt;github-repository&gt; identity: buildPass the result to whichever step needs it as ${{ steps.octo-sts.outputs.token }}. Using Octo STS also requires installing its GitHub App on your organization and committing a trust policy to .github/chainguard/build.sts.yaml, where build matches the identity input. Refer to the Octo STS overview for more information.
Testing your workflow Before deploying your CI/CD workflow to production, test it thoroughly to ensure builds complete successfully and authentication works correctly. Start by triggering a manual build and reviewing the logs for each step. Verify that images are built with the expected packages and configurations, and confirm that signatures and attestations are properly generated. Testing in a non-production environment or with a dedicated test repository helps catch configuration issues early without impacting your production image builds.
Testing the GitHub Action example Before using the GitHub action in this guide, make sure to update the placeholders:
&lt;chainguard-org-id&gt;/&lt;chainguard-identity-id&gt;: The full ID of the identity you created in Step 1 CUSTOM_IMAGE: &quot;cgr.dev/&lt;chainguard-org&gt;/&lt;image-name&gt;&quot;: Your image registry path IMAGE_SYNCER: &quot;&lt;chainguard-org-id&gt;/&lt;image-syncer-id&gt;&quot;: Your image-syncer identity CUSTOM_IMAGE_BUILDER: &quot;&lt;chainguard-org-id&gt;/&lt;custom-image-builder-id&gt;&quot;: Your custom-image-builder identity --parent &lt;chainguard-org&gt; --repo &lt;image-name&gt;: Your Chainguard organization and image repository names ca-images-iac/custom-jre.yaml: The directory in your GitHub repository that holds the apko overlay files, and the overlay file name Note: Older Chainguard organizations name the image-syncer and custom-image-builder identities catalog_syncer and apko_builder instead. The names are interchangeable: each pair points at the same account association, so the signatures verify the same way. The workflow&rsquo;s environment variable names are arbitrary — only the identity IDs they hold matter.
To test your GitHub Action:
In GitHub, go to the Actions tab, select your workflow, then click Run workflow. Check the detailed logs for each step. Confirm that the images appear in your Chainguard registry. Additional resources Custom Assembly overview apko overview Assumable identity documentation Demo Repository: custom-assembly-as-code Chainguard Support 
