Skip to content

Wrapper Environment Variables

The cloudtaser wrapper is the process-wrapping binary that authenticates to OpenBao, fetches secrets into memory, and launches your application with secrets available as environment variables. These environment variables configure the wrapper's behaviour.

Automatic vs manual configuration

In Kubernetes, the mutating admission webhook sets all wrapper environment variables automatically based on pod annotations or a CloudTaserConfig CR. Manual configuration is only needed for systemd services, standalone testing, or non-Kubernetes environments.


OpenBao Connection

VAULT_ADDR

Required Yes
Default --
Example https://vault.eu-west-1.example.com:8200

The address of the OpenBao or HashiCorp Vault server. Must include the scheme (https://) and port.

VAULT_TOKEN

Required Only when VAULT_AUTH_METHOD=token
Default --

A pre-existing OpenBao token. Only used when the auth method is set to token. For Kubernetes deployments, the wrapper obtains a token automatically via Kubernetes auth.

VAULT_AUTH_METHOD

Required No
Default kubernetes
Values kubernetes, token

The authentication method used to obtain an OpenBao token.

  • kubernetes -- Authenticates using the pod's service account token (automatic in K8s).
  • token -- Uses a static token from VAULT_TOKEN.

VAULT_AUTH_ROLE

Required Yes (when VAULT_AUTH_METHOD=kubernetes)
Default --

The OpenBao Kubernetes auth role name. This role must be configured in OpenBao to accept service account tokens from the pod's namespace and service account.

VAULT_AUTH_MOUNT_PATH

Required No
Default auth/kubernetes

The mount path of the Kubernetes auth method in OpenBao.

VAULT_SKIP_VERIFY

Required No
Default false
Values true, false

Disables TLS certificate verification for the OpenBao connection.

Do not use in production

Disabling TLS verification removes a critical security control. An attacker can intercept secrets in transit with a man-in-the-middle attack. Use only in development environments with self-signed certificates.


Secret Configuration

CLOUDTASER_SECRET_PATHS

Required Yes
Default --
Example secret/data/myapp/db,secret/data/myapp/api

Comma-separated list of OpenBao KV secret paths to fetch. The wrapper reads all key-value pairs from each path.

CLOUDTASER_ENV_MAP

Required Yes
Default --
Example username=DB_USER,password=DB_PASS;api_key=STRIPE_KEY

Maps OpenBao secret fields to environment variable names. Semicolons separate mapping groups (one per secret path), commas separate individual field=VAR mappings within a group. See Annotations Reference for detailed format documentation.


Process Wrapping

CLOUDTASER_ORIGINAL_CMD

Required Yes
Default --
Example /usr/bin/node

The original command (entrypoint) of the application container. The wrapper launches this command after secrets have been fetched and placed into the process environment.

CLOUDTASER_ORIGINAL_ARGS

Required No
Default --
Example server.js,--port,8080

Comma-separated arguments to pass to the original command.

How the operator fills these in

The webhook sets both variables when it injects the wrapper, so you normally never write them yourself:

  • If the container spec sets command:, its first element becomes CLOUDTASER_ORIGINAL_CMD, and the rest of command: followed by args: become CLOUDTASER_ORIGINAL_ARGS. The registry is not queried.
  • Otherwise the operator reads the image's ENTRYPOINT and CMD from the registry, using the operator pod's image pull credentials. ENTRYPOINT[0] is the command, and the rest of ENTRYPOINT plus CMD are its arguments. Pod args: replace the image CMD. With no ENTRYPOINT, CMD[0] is the command.
  • Lookups time out after 5 seconds and are cached in memory: successes for 10 minutes, failures for 30 seconds, tunable with CLOUDTASER_DIGEST_*_SECONDS.
  • If the image can't be resolved, or has neither ENTRYPOINT nor CMD, admission fails. The pod is never started unwrapped.

How the wrapper launches your application

The wrapper fork+execs CLOUDTASER_ORIGINAL_CMD with CLOUDTASER_ORIGINAL_ARGS and remains as PID 1 for lease renewal, signals, rotation, and eBPF agent communication. With the interposer active, the default strips mapped secret keys from the child's exec environment and serves them through getenv(). Static/musl fallback or CLOUDTASER_KEEP_SECRETS_IN_ENV=true instead places values in the child's exec environment. The wrapper fetches secrets after its own startup, so its original /proc/1/environ snapshot did not contain them. eBPF enforcement restricts external reads of protected processes in every delivery mode.


Secret Delivery Mode

These variables control delivery for eligible dynamic programs where the LD_PRELOAD interposer (libcloudtaser.so) is active. The wrapper skips interposer setup for static binaries and musl environments and uses inherited environment delivery via execve(). Snapshot strip mode needs an active interposer; it cannot add exec stripping to that fallback. See Runtime Compatibility for a Python setup and presence check.

CLOUDTASER_KEEP_SECRETS_IN_ENV

Required No
Default false
Values true, false

When true, secret values remain as real KEY=VALUE entries in the child process's exec-time environ. Every fork+exec descendant (subprocess, shell pipeline, helper binary) inherits the secret values in its own /proc/<pid>/environ.

This is the simplest delivery mode and works with every runtime, including environ-snapshot runtimes (CPython os.environ, bash/sh $VAR, Perl %ENV) that read the C environ array at interpreter startup and never call getenv() per access.

Security trade-off

With KEEP_SECRETS_IN_ENV=true, secret values are physically present in the kernel-managed environ area of every descendant process. eBPF enforcement blocks cross-process reads of /proc/<pid>/environ on Tier 1 and Tier 2 platforms, but the values are there for every child in the tree. If eBPF coverage is absent (Tier 4 or load failure), /proc/<pid>/environ is readable by any process running as the same UID. Prefer CLOUDTASER_ENV_SNAPSHOT_MODE=strip for environ-snapshot runtimes.

CLOUDTASER_ENV_SNAPSHOT_MODE

Required No
Default unset; shell auto-selection uses strip on builds that support it
Values unset/empty, strip
Since wrapper v0.2.184

Controls how the interposer handles runtimes that snapshot the C environ array at startup rather than calling getenv() per access.

When unset or empty (default): Builds with shell auto-selection use strip automatically for sh, bash, dash, ash, ksh and zsh entrypoints, unless native-FD or inherited-env delivery is active. Older wrappers need an explicit setting for those shells. Other entrypoints retain strict getenv()/secure_getenv() delivery: startup snapshots such as CPython os.environ and Perl %ENV do not receive mapped keys without explicit snapshot configuration. Auto-selection does not place fetched secret values in the initial exec-time environment.

When set to strip: The interposer populates the process environ with secret values at library init time (so os.environ, $VAR, and %ENV see them at interpreter startup) AND interposes the full exec family to strip the mapped keys from the envp handed to the kernel at every exec boundary. Fork+exec descendants' /proc/<pid>/environ does NOT carry the secrets.

Interposed calls: execve, execv, execvp, execvpe, execl, execlp, execle, fexecve, posix_spawn, posix_spawnp, system, popen/pclose, and execveat (where the libc exports it).

Hooked libc exec behavior (inferred): If the interposer cannot strip the envp (more than 32768 entries, or a required real function pointer is NULL), the exec-family hook returns an error (errno=ENOMEM for exec calls, ENOMEM for posix_spawn calls) rather than passing the unstripped envp to the kernel. The entire strip path is async-signal-safe (stack-based, no heap allocation) and safe in vfork children.

Comparison with KEEP_SECRETS_IN_ENV=true

ENV_SNAPSHOT_MODE=strip KEEP_SECRETS_IN_ENV=true
CPython os.environ sees secrets Yes Yes
bash $VAR sees secrets Yes Yes
Perl %ENV sees secrets Yes Yes
Descendant /proc/<pid>/environ clean Yes (stripped at exec) No (inherits plaintext)
getenv() callers see rotated values on SIGHUP Yes No (requires restart)
system()/popen() descendants stripped Yes (reimplemented with stripped environ) No
Recommended for CPython, bash, Perl entrypoints Static binaries, runtimes with no alternative

Known limitations of strip mode

SIGHUP rotation staleness: The setenv()'d values and the interpreter's snapshot (os.environ, $VAR, %ENV) are captured at interposer constructor time (library init, before main()). A SIGHUP rotation rewrites the memfd slots so getenv() callers see the new values, but the process-level environ array and interpreter snapshots retain the constructor-time values. Applications that cache secrets at startup (e.g. password = os.environ["DB_PASSWORD"]) hold stale values until the next restart rotation.

Raw syscall bypass: Programs that call execveat via raw syscall(SYS_execve, ...) or syscall(SYS_execveat, ...) bypass all PLT hooks unconditionally. This is an inherent limitation of LD_PRELOAD interposition. Statically linked spawners bypass the interposer entirely. Both cases are covered by the eBPF LSM hooks on Tier 1/2 platforms, which block /proc/<pid>/environ reads regardless of how the child was spawned.

/proc/<pid>/environ of the wrapped process itself holds the setenv()'d values. Cross-process reads of this path are blocked by the eBPF LSM hooks on Tier 1/2 platforms.

See the wrapper's KNOWN-LIMITATIONS.md for the full analysis.


Rotation

CLOUDTASER_ROTATION

Required No
Default restart
Values restart, sighup, none

Controls behaviour when OpenBao secrets are updated during the lease or renewal cycle.

Value Behaviour
restart Terminate the application and re-launch with new secrets
sighup Send SIGHUP to the application process
none No rotation; secrets are fetched once at startup

RENEWAL_INTERVAL

Required No
Default 30s
Example 30s, 5m, 1h

How often the wrapper checks and renews OpenBao leases. Uses Go duration format.


eBPF Integration

CLOUDTASER_EBPF_AGENT_ADDR

Required No
Default --
Example unix:///var/run/cloudtaser/ebpf.sock

The gRPC address of the eBPF agent running on the node. When set, the wrapper registers with the eBPF agent at startup to enable kernel-level secret protection for the application process.

CLOUDTASER_POD_UID

Required No
Default --

The Kubernetes pod UID, used by the eBPF agent to identify the pod's cgroup. Automatically set by the webhook via the downward API.


Health and Observability

CLOUDTASER_HEALTH_ADDR

Required No
Default :8199
Example :8199, 0.0.0.0:9090
Legacy alias HEALTH_ADDR (still accepted)

The address for the wrapper's health check TLS endpoint. The webhook configures liveness and readiness probes to hit this address. The health server starts with TLS from the first listener bind (using an ephemeral self-signed certificate until the operator delivers the broker-minted certificate via /v1/unseal).

Health endpoints

Endpoint Method Description
/healthz GET Returns 200 OK when the wrapper PID 1 is alive. Always returns 200 regardless of sealed/unsealed/secret-not-provisioned state -- the wrapper is alive in all of these states.
/readyz GET Returns 200 OK when the application process has been launched and is running. Returns 503 when sealed (waiting for unseal token), when secrets have not been provisioned yet ({"reason":"secret_not_provisioned"}), or when the child process is not alive.
/v1/pubkey GET Returns the wrapper's ephemeral X25519 ECDH public key for encrypted unseal.
/v1/unseal POST Accepts the vault token (ECDH-encrypted or plaintext JSON) to unseal the wrapper. Authenticated via JWT + optional mTLS.
/v1/status GET Returns current sealed/unsealed and ready status.

/readyz response states

State HTTP Status Response body
Sealed (waiting for token) 503 {"ready":false,"sealed":true}
Secret not provisioned 503 {"ready":false,"reason":"secret_not_provisioned"}
Ready, child alive 200 {"ready":true,"child_alive":true,"sealed":false}
Ready, child dead 503 {"ready":true,"child_alive":false,"sealed":false}

Secret-not-provisioned behaviour (wrapper v0.2.12+)

When the upstream secret has not been provisioned in vault yet, the wrapper stays alive indefinitely instead of crash-looping. /healthz continues returning 200 (preventing kubelet Liveness-driven restarts and CrashLoopBackOff), while /readyz returns 503 with reason=secret_not_provisioned. The wrapper retries with bounded exponential backoff (5s base, doubling to 60s cap, +25% jitter) until the secret appears. Only configuration errors (auth failure, invalid vault path) cause os.Exit.

Health server hardening defaults

The health server applies the following timeouts and limits to protect against slowloris, amplification, and brute-force attacks:

Setting Value Purpose
ReadHeaderTimeout 5s Prevents slow-header connection holding
ReadTimeout 30s Outer bound on full request read
WriteTimeout 30s Prevents slow-consumer connection holding
IdleTimeout 120s Closes idle keepalive connections
MaxHeaderBytes 64 KB Rejects oversized headers
Unseal body limit 64 KB LimitReader on /v1/unseal body
Unseal body read deadline 10s Inner bound on body read (prevents slowloris-style byte-at-a-time attacks)

Unseal rate limiting

The /v1/unseal endpoint is rate-limited to prevent brute-force token guessing and denial-of-service by co-located attackers:

Limit Value Purpose
Per-IP rate 100 req/min (burst 10) Prevents a single attacker from exhausting the budget
Global rate 1000 req/min (burst 50) Prevents fan-out from many IPs exhausting goroutines
Stale entry cleanup 5 minutes Per-IP entries not seen in 5 minutes are lazily removed

Rate limiting is applied AFTER JWT authentication -- an unauthenticated attacker cannot exhaust the rate limit budget and lock out the legitimate operator.


Memory Protection

CLOUDTASER_REQUIRE_MLOCK

Required No
Default true
Values true, false

When true, the wrapper calls mlockall() to prevent secret-containing memory pages from being swapped to disk. If the mlock syscall fails (e.g., due to missing CAP_IPC_LOCK), the wrapper exits with an error rather than running without memory protection.

Requires CAP_IPC_LOCK

The pod's security context must allow CAP_IPC_LOCK, or the container runtime must set appropriate ulimit values. The operator automatically adds the required capability when injecting the wrapper.

CLOUDTASER_REQUIRE_MEMFD_SECRET

Required No
Default false
Values true, false

When true, the wrapper stores secrets in memory regions created with memfd_secret(), which remove their direct-map alias and restrict ordinary external reads. This is not isolation from arbitrary guest-kernel code or ordinary application copies. Requires Linux kernel 5.14 or later.

Kernel requirement

memfd_secret() is available on Linux 5.14+. Most managed Kubernetes services (GKE, EKS, AKS) run kernels that support this syscall. See Kernel Compatibility for details.


Broker TLS (Platform Integration)

CLOUDTASER_BROKER_TLS_CERT

Required No
Default --

TLS client certificate for connecting to the cloudtaser Platform broker -- either an inline PEM block or a filesystem path. The operator (v0.2.0+) injects PEM content directly; older operators injected file paths. Both forms are accepted.

CLOUDTASER_BROKER_TLS_KEY

Required No
Default --

TLS client key -- either an inline PEM block or a filesystem path.

CLOUDTASER_BROKER_TLS_CA

Required No
Default --

CA certificate for verifying the Platform broker's TLS certificate -- either an inline PEM block or a filesystem path.


Complete Reference Table

Variable Required Default Description
VAULT_ADDR Yes -- OpenBao server address
VAULT_TOKEN Conditional -- Static OpenBao token
VAULT_AUTH_METHOD No kubernetes Auth method
VAULT_AUTH_ROLE Conditional -- K8s auth role
VAULT_AUTH_MOUNT_PATH No auth/kubernetes Auth mount path
VAULT_SKIP_VERIFY No false Skip TLS verification
CLOUDTASER_SECRET_PATHS Yes -- OpenBao KV paths
CLOUDTASER_ENV_MAP No -- Field-to-env mappings
CLOUDTASER_ORIGINAL_CMD Yes -- Application command
CLOUDTASER_ORIGINAL_ARGS No -- Application arguments
CLOUDTASER_ROTATION No restart Rotation strategy
RENEWAL_INTERVAL No 5m Token renewal interval
CLOUDTASER_EBPF_AGENT_ADDR No -- eBPF agent gRPC address
CLOUDTASER_POD_UID No -- Pod UID for eBPF
CLOUDTASER_HEALTH_ADDR No :8199 Health endpoint address (legacy alias: HEALTH_ADDR)
CLOUDTASER_KEEP_SECRETS_IN_ENV No false Keep secret values in child exec-time environ (all descendants inherit)
CLOUDTASER_ENV_SNAPSHOT_MODE No unset; shell auto-selection when supported strip: populate environ for snapshot runtimes, strip at interposed exec boundaries (v0.2.184+)
CLOUDTASER_REQUIRE_MLOCK No true Require mlock
CLOUDTASER_REQUIRE_MEMFD_SECRET No false Require memfd_secret
CLOUDTASER_BROKER_TLS_CERT No -- Platform TLS cert
CLOUDTASER_BROKER_TLS_KEY No -- Platform TLS key
CLOUDTASER_BROKER_TLS_CA No -- Platform TLS CA

Systemd Usage Example

For protecting systemd services outside of Kubernetes:

/etc/systemd/system/myapp.service
[Unit]
Description=My Application (cloudtaser protected)
After=network-online.target
Wants=network-online.target

[Service]
Type=exec
Environment=VAULT_ADDR=https://vault.eu-west-1.example.com:8200
Environment=VAULT_AUTH_METHOD=token
Environment=VAULT_TOKEN_FILE=/run/secrets/vault-token
Environment=CLOUDTASER_SECRET_PATHS=secret/data/myapp/config
Environment=CLOUDTASER_ENV_MAP=db_password=DB_PASS,api_key=API_KEY
Environment=CLOUDTASER_ORIGINAL_CMD=/usr/bin/myapp
Environment=CLOUDTASER_ORIGINAL_ARGS=--config,/etc/myapp/config.yaml
Environment=CLOUDTASER_REQUIRE_MLOCK=true
Environment=CLOUDTASER_ROTATION=none
Environment=CLOUDTASER_HEALTH_ADDR=:8199
ExecStart=/usr/local/bin/cloudtaser-wrapper
Restart=on-failure
LimitMEMLOCK=infinity

[Install]
WantedBy=multi-user.target

LimitMEMLOCK

Set LimitMEMLOCK=infinity in the systemd unit to allow the wrapper to call mlockall(). Without this, the wrapper will fail to start when CLOUDTASER_REQUIRE_MLOCK=true.

Strip-mode hook errors refuse the intercepted libc call. Direct raw exec syscalls can bypass interposition; this is not a process-wide refusal guarantee. Cgroup anchor setup can log a warning and continue when cgroup v2 or resolution is unavailable. Verify the actual anchor and enforcement state before trusting it.