Sanitizing Kubernetes Manifests
Kubernetes YAML is where secrets and structure collide. A single applied file can define a Deployment with env-block credentials, a ConfigMap carrying a connection string, and a Secret whose base64 data decodes in one command,1 and teams share these files constantly for reviews, debugging, and documentation. The Cloud Config Sanitizer treats Kubernetes as a first-class input: multi-document files split on the --- separator are analyzed document by document, each labeled with its kind, and Secret resources receive an unconditional rule where every value under .data or .stringData is flagged critical regardless of content. Beyond credentials, five securityContext smells are checked on the same pass.2 Consequently, one paste answers both questions a reviewer should ask of any manifest: what does this leak, and what does it permit.
Where credentials hide in Kubernetes YAML
The env block is the front door. Deployment and StatefulSet specs carry environment variables inline, and a value: field holding a real token is the single most common finding in shared manifests, followed closely by ConfigMaps used as if they offered confidentiality, which Kubernetes documentation is explicit they do not.3 Secret manifests round out the top three, since their base64 values look opaque enough that people paste them freely.1 Less obvious carriers include annotations holding webhook URLs with embedded tokens, initContainer commands with inline passwords, and volume definitions for credential files. Because the analyzer walks every string value at every depth, a token four levels into a pod template surfaces with its full dotted path, such as spec.template.spec.containers[0].env[2].value.
CronJobs hide credentials one level deeper
CronJob specs deserve the same attention, since their nested jobTemplate wraps an entire pod spec and hides credentials one level deeper than a typical Deployment, and a reviewer skimming the top level can miss a credential that a Deployment would have surfaced immediately. The analyzer does not care about that extra nesting, since it walks every string value at every depth regardless of how many templates wrap the field.
InitContainer commands and volume definitions for credential files deserve the same scrutiny, since they read as infrastructure plumbing rather than as places a secret would show up. Treating every string value in a manifest as a candidate, rather than only the fields that look obviously secret-shaped, is what the full-depth walk is actually for.
How multi-document analysis works
Real Kubernetes files are bundles. A typical application ships as Deployment plus Service plus ConfigMap plus Ingress in one file separated by --- lines, and the sanitizer parses these with a multi-document YAML parser rather than treating the file as one blob. Each document is analyzed independently: findings carry a document number and the kind where one is declared, so you can tell instantly that the critical rows belong to document 3, the Secret, while document 1, the Deployment, contributed two info-level smells. Furthermore, the kind detection is what arms the Secret-specific rule; only documents whose kind is Secret get the unconditional .data treatment. The sanitized output preserves the document order and separators, ready to apply once real values are injected.
Reading a findings table for a bundled file
A findings table for a bundled apply file reads fastest when you scan by document number first, confirming which document each critical row belongs to before worrying about the specific field. A twelve-document file that produces three critical rows all under document 7 tells a very different story than the same three rows spread across three separate Secrets.
Trusting the kind label over a document's position in the file matters too, because reordering documents during an edit does not change which one is the actual Secret, and the label is what the reviewer should anchor on rather than where a document happens to sit in the file. That is exactly the property multi-document parsing exists to protect, since a manual skim would have to redo the same check by eye every time.
From sanitized manifest to safe deployment
Redaction leaves you with placeholders to resolve, and Kubernetes gives you clean mechanisms for resolving them. Values that were inline env entries should become secretKeyRef references to a Secret created outside the shared manifest, or arrive via envFrom against a Secret synced by External Secrets Operator from a cloud secret store.4 The .env template download lists every critical and high finding's key as an empty variable, which doubles as the checklist for building that Secret. Building on this, the securityContext findings from the same paste tell you which hardening fields to add while you are already editing: explicit runAsNonRoot: true and readOnlyRootFilesystem: true close the two absence flags.5
Two passes, every time, not just once
That two-pass habit is what separates a one-off cleanup from a practice a team can actually rely on across dozens of manifests. The manifest that emerges is shareable by default, which is the property that makes every later paste safe.
Applying the same two-pass review, credentials first and hardening second, to every manifest before it merges keeps the whole practice consistent across a team rather than dependent on any one reviewer's memory. A checklist that lives in the PR template, rather than in any single reviewer's head, is what keeps that consistency from depending on who happens to review a given change.
When to use this
Sanitize before attaching a manifest to a ticket or PR description, before pasting cluster YAML into an AI assistant or chat channel, and before committing anything under a GitOps repository. Output from kubectl get -o yaml deserves the same pass, since live objects frequently contain injected credentials that the original source files never showed.
Notes
Detection is kind-aware. The parser reads each document's kind field, so a Secret gets the unconditional .data redaction while a ConfigMap relies on the pattern, entropy, and key-name rules; findings are grouped per document with the kind shown, which keeps a twelve-document apply file readable. Boolean smells like privileged: true are surfaced at info severity but never rewritten, since changing them would change workload behavior. The sanitized download reassembles all documents with --- separators preserved.
Examples
Secret data value
data.api-token
Flagged critical unconditionally because the document kind is Secret; content is never inspected.
Inline env credential
spec.template.spec.containers[0].env[1].value
Caught by provider patterns, entropy, or key-name rules depending on the value's shape.
Privileged container
securityContext.privileged: true
Info-severity smell; surfaced with its path but never rewritten in the sanitized output.
Try in the tool
What this page covers
- Secret .data / .stringData flagged critical unconditionally, regardless of content
- Multi-document files split on ---, analyzed and labeled by document number and kind
- Boolean smells surfaced at info severity, never rewritten since that would change behavior
Verify with the Cloud Config Sanitizer tool.
Try it in the tool ↑- 1.
Kubernetes, "Secrets," kubernetes.io, accessed July 2026. https://kubernetes.io/docs/concepts/configuration/secret/
- 2.
CISA, "Updated: Kubernetes Hardening Guide," cisa.gov, accessed July 2026. https://www.cisa.gov/news-events/alerts/2022/03/15/updated-kubernetes-hardening-guide
- 3.
Kubernetes, "ConfigMaps," kubernetes.io, accessed July 2026. https://kubernetes.io/docs/concepts/configuration/configmap/
- 4.
External Secrets Operator, "Introduction - External Secrets Operator," external-secrets.io, accessed July 2026. https://external-secrets.io/latest/
- 5.
Red Hat, "Managing Security Context Constraints," redhat.com, accessed July 2026. https://docs.redhat.com/en/documentation/openshift_container_platform/4.19/html/authentication_and_authorization/managing-pod-security-policies
No, and it does not need to. Every value under a Secret's .data or .stringData map is flagged critical and redacted purely because of where it lives; the rule is positional, not content-based. This catches encoded credentials that no pattern would recognize, including base64-wrapped PEM keys and JSON service-account files.
Each document separated by --- is parsed and analyzed on its own, and its findings section is labeled with the document number and Kubernetes kind where one is declared. A bundle of Deployment, Service, and Secret therefore reads as three short reports rather than one merged list, and the sanitized download preserves the original document order.
Yes. Live-object YAML parses like any other manifest, including the status and metadata fields the API server adds. It often contains more secrets than the source file did, because controllers inject tokens and generated credentials at runtime, which makes sanitizing before sharing live output even more important than for source manifests.
Yes. CapyToolkit doesn't transmit or store your YAML; parsing, kind detection, and every rule run in your browser, and closing the tab discards the lot. The tool also works offline after the page loads, which suits air-gapped operational environments.
References, not values. Point env entries at a Secret via secretKeyRef or envFrom, create that Secret through your pipeline or an operator like External Secrets, and keep the applied manifest identical to the shareable one. The .env template names every variable the Secret must supply, so nothing gets missed in the translation.
Sanitizing Terraform and OpenTofu Configs
Terraform's dirtiest secret is the state file. HashiCorp's own documentation warns that state can contain sensitive data, database passwords and generated keys included, stored in plaintext JSON, and teams share exactly these files when debugging plans and drift.1 Marking a variable sensitive = true changes none of this; the flag masks values in CLI output while the state and plan files keep the real strings.2 With Terraform and its open-source fork OpenTofu leading infrastructure-as-code adoption in 2026, the volume of these files moving through tickets, reviews, and AI-assistant prompts keeps growing. The Cloud Config Sanitizer handles the JSON side of this ecosystem natively: state files, plan JSON, and HCL-in-JSON configs all parse directly, with provider credentials, connection strings, and high-entropy generated values surfacing as redactable findings.
What actually leaks from a Terraform workflow
Three artifacts carry most of the risk. State files hold every attribute of every managed resource, which includes generated database passwords, initial admin credentials, and private keys that providers return at create time; HashiCorp documents this plainly, recommending remote state with encryption and restricted access.1 Plan output in JSON form embeds the values that will change, so a shared plan can leak the new password it is about to set.
Variable files complete the trio, since terraform.tfvars accumulates real credentials on developer machines and then slips into commits. Consequently, the sensitive = true flag should be understood as cosmetic for sharing purposes: it protects terminal scrollback, not the files themselves, and any of the three artifacts pasted raw into a ticket carries live values.2 Module outputs marked sensitive follow the identical rule, masked in the CLI yet fully readable wherever the calling configuration's state or plan gets exported.
The cosmetic scope of sensitive = true
That masking is easy to over-trust precisely because it looks like real protection: the CLI output genuinely shows asterisks in place of the value, which reads as redaction even though nothing downstream has actually changed. Anyone who has only ever seen the masked terminal output can reasonably assume the underlying files got the same treatment.
They did not, and the gap matters most exactly when a state file or plan gets shared for debugging, since that is the moment the masked value in a screenshot and the plaintext value in the attached file diverge. Treating every artifact, not just the terminal session, as the thing that needs sanitizing closes that gap.
Getting HCL content into the sanitizer
The path runs through JSON. Running terraform show -json against a state file or saved plan emits the machine-readable form the sanitizer parses directly, and tofu show -json does the same for OpenTofu;3 paste the output or drop the file, and the analyzer walks every value in the resource attributes. For configuration itself, Terraform's JSON configuration syntax means .tf.json files parse as-is, while native HCL .tf files need conversion first because the parser deliberately sticks to standard YAML and JSON deserialization. In the findings, AKIA-prefixed provider keys and connection strings surface as critical, while the long random strings Terraform generates, tokens, initial passwords, and webhook secrets, reliably cross the 4.5-bit entropy threshold and appear as high-severity rows with their full attribute paths.
Converting HCL is a one-time step, not a workaround
Running the show -json conversion is not a workaround for a missing feature; the sanitizer's JSON-only parsing is a deliberate boundary, since HCL's block syntax would require a much larger parser to support reliably. The conversion step itself takes one command and produces a file that is arguably easier to grep and diff than the original HCL anyway.
Keeping .tf.json alongside native .tf files, where a team already prefers JSON configuration syntax, means those files need no conversion step at all before a sanitizer pass. Either path lands in the same place: a JSON document every value of which the analyzer can walk and flag. Neither route asks a reviewer to learn a second syntax just to check a file for stray credentials.
Structural fixes after the redaction pass
Placeholders point at process problems. A provider block whose access_key was redacted should stop carrying keys entirely, since AWS and other providers read credentials from environment variables or shared credential files without any HCL changes.4 Redacted tfvars values belong in environment-variable form using the TF_VAR_ prefix, or in a secrets manager the configuration reads through a data source at plan time. For state, the durable fix is remote backends with encryption and access control rather than local files that travel; OpenTofu additionally offers built-in state encryption, which protects the file at rest even if a copy escapes.5
Backend access deserves the same scrutiny as the secret
Encrypting the backend does not finish the job if the list of principals who can read it is still too broad for what it protects. Building on this, the .env template from the sanitizer maps each redacted attribute to a variable name, giving you the skeleton of the TF_VAR_ migration in one download.
Access to the remote backend itself deserves the same scrutiny as the credentials inside it, since a state file readable by too many principals reintroduces the exact exposure the migration was meant to close. Reviewing backend IAM policy or bucket ACLs alongside the encryption setting turns a partial fix into a complete one, closing the gap between what protects data at rest and who is actually allowed to request it.
When to use this
Sanitize whenever state or plan JSON leaves your machine: attaching output to an issue, asking an AI assistant why a plan diff looks wrong, or sharing a module example that was tested with real values. It also fits migration audits, where converting legacy configs to JSON and scanning them inventories the hardcoded credentials before a move to OpenTofu or a new backend.
Notes
Native HCL syntax is not supported; the parser accepts YAML and JSON only. Convert with terraform show -json for state and plans, or author .tf.json files where JSON syntax is already in use. OpenTofu's tofu show -json produces identical structures. One caution applies in reverse: the sanitized JSON output is for sharing and review, not for feeding back into Terraform operations, since redacted state is no longer a faithful record of infrastructure.
Examples
Convert state for scanning
terraform show -json terraform.tfstate
Produces parseable JSON; OpenTofu users run tofu show -json with identical results.
Provider credential finding
values.root_module.resources[0].values.access_key
AKIA-format keys in state attributes are flagged critical with the full path.
Generated password finding
values.root_module.resources[3].values.password
Random generated values cross the entropy threshold; the key name password also trips the key-name rule.
Try in the tool
What this page covers
- terraform show -json / tofu show -json converts state or a saved plan into the JSON this tool parses
- sensitive = true masks CLI output only; the state and plan files still hold the real values
- Native .tf (HCL) not supported directly; only YAML and JSON parse, including .tf.json
Verify with the Cloud Config Sanitizer tool.
Try it in the tool ↑- 1.
HashiCorp, "Manage Sensitive Data in Your Configuration," developer.hashicorp.com, accessed July 2026. https://developer.hashicorp.com/terraform/language/manage-sensitive-data
- 2.
HashiCorp, "Use Input Variables to Add Module Arguments," developer.hashicorp.com, accessed July 2026. https://developer.hashicorp.com/terraform/language/values/variables
- 3.
OpenTofu, "Command: show," opentofu.org, accessed July 2026. https://opentofu.org/docs/cli/commands/show/
- 4.
AWS, "Configuration and Credential File Settings in the AWS CLI," docs.aws.amazon.com, accessed July 2026. https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html
- 5.
OpenTofu, "State and Plan Encryption," opentofu.org, accessed July 2026. https://opentofu.org/docs/language/state/encryption/
Native HCL is its own syntax, and the sanitizer's parser deliberately supports only standard YAML and JSON. Convert first: terraform show -json for state and plans, or use JSON configuration syntax for the config itself. The error message you see when pasting HCL is the parser reporting that the content is neither valid YAML nor JSON.
No. The sensitive flag masks values in plan and apply output on your terminal; the state file and plan JSON retain the real values, which HashiCorp's documentation on sensitive variables states directly. Treat state as a secret-bearing artifact regardless of how variables are marked, and sanitize any excerpt before it travels.
No, and you should not try. Redaction replaces real attribute values with placeholders, so the file no longer describes your infrastructure accurately. The sanitized output exists for sharing and review. Your working state should stay in a remote backend with encryption and access controls, untouched by this workflow.
The leak surfaces are identical, since OpenTofu maintains compatibility with Terraform's state and plan formats, and tofu show -json output parses the same way. One improvement is native state encryption, which OpenTofu offers built-in, protecting state at rest. Files you share for debugging still deserve the same sanitization pass either way.
Yes, within the tool itself: CapyToolkit doesn't upload your input, and the entire analysis runs in your browser, even offline after page load. Bear in mind that state files can be large; the analyzer processes documents in a single pass, so even multi-thousand-line state JSON stays responsive during review.
Sanitizing Helm Values Files
values.yaml is where chart secrets congregate. Helm's design funnels every environment-specific setting into values files, and credentials are the most environment-specific settings there are, so real installations accumulate database passwords, registry credentials, and API tokens in exactly the file that gets shared when someone asks how a chart is configured. The file is plain YAML, which means the Cloud Config Sanitizer parses it directly: paste a values file and provider-format tokens surface as critical findings, random generated values trip the entropy rule, and fields with names like password are caught by the key-name rule even when their values look mundane.1 Furthermore, the rendered output deserves the same scrutiny, because helm template merges values into manifests and the merge is where redacted-looking defaults meet real --set overrides.2
Why values files leak more than templates
Templates hold placeholders; values hold reality. A chart's templates directory references .Values.database.password without containing any password, which makes templates safe to publish and is precisely why public charts exist at all.1 The values file inverts this: it is the layer users fill with real strings, and the common practice of copying values-production.yaml to create values-staging.yaml multiplies every embedded credential across environments. Consequently, a request to share your values so I can reproduce the issue is a request to share credentials unless a sanitization pass happens first. The findings table makes the triage fast, since each row carries the dotted path like database.password or imageCredentials.registry.password, mapping one-to-one onto the values structure you already know. Nested subchart values compound the risk, since a parent chart's values file often embeds credentials for several dependencies under their own namespaced keys.
Subchart values multiply, not just accumulate
A parent chart's values file typically nests each dependency's settings under that dependency's own key, so a database subchart's credentials, a cache subchart's credentials, and the parent chart's own credentials all end up in the same file without any of them looking unusual next to the others. Scanning by dotted path is what keeps that nesting from hiding anything, since a finding under redis.auth.password reads just as clearly as one under database.password.
Charts that pull in several dependencies can easily carry more credentials in their values file than in any of their own templates, which is exactly the kind of file most likely to get pasted into a support thread when a subchart misbehaves. Sanitizing before that paste matters as much for the third-party dependency's credentials as it does for the parent chart's own.
Auditing the rendered output with helm template
Rendering collapses every layer into the truth. Running helm template with your release's values files and --set flags produces the final multi-document manifest stream, and that stream is what actually reaches the cluster, so it is the artifact worth auditing before a deployment review. Values injected by --set never live in any file, which means a file-only audit misses them; the rendered output catches everything. Paste the render into the sanitizer and each document arrives labeled with its kind: Secrets get the unconditional .data redaction, Deployments contribute env-block findings, and securityContext smells appear at info severity.
Comparing values against the rendered stream
Diffing what the sanitizer finds in the raw values file against what it finds in the rendered output is a useful audit on its own, since a credential that only shows up after rendering came from a --set flag, a chart default, or a dependency's own values, none of which the source file alone would have revealed.
That comparison also tells you where a chart routes each credential internally: a value that lands in a Secret's .data map downstream is handled the way Kubernetes recommends, while the same value surfacing in a ConfigMap or a plain env block signals a template worth raising with the chart's maintainers.3 Building on this, comparing findings between the raw values file and the rendered output shows you exactly which credentials the chart templates move into Secret resources and which leak into ConfigMaps or env blocks.
Keeping credentials out of values permanently
The durable fix is values that reference rather than contain. Charts increasingly accept existingSecret-style parameters, where the values file names a pre-created Kubernetes Secret and the chart mounts it, so the values file carries only the Secret's name; combined with External Secrets Operator syncing that Secret from a cloud store, no credential exists in the Helm layer at all.4 Where a chart lacks such parameters, the values file can stay clean by supplying secret values at install time from environment variables or a wrapper like helm-secrets with SOPS encryption.5 The sanitizer's .env template supports the migration: every redacted key becomes a named variable, which is the list of values your install pipeline must provide through one of these mechanisms instead of the file.
Contributing existingSecret support upstream
Not every chart accepts an existingSecret-style parameter yet, and forking a chart just to add one is rarely worth it when a small upstream pull request accomplishes the same thing for every future user. Most chart maintainers are receptive to the change, since it is a narrow, additive parameter rather than a redesign of how the chart works.
Until that parameter lands, the .env template from a sanitizer pass is the practical bridge: it names every credential the chart currently expects inline, which is exactly the list an install pipeline needs to supply through environment variables or a wrapper like helm-secrets in the meantime. Contributing an existingSecret parameter upstream, where a chart lacks one, benefits every future user of that chart and not just your own installation.
When to use this
Sanitize values files before attaching them to issues on chart repositories, before pasting them into AI assistants for configuration help, and before committing environment-specific values to any shared repo. Render-and-scan with helm template belongs in pre-deployment review, especially for third-party charts whose templates you have not read line by line. It is also worth running whenever a chart is upgraded to a new major version, since new parameters sometimes introduce new places for credentials to land.
Notes
Both layers of a Helm workflow are plain YAML, so both sanitize directly: the values file you edit and the manifests helm template renders. Audit rendered output before sharing it, since it reflects the final merge of chart defaults, values files, and --set flags, including values that never appear in any file. Multi-document render output is split on --- and analyzed per document with kind labels, exactly like hand-written Kubernetes manifests.
Examples
Render a release for auditing
helm template my-release ./chart -f values-prod.yaml
Produces the final multi-document YAML stream, which pastes directly into the sanitizer.
Typical values finding
database.password
Caught by the key-name rule at medium severity, or by entropy at high severity when the value is a generated string.
Registry credential finding
imageCredentials.registry.password
Registry passwords in values files render into dockerconfigjson Secrets; both layers show findings.
Try in the tool
What this page covers
- helm template renders the final merge of defaults, values files, and --set flags for auditing
- values.yaml paths e.g. database.password or imageCredentials.registry.password
- existingSecret parameters let the chart reference a pre-created Secret instead of holding a value
Verify with the Cloud Config Sanitizer tool.
Try it in the tool ↑- 1.
The Helm Project, "Values Files," helm.sh, accessed July 2026. https://helm.sh/docs/chart_template_guide/values_files/
- 2.
The Helm Project, "helm template," helm.sh, accessed July 2026. https://helm.sh/docs/helm/helm_template/
- 3.
Kubernetes, "Secrets," kubernetes.io, accessed July 2026. https://kubernetes.io/docs/concepts/configuration/secret/
- 4.
External Secrets Operator, "Introduction - External Secrets Operator," external-secrets.io, accessed July 2026. https://external-secrets.io/latest/
- 5.
Jan-Otto Kröpke, "helm-secrets," github.com, accessed July 2026. https://github.com/jkroepke/helm-secrets
Both, for different reasons. The values file is what you share when discussing configuration, so it needs redaction before any paste. The rendered output from helm template reflects the complete merge including --set flags, so it is the layer to audit before deployment reviews. Each is plain YAML and parses directly.
Judge by the value's shape. Obvious placeholders like changeme fall below the entropy threshold and only trip the key-name rule at medium severity, which you can note and leave. A default that looks random was probably pasted from a real environment at some point, and treating it as live until proven otherwise is the safer read.
No, because they never enter the file; they exist only in the command line and the rendered output. This is exactly why auditing helm template output matters: it is the only artifact that contains every value from every source. Shell history containing --set password=... is a separate exposure worth clearing.
They shrink it. When a chart accepts the name of a pre-created Secret, the values file carries a reference with no sensitive content, so sanitizing it produces no credential findings by construction. The Secret itself is then managed outside Helm, ideally synced from a secret store, and audited under Kubernetes practices rather than chart practices.
Yes. CapyToolkit doesn't transmit pasted content anywhere; parsing and all rules execute in your browser, and the page works offline after loading. The sanitized download you send back to the client is generated locally too, so the only copies of their credentials remain theirs.
Sanitizing Docker Compose Files
Compose files are shared more casually than any other manifest. A compose.yaml defines a whole development stack in one readable file, which makes it the default attachment for bug reports, README examples, and how do I run this questions, and the environment blocks inside it are where database passwords and API keys settle first.1 Compose is plain YAML, so the Cloud Config Sanitizer parses it directly and applies the full rule set: connection strings in environment values are critical findings, random tokens cross the entropy threshold, and keys named password or api_key trip the key-name rule. Yet Compose also offers real alternatives, env_file indirection and the top-level secrets element,2 and the findings list is effectively a map of which values should migrate to them before the file travels again.
The environment block problem
Convenience wrote these credentials in. A service needs a database, the database needs a password, and the shortest path is typing both into the environment map, with POSTGRES_PASSWORD in the db service and a matching connection string in the app service; the stack works, and the file ships. Every later copy compounds the exposure, from the README that quotes the file to the issue report that attaches it. In the findings table, this pattern is unmistakable: the same secret often appears twice, once as a bare password caught by the key-name or entropy rule and once inside a postgres:// URL caught at critical severity.
Deduplication and multi-service stacks
That drop in count is a good sign, not a missed finding, since it means the same credential was simply typed in two places rather than two separate secrets existing. Consequently, redacting a compose file frequently deduplicates into fewer .env template entries than there were findings, since the template lists unique variable names.
Multi-service stacks compound the count further, since a message queue, a cache, and a search index each add their own credential pair to the same file. Auditing a stack with four or five services usually means checking four or five near-identical credential pairs rather than one, so scanning the whole file once beats reviewing each service block by hand.
Compose's three mechanisms, ranked
Interpolation is the everyday answer. Writing ${DB_PASSWORD} in the compose file makes Compose substitute the value from the shell environment or a local .env file at project root, so the committed file carries a reference while the value stays on the machine that runs it.2 The env_file element is the second mechanism, pointing a service at a file of KEY=value lines that never needs committing; it moves whole blocks of configuration out of the shared artifact, and the top-level secrets element is the strongest, mounting each secret into containers as a file rather than an environment variable, which keeps values out of docker inspect output and process environments entirely.1
Ranking the three mechanisms by where the value lives
Interpolation is the easiest migration since it requires no new file and no new element in the compose spec, just a rewritten value and a shell or .env-provided replacement. env_file is the next step up in isolation, since it moves a whole block of KEY=value pairs out of the tracked file entirely rather than one value at a time.
The secrets element earns its top ranking because of where the value ends up at runtime: a file mounted into the container rather than an entry in the process environment, which closes off an entire class of exposure through environment dumps and inherited child processes. Building on this, the sanitizer's .env template gives you the variable list that any of the three mechanisms will need, named and deduplicated.
Development files with production consequences
The dev-only label rarely stays true. Compose files written for local development get promoted into CI pipelines, staging boxes, and small production deployments, carrying their hardcoded credentials along, and even genuinely local files teach the habit that ends with a production compose file built the same way.3 Worse, local credentials are seldom unique; the database password in a dev compose file has a way of matching the one in staging.4
From suspicion to evidence in one paste
Guessing whether a dev compose file is safe to share wastes more time than just running it through the scan, since the answer either way takes seconds to get. Scanning takes seconds and settles the question with evidence. Paste the file, read the findings, and migrate anything critical or high into interpolation or env_file form before the file is shared again.5
A compose file whose only secrets are ${VARIABLE} references is safe in a README, a repo, or an AI prompt without further thought. Building this discipline once, at the start of a project, costs far less than retrofitting it after a compose file has already circulated through several teams. New projects that adopt the pattern from their first commit never have that retrofit to do at all.
When to use this
Sanitize before publishing a compose file in documentation or a repository, before attaching one to a bug report or support thread, and before pasting a stack definition into an AI assistant for help. Files inherited from other teams deserve a scan on arrival, since compose files accumulate credentials silently over their working life. Onboarding a new contributor is another good trigger, since the compose file they receive is often the first config file they ever read in the project.
Notes
Compose's environment element accepts both map and list syntax, and both parse as ordinary YAML, so findings carry paths like services.api.environment.DATABASE_URL or an indexed list position. Variable interpolations like ${DB_PASSWORD} contain no secret and produce no findings, which makes them the pattern to migrate toward. The sanitized download preserves the file's structure, so a redacted compose file still documents the stack completely.
Examples
Connection string in a service
services.api.environment.DATABASE_URL
postgres:// URLs with credentials are critical findings; the variable name lands in the .env template.
Interpolation reference (no finding)
POSTGRES_PASSWORD: ${DB_PASSWORD} References contain no secret and produce no findings; this is the target pattern after migration.
File-based secret mount
secrets.db_password.file: ./db_password.txt
The compose file references a path; the secret value itself never appears in the YAML.
Try in the tool
What this page covers
- Interpolation ${DB_PASSWORD} substitutes from the shell or a local .env file; produces no finding
- env_file points a service at KEY=value lines that never need committing
- top-level secrets element mounts a secret as a file in the container, not a process environment variable
Verify with the Cloud Config Sanitizer tool.
Try it in the tool ↑- 1.
Docker Docs, "Set, use, and manage variables in a Compose file with interpolation," docs.docker.com, accessed July 2026. https://docs.docker.com/compose/how-tos/environment-variables/variable-interpolation
- 2.
Docker Docs, "Manage secrets securely in Docker Compose," docs.docker.com, accessed July 2026. https://docs.docker.com/compose/how-tos/use-secrets
- 3.
Antonio Biondillo, "Manage credentials with Tekton and OpenShift on IBM Cloud," developers.redhat.com, December 2025. https://developers.redhat.com/articles/2025/12/16/manage-credentials-tekton-openshift-ibm-cloud
- 4.
GitGuardian, "The State of Secrets Sprawl 2026," blog.gitguardian.com, March 2026. https://blog.gitguardian.com/the-state-of-secrets-sprawl-2026/
- 5.
OWASP, "Secrets Management Cheat Sheet," cheatsheetseries.owasp.org, accessed July 2026. https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
Yes. The map form and the list form of KEY=value strings are both valid YAML, and the analyzer walks whichever structure it finds. Findings under the list form carry an indexed path to the exact entry, while the map form yields the variable name directly in the dotted path.
Because they contain nothing sensitive. An interpolation placeholder is a reference resolved by Compose at runtime from the shell or a local .env file, so the committed YAML never holds the value. That is precisely why migrating hardcoded values to interpolation is the standard fix for the findings the scan does raise.
For sensitive values, yes. Secrets mount as files inside the container instead of living in the process environment, which keeps them out of docker inspect, crash dumps, and child-process environments. Environment variables remain fine for non-sensitive configuration, and interpolation keeps either mechanism's values out of the shared file.
It is still worth the seconds. Local files migrate into CI and small deployments, get quoted in documentation, and share passwords with less local environments more often than anyone intends. A scan that finds nothing costs almost nothing; one that finds a reused credential just saved you a rotation.
No. CapyToolkit doesn't upload or log pasted content; the YAML parser and every detection rule run inside your browser, and the sanitized download plus .env template are generated locally. After the initial page load the tool even works offline, so nothing about the check requires a network.
Sanitizing GitHub Actions Workflows
A workflow file in a public repository is a published document.1 Everything under .github/workflows ships with the code, is indexed by search engines and scanners alike, and runs with whatever credentials it can reach, which makes a hardcoded token in a workflow the fastest possible route from commit to compromise.2 GitHub's answer is the secrets context: values stored as repository or environment secrets and referenced as expressions that resolve only at run time.3 The failure mode is bypassing it, pasting a real token into an env: block to get a deploy working under deadline. Workflow files are ordinary YAML, so the Cloud Config Sanitizer audits them directly, flagging real credentials while correctly ignoring the expression references, and the distinction between those two is the entire security model of Actions configuration.
How real tokens end up beside the secrets context
Deadlines write the hardcoded ones. A deploy step fails with an authentication error, the fix needs proving before a demo, and the fastest experiment is replacing ${{ secrets.DEPLOY_TOKEN }} with the literal token to rule out a secrets configuration problem; the experiment works, the commit lands, and the token is now in the repository. Third-party action examples are the second source, since documentation snippets sometimes show placeholder keys that get replaced with real ones during copy-paste.1 In the findings table, a ghp_ GitHub token in a workflow is a critical row with its path, such as jobs.deploy.steps[2].env.GH_TOKEN,4 and the same goes for AWS keys and Slack tokens wired into notification steps.5
A quieter variant: composite action defaults
That default hides in plain sight, since a composite action's inputs read like ordinary configuration rather than like the one place a real token could leak to every caller. Consequently, a pre-commit scan of changed workflow files catches precisely the highest-blast-radius mistake Actions users make.
Composite action inputs are a quieter variant of the same slip, since a hardcoded default value in action.yml ships to every workflow that calls it. A repository that reviews env: blocks in its own workflows but never opens third-party action.yml files is still exposed through exactly this path, since the default travels silently into every caller's run.
What the secrets context does and does not protect
The expression syntax solves storage, not usage. A value referenced as ${{ secrets.NAME }} never appears in the committed YAML, and GitHub masks it in logs when it appears verbatim,3 which covers the file-level leak completely. Yet workflows can still exfiltrate resolved secrets at run time: an echo that transforms the value defeats log masking, an untrusted action receives whatever env its step passes,2 and pull requests from forks interact with secrets under special rules GitHub documents for exactly that reason.
Where the scan's coverage actually ends
The distinction is worth stating precisely: the sanitizer confirms that a workflow file references secrets rather than embedding them, which is a file-level guarantee about what gets committed. It says nothing about what a step does with a resolved value once the runner has it in memory.
An echo that prints a transformed version of a secret, a third-party action that logs more than it should, or a step that writes a value to a file inside the workspace all happen after the file-level check has already passed. The sanitizer's scope is the file layer: it verifies that the YAML you commit or share contains references rather than values. Runtime hygiene, minimal env scoping per step, pinned action versions, and reviewed third-party actions, is the complementary discipline the scan cannot check. Treat the file-layer pass as the first gate a workflow must clear, not the only one it needs.
Auditing workflow files at scale
Workflows multiply quietly. A mature repository accumulates deploy, test, release, and scheduled maintenance workflows, plus reusable ones that others call, and each is a YAML file that parses in one paste. Because the expressions produce no findings, scanning an entire directory's worth of workflows yields a short, high-signal list: clean files return only the occasional entropy hit on a pinned action SHA, which the key path identifies immediately as benign, while any credential-shaped finding deserves attention.
A clean scan across a whole workflows directory
Running the same paste against every file under .github/workflows one at a time turns an assumption about the repository's hygiene into a checked fact, since a workflow added two years ago by a since-departed contributor gets exactly the same scrutiny as one added yesterday.
Reusable workflow inputs deserve a second look during that same pass, since a default value baked into the reusable workflow itself propagates to every repository that calls it, not just the one where it was defined. Building on this, the same audit habit extends to composite actions and reusable workflow inputs, where defaults occasionally carry real values. A repository whose workflows all scan clean has effectively proven that its CI credentials live in the secrets store where rotation and access control actually work.
When to use this
Scan workflow files before committing changes to .github/workflows, when making a private repository public, and when reviewing contributions that touch CI configuration. The check matters most for public repositories, where workflow files are world-readable the moment they merge, but private repos benefit equally since their audiences grow over time. External contributors submitting workflow changes through a fork deserve the same scan before their pull request merges, since their commits carry the same risk as an internal change.
Notes
Expression references like ${{ secrets.DEPLOY_KEY }} are literal strings to the parser and match no secret pattern, so they produce no findings; only actual credential values are flagged. This makes the scan's signal unusually clean for workflows: any critical or high finding in a workflow file is almost certainly a real hardcoded secret. GitHub-format tokens (ghp_) are matched by a dedicated critical rule, and cloud provider keys by theirs.
Examples
Safe reference (no finding)
GH_TOKEN: ${{ secrets.GH_TOKEN }} Expression references resolve at run time and contain no secret; the scan correctly ignores them.
Hardcoded token finding
jobs.deploy.steps[2].env.GH_TOKEN
A literal ghp_ value here is a critical finding; rotate the token and move the value to repository secrets.
Pinned action SHA (benign entropy hit)
uses: actions/checkout@8f4b7f8
Commit SHAs can score as high entropy; the key path shows it is a version pin, not a credential.
Try in the tool
What this page covers
- Expression references ${{ secrets.DEPLOY_TOKEN }} matches no secret pattern and produces no finding
- ghp_ tokens a literal GitHub token in an env: block is flagged by a dedicated critical rule
- Composite action defaults a hardcoded default in action.yml ships to every workflow that calls it
- Pinned action SHA can score as high entropy, but the key path shows it is a version pin, not a credential
Verify with the Cloud Config Sanitizer tool.
Try it in the tool ↑- 1.
OWASP, "Secrets Management Cheat Sheet," cheatsheetseries.owasp.org, accessed July 2026. https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html
- 2.
GitHub Docs, "Secure use reference for Actions," docs.github.com, accessed July 2026. https://docs.github.com/en/actions/reference/security/secure-use
- 3.
GitHub Docs, "Using secrets in GitHub Actions," docs.github.com, accessed July 2026. https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/use-secrets
- 4.
Indigo K, "Behind GitHub's new authentication token formats," github.blog, April 2021. https://github.blog/engineering/platform-security/behind-githubs-new-authentication-token-formats/
- 5.
Eric Fourrier, "The State of Secrets Sprawl 2024," blog.gitguardian.com, accessed July 2026. https://blog.gitguardian.com/the-state-of-secrets-sprawl-2024/
Because the committed file contains only the expression text, which matches no credential pattern and has low entropy. The real value lives in GitHub's secrets store and is injected at run time. This is the correct outcome: the reference pattern is what the scan is verifying you use.
Rotate the credential immediately, since public workflow files are scanned by third parties within minutes of pushing. GitHub automatically revokes its own token formats found in public repos, but cloud keys and third-party tokens need manual rotation. Then rewrite history if feasible, and move the replacement value into repository or environment secrets.
No, they cover different layers. GitHub masks registered secret values that appear in workflow logs at run time. The sanitizer audits the workflow file itself before it is committed or shared. A hardcoded token defeats both eventually, which is why catching it at the file layer, before the commit, is the effective moment.
Yes, arguably more so. Scheduled workflows run with standing credentials and get reviewed less often after initial setup, and reusable workflows propagate whatever their defaults contain to every caller. Both are plain YAML and scan in one paste, with input defaults appearing at clear dotted paths in any findings.
Yes. CapyToolkit doesn't transmit pasted workflow content; every rule executes in your browser and nothing persists after the tab closes. The sanitized copy you share back, with placeholders in place of any hardcoded values, is generated locally as well.
Sanitizing Crossplane Compositions
Crossplane manages cloud credentials as Kubernetes resources, for better and worse. Compositions, claims, and ProviderConfigs are all YAML manifests, which means the machinery that provisions your databases and buckets is defined in files that travel through the same reviews, tickets, and repositories as any other config. The design keeps credentials out of most files: ProviderConfigs reference Secrets rather than containing keys,1 and provisioned resources write their generated credentials to connection secrets via writeConnectionSecretToRef.2 Yet the referenced Secrets themselves, pasted alongside compositions for debugging, carry real cloud keys in base64,3 and the Cloud Config Sanitizer flags every value in them as critical by position. Because Crossplane files are ordinary multi-document YAML, the whole stack audits in one paste, with each document labeled by its kind.4
Where credentials actually live in a Crossplane setup
Indirection is the architecture. A ProviderConfig for AWS points its credentials field at a Kubernetes Secret through secretRef,1 so the ProviderConfig itself contains a name and namespace rather than a key; compositions and claims sit even further from the credentials, describing desired infrastructure with no secret material at all. The Secret at the end of the chain is the concentrated risk: it holds the cloud credential file, often a full access key pair in base64, that grants Crossplane its power to create and destroy infrastructure. Consequently, the dangerous paste is rarely the composition alone; it is the debugging bundle where someone includes the referenced Secret so a helper can see the whole picture. Scanning the bundle flags that Secret's every value as critical while the composition documents pass clean.
One credential, many managed resources
That fan-out is the part easy to underestimate when staring at a single composition file, since nothing in the YAML itself hints at how many resources trace back to the same ProviderConfig. Furthermore, a single ProviderConfig commonly backs dozens of managed resources across a cluster,5 so one exposed credential can carry consequences far beyond the composition being debugged. That scale is exactly why rotating the credential promptly matters so much once it is exposed.
Connection secrets and generated credentials
Provisioning creates new secrets to leak. When Crossplane creates a database, the generated admin password and endpoint arrive as a connection secret, placed where writeConnectionSecretToRef2 pointed, and consuming applications mount it from there; the pattern is sound because the value never touches a source file. The exposure returns through observation: running kubectl get secret -o yaml to inspect a connection secret3 produces YAML with the live credentials in base64, and that output is exactly what gets pasted into tickets when a connection fails.
Connection secrets never touch a source file, until someone reads one
The design genuinely holds for the written path: a generated password created by Crossplane goes straight into a connection secret and a consuming pod's env, with no step where it passes through a file a human edits or commits. That is real protection, and it is also easy to mistake for complete protection.
The gap opens the moment someone needs to debug the connection instead of just consuming it, since kubectl get secret -o yaml is the natural next command and its output is exactly as sensitive as the source manifests the rest of this workflow already protects. Building on this, the sanitizer's unconditional Secret rule catches these pastes completely, since every .data value is redacted by position, and the entropy rule provides a second net for generated passwords that appear anywhere else, comfortably above the 4.5-bit threshold as machine-generated values are.
Auditing a Crossplane repository end to end
Platform repositories mix trust levels. A typical Crossplane setup keeps XRDs, Compositions, ProviderConfigs, and example claims in one repository, and the examples are where hygiene slips, since a working example is often a production claim with the names changed, occasionally accompanied by the Secret manifest that made it work. Pasting each file, or each multi-document bundle,4 into the sanitizer sorts the repository quickly: kind labels identify what each document is, credential findings concentrate in Secret documents and the occasional hardcoded endpoint string, and info-level smells surface on any embedded pod specs.
Keeping the habit current as the platform grows
A GitOps controller does not pause to ask whether a manifest is safe before applying it, which is exactly why the reference-only check has to happen earlier in the pipeline. For files heading into a GitOps flow, the same pass verifies the repository holds only references before a controller starts applying whatever it contains.
The sanitized outputs, meanwhile, make safe documentation examples by construction. Repeating the scan whenever a new provider or composition function is added keeps the habit current instead of letting it lapse after the initial platform build-out. New composition functions in particular tend to introduce new places for a hardcoded value to slip in, since they are new code paths nobody has reviewed for this yet.
When to use this
Scan Crossplane files before sharing debugging bundles that pair compositions with their referenced Secrets, before publishing example claims in documentation, and when auditing a platform repository ahead of GitOps adoption. Output from kubectl get on connection secrets should always pass through the sanitizer before leaving your terminal for a ticket or chat.
Notes
Crossplane's resource model works in the sanitizer's favor: kinds like Composition, CompositeResourceDefinition, and ProviderConfig appear as document labels, making a multi-document audit readable at a glance. The high-value target is any document of kind Secret accompanying the Crossplane resources, since ProviderConfig credential references point at exactly those. Connection secrets written by Crossplane at runtime never appear in source manifests, but kubectl get -o yaml output of them does, and deserves the same scan before sharing.
Examples
ProviderConfig credential reference (no finding)
spec.credentials.secretRef.name: aws-creds
The reference names a Secret without containing it; scanning the ProviderConfig alone yields no credential findings.
Referenced Secret value
data.creds
The AWS credential file in the referenced Secret is flagged critical unconditionally, without base64 decoding.
Connection secret output
kubectl get secret db-conn -o yaml
Generated database credentials in the output are caught by the Secret rule; sanitize before pasting anywhere.
Try in the tool
What this page covers
- ProviderConfig secretRef names a Secret without containing it; scanning the reference alone finds nothing
- writeConnectionSecretToRef generated credentials land in a connection Secret, never in source manifests
- kubectl get secret -o yaml live output has the same critical exposure as the source Secret and needs the same scan
Verify with the Cloud Config Sanitizer tool.
Try it in the tool ↑- 1.
Crossplane contributors, "ProviderConfig — Crossplane Docs," github.com, accessed July 2026. https://docs.crossplane.io/latest/get-started/get-started-with-managed-resources/
- 2.
Crossplane contributors, "writeConnectionSecretToRef — Crossplane Docs," github.com, accessed July 2026. https://docs.crossplane.io/latest/managed-resources/managed-resources/
- 3.
Kubernetes contributors, "Distribute Credentials Securely Using Secrets," kubernetes.io, accessed July 2026. https://kubernetes.io/docs/tasks/inject-data-application/distribute-credentials-secure
- 4.
Stack Overflow, "Getting error while installing Crossplane ProviderConfig," stackoverflow.com, accessed July 2026. https://stackoverflow.com/questions/76458072/getting-error-while-installing-the-crossplane-providerconfig-in-kubernetes-clust
- 5.
Kubernetes contributors, "Managing Workloads," kubernetes.io, accessed July 2026. https://kubernetes.io/docs/concepts/workloads/management
Rarely, and that is by design. Compositions describe resource shapes and patches, referencing credentials through ProviderConfigs and writing generated ones to connection secrets. Findings in a composition are more often hardcoded endpoints or tokens in annotations. The Secret manifests that travel alongside compositions in debugging bundles are where the critical findings concentrate.
Each document separated by --- is parsed and reported independently, labeled with its kind. The XRD and Composition typically pass clean, while every value in the Secret document is flagged critical by position. The sanitized download preserves all documents and their order, with only the Secret values replaced by placeholders.
Yes. The rule is positional: any value under .data or .stringData in a document whose kind is Secret is critical regardless of content. Base64-wrapped key files, JSON service accounts, and access key pairs are all redacted identically, which is safer than relying on decoded pattern matches.
Kind names and API versions do not gate the scanning rules. Whatever kinds your Crossplane version uses, string values are checked against the provider patterns, entropy threshold, and key-name list, and any document whose kind is Secret gets the unconditional treatment. Kind labels in the findings simply reflect what your file declares.
Yes, into this tool specifically. CapyToolkit doesn't send your manifests anywhere; analysis runs in your browser and survives going offline after page load. The caution belongs to the next paste: share the sanitized version, since the raw bundle may grant whoever reads it the same infrastructure control Crossplane has.