Skip to content
Shell & TerminalDeep Dive Published Updated 5 min readViews unavailable

envsubst Templates: Allowlisted Variables Without Shell Evaluation

Render configuration templates with envsubst while limiting substituted variables, validating required values, and keeping template text out of shell evaluation.

GNU gettext’s envsubst copies standard input to standard output while replacing references to environment variables. It performs a deliberately restricted subset of shell substitution: ordinary dollar-prefixed variable names are supported, but shell parameter defaults, command substitutions, arithmetic, and backticks are not evaluated. That restriction is useful for configuration templating because shell-looking text can remain literal. It is not a complete template engine, and it does not automatically make rendered configuration valid or safe.

A production renderer should define an allowlist of variables, validate every required value before rendering, control which environment reaches the process, and validate output with the application’s real parser. Keep template data as data. Never pipe rendered text into eval or source it merely because envsubst created it.

Use a shell-format allowlist

Without an explicit format operand, envsubst replaces every recognized variable reference in standard input. Passing a format restricts substitution to the names listed:

envsubst '$SERVICE_HOST $SERVICE_PORT' < config.template > config.rendered

The shell-format string is single-quoted so the current shell passes the dollar signs literally. A template containing HOME, PATH, or an unrelated deployment variable remains unchanged unless explicitly allowed. Review this list like an input schema, and fail the build if it drifts from the variables the service actually needs.

The –variables option lists names from a shell-format operand without reading the template. This helps diagnostics but does not prove that a template uses only declared variables or that values are present. Compare the template’s recognized placeholders with the approved set, and report missing names separately from unused names.

Validate variables before rendering

An unset or empty environment variable may be replaced with empty text. That can turn a URL into an invalid endpoint, remove a database name, or produce a syntactically valid but unsafe default. Check required values before invoking envsubst:

if [ -z "$SERVICE_HOST" ] || [ -z "$SERVICE_PORT" ]; then
    printf '%s\n' 'SERVICE_HOST and SERVICE_PORT are required' >&2
    exit 2
fi
case $SERVICE_PORT in
    *[!0-9]*) printf '%s\n' 'SERVICE_PORT must be numeric' >&2; exit 2 ;;
esac

Numeric text is not automatically a valid TCP port, and a host value is not automatically a safe URI authority. Validate ranges, character sets, and semantic constraints according to the destination format. Reject carriage returns or newlines where values are inserted into line-oriented configuration.

Use an explicit environment map when reproducibility matters. A large interactive environment can accidentally influence output. Avoid logging full environments because they often contain credentials, tokens, proxy settings, or cloud account metadata.

envsubst does not evaluate shell programs

The braced shell form that adds a default value is not implemented by envsubst; neither are command substitutions or backtick expressions. This is an intentional security property documented by GNU gettext. It also means a shell script used as a template will not behave like sourcing that script. Keep templates declarative and avoid promising general shell interpolation.

The destination format still matters. Environment values are inserted as text, not automatically escaped as JSON strings, YAML scalars, SQL literals, HTML, or shell arguments. A value containing a quote, newline, ampersand, or dollar sign can change output meaning. Use a serializer that understands the target format when arbitrary values must be encoded. For constrained formats, validate and escape with a format-specific routine rather than relying on shell quoting.

Do not use rendered output as code. A safe template engine that performs no evaluation can be followed by an unsafe eval or source step that reintroduces code execution. If a downstream shell must consume a value, pass it through an environment variable or argument array instead of generating shell syntax.

Render to staging and validate semantics

Do not overwrite a live config before checking the output. Render to a private temporary path, parse it using the service’s native validator, and replace the destination only after validation succeeds. The temporary file should be on the destination filesystem if rename atomicity is required, and its permissions must be restrictive when secrets appear in output.

For JSON, parse with a JSON parser and assert required fields and types. For YAML, use the deployed YAML parser and validate the expected document count. For a web server, run its configuration test before reload. A successful envsubst exit says substitution completed; it does not establish that the result is syntactically valid, semantically correct, or safe to publish.

If a template should contain exactly one placeholder for a critical setting, test cardinality. A typo can leave a literal variable reference in the final file while the command exits successfully. Search for unresolved approved placeholders after rendering and distinguish an intentionally escaped dollar sign from a missing value.

Test and observe the contract

Create tests for every approved variable, missing values, allowed empty values, shell-looking text, quotes, whitespace, newlines, non-ASCII characters, and references outside the allowlist. Check exact output bytes and application-level parse behavior. Confirm that command-substitution-like text remains literal and is not executed. Ensure test logs do not disclose secrets.

Record the gettext version and template digest with a deployment artifact if exact reproducibility matters. Review the source template and variable manifest together. envsubst is valuable because its behavior is small and predictable: explicitly name substitutions, validate values, render to staging, and let the destination application’s parser decide whether the result is valid.

Related:

Sources:

Comments