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 OpenBao 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 fromVAULT_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 wrapper launches your application
The wrapper fork+execs CLOUDTASER_ORIGINAL_CMD with CLOUDTASER_ORIGINAL_ARGS. Secrets are injected into the child process's environment via BuildChildEnv -> execve(). The wrapper remains as PID 1 (parent process) for lease renewal, signal forwarding, secret rotation, and eBPF agent communication. The wrapper's own /proc/1/environ is structurally clean -- secrets are fetched from vault into Go heap memory only after the wrapper has already exec'd, so the kernel's frozen env_start..env_end snapshot for PID 1 never contained them. The child's /proc/<child>/environ does contain the secrets (that is how the application reads them); cross-process reads of the child's environ are blocked by the eBPF kprobe on openat for all monitored PIDs.
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 are invisible to the kernel and cannot be read via /proc/pid/mem. 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_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:
[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.