# How Guarded Entrypoint works

URL: https://chainguard-docs-preview-git-tpchecks-org-config-scope.chainguard.app/chainguard/containers/custom-assembly/guarded-entrypoint/how-it-works.md
Last Modified: October 21, 2026
Tags: Chainguard Containers, Custom Assembly, Conceptual, Reference

What the Guarded Entrypoint wrapper does at container start: secret references, fail mode, the Java truststore, preflight checks, command override, and variable expansion.


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

This page describes what the Guarded Entrypoint binary does when a container starts. The page calls the binary the wrapper. To turn Guarded Entrypoint on, refer to [Guarded Entrypoint for Custom Assembly](/chainguard/containers/custom-assembly/guarded-entrypoint/overview/).

## What the wrapper does

When you turn on Guarded Entrypoint, Chainguard rebuilds the image with `/usr/bin/guarded-entrypoint` as the first element of its entrypoint. The image's original entrypoint follows it. Chainguard stores your settings in environment variables in the image configuration. The names of these variables start with `GUARDED_`. `docker inspect` shows them. The `GUARDED_` prefix is reserved, and the API rejects it in your own `environment` keys.

On every start, the wrapper runs these steps in order:

1. **Check for the escape hatch.** If `GUARDED_DISABLE` is set, the wrapper starts the original entrypoint and does nothing else. An empty value, `0`, `false`, `no`, and `off` don't count as set. Refer to [Troubleshoot a wrapped container](/chainguard/containers/custom-assembly/guarded-entrypoint/troubleshooting/).
1. **Resolve secret references.** The wrapper replaces every environment value of the form `cg+BACKEND://REF` with the secret it names.
1. **Write the Java truststore.** If you configured a truststore, or set `USE_SYSTEM_CA_CERTS`, the wrapper writes a Java truststore and points the JVM at it. This step stops the container with exit code `128` when it fails, in every fail mode. Refer to [Java truststore](#java-truststore).
1. **Resolve the command.** The wrapper applies `command_override`, if you set one, and expands `${VAR}` in it.
1. **Run preflight checks.** The wrapper waits for the TCP endpoints and paths you listed.
1. **Start your application.** The application runs as the wrapper's child process.

While the application runs, the wrapper forwards every signal it can catch to the application and reaps orphaned processes. When the application exits, the wrapper exits with the same exit code. If a signal kills the application, the wrapper exits with 128 plus the signal number.

The wrapper writes its own messages as JSON, one object per line, to standard error. Set `GUARDED_ENTRYPOINT_LOG` on the container to `debug`, `info`, `warn`, or `quiet` to change the level. The `quiet` level also hides the fail-open warning. The wrapper never logs a resolved secret value. It replaces any value it resolved or expanded into the command with `***` in its log lines. It doesn't scrub a value that only a preflight target expanded.

### Runtime overrides of the entrypoint

The wrapper runs only when the container starts with the image's entrypoint. A Kubernetes `command:` field or `docker run --entrypoint` replaces the whole ENTRYPOINT, wrapper included. With such an override, no secret references resolve and no checks run. To pass your own arguments, use the Kubernetes `args:` field. If you must set `command:`, make `/usr/bin/guarded-entrypoint` its first element.

### What the wrapper doesn't do

* It doesn't resolve references in the container's arguments. It resolves only environment values. Kubernetes expands `$(VAR)` in `args` before the wrapper runs, so `--password=$(DB_PASSWORD)` reaches the application as the literal `cg+...` reference. Have the application read secrets from its environment, or pass them through `command_override`, which expands after resolution.
* It doesn't renew or revoke Vault leases. It reads each secret once, at startup.
* It doesn't run code that you upload. It runs your application and, if you set `command_override`, the command you list.
* It doesn't connect to Chainguard. Refer to [Guarded Entrypoint trust boundary](/chainguard/containers/custom-assembly/guarded-entrypoint/trust-boundary/).

## Secret references

A secret reference is an environment variable whose value has the form `cg+BACKEND://REF`. Put references in the `environment` key of the repo's manifest. The wrapper also resolves references in variables that you set in your pod spec or `docker run` command.

The `cg+` prefix is reserved. A value without it, such as a bare `vault://` or `consul://` URI, passes through to your application as a plain string. A value with the prefix must resolve. A malformed reference or an unknown backend always stops the container before the application starts. A failed lookup or a secret that contains a NUL byte stops it too, unless you set [fail mode](#fail-mode) to `open`.

| Backend | Reference | Reads |
| --- | --- | --- |
| Vault | `cg+vault://PATH[?version=N]#KEY` | One field, `KEY`, of the secret at the Vault API path `PATH` |
| Consul | `cg+consul://KEY` | The whole value of one key in the Consul key-value store |
| Google Secret Manager | `cg+gsm://projects/P/secrets/S/versions/V` | One version of a secret. `V` is a version number, `latest`, or an alias. |

The wrapper makes up to three attempts for each request, with backoff. It retries connection errors and the HTTP statuses 429, 500, 502, 503, and 504. For Vault, it also retries 412. Resolution has a budget of 30 seconds in total.

### Vault references

`PATH` is the path of the Vault API request, as in `envconsul` and Vault Agent templates. For a KV version 2 mount, the path includes `/data/`. The wrapper does not look up mounts or insert `/data/`. `#KEY` is required.

| Reference | Reads |
| --- | --- |
| `cg+vault://secret/data/app#password` | KV version 2 mount `secret`, secret `app`, latest version |
| `cg+vault://secret/data/app?version=3#password` | The same secret, version 3 |
| `cg+vault://kv/app#password` | KV version 1 mount `kv`, secret `app` |

A string value is set as is. A number, boolean, null, object, or array is set as its compact JSON text. A key that the secret lacks is an error.

The wrapper reads its Vault settings from the container's environment. They must be literal values, not references.

| Variable | Meaning |
| --- | --- |
| `VAULT_ADDR` | Required. For example, `https://vault.example.com:8200`. An `http://` address sends the token in the clear. |
| `VAULT_TOKEN` | A token to read with. It takes precedence over Kubernetes authentication. |
| `VAULT_K8S_ROLE` | Without `VAULT_TOKEN`, log in with the Kubernetes auth method as this role. |
| `VAULT_K8S_MOUNT` | The Kubernetes auth mount path. The default is `kubernetes`. |
| `VAULT_K8S_TOKEN_PATH` | The service account token to log in with. The default is `/var/run/secrets/kubernetes.io/serviceaccount/token`. |
| `VAULT_NAMESPACE` | The Vault Enterprise or HCP namespace. |
| `VAULT_CACERT` | A PEM bundle, at most 1 MiB, that verifies Vault's certificate in place of the system roots. |

With neither `VAULT_TOKEN` nor `VAULT_K8S_ROLE`, resolution fails with `no Vault credentials`. Use Kubernetes authentication where you can. A `VAULT_TOKEN` in the image or the pod spec is a long-lived secret that anyone with access to the spec can read. The wrapper doesn't follow redirects, so `VAULT_ADDR` must name the active Vault node or a load balancer in front of it. Auth methods other than tokens and Kubernetes aren't supported. With an `https://` `VAULT_ADDR`, the wrapper honors `HTTPS_PROXY` and `NO_PROXY` from the container's environment. These must be literal values too. It never uses a proxy for an `http://` address.

### Consul references

`KEY` is one or more segments separated by `/`. A segment can contain letters, digits, and `. _ ~ @ : + = , -`. A key that ends in `/`, an empty segment, a `.` or `..` segment, a query, and a fragment are all invalid. A key stored with no value resolves to an empty string.

The wrapper reads its Consul settings from the container's environment, as the `consul` command does. They must be literal values.

| Variable | Meaning |
| --- | --- |
| `CONSUL_HTTP_ADDR` | `HOST:PORT`, `http://HOST:PORT`, or `https://HOST:PORT`. The default is `127.0.0.1:8500`. |
| `CONSUL_HTTP_SSL` | Set to `true` to use HTTPS with a bare `HOST:PORT`. |
| `CONSUL_HTTP_TOKEN` | The token to read with. Without a token, the read is anonymous. |
| `CONSUL_HTTP_TOKEN_FILE` | A file that holds the token. It wins over `CONSUL_HTTP_TOKEN`. |
| `CONSUL_CACERT` | A PEM file of CAs to trust in place of the system roots. Mount the file in the container. |

The wrapper sends the token only in the `X-Consul-Token` header, doesn't follow redirects, and doesn't use a proxy. It doesn't support `unix://` addresses, `CONSUL_CAPATH`, client certificates, `CONSUL_TLS_SERVER_NAME`, `CONSUL_HTTP_SSL_VERIFY`, `CONSUL_HTTP_AUTH`, or Consul Enterprise namespaces and partitions.

### Google Secret Manager references

The wrapper authenticates with Application Default Credentials. `/versions/latest` is the default version. The wrapper supports only global secrets. A regional secret, `projects/P/locations/L/secrets/S`, is an invalid reference. A service account needs only secret access.

## Fail mode

`fail_mode` sets what happens when a secret reference can't be resolved. It takes one of two values.

| Value | When a reference can't be resolved |
| --- | --- |
| `closed` | The container stops with exit code 121 before your application starts. This is the default. |
| `open` | Your application starts anyway. The variable keeps its literal `cg+...` value, and the wrapper logs one warning for it. |

With `open`, a lookup that fails, missing credentials, a secret that contains a NUL byte, and the 30-second budget running out all leave the reference in place. The warning looks like the following:

```json
{"time":"...","level":"WARN","msg":"secret unresolved, continuing","src":"guarded-entrypoint","name":"DB_PASSWORD","fail_mode":"open","error":"DB_PASSWORD: secret resolution timed out after 30s (context deadline exceeded)"}
```

An unresolved variable is not a secret. Your application sees the literal `cg+...` reference, and anyone who can read the image configuration or the pod spec knows that value. Don't choose `open` for a variable that your application uses as a password, token, or other credential. During an outage the application would start with a known value as its credential.

`open` covers a store that can't serve a reference. It doesn't cover a reference that can never resolve. These configuration errors still stop the container with exit code 121:

* A malformed reference
* An unknown backend, such as a `cg+gms://` typo
* A reference that the backend rejects by its shape
* A `${VAR}` in the command that names a variable that is unset or that `open` left unresolved

A repo that sets `fail_mode: open` has `GUARDED_FAIL_MODE=open` in its image environment. A `closed` repo's image carries no setting.

## Java truststore

A Java application trusts the certificate authorities (CAs) in its image. A CA that exists only at run time isn't in the image. Two examples are an OpenShift service CA that rotates, and a private CA in your cluster. You mount such a CA into the container as a file.

The truststore setting covers this case. Before your application starts, the wrapper writes a Java truststore that holds the image's CAs and the mounted CAs, and points the JVM at it. The wrapper writes the store on every start, so a rotated CA takes effect the next time the container starts.

A CA that you know at build time belongs in `certificates.additional`. Refer to [Custom Assembly and Java truststores](/chainguard/containers/custom-assembly/custom-assembly-certs/#custom-assembly-and-java-truststores).

### Configure a truststore

In a Custom Assembly manifest, add a `truststore` entry. It requires `guarded_entrypoint: true`. The JVM reads one truststore, so the manifest holds one entry. The entry has the following keys:

| Key | Meaning | Default |
| --- | --- | --- |
| `sources` | Required. A list of absolute paths. Each path is a PEM file or a directory of PEM files. | None |
| `output` | Required. The absolute path of the store file. | None |
| `system` | `true` starts from the image's own CAs. `false` trusts the sources only. | `true` |
| `password_env` | The name of the environment variable that holds the store's password. | The JDK's default password |
| `format` | The store format. `jks` is the only value. | `jks` |

Notes:

* A directory in `sources` works with a Kubernetes ConfigMap or Secret volume, and the wrapper reads each certificate once. The wrapper adds every certificate in each file, so a bundle of several certificates needs no splitting.
* The directory of `output` must exist, and the container's user must be able to write to it. With a read-only root filesystem, mount an `emptyDir` volume there. The path can't contain spaces or quotes.
* The JVM doesn't need a password to read the store. Set `password_env` only if other tools open the store. The variable can hold a `cg+` secret reference.

For an example, refer to [Trust a mounted CA in a Java service](/chainguard/containers/custom-assembly/guarded-entrypoint/examples/#trust-a-mounted-ca-in-a-java-service).

### Set a truststore at deploy time

You can also set the truststore on the deployment. The image must have been rebuilt with wrapper 0.2.385 or later. Check the `version` field on the wrapper's `ready` log line. Set `GUARDED_TRUSTSTORE_0` to comma-separated `key=value` pairs. The keys are the same as in the manifest, with `source` in place of `sources`. Repeat `source` for more than one path:

```text
GUARDED_TRUSTSTORE_0=source=/etc/service-ca,source=/etc/corp-ca/ca.pem,output=/truststore/cacerts
```

Values can't contain `,` or `=`. A value that you set on the deployment replaces the one that a manifest stored in the image. The API rejects `GUARDED_` keys in `environment`, so in a Custom Assembly manifest, use the `truststore` key.

### How the wrapper builds the store

When the container starts, the wrapper does the following:

1. Starts from the image's CAs, unless you set `system: false`.
1. Adds every certificate from your sources. It rejects a private key, and anything else that isn't a certificate.
1. Writes the store to `output`.
1. Appends `-Djavax.net.ssl.trustStore=` and the path of the store to `JAVA_TOOL_OPTIONS`, and keeps the options that are already there. It also sets `JRE_CACERTS_PATH` to the path of the store, as Temurin images do.
1. Logs a `truststore written` line.

### Mount the volumes

Mount the following volumes:

* **The CA files.** Mount the ConfigMap, Secret, or other volume that holds the CA at the path in `sources`.
* **A writable directory for the store.** Mount an `emptyDir` volume at the directory of `output`. The container can run as an arbitrary user ID, such as one that OpenShift assigns.
* **A writable `/tmp` on FIPS images.** A FIPS Java image with a read-only root filesystem needs a writable `/tmp` that allows executables, with or without a truststore. A Kubernetes `emptyDir` volume works. With Docker, `--tmpfs` blocks executables by default, so add the `exec` option.

On the JDK 21 and earlier FIPS images, Keycloak's start script drops `JAVA_TOOL_OPTIONS`, so the truststore setting doesn't reach Keycloak there. Give Keycloak the mounted PEM files through its own `truststore-paths` option instead.

### Temurin compatibility

If you come from Eclipse Temurin images, `USE_SYSTEM_CA_CERTS` works the same way. With no truststore set, a non-empty value makes the wrapper build a store from the image's CAs and the files in `/certificates` whose names end in `crt`. The wrapper writes the store under `/tmp`, so `/tmp` must be writable. A missing `/certificates` isn't an error.

A truststore setting wins over `USE_SYSTEM_CA_CERTS`. The wrapper logs a `USE_SYSTEM_CA_CERTS ignored` warning when you set both. On the `-full` Java images, the image's own entrypoint script handles `USE_SYSTEM_CA_CERTS`, and the wrapper leaves it alone.

### Truststore errors

A malformed setting stops the container with exit code `120`. A failure to read your sources or write the store stops it with exit code `128`. Examples are a missing source, a file with no PEM certificate, and an `output` directory that doesn't exist. For how to read these errors, refer to [Troubleshoot a wrapped container](/chainguard/containers/custom-assembly/guarded-entrypoint/troubleshooting/#exit-codes-before-the-application-starts).

## Preflight checks

A preflight check waits for a dependency before your application starts. Each entry in `preflight` has the following keys:

| Key | Meaning | Default |
| --- | --- | --- |
| `tcp` | A `host:port` to wait for a TCP connection to. | None |
| `path` | A filesystem path to wait for. | None |
| `timeout` | The total time to wait for this check, as a Go duration such as `30s` or `2m`. A value of `0` means the default, not forever. | `30s` |
| `interval` | The pause between attempts, as a Go duration. | `500ms` |
| `on_failure` | `fail` stops the container with exit code 122. `continue` logs a warning and moves on. | `fail` |

Set exactly one of `tcp` and `path` in each entry. The wrapper expands `${VAR}` in both from the resolved environment. A target that expands to nothing, or to an empty host, fails at once. An overlay or repo can hold at most 32 entries. Values can't contain `,` or `=`.

The checks run in order, after secret resolution. Because a preflight target is not scrubbed from logs, don't use a variable that holds a secret in one.

A preflight target expands a variable that `open` left unresolved to its literal `cg+...` value. Don't name such a variable in a target.

## Command override

`command_override` changes what the wrapper starts. It has two keys: `mode` and `command`. The wrapper receives the image's own arguments, which are its original ENTRYPOINT followed by its CMD, or the arguments that you pass when you run the container. The `mode` sets how `command` combines with them.

| Mode | What the wrapper starts |
| --- | --- |
| `default` | `command`, only when the wrapper receives no arguments at all, which means the image has no ENTRYPOINT or CMD of its own and nothing is passed at run time. Otherwise, the arguments it received. |
| `prepend` | `command` followed by the container's arguments. |
| `override` | `command` alone. The wrapper drops the image's ENTRYPOINT and CMD, and any arguments passed at run time. |

`default` is the mode when you leave `mode` out. The `prepend` and `override` modes require a non-empty `command`.

The image's ENTRYPOINT and CMD are the same in every mode. The mode only changes what the wrapper starts. To replace both an ENTRYPOINT and a CMD, use `override`.

The wrapper looks up the first element of `command` on the container's `PATH`. Use an absolute path when the lookup matters.

An empty `command` with `command_override` set is a setting. It means "default mode, no command". When you use Custom Assembly Overlays, it cancels the override from a broader binding.

Chainguard stores `command` in the image configuration, where it is visible to anyone who can pull the image. Don't put a secret in it as a literal. Use a `${VAR}` reference, as the next section describes. Also refer to [Guarded Entrypoint trust boundary](/chainguard/containers/custom-assembly/guarded-entrypoint/trust-boundary/).

## Variable expansion

The wrapper expands variables in each `command` entry and in preflight `tcp` and `path` values. It expands from the environment after secret references resolve.

* `${NAME}` expands to the value of `NAME`. A name starts with a letter or `_` and continues with letters, digits, or `_`.
* `$$` is a literal `$`.
* Any other `$` stays as it is. `$NAME` is not expanded.
* A `${NAME}` whose variable is not set stops the container with exit code 121 in a command, and fails the check in a preflight. It never expands to an empty string. A variable that is set and empty expands to nothing.
* The wrapper never expands the container's own arguments.

The wrapper doesn't support shell forms such as `${NAME:-default}`. The API rejects a `${...}` reference with an invalid name in `command_override`. It doesn't check preflight targets. A preflight target such as `${REDIS_HOST:-redis}:6379` passes `chainctl` and the API, and then every container start exits with code 120, whatever `on_failure` says. To pass a `$` to a shell that runs in the image, write `$$`. For example, the following command override passes `${PORT:-8080}` to `sh` unchanged. It needs an image that includes a shell:

```yaml
guarded_entrypoint: true
command_override:
  mode: override
  command:
    - sh
    - -c
    - exec my-app --port $${PORT:-8080}
```

In a command, the wrapper never expands a variable that `open` left unresolved. An expanded value is visible in the application's command line to anything that can read the process's `/proc/PID/cmdline`. The wrapper can't hide it there.

## Supported and refused entrypoints

The wrapper wraps these entrypoints:

* **A command.**
* **A shell fragment.** The wrapper runs `/bin/sh -c` with the fragment, so the fragment expands the resolved values.
* **A service bundle.** The wrapper runs in front of the supervisor and resolves the environment once for every service.

### Service bundles and command override

For a service bundle, `command_override` changes what runs in place of the supervisor:

* `override` runs your command instead of the supervisor, so the image's services don't start.
* `prepend` runs your command with the supervisor's command, `/bin/s6-svscan /sv`, as its arguments. Your command must run them itself, for example with `exec`.
* `GUARDED_DISABLE` starts the supervisor directly, as in the unwrapped image.

### Shell fragments and exec

A shell fragment that starts your application without `exec` keeps the shell between the wrapper and your application. On shutdown the shell receives SIGTERM and exits, and your application is stopped without a chance to shut down gracefully. Without Guarded Entrypoint, the shell is PID 1 and ignores SIGTERM, so the container runs until the stop timeout and is killed. Neither case shuts down gracefully. To give your application a graceful shutdown, start it with `exec`, for example `exec my-app --port 8080`.

### Init systems aren't supported

Images whose entrypoint is an init system that must run as PID 1, such as systemd (`/sbin/init`) or s6-overlay's `/init`, aren't supported. An image that uses an s6 service bundle, which the wrapper runs in front of, is a different case and is supported. Under the wrapper, systemd exits at start and s6-overlay's shutdown is cut short. Don't turn on Guarded Entrypoint for these repos. If it's already on, set `GUARDED_DISABLE` on the deployment.

### Refused builds

Chainguard refuses to build a wrapped image in two cases. The image's environment sets `GUARDED_DISABLE`, or the repo pins the wrapper package to a release that is too old for the repo's settings. The build then fails with a message that names the reason.

<!-- TODO: when the generated lists of supported and refused entrypoints are published, add a sentence here linking to them. -->

For how a refusal appears in `chainctl`, refer to [Entrypoints the wrapper refuses](/chainguard/containers/custom-assembly/guarded-entrypoint/troubleshooting/#entrypoints-the-wrapper-refuses).

## Learn more

* [Guarded Entrypoint examples](/chainguard/containers/custom-assembly/guarded-entrypoint/examples/)
* [Troubleshoot a wrapped container](/chainguard/containers/custom-assembly/guarded-entrypoint/troubleshooting/)
* [Guarded Entrypoint trust boundary](/chainguard/containers/custom-assembly/guarded-entrypoint/trust-boundary/)

