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:
- How Guarded Entrypoint works
- Guarded Entrypoint examples
- Troubleshoot a wrapped container
- Guarded Entrypoint trust boundary
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:
- Resolves secret references in the container’s environment.
- Runs the preflight checks you configured.
- 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. Runchainctl updateto update it. An olderchainctldrops 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 ofchainctlthat 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_entrypointwith 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-pythonTurn on Guarded Entrypoint with chainctl
A repo’s Custom Assembly configuration is a YAML manifest. Guarded Entrypoint adds four keys to it:
| Key | Meaning |
|---|---|
guarded_entrypoint | Set to true to wrap the image’s entrypoint. Every other key in this table requires it. |
fail_mode | closed (the default) or open. Sets what happens when a secret reference can’t be resolved. |
preflight | A list of checks to run before the application starts. |
command_override | A 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 $ORGANIZATIONAdd 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_passwordKeep 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 --yesThe --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 $ORGANIZATIONWhen 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.
environmentandpreflightare 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 \
--allFor 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:
| Field | When several bindings match one tag |
|---|---|
guarded_entrypoint | The wrapper is on if any matching binding sets it to true. A more specific binding can’t turn it off. |
fail_mode | The most specific binding that sets it wins. A binding that leaves it unset uses the value from a broader binding. |
command_override | The most specific binding that sets it wins. A binding that leaves it unset uses the value from a broader binding. |
preflight | The 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: openAn exact binding on the 3.13 tag uses an overlay that sets fail-closed:
guarded_entrypoint: true
fail_mode: closedThe 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: defaultBindings 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.
| Trigger | Code | Message |
|---|---|---|
guarded_entrypoint: true on an organization that doesn’t have Guarded Entrypoint enabled. Contact Chainguard customer support to get access. | PermissionDenied | using <prefix>.guarded_entrypoint is not allowed |
preflight set without guarded_entrypoint | InvalidArgument | <prefix>.preflight requires guarded_entrypoint |
command_override set without guarded_entrypoint | InvalidArgument | <prefix>.command_override requires guarded_entrypoint |
fail_mode set without guarded_entrypoint | InvalidArgument | <prefix>.fail_mode requires guarded_entrypoint |
| More than 32 preflight entries | InvalidArgument | <prefix>.preflight: at most 32 entries |
Preflight entry with neither or both of tcp and path | InvalidArgument | <prefix>.preflight[i]: rpc error: code = InvalidArgument desc = exactly one of tcp or path is required |
Preflight timeout or interval is not a Go duration | InvalidArgument | <prefix>.preflight[i]: rpc error: code = InvalidArgument desc = timeout "abc": time: invalid duration "abc" |
Preflight timeout or interval is negative | InvalidArgument | <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 mode | InvalidArgument | <prefix>.command_override: rpc error: code = InvalidArgument desc = mode "99" must be "default", "prepend", or "override" |
prepend or override with an empty command | InvalidArgument | <prefix>.command_override: rpc error: code = InvalidArgument desc = mode "prepend" requires a non-empty command |
command entry contains a NUL byte | InvalidArgument | <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 name | InvalidArgument | <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 mode | InvalidArgument | <prefix>.fail_mode must be one of "closed" or "open", got "99" |
environment key starts with GUARDED_ | InvalidArgument | environment variable "GUARDED_DISABLE" uses reserved prefix 'GUARDED_' |
environment key starts with CHAINGUARD_ | InvalidArgument | environment variable "CHAINGUARD_X" uses reserved prefix 'CHAINGUARD_' |
Version 1 repo API: sync_config.apko_overlay.environment key starts with GUARDED_ | InvalidArgument | sync_config.apko_overlay.environment: variable "..." uses reserved prefix 'GUARDED_' |
| Overlay or binding path: organization is not enrolled in tag-based Custom Assembly | FailedPrecondition | Precondition 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 one | FailedPrecondition | repository custom overlay and overlay binding not allowed |
Overlay path: config sets a field that overlays don’t support | InvalidArgument | config 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 nothing | InvalidArgument | config 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 values | FailedPrecondition | Precondition 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 to | FailedPrecondition | Precondition 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.