Home » Secrets in Development: Keeping Credentials Out of Code and Logs

Secrets in Development: Keeping Credentials Out of Code and Logs

0 comments
An ivory ceramic sphere rests inside nested protective shells on dark velvet, with a green-lined lid lifted behind it.

Keep credential values outside versioned code, give each development workflow only the authority it needs, and design logs so they never receive secret-bearing data by default. Make revocation part of the setup procedure: a credential that cannot be quickly invalidated is difficult to contain after exposure.

The important lifecycle is provision, deliver, use, observe, rotate, and revoke. Ignoring a local configuration file addresses only one possible route into Git. It does not address shell history, process environments, debug logs, build artifacts, screenshots, or credentials copied into a support conversation.

Start with the capability, not the variable name

Before creating a development credential, describe the work it authorizes. A local catalog viewer may need to read a test dataset. It does not need to administer production users or change billing configuration. Use separate identities or scopes where the provider supports them, and keep production access out of ordinary development environments.

OWASP's secrets-management guidance emphasizes fine-grained access, lifecycle management, auditability, and rapid revocation. Apply those principles to a concrete record rather than a collection of shared values:

Field Example without a secret value
Purpose Read synthetic catalog items during local development
Principal and environment Dedicated development service identity; test account
Allowed effect Read catalog items; no customer or administrative operations
Delivery Approved local secret-store integration
Owner Team responsible for the catalog integration
Expiry and revocation Provider-supported expiry; documented credential identifier and revocation path

Do not record the credential itself in that inventory. Record enough to find and revoke it without circulating the value. A masked suffix can help an operator identify a key when the provider exposes one, but it should not become a universal logging convention for every secret type.

Deliver the value only where it is needed

For local work, prefer the operating system's credential storage or the team's supported secret manager when an integration exists. Some tools require environment variables or a restricted local file. Use their documented interface, limit who can read the value, and avoid expanding its scope to every process in a long-lived shell session.

An environment variable is a delivery mechanism, not a security boundary. Child processes can inherit it, and diagnostics may print process configuration. A local file can also be read by any process with the same effective access. The relevant question is which processes can reach the value during the task.

Keep checked-in examples explicit and inert:

CATALOG_API_BASE=https://catalog.example.invalid
CATALOG_API_TOKEN=replace-with-a-development-credential

Those strings are illustrative placeholders. The application should reject missing or placeholder configuration before contacting a provider. Do not place a real key in an example file and rely on developers to remove it later.

If using a local configuration file, verify the ignore rule before adding the file, and remember that ignore rules do not stop tracking a file already in Git. A pre-commit or server-side secret scanner provides another detection layer; it cannot prove that a value has never appeared in history, logs, artifacts, or copied text.

Log an event, not an arbitrary object

The safest redaction problem is the one the logger never receives. For a provider call, log an event name, correlation identifier, status, and an approved error code. Avoid passing the entire request, response, environment, or exception object into a generic serializer.

We executed a small Python example with a deliberately fictional credential marker. Both the success and authentication-failure inputs contained secret-bearing fields, but the logging function selected only allowed fields:

def record(request_id, result):
    logger.info(json.dumps({
        "event": "provider_request",
        "request_id": request_id,
        "status": result["status"],
        "error_code": result.get("error_code"),
    }))

The check captured the two records and asserted that the fictional marker, Authorization field, and response-body field were absent. It also checked that the error code and correlation identifiers remained available. The fixture passed locally.

This proves the behavior of that function for those inputs. It does not cover an HTTP library's debug output, a traceback containing request data, another handler, or an observability exporter. Exercise those paths with synthetic canaries as part of your own integration test. Never run a deliberate exposure test with a live credential.

Allowlisting also needs review. If an upstream service can put arbitrary text into error_code, the field is no longer the controlled classification this example assumes. Translate provider errors into a bounded local vocabulary before logging them, and keep raw diagnostic payloads out of ordinary logs.

Treat masking as a fallback

CI platforms can redact known secrets from output, but transformed values and unexpected log paths complicate matching. GitHub's secure-use guidance explicitly warns that automatic redaction is not guaranteed and recommends reviewing logs for valid and invalid inputs.

The same reasoning applies locally. A regular expression for one token format will miss other credentials and may fail when values are encoded or split across messages. Redaction can reduce accidental disclosure, but it should supplement restricted access and deliberate event design.

Also inspect artifacts. A test report may embed a failed command; a crash dump may contain memory; a container build may preserve configuration in its history or layers. Docker's build-variable documentation warns against using build arguments or persistent environment values to pass build secrets. Use the builder's supported secret mechanism when a build legitimately needs one, and verify that the build command itself does not copy or print the value.

Respond to exposure at the credential's source

If a real credential enters a repository or log, first determine the affected identity and revoke or rotate the value through the issuing system. Deleting a file or editing the latest commit does not invalidate copies. Preserve the incident evidence through the organization's process without reproducing the secret in additional tickets or messages.

Update the applications that legitimately used the credential, confirm that the replacement works, and test that the old credential is rejected where feasible. Review access records for the relevant period and address the path that caused the disclosure. Repository-history cleanup and log removal may still be necessary, but they complement revocation rather than replace it.

The goal is a development workflow that remains usable with narrow, replaceable credentials and useful diagnostics. The local example verifies one small piece of that design: an error can be understandable without printing the credential that caused it.

You may also like