Guarded Entrypoint trust boundary
What the Guarded Entrypoint wrapper connects to, what it never does, and what is visible in the image configuration.
For the complete documentation index, see llms.txt.
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.
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:
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.cg+BACKEND://REF with the secret it names.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.command_override, if you set one, and expands ${VAR} in it.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.
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.
$(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.command_override, the command you list.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 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.
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.
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.
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 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:
{"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:
cg+gms:// typo${VAR} in the command that names a variable that is unset or that open left unresolvedA repo that sets fail_mode: open has GUARDED_FAIL_MODE=open in its image environment. A closed repo’s image carries no setting.
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.
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:
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.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.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.
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:
GUARDED_TRUSTSTORE_0=source=/etc/service-ca,source=/etc/corp-ca/ca.pem,output=/truststore/cacertsValues 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.
When the container starts, the wrapper does the following:
system: false.output.-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.truststore written line.Mount the following volumes:
sources.emptyDir volume at the directory of output. The container can run as an arbitrary user ID, such as one that OpenShift assigns./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.
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.
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.
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 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.
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 $.$ stays as it is. $NAME is not expanded.${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 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:
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.
The wrapper wraps these entrypoints:
/bin/sh -c with the fragment, so the fragment expands the resolved values.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.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.
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.
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.
For how a refusal appears in chainctl, refer to Entrypoints the wrapper refuses.
Last updated: 2026-10-21 19:07