For the complete documentation index, see llms.txt.

Guarded Entrypoint for Custom Assembly

Note: Guarded Entrypoint is in beta. To use it, contact Chainguard customer support to enable it for your organization.

Guarded Entrypoint lets a Custom Assembly image run startup logic without a derived image build. You declare the logic as part of your Custom Assembly configuration. Chainguard builds it into the image and signs the result.

This page explains what Guarded Entrypoint is and how to turn it on. The other pages in this section cover the details:

What Guarded Entrypoint is

Many applications need to do some work before they start. They read secrets into environment variables, wait for a database to accept connections, or change the command they run.

A Chainguard image gives you one way to change its entrypoint: build a new image on top of it. That derived image is no longer the image Chainguard signs and rebuilds. Scanners report the difference, and the image does not pick up Chainguard’s rebuilds unless you rebuild it too.

Guarded Entrypoint removes the need for the derived build. When you turn it on for a Custom Assembly repo, Chainguard sets the image’s entrypoint to a small binary that Chainguard builds, /usr/bin/guarded-entrypoint. When the container starts, the binary does the following:

  1. Resolves secret references in the container’s environment.
  2. Runs the preflight checks you configured.
  3. Starts your application as its child process.

The binary forwards signals to your application and exits with your application’s exit code. The image you deploy is the image Chainguard built, with your startup settings stored in its configuration.

Prerequisites

Before you start, you need the following:

  • A Custom Assembly repo. See the Custom Assembly overview to create one.
  • A role that lets you edit Custom Assembly repos. See the Custom Assembly permissions requirements.
  • The latest chainctl. Run chainctl update to update it. An older chainctl drops the Guarded Entrypoint keys when it reads a repo, so a teammate who edits the repo with an older version can turn the feature off without noticing. Update every copy of chainctl that edits the repo.
  • Guarded Entrypoint enabled for your organization. It’s a beta feature, so contact Chainguard customer support to enable it. Until then, the API rejects guarded_entrypoint with the error in API errors.

The examples on this page use the following environment variables. Set them to match your organization and repo:

export ORGANIZATION=example.com
export REPO=my-custom-python

Turn on Guarded Entrypoint with chainctl

A repo’s Custom Assembly configuration is a YAML manifest. Guarded Entrypoint adds four keys to it:

KeyMeaning
guarded_entrypointSet to true to wrap the image’s entrypoint. Every other key in this table requires it.
fail_modeclosed (the default) or open. Sets what happens when a secret reference can’t be resolved.
preflightA list of checks to run before the application starts.
command_overrideA command to run in place of, or in front of, the image’s own command.

Secret references go in the existing environment key. For details on each key, see How Guarded Entrypoint works.

To turn it on interactively, open the repo’s manifest in your editor:

chainctl images repos build edit --repo $REPO --parent $ORGANIZATION

Add the keys you need. The following manifest turns on Guarded Entrypoint and resolves one secret from Vault:

guarded_entrypoint: true
environment:
  VAULT_ADDR: https://vault.example.com:8200
  VAULT_K8S_ROLE: orders-service
  DB_PASSWORD: cg+vault://secret/data/orders#db_password

Keep the keys that are already in the manifest. Applying a manifest replaces the repo’s stored configuration, so a key you remove from the manifest is removed from the repo.

Save and close the editor. chainctl shows a diff and asks you to confirm. After you confirm, Chainguard rebuilds the repo’s images with the wrapper as their entrypoint.

To apply a manifest without an editor, put it in a file and use apply:

chainctl images repos build apply -f build.yaml --repo $REPO --parent $ORGANIZATION --yes

The --yes flag skips the confirmation prompt. To preview the change first, use --dry-run in place of --yes. The command prints the diff and exits with a non-zero status when it finds a change to apply. In a pipeline, pass --yes to apply or --dry-run to preview, and pass --parent so chainctl doesn’t prompt you to choose a group. A structured --output format on its own doesn’t suppress the confirmation prompt.

chainctl checks the manifest before it sends anything to the API. For example, it rejects preflight without guarded_entrypoint.

For more on edit and apply, see Using chainctl to manage Custom Assembly resources.

Check the result

To see the builds, run the following command:

chainctl images repos build list --repo $REPO --parent $ORGANIZATION

When Chainguard refuses to wrap an image, or when two bindings conflict, the build is recorded as a failure and the Reason column shows why. The column is empty for an ordinary build failure. To read a reason in full, run chainctl images repos build logs --repo $REPO --parent $ORGANIZATION and select the failed build. For the reasons that relate to Guarded Entrypoint, see Entrypoints the wrapper refuses.

To confirm that a rebuilt image uses the wrapper, check its entrypoint. The first element is /usr/bin/guarded-entrypoint. What follows depends on the image. For an image with a command, it is that command. For a shell fragment, it is /bin/sh -c and the fragment. For a service bundle, it is /bin/s6-svscan /sv. An image that has only a CMD has the wrapper alone:

crane config cgr.dev/$ORGANIZATION/$REPO:latest | jq '.config.Entrypoint'

Turn off Guarded Entrypoint

To remove the wrapper from the image, edit the manifest and delete guarded_entrypoint, fail_mode, preflight, and command_override. The API rejects the other three keys when guarded_entrypoint is not set. Also remove any cg+... values from environment. Without the wrapper, they ship as literal strings. The next rebuild produces an image with its original entrypoint.

To bypass the wrapper on a running container without a rebuild, see Troubleshoot a wrapped container.

Turn on Guarded Entrypoint with the API

The Chainguard API accepts the same four fields. They are guardedEntrypoint, failMode, preflight, and commandOverride on the repo’s customOverlay. For general guidance on authenticating and calling the API, see Using the Chainguard API.

The API’s enum fields take enum names. failMode is FAIL_MODE_CLOSED or FAIL_MODE_OPEN. A commandOverride mode is MODE_DEFAULT, MODE_PREPEND, or MODE_OVERRIDE. A preflight onFailure is ON_FAILURE_FAIL or ON_FAILURE_CONTINUE.

The following request turns on Guarded Entrypoint for a repo, with a fail-open setting and one preflight check:

export TOKEN=$(chainctl auth token)
export API=https://console-api.enforce.dev
export REPO_UID=YOUR_REPO_UID

curl -s -X PATCH -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$API/registry/v2/repos/$REPO_UID" \
  -d '{
    "customOverlay": {
      "guardedEntrypoint": true,
      "failMode": "FAIL_MODE_OPEN",
      "preflight": [
        {
          "tcp": "db.internal:5432",
          "timeout": "60s",
          "interval": "1s",
          "onFailure": "ON_FAILURE_FAIL"
        }
      ],
      "environment": {
        "CONSUL_HTTP_ADDR": "https://consul.example.com:8501",
        "FEATURE_FLAGS_URL": "cg+consul://apps/web/feature-flags-url"
      }
    }
  }'

The request merges into the repo’s stored overlay. The API builds an update mask from the fields in the request body, so only the fields you send change:

  • A field you leave out keeps its stored value. You can’t turn Guarded Entrypoint off by leaving its fields out.
  • environment and preflight are replaced as a whole. The example request sets the repo’s environment variables to the two it lists and removes the others, so include every variable you want to keep.

To replace the whole overlay with exactly what you send, add ?update_mask=custom_overlay to the URL. Use this form to turn Guarded Entrypoint off, with a body that leaves out the four fields.

The API validates the request with the rules in API errors.

Use Guarded Entrypoint with tag-based Custom Assembly

An overlay can carry the same four fields. This lets you apply Guarded Entrypoint to some of a repo’s tags, or to many repos at once. See the overview of tag-based Custom Assembly for overlays, bindings, and tag selectors.

Tag-based Custom Assembly is a separate feature with its own enrollment. To use Guarded Entrypoint on overlays and bindings, your organization needs both features enabled. Contact your Chainguard account team to enable tag-based Custom Assembly. Contact Chainguard customer support to enable Guarded Entrypoint. Setting the fields on a repo with chainctl images repos build edit, as described earlier on this page, needs only Guarded Entrypoint.

A repo uses its own configuration or overlay bindings, not both. The tag-based examples that follow use a different repo from the one you configured with build edit. Attaching an overlay to a repo that has its own configuration fails with the error repository custom overlay and overlay binding not allowed. Setting a configuration on a repo that has bindings fails the same way.

Write the overlay as a YAML file in the same shape as a repo manifest, create the overlay from it, and bind it to tags:

cat > startup.yaml <<EOF
guarded_entrypoint: true
fail_mode: closed
preflight:
  - tcp: db.internal:5432
    timeout: 60s
    interval: 1s
EOF

chainctl images overlays create startup --parent $ORGANIZATION -f startup.yaml

export TAG_REPO=my-tagged-python

chainctl images overlays attach \
  --overlay startup \
  --repo $TAG_REPO \
  --parent $ORGANIZATION \
  --all

For the other selectors, see Managing tag-based Custom Assembly with chainctl.

Through the API, create the overlay with POST /registry/v2beta1/overlays/$ORG_ID and bind it with POST /registry/v2beta1/overlayBindings/$TAG_REPO_UID, where $TAG_REPO_UID is the UID of a repo that has no configuration of its own. The overlay’s config takes the same fields as the repo’s customOverlay:

export ORG_ID=YOUR_ORG_ID

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$API/registry/v2beta1/overlays/$ORG_ID" \
  -d '{
    "name": "startup",
    "config": {
      "guardedEntrypoint": true,
      "failMode": "FAIL_MODE_CLOSED",
      "preflight": [
        { "tcp": "db.internal:5432", "timeout": "60s", "interval": "1s" }
      ]
    }
  }'

curl -s -X POST -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  "$API/registry/v2beta1/overlayBindings/$TAG_REPO_UID" \
  -d '{
    "overlay": "startup",
    "tagSelector": { "kind": "KIND_ALL" }
  }'

How the fields combine across bindings

A tag can match several bindings. Chainguard layers them from the broadest to the most specific. Bindings that apply to every repo in your organization (all-repos bindings) come first, in the order all, variant, exact. The repo’s own bindings come next, in the same order. A repo’s own binding always wins over an all-repos binding. Each of the four fields combines in its own way:

FieldWhen several bindings match one tag
guarded_entrypointThe wrapper is on if any matching binding sets it to true. A more specific binding can’t turn it off.
fail_modeThe most specific binding that sets it wins. A binding that leaves it unset uses the value from a broader binding.
command_overrideThe most specific binding that sets it wins. A binding that leaves it unset uses the value from a broader binding.
preflightThe checks accumulate. Checks from broader bindings run first, and identical entries are dropped.

Each overlay that sets fail_mode, command_override, or preflight must also set guarded_entrypoint: true itself, even when a broader binding already sets it.

The limit of 32 preflight entries applies to each overlay. The combined list for a tag can be longer.

Any organization that has Guarded Entrypoint enabled can set fail_mode: open. It needs no separate approval.

Pin a tag to fail closed

A broader binding can set fail_mode: open, and a tag inherits that value. To keep one tag fail-closed, bind an overlay to that tag on the repo itself with a more specific selector, and set fail_mode: closed in it. An all-repos binding can’t override a repo’s own binding.

For example, an all binding uses an overlay that sets fail-open:

guarded_entrypoint: true
fail_mode: open

An exact binding on the 3.13 tag uses an overlay that sets fail-closed:

guarded_entrypoint: true
fail_mode: closed

The 3.13 tag is fail-closed. Every other tag is fail-open. If the open value comes from the tag’s own exact binding, change that binding’s overlay instead.

Cancel a broader command override

An empty command with command_override set means “default mode, no command”. It counts as set, so it cancels the override from a broader binding. To drop the override on one tag, bind an overlay like the following to that tag with a more specific selector:

guarded_entrypoint: true
command_override:
  mode: default

Bindings of the same kind

Two bindings of the same kind can match the same tag. Chainguard rejects the second binding when it is created if the two overlays set fail_mode or command_override to different values. Identical values merge.

The same check runs when you update an overlay, against every repo the overlay is bound to, and when you update a binding’s selector. The error for an overlay update differs from the error for a new binding. See API errors. To fix a conflict, make the two overlays agree, or bind them to selectors that don’t match the same tags.

API errors

The API validates the Guarded Entrypoint fields the same way for repos and for overlays. In the messages, <prefix> is custom_overlay when you set the fields on a repo, and config when you set them on an overlay. On the overlay path, the API adds the text Invalid argument: config: and a space to the start of each InvalidArgument message. For FailedPrecondition errors on either path, the API adds Precondition failed: and a space, except where the table shows the message without it. In a message, [i] is the index of the entry in the list, starting at 0.

TriggerCodeMessage
guarded_entrypoint: true on an organization that doesn’t have Guarded Entrypoint enabled. Contact Chainguard customer support to get access.PermissionDeniedusing <prefix>.guarded_entrypoint is not allowed
preflight set without guarded_entrypointInvalidArgument<prefix>.preflight requires guarded_entrypoint
command_override set without guarded_entrypointInvalidArgument<prefix>.command_override requires guarded_entrypoint
fail_mode set without guarded_entrypointInvalidArgument<prefix>.fail_mode requires guarded_entrypoint
More than 32 preflight entriesInvalidArgument<prefix>.preflight: at most 32 entries
Preflight entry with neither or both of tcp and pathInvalidArgument<prefix>.preflight[i]: rpc error: code = InvalidArgument desc = exactly one of tcp or path is required
Preflight timeout or interval is not a Go durationInvalidArgument<prefix>.preflight[i]: rpc error: code = InvalidArgument desc = timeout "abc": time: invalid duration "abc"
Preflight timeout or interval is negativeInvalidArgument<prefix>.preflight[i]: rpc error: code = InvalidArgument desc = timeout "-5s" must be non-negative
Preflight value contains , or =InvalidArgument<prefix>.preflight[i]: rpc error: code = InvalidArgument desc = tcp "a:1,b:2" must not contain ',' or '=' (the message names the field that holds the character)
command_override.mode is a number that isn’t a declared modeInvalidArgument<prefix>.command_override: rpc error: code = InvalidArgument desc = mode "99" must be "default", "prepend", or "override"
prepend or override with an empty commandInvalidArgument<prefix>.command_override: rpc error: code = InvalidArgument desc = mode "prepend" requires a non-empty command
command entry contains a NUL byteInvalidArgument<prefix>.command_override: rpc error: code = InvalidArgument desc = command[i] contains a NUL byte
command entry has a ${ with no closing }InvalidArgument<prefix>.command_override: rpc error: code = InvalidArgument desc = command[i] has an unterminated ${ reference
command entry has a ${...} reference with an invalid variable nameInvalidArgument<prefix>.command_override: rpc error: code = InvalidArgument desc = command[i] has an invalid variable name in a ${...} reference
fail_mode is a number that isn’t a declared modeInvalidArgument<prefix>.fail_mode must be one of "closed" or "open", got "99"
environment key starts with GUARDED_InvalidArgumentenvironment variable "GUARDED_DISABLE" uses reserved prefix 'GUARDED_'
environment key starts with CHAINGUARD_InvalidArgumentenvironment variable "CHAINGUARD_X" uses reserved prefix 'CHAINGUARD_'
Version 1 repo API: sync_config.apko_overlay.environment key starts with GUARDED_InvalidArgumentsync_config.apko_overlay.environment: variable "..." uses reserved prefix 'GUARDED_'
Overlay or binding path: organization is not enrolled in tag-based Custom AssemblyFailedPreconditionPrecondition failed: this organization is not enrolled in Custom Assembly Overlays. Contact your Chainguard account team to enroll.
Overlay or binding path: the repo has its own configuration, or a repo with bindings gets oneFailedPreconditionrepository custom overlay and overlay binding not allowed
Overlay path: config sets a field that overlays don’t supportInvalidArgumentconfig may set only contents.packages, contents.runtime_repositories, contents.runtime_keyring, environment, annotations, accounts, certificates.additional, guarded_entrypoint, command_override, preflight, and fail_mode
Overlay path: config sets nothingInvalidArgumentconfig must set at least one customization field
Binding path: two bindings of one kind that match the same tag set different fail_mode or command_override valuesFailedPreconditionPrecondition failed: overlay config does not merge commutatively with co-matching binding(s): binding "..." (overlay "...", selector ALL) on fields [fail_mode]
Overlay update: the new config conflicts with a co-matching binding on a repo the overlay is bound toFailedPreconditionPrecondition failed: overlay config update does not merge commutatively with co-bound overlay(s): binding "..." and binding "..." (overlay "...") on repo "..." conflict on fields [fail_mode], or Precondition failed: overlay config update conflicts with the overlay of a co-matching binding outside your visible scope or beyond the inspected repos

The binding and overlay conflict errors also carry the violation type OVERLAY_BINDING_CONFLICT. The message names the bindings and the fields that conflict. A command_override conflict lists command_override in the fields.

The enum fields take enum names in JSON. A request with an enum name that doesn’t exist fails when the API parses it, before the checks in the table run. A preflight onFailure value that isn’t declared is treated as fail.

The custom_overlay and config prefixes show up in the message text only. In JSON requests, the fields are customOverlay and config.

Learn more