environment attestor
Captures the host OS, hostname, username, and the full process environment (with sensitive variables obfuscated by default) at attestation time.
| Name | environment |
|---|---|
| Predicate type | https://aflock.ai/attestations/environment/v0.1 |
| Lifecycle | prematerial |
| Default binary? | Yes |
| Recommended trace | off — no syscall tracing needed |
| Auto-attaches when | Not auto-detected — attach explicitly with -a. |
The facts in this box are generated from the CI/lock binary's own catalog (cilock tools list). Do not hand-edit — run npm run gen:catalog.
What it captures
Four json-tagged fields on the Attestor struct:
os—runtime.GOOS(e.g.linux,darwin,windows). No kernel version, no architecture.hostname—os.Hostname().username—user.Current().Username.variables— map of everyKEY=VALentry returned byos.Environ(), after the sensitive-variable filter has been applied. Keys are always preserved; only values are touched.
Sensitive-variable filter behavior (plugins/attestors/environment/filter.go, obfuscate.go):
- The blocklist is split into two sets at runtime. Entries containing
*are compiled as gobwas/glob patterns; entries without*become case-insensitive exact-match keys. - Both the env-var key and the blocklist entry are upper-cased before comparison, so
aws_secret_keymatchesAWS_SECRET_ACCESS_KEYand*token*matchesMyToken. - Default mode is obfuscate: matched values are replaced with the literal string
******. The key still appears invariables. - With
--env-filter-sensitive-vars, matched entries are dropped entirely — neither key nor value appears invariables. --env-allow-sensitive-keyexcludes specific keys from the filter (exact-match only, no globs).- Credentials inside URL-shaped values are redacted whatever the key (
url_credentials.go, releases after 4.4.0). The userinfo of a URL in any value becomes******and the scheme, host, port and path stay, soHTTP_PROXY=http://user:pass@proxy:3128is recorded ashttp://******@proxy:3128. The username goes too, because a token can sit in either slot. A scheme-lessuser:pass@proxy:3128, which curl and Go read ashttp://, is redacted the same way. So is a scheme-relative//user:pass@proxy:3128, which Python's urllib reads as a proxy URL, to//******@proxy:3128. A colon-separated path list such asPATHorPKG_CONFIG_PATHis not a URL and is left alone, even when an entry holds an@(/opt/homebrew/opt/[email protected]/bin). This runs in both modes and neither--env-disable-default-sensitive-varsnor--env-allow-sensitive-keyturns it off.
The default blocklist (attestation/sensitive_env_vars.go::DefaultSensitiveEnvList) ships these glob patterns: *TOKEN*, *SECRET*, *API_KEY*, *PASSWORD*, *JWT*, *sshKey*, *passphrase*. It also carries ~70 explicit keys including AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AZURE_CLIENT_SECRET, GH_TOKEN, GOOGLE_APPLICATION_CREDENTIALS, CI_JOB_JWT, CI_JOB_JWT_V2, CI_JOB_TOKEN, VAULT_TOKEN, ACTIONS_RUNTIME_TOKEN, ACTIONS_ID_TOKEN_REQUEST_TOKEN, DOCKER_PASSWORD, NPM_AUTH_TOKEN, SLACK_TOKEN, SNYK_TOKEN, STRIPE_API_KEY, cloud-provider keys for Heroku/DigitalOcean/Vultr/Sauce/Sentry/Percy/Twilio, and the bare key TOKEN.
When to use
Always. It is one of two default-on attestors (with git) and provides the baseline host/identity context every downstream collection links back to. Disable it only if you are intentionally producing an environment-free predicate.
Flags
| Flag | Default | Effect |
|---|---|---|
--env-filter-sensitive-vars | false | Switch from obfuscation to outright removal — matched keys and values are dropped from variables. |
--env-disable-default-sensitive-vars | false | Skip the built-in DefaultSensitiveEnvList; only --env-add-sensitive-key entries are sensitive. |
--env-add-sensitive-key | [] | Repeatable. Adds keys or globs (e.g. *INTERNAL*) to the blocklist. |
--env-allow-sensitive-key | [] | Repeatable. Allowlist that bypasses the filter for specific keys. No glob support. |
These four flags live on the cilock run command (in cilock/internal/options/run.go), not as --attestor-environment-* flags. The environment attestor registers no per-attestor flags of its own.
Output shape
{
"os": "linux",
"hostname": "runner-abc123",
"username": "runner",
"variables": {
"PATH": "/usr/local/bin:/usr/bin:/bin",
"HOME": "/home/runner",
"GH_TOKEN": "******",
"AWS_ACCESS_KEY_ID": "******",
"CI_JOB_JWT": "******"
}
}
Gotchas
- The key still leaks. In the default obfuscate mode only the value becomes
******—GH_TOKEN,AWS_ACCESS_KEY_ID,CI_JOB_JWT_V2etc. remain visible in the predicate. If even the presence of a key is sensitive, use--env-filter-sensitive-vars. - cilock 4.4.0 and earlier sign URL credentials verbatim. A proxy password in
HTTP_PROXY,ALL_PROXYand their siblings lands in the predicate, because those keys match nothing on the blocklist. On those versions pass--env-add-sensitive-key '*PROXY*', plus the name of any other variable that holds a credential URL. - URL redaction never names a host the value does not dial, and errs toward naming none. The marker keeps
@hostonly when Go, Python, WHATWG and curl read one userinfo and the same host after it. Where they do not, the URL keeps only its scheme and******@, in which none of them finds a host: two@in the authority (https://[email protected]@github.com/x), a password holding/,?,#or\(https://alice:sec/[email protected]/acme), or an@after the authority. An@after a?or#with no/before it counts too: Python's proxy parser ends the authority only at a/after the userinfo's@, sohttp://u:pass@proxy-a?tail@proxy-b:3128sendspass@proxy-a?tailtoproxy-band becomeshttp://******@. An empty userinfo does not end that search:http://@proxy-a?u:pass@proxy-b:3128is an empty user atproxy-ato Go, curl and WHATWG, sendspasstoproxy-bfrom Python, and becomeshttp://******@. A/first ends the authority for every parser, sogit+https://u:pass@host/o/r.git@v1keeps its host and path.https://evil.example/[email protected]dialsevil.examplein Go, WHATWG and curl, while Python's proxy parser sendsevil.example/xas a userinfo toproxy.golang.org, so it becomeshttps://******@, and so doeshttps://registry.npmjs.org/@scope/pkg. A scheme-less value is held to the same rule:evil.example:8080/[email protected]andDOMAIN\user:pass@proxy:8080(which a WHATWG parser dials atdomain) become******@, and so does a compound scheme that carries a credential (jdbc:postgresql://u:pass@db/app). One pair of matching quotes around a value ("u:pass@proxy:3128", asdocker --env-filekeeps them) is looked through, with or without a scheme, so a space in a quoted URL's password does not end it. A value that holds a credential only once tabs and newlines are removed, as WHATWG removes them (http:/<TAB>/u:pass@h), becomes******@. A path list is told from a scheme-less credential by a/in its first entry, or a/or\that starts its second, so one without (.:[email protected]) is read asuser:pass@hostand redacted. A\in the user does not count, because Python's urllib sendsDOMAIN\user:pass@proxy:8080as a credential. A domain holding;(dom;ain\u:\pass@proxy:8080) is read as a Windows path list and kept; NetBIOS names cannot hold;. A value that opens with//is held to the rules of a URL with a scheme whatever its first entry looks like, so a path list that starts with a network path (//server/share/bin:/opt/x@1/bin) becomes//******@. A package URL (pkg:npm/[email protected]), a scoped npm alias (npm:@scope/[email protected]), amailto:address and a full-length image digest (docker://ghcr.io/o/img@sha256:<64 hex>) are references, not userinfo, and are kept. Each shape is held tight: add a port, a list entry or a second@and the value is redacted. What shape leaves open is a proxy user literally namedmailto, orpkgornpmwith a password shaped like the rest of a reference, on a proxy with no port. A token in a query string (?token=, a presignedX-Amz-Signature) is not userinfo and is not redacted; name its variable with--env-add-sensitive-key. - Glob vs exact is decided by
*. A blocklist entry likeMY_SECRETis exact-match (case-insensitive);MY_SECRET*is a glob. Half-baked patterns likeMY_SECRET_will only match that literal key. There is no regex support. - Glob patterns are normalised to uppercase. Mixed-case patterns like
*sshKey*and*passphrase*in the default list are upper-cased at compile time, then matched against upper-cased keys — so they catch any casing. - Panic-safe matching.
safeGlobMatchrecovers fromgobwas/globpanics on adversarial patterns and treats them as non-matches; an unmatched-due-to-panic value is not filtered. Audit custom patterns before trusting them. - No kernel, no arch. Despite what older witness docs imply, this attestor records only
runtime.GOOS— not kernel version, notruntime.GOARCH. Hardware/kernel context must come from a separate attestor. - Shared capturer.
Attestwrites the capturer intoAttestationContext.SetEnvironmentCapturerso other attestors (e.g.command-run) apply the same filter to any environment they record.
CLI example
Default. Captures the process's ENV vars; sensitive keys (TOKEN, KEY, SECRET, PASSWORD) are redacted to ****.
cilock run --step my-step \
--signer-file-key-path key.pem --outfile attestation.json \
--attestations environment \
-- echo hi
Validated. Default attestor. See the full real-data example at https://github.com/aflock-ai/attestor-compliance-examples/tree/main/04-environment.
See also
- Catalog row
- Upstream: witness/environment.md