# Do credential placeholders still reveal operational details?

A redacted credential can still explain your production layout to anyone who sees it. Teams often congratulate themselves for replacing a token with `***`, then leave the tenant subdomain, administrator role, region, account number, secret path, and injection syntax untouched. The token stayed private. The operating model did not.

That distinction matters most when configurations, logs, prompts, tickets, and agent transcripts travel farther than the credentials themselves. A determined intruder does not need every secret in one screenshot. They need enough accurate detail to choose a target, impersonate a workflow, write a convincing support request, or turn one foothold into a useful map.

## A placeholder is metadata with a different blast radius

A credential value proves possession. A placeholder usually does not. That makes the two kinds of data different, but it does not make the placeholder harmless.

Consider this deployment fragment:

```yaml
billing_export:
  url: https://acme-prod.eu.example.net/v2/exports
  authorization: Bearer ${ACME_PROD_EU_BILLING_ADMIN_TOKEN}
  tenant: northstar-retail
  credential_ref: vault://teams/finance/prod/billing-export-admin
```

No raw token appears here. Yet a reader learns that the organization has a production tenant in Europe, runs an export API, separates finance credentials, uses a vault, and has an administrator-grade identity for billing exports. The string `northstar-retail` may identify a customer. The hostname may expose a naming convention that applies to other services. The vault path reveals which group likely owns the credential and where an attacker might look after compromising an internal developer tool.

Security reviews often collapse this into a binary question: "Does it contain a secret?" That question is too narrow. Ask two questions instead:

1. Can someone authenticate or authorize an action with this value?
2. Can someone use this value to understand, target, impersonate, or correlate our operation?

The first answer determines whether you have leaked a credential. The second determines whether you have exposed operational metadata. Both deserve controls, but they need different controls. Trying to treat every alias as a password produces unusable logs. Treating every alias as public produces reconnaissance material by accident.

The AWS IAM documentation makes the distinction plainly for Amazon Resource Names. An ARN identifies a resource using fields such as partition, service, region, account ID, resource type, and resource ID. AWS says ARNs are not credentials, and that is correct. But the same documentation shows why they can carry useful structural information. A resource reference may identify the cloud partition, regional placement, owning account, service, and a resource name or path. "Not secret" is not the same claim as "safe in every audience."

## Aliases often disclose ownership and privilege

Aliases exist to help humans remember what a credential does. That convenience is exactly why they leak context.

`STRIPE_TOKEN` says little beyond provider choice. `PROD_US_CARD_REFUNDS_SUPERVISOR_TOKEN` says an enormous amount. It points to production, a geography, card payments, refunds, a likely business function, and elevated authority. In an incident channel, an attacker who has seen that label can refer to the right system with the right vocabulary. That improves phishing, pretexting, and social engineering before any technical exploit begins.

The worst names combine four types of information:

- Environment: `prod`, `staging`, `dr`, `sandbox`
- Authority: `admin`, `root`, `write`, `breakglass`
- Business function: `payroll`, `claims`, `refunds`, `identity`
- Tenant or owner: a customer name, acquisition code, team name, or account number

A label can also expose relationships. `SALESFORCE_TO_ERP_SYNC_PROD` tells a reader that two systems exchange data. `PAYROLL_SFTP_VENDOR_A` suggests an external transfer path. `EMERGENCY_DB_RESTORE_KEY` identifies a credential worth pursuing even if the actual value remains out of reach.

Do not solve this by giving every credential a meaningless random name and calling the problem finished. Operators need to know what they are approving, rotating, and debugging. The answer is to separate the human-facing name by audience.

Use a detailed record in the restricted credential registry. That record can say who owns the credential, which account it reaches, what it can do, and why it exists. Use a less descriptive runtime alias in broad surfaces such as CI output, agent prompts, or approval cards. For example:

```text
Restricted registry record
Owner: Revenue systems
Purpose: Submit production refund adjustments
Target: payments tenant northstar-retail in EU
Authority: write refund adjustments
Runtime alias: cred_4d91

Broad operational event
credential=cred_4d91 action=refund_adjustment outcome=denied
```

The runtime alias still supports correlation. A responder can find repeated failures tied to `cred_4d91`, while only people with registry access can resolve it to the full business context.

Be selective. An approval dialog may need to show that an action will modify production refunds, because hiding the consequence from the human defeats approval. It does not need to show the customer tenant, cloud account identifier, vault path, or credential name to make that decision.

## Formats reveal more than most redaction rules expect

A fixed format tells observers what system generated the value, how to validate it, and sometimes which fields they should hunt for elsewhere.

Take these references:

```text
arn:aws:iam::123456789012:role/ci-prod-deployer
projects/810245991002/secrets/payments-prod-api/versions/latest
https://tenant-44.api.vendor.example/v1/invoices
postgresql://reporting:${DB_PASSWORD}@db-prod-2.internal:5432/revenue
```

The password can be absent in every case, but the remaining syntax exposes different things. The ARN carries cloud partition, service, account structure, and a role name. The secret-manager reference identifies a project, a secret purpose, and a versioning practice. The API hostname shows a tenant model. The database connection shape identifies a protocol, host naming scheme, port, database name, and a password injection point.

RFC 3986 defines the major parts of a URI, including scheme, authority, host, port, path, query, and fragment. It also calls the `user:password@host` form in URI user information deprecated and warns applications not to render data after the first colon as clear text. The practical lesson extends beyond literal passwords: a URI is a container with multiple fields, and masking one field does not erase the rest of the operational story.

Formats create two recurring mistakes.

The first is partial masking. A log transformer sees `Authorization: Bearer` and replaces the next token, but prints a complete signed URL whose query parameters include `X-Amz-Credential`, an access key identifier, date, region, service, and scope. The secret signature may be hidden, but the request still exposes the identity format and target.

The second is treating a reference as an opaque string when it is structured. A pattern such as `vault://path/to/item#field` contains separate pieces with separate sensitivity. If one team removes the field value and another prints the complete path, neither team has defined an audience rule. They have only scattered string substitutions.

Use structured handling where your tooling allows it. Parse a URI into fields. Parse a cloud resource identifier by its documented grammar. Assign a classification to each component. Do not rely on one regular expression that assumes every sensitive string looks like an API key.

## Tenant names and endpoints can identify people and systems

Tenant labels are especially easy to dismiss because they are often visible in normal product URLs. Their risk depends on what they connect.

A public company name in a public hostname may add little on its own. The same name beside `prod`, a privileged API route, an internal host, a support case, or an error message creates a useful correlation point. It can tell an observer that a named customer uses a particular product, resides in a specific region, or has access to an integration that other customers do not.

Internal names cause more trouble. Teams use labels such as `payer-west`, `acquisition-cedar`, `health-data`, or `gov-contracts` because the names make operations faster. They also disclose business activity, regulated workloads, and organizational relationships. A placeholder like `${ACQUISITION_CEDAR_SFTP_KEY}` can reveal a transaction before anyone intended to announce it.

The same applies to endpoint structure. Compare these two events:

```text
request failed: credential=cred_4d91 target_class=payment_export status=403

request failed: POST https://northstar-retail.prod-payments.eu.internal/v3/refunds/export
credential=PROD_NORTHSTAR_REFUNDS_ADMIN status=403
```

The first one is still actionable if the event links to a restricted trace. The second one is a compact account map. It tells the reader the tenant, stage, domain layout, region, functional service, route, operation, and privilege level.

Do not remove all target information by reflex. Operators cannot investigate a vague event like `request failed` when a service is down. Replace exact identifiers with controlled classes when broad visibility is necessary: `payment_export`, `customer_data_write`, `artifact_publish`, `repository_deploy`. Put the exact endpoint in a restricted event record where access is justified.

This is where many teams get the classification wrong. They classify a tenant name as ordinary text because it is not a credential. The sensible classification is contextual. A tenant name in a private, access-controlled audit system may be appropriate. The same name in an agent transcript copied into a pull request comment may not be.

## Substitution markers expose the trust path

A marker such as `${TOKEN}` has meaning beyond the missing value. It says a process will supply a value later. The spelling often says which process.

These examples look similar to a developer, but they reveal different trust boundaries:

```text
${GITHUB_ACTIONS_DEPLOY_TOKEN}
${{ secrets.DEPLOY_TOKEN }}
{{ vault "kv/prod/deploy" "token" }}
secretKeyRef: name: prod-deployer key: token
op://Infrastructure/Production Deploy/token
```

The first implies an environment-variable injection model and a CI identity. The second identifies a workflow secret context. The third suggests a template engine and a vault path. The fourth points to an orchestrator object with a namespace and key. The fifth tells a reader that a particular password manager naming convention exists.

An attacker who sees these strings has not stolen a secret. They have learned where to focus after gaining code execution, a build-log role, repository access, or a support-channel foothold. They may know whether to look for environment variables, mounted files, an API to a secret store, or a local desktop session.

The marker can also reveal when substitution happens. A reference committed to source may resolve during CI. A reference in a generated file may resolve during deployment. A placeholder inside a request template may resolve at runtime. Those moments have different controls and different likely logs. If you do not document them, people will add broad debug output when a failure occurs, and that is often when the actual secret escapes.

A useful inventory tracks the trust path without putting it in every artifact. For each credential, record the source of the reference, the process that resolves it, the process that consumes it, and the places that may log the result. That makes it possible to review exposure deliberately instead of discovering it during a postmortem.

## A masked log can still hand an attacker a working plan

The failure usually arrives through a debugging change that seemed reasonable at the time.

Imagine a deployment job that calls a vendor API. The token is held in a CI secret. The shell script enables command tracing after intermittent authentication failures. The CI platform masks the literal token, so the team believes the logs are safe.

The trace produces this:

```text
+ API_BASE=https://tenant-44.eu.vendor.example
+ TOKEN=***
+ curl -X POST https://tenant-44.eu.vendor.example/v1/admin/export \
  -H 'Authorization: Bearer ***' \
  -H 'X-Account: 784221' \
  -H 'X-Client-Name: finance-nightly-export'
< HTTP/2 403
< x-request-id: 81b5b8e1
< x-region: eu-central
```

Nobody can reuse the masked bearer token. But a person with log access now knows the vendor, tenant pattern, administrative endpoint, account number, workload name, region, and rough schedule. They can search the same organization for `finance-nightly-export`, target the account owner with a credible request, or use the endpoint details after finding a separate credential.

A common response is "we will mask more." That helps, but it misses the design failure. The job printed a complete request plan into a broadly retained log. A redactor cannot know whether `tenant-44`, `784221`, or `finance-nightly-export` matters to your business. It can only match strings you tell it to match.

GitHub's security documentation makes a related warning: secret redaction largely depends on exact matches, and structured data such as JSON, XML, or YAML makes reliable masking less likely. It also advises registering generated sensitive values, such as a JWT created from another secret, for masking. That is sound advice, but it is a backstop. The safer debugging contract is to emit a request summary designed for people, not a shell trace designed for a terminal.

Replace the trace with a bounded event:

```text
outbound_call
operation=finance_export
credential=cred_4d91
target_class=vendor_admin_api
method=POST
result=403
request_id=81b5b8e1
```

Keep the exact request only in a restricted diagnostic record if the vendor requires it for support. Set a short retention rule for that record. Do not paste it into an issue, chat thread, or agent task.

## Classify the reference, not only the resolved secret

A practical policy needs more than two labels. "Secret" and "not secret" force bad tradeoffs because the data around credentials has several exposure levels.

Use a small classification scheme that people can apply during code review:

| Class | Examples | Broad logs and prompts | Restricted operational records |
| --- | --- | --- | --- |
| Credential value | tokens, private keys, passwords, signed request signatures | Never | Only when unavoidable, encrypted and tightly retained |
| Direct identifier | tenant names, account IDs, exact hostnames, vault paths | Usually remove or replace | Permit when needed for investigation |
| Structural metadata | provider, environment, service class, credential injection model | Permit only when the audience needs it | Permit |
| Operational label | opaque alias, action class, outcome, correlation token | Permit | Permit |

The table is intentionally not a compliance standard. Your business may need a stricter rule for customer identifiers or a looser rule inside a locked incident system. The point is that someone has to decide before an error handler emits the text.

OWASP's Logging Cheat Sheet takes the same general position for logs. It says access tokens, passwords, database connection strings, encryption keys, and commercially sensitive information should usually be removed, masked, sanitized, hashed, or encrypted. It also calls out file paths and internal network names as data that may need special treatment. That last category is where credential placeholders often belong. Their value is absent, but the surrounding path and name can still be sensitive.

Do not label every structural field as prohibited. If every event loses its system and action context, responders will route around the logging policy with screenshots and ad hoc debug flags. A good event gives them enough to answer: what attempted an action, which approved capability did it use, what class of target did it reach, and what happened? It leaves out details that only matter to someone with approved access to the investigation.

## Build references for the least trusted audience

The fastest fix is to decide where a reference will appear before you decide what it should be called. A credential name that works in a private vault interface may be wrong for a build log or an agent approval card.

Start with four surfaces: source control, runtime configuration, user-visible approval, and audit records. For each one, identify the least trusted legitimate reader. That may be every repository contributor, a CI log viewer, a support engineer, an automated agent, or a small incident group. Then give that surface the minimum information needed for its job.

A workable convention has three parts:

```yaml
# Broadly visible configuration
export_job:
  action: finance_export
  credential_alias: cred_4d91
  target_class: vendor_admin_api

# Restricted credential registry
cred_4d91:
  owner: revenue-systems
  approved_action: finance_export
  exact_target: https://tenant-44.eu.vendor.example/v1/admin/export
  secret_reference: restricted-store-record
```

The configuration remains readable. A reviewer can see that the job exports finance data through a vendor administrative API. The exact tenant, endpoint, and secret-store reference remain in the registry, where they belong.

This convention prevents a second failure too: aliases drift less than copied endpoints. When a tenant migrates or a provider changes its hostname, update the restricted mapping and retain the same action-level interface. The calling job does not need to know every infrastructure detail.

A small scanner can catch the obvious cases before configuration lands in a shared repository. This example flags aliases and references that contain environment, privilege, or tenant terms. It is deliberately simple. It should start a review, not block a release without human judgment.

```python
import re
from pathlib import Path

pattern = re.compile(
    r"(?i)(prod|staging|admin|root|breakglass|tenant|customer|"
    r"account|vault://|secretkeyref|secrets\.)"
)

for path in Path(".").rglob("*"):
    if path.is_file() and path.suffix in {".yml", ".yaml", ".json", ".env", ".txt"}:
        for number, line in enumerate(path.read_text(errors="ignore").splitlines(), 1):
            if pattern.search(line):
                print(f"REVIEW {path}:{number}: {line.strip()}")
```

A useful output looks like this:

```text
REVIEW deploy.yaml:7: credential_alias: PROD_NORTHSTAR_REFUNDS_ADMIN
REVIEW deploy.yaml:11: secret_reference: vault://finance/prod/northstar/refunds
```

The reviewer should ask whether the term needs to be present in that file and whether the file's readers need it. Replacing every word mechanically is how teams lose traceability. Moving the sensitive detail to the restricted mapping is usually the better repair.

## Agent access needs action context, not credential anatomy

Autonomous coding agents make this issue sharper because they consume configuration and emit transcripts at high volume. If an agent can read a repository, its prompt context may include aliases, secret references, endpoint templates, and failed command output. Even if the agent never receives a token, it can receive an operating manual for the credentials it cannot read.

Give an agent the smallest useful action vocabulary. It may need to request `finance_export` against a `vendor_admin_api` target class. It rarely needs the vault path, a tenant ID, the authorization header format, or the exact environment variable that would resolve a secret. If it cannot see those fields, it cannot repeat them into a patch description, terminal transcript, or external tool call.

This also improves human approval. A person approves an action based on consequence: "send a finance export to the approved vendor API." They do not make a better security decision because an interface displays `vault://teams/finance/prod/...` or a customer-specific hostname. Those details create noise for the approver and information for anyone who later sees the record.

Sallyport follows this separation by keeping API and SSH credentials in its encrypted vault and executing the action rather than passing credentials to the agent. Its session and activity records can show the agent run and individual calls without making secret values available to the agent. That boundary is useful, but the action names, endpoints, and aliases you choose still need the same metadata review.

Do not wait for a token leak to inspect these artifacts. Pull a representative CI log, agent transcript, approval prompt, configuration file, and support ticket. Read them as a contractor with broad project access would read them. Circle every field that identifies a tenant, a privileged role, a secret store, a cloud account, or a network target. Then decide which fields support the task and which fields merely tell a story your systems did not need to publish.

The credential value is the first thing to protect. The map around it is the next thing to stop handing out.
