envilder validate reference
How do you know a map file will work before a pull, a deploy or envilder exec runs? envilder validate checks that the map file is valid and that every declared secret exists in AWS SSM or Azure Key Vault, and can compare a local .env file with it. It never prints a secret value, and it exits with 1 when anything fails, so it fits as a CI step.
Prerequisites
- The envilder CLI. --offline is available since version 0.18 and the secret existence check since 0.20. --envfile, --compare-values and --format json come with the next release.
- A map file. Without --offline, credentials for the provider and the permissions listed under Permissions.
Synopsis
envilder validate [--offline] [--envfile <path> [--compare-values]] \
[--format <text|json>] [--map <path>] [--provider <name>] \
[--profile <name>] [--vault-url <url>]By default, validate checks the map file and then asks the provider whether each declared secret exists. With --envfile it also compares the keys of a local env file with the map file, and with --compare-values their values with the remote ones. With --offline only local files are read: no provider is called and no cloud credentials are needed. It never prints a secret value.
envilder validate
envilder validate --map=config/envilder.json --profile=dev
envilder validate --provider=azure --vault-url=https://my-vault.vault.azure.net
envilder validate --offline
envilder validate --envfile=.env --compare-values
envilder validate --format=jsonOptions
| Option | Description |
|---|---|
--offline | Validate the map file locally without calling the provider |
--envfile | Compare the keys of this env file with the map file. Not read unless given |
--compare-values | Also compare each declared value in the env file with its remote value. Requires --envfile; cannot be combined with --offline |
--format | text (default) for human-readable output, or json for one JSON document on stdout |
--map | Map file to check (default: envilder.json) |
--provider | aws or azure. Overrides $config.provider |
--profile | AWS CLI profile. Overrides $config.profile |
--vault-url | Azure Key Vault URL. Overrides $config.vaultUrl |
The options have the same names, defaults and precedence as pull and exec: flags win over $config, and an empty flag is ignored. Put them after validate; options written before it are rejected. As in push, an empty --envfile value is an error.
--compare-values without --envfile or together with --offline, and a --format other than text or json, are rejected before any file is read or any provider is called.
What is checked
The map file (always)
The map file is checked first, with the same messages pull, push and exec use. Every problem is reported, not only the first, each with the key that causes it ($config.profile, --vault-url, DB_URL...). Messages name keys and types only, never mapped values.
- The file parses as JSON. A parse error gives the path and, when the parser has one, the line and column, but never quotes the text around it, because it can be a secret pasted into the file.
- The document matches the published map file schema (map-file.v1.json): known $config fields, valid variable names and string mappings.
- The provider configuration is coherent after the flags are applied: aws or azure, profile only with aws, vaultUrl only with azure, and azure has a vaultUrl.
When the map file is invalid, validate stops there: no provider is called, with or without --offline.
The remote keys (without --offline)
Every secret path declared in the map file is checked in the provider. Envilder never uses or prints a value: on AWS, String parameters can come back in the response, and they are discarded. A path that several variables map to is checked once. Each variable gets one status:
| Status | Meaning |
|---|---|
exists | The secret exists and the identity can see it |
missing | The provider answered that there is no such secret |
access denied | The identity is not allowed to check the secret, so whether it exists is unknown. Never reported as missing |
authentication failed | The provider rejected the credentials, or none could be found |
provider error | Anything else, for example an unreachable provider or an invalid secret name |
Expired AWS credentials and SSO sessions are reported with the same message as pull and exec. On an interactive terminal, an expired SSO session offers to run aws sso login and then retries.
The env file keys (with --envfile)
Available from the next release. The env file is read with the parser pull and push use, so comments, export, quotes and multiline values behave the same. Its keys are compared with the variables the map file declares, by exact, case-sensitive name:
| Finding | Effect |
|---|---|
missing from .env | A declared variable is absent from the env file. Fails |
duplicated in .env | A key is assigned more than once. Fails |
not declared in map file | A key is only in the env file. A warning that does not change the exit code |
KEY= with an empty value counts as present, and a missing env file fails with Env file not found. This check reads local files only, so it runs before any provider call and with --offline too. Values are neither compared nor printed.
The env file values (with --compare-values)
For every declared variable that is in the env file and exists in the provider, the remote value is read the way pull reads it and compared with the local one. When a key repeats, the last assignment is compared, as dotenv loads it:
| Status | Meaning |
|---|---|
matches | The two values are identical |
differs | The two values are not identical. Fails |
unreadable | The secret exists but its value could not be read, with the provider's reason. Fails |
Neither value is ever printed, and each remote value is dropped right after the comparison. Variables already reported as missing are not read. Without --compare-values, no value is read at all.
Permissions
The existence check uses a provider call that pull does not, so the identity needs one more permission than a pull. --offline needs no credentials at all.
AWS SSM
Paths are checked with GetParameters, ten per call, with WithDecryption: false: SecureString parameters are never decrypted. This is a separate IAM action from the ssm:GetParameter pull uses, so add it to the policy, scoped to the same paths:
{
"Effect": "Allow",
"Action": ["ssm:GetParameter", "ssm:GetParameters"],
"Resource": "arn:aws:ssm:*:*:parameter/myapp/*"
}The existence check needs no kms:Decrypt permission. --compare-values reads values as pull does, so it needs the same KMS access as pull: kms:Decrypt on the key of each SecureString parameter encrypted with a customer managed key. With kms:Decrypt, ssm:GetParameters can also read decrypted values, so scope it to the same paths as ssm:GetParameter. When a call is denied, each path in it is checked on its own, so access denied is reported only for the paths the identity cannot read.
Azure Key Vault
The secret names of the vault are listed once (metadata only) and each declared name, after the usual / to - translation, is matched against that list ignoring case. A disabled secret is reported as missing, because pull cannot read it. Listing needs:
- Azure RBAC: Microsoft.KeyVault/vaults/secrets/readMetadata/action on the vault, included in the built-in Key Vault Secrets User and Key Vault Reader roles.
- Access policies: the List secret permission, besides Get.
az role assignment create \
--role "Key Vault Secrets User" \
--assignee {YOUR_OBJECT_ID} \
--scope /subscriptions/{SUB}/resourceGroups/{RG}/providers/Microsoft.KeyVault/vaults/{VAULT}az keyvault set-policy \
--name {VAULT_NAME} \
--object-id {YOUR_OBJECT_ID} \
--secret-permissions get listThe list covers the whole vault, so an identity without it gets access denied for every key, with the reason Key Vault returns.
--compare-values also reads values, so it needs the same permissions as a pull on top of these.
Output
Every secret exists (exit code 0):
✓ Map file valid: envilder.json
☁ AWS identity · account=123456789012 · region=eu-west-1 · profile=dev
Checking 3 secrets in aws, profile=dev
✓ DB_URL /myapp/db-url exists
✓ DATABASE_URL /myapp/db-url exists
✓ API_KEY /myapp/api-key exists
✓ All 3 secrets existSome secrets are missing or cannot be checked (exit code 1; the summary goes to stderr):
✓ Map file valid: envilder.json
Checking 3 secrets in aws, profile=dev
✓ DB_URL /myapp/db-url exists
✗ API_KEY /myapp/api-key missing
✗ TOKEN /myapp/token access denied (User: arn:aws:sts::123456789012:assumed-role/dev/ci is not authorized to perform: ssm:GetParameters ...)
✗ Validation failed: 1 missing, 1 access deniedWith --offline:
✓ Map file valid: envilder.json
Remote validation skipped (--offline): no provider was called, so the secrets were not checked.With --envfile=.env --compare-values (exit code 1):
✓ Map file valid: envilder.json
Checking env file .env
✗ TOKEN missing from .env
⚠ DEBUG not declared in map file
Checking 3 secrets in aws, profile=dev
✓ DB_URL /myapp/db-url exists
✓ API_KEY /myapp/api-key exists
✓ TOKEN /myapp/token exists
Comparing 2 values with aws, profile=dev
✓ DB_URL matches
✗ API_KEY differs
✗ Validation failed: 1 missing from .env, 1 differsWhen every declared variable is in the env file, its block ends with ✓ All 3 declared variables are in .env, and when every compared value matches, the values block ends with ✓ All 2 values match.
An invalid map file, on stderr:
✗ Map file invalid: envilder.json (3 problems)
• $config.region is not a known field. Allowed fields: provider, profile, vaultUrl, projectId, namespace, path, name, description, owner, environment.
• DB_PORT must map to a string secret path, not a number.
• $config.profile is only supported with the aws provider (current provider: azure).JSON output (--format json)
Available from the next release. With --format=json, validate prints exactly one JSON document to stdout and nothing else, whatever the outcome. Logs, the AWS identity banner, warnings, errors and the SSO login prompt go to stderr, so stdout can be piped to jq or saved as a CI artifact. Exit codes are the same as with text output. Option errors such as --format=yaml are reported on stderr before anything runs, without a document.
{
"valid": false,
"offline": false,
"mapFile": { "path": "envilder.json", "valid": true, "errors": [] },
"keys": [
{ "name": "DB_URL", "provider": "aws", "path": "/myapp/db-url", "status": "exists" },
{ "name": "API_KEY", "provider": "aws", "path": "/myapp/api-key", "status": "exists" },
{
"name": "TOKEN",
"provider": "aws",
"path": "/myapp/token",
"status": "access_denied",
"reason": "User: arn:aws:sts::123456789012:assumed-role/dev/ci is not authorized to perform: ssm:GetParameters ..."
}
],
"envFile": {
"path": ".env",
"valid": false,
"keys": [
{ "name": "DB_URL", "status": "present", "value": "matches" },
{ "name": "API_KEY", "status": "present", "value": "differs" },
{ "name": "TOKEN", "status": "missing" }
],
"duplicates": [],
"undeclared": ["DEBUG"]
}
}| Field | Description |
|---|---|
valid | true exactly when the exit code is 0 |
offline | true with --offline |
mapFile | path, valid and errors (one { key, message } per problem, the same messages as the text output). Left out when the map file could not be read |
keys | One entry per declared variable, in map file order: name, provider, path, status and, when the provider gave one, reason. status is exists, missing, access_denied, authentication_failed or provider_error. Left out when the secrets were not checked |
envFile | Left out without --envfile. Otherwise path, valid, keys (each declared variable with status present or missing, plus value matches, differs or unreadable with --compare-values), duplicates ({ name, count }) and undeclared |
error | { message }, only when the run stopped before every check finished (expired credentials, a missing env file, a declined SSO login). Sections that completed before it are still filled in |
The document never holds a secret value, local or remote, and no field is ever null: a section that does not apply is left out. A section can gain fields in later versions; existing fields keep their meaning.
Exit codes
| Code | Meaning |
|---|---|
0 | The map file is valid; without --offline, every declared secret exists; with --envfile, every declared variable is in the env file exactly once; with --compare-values, every compared value matches. Keys only in the env file do not change it |
1 | Anything else: missing or invalid map file, invalid options, a secret that is missing or cannot be checked, missing or expired credentials, or an env file that is missing, lacks a declared variable, repeats a key or holds a value that differs or cannot be compared |
In CI
Run the offline check on every pull request, including from forks, since it needs no cloud credentials. Run the existence check where the job has the same credentials as a pull, plus the permissions above:
- name: Check the map file
run: npx envilder validate --offline --map=envilder.json
- name: Check that every secret exists
run: npx envilder validate --map=envilder.jsonFrom the next release, to check that a committed .env.example declares every variable without credentials, add --offline --envfile=.env.example to the first step.
From the next release, to keep the result for later steps or other tools, use JSON output. The step still fails when validation fails:
- name: Check that every secret exists
run: npx envilder validate --map=envilder.json --format=json > validate.jsonvalidate exits with 1 on any failure, so it can gate a deploy step before pull or envilder exec runs.
From the SDKs
The .NET, Python and Node.js SDKs (version 1.1 or later) run the same map file and key existence check from application code, with the same statuses, without the env file comparison:
| SDK | Check, returns a result | Fail the load when it does not pass |
|---|---|---|
| Node.js | await Envilder.fromMapFile(path).validateContract() | .withContractValidation() |
| Python | Envilder.from_map_file(path).validate_contract() | .with_contract_validation() |
| .NET | await Env.FromMapFile(path).ValidateContractAsync(ct) | .WithContractValidation() |
Contract validation is different from secret validation: it checks the map file and that each secret exists before loading, while secret validation checks that the values already resolved are not empty.
What validate does not do
- It never prints secret values, and reads them only with --compare-values.
- With --offline it does not check that the secrets exist or that the credentials can read them.
- It does not fix the env file: it reports what to change.
- It does not check what the values look like (types, URLs, length).