Before releasing an application, developers usually review code changes, run automated tests, and check for known vulnerabilities. However, one important security risk can remain hidden in the repository: exposed secrets.
API keys, database passwords, access tokens, private keys, and cloud credentials sometimes end up in source code, configuration files, test data, or Git history. A developer might commit a temporary debugging value and remove it in the next commit, assuming the problem is resolved.
Unfortunately, deleting a secret from the latest version of a file does not necessarily remove it from the repository's history. Anyone with access to the repository may still be able to retrieve the original value.
Exposed credentials can allow unauthorized access to databases, cloud resources, third-party services, and internal systems. Depending on the permissions associated with a credential, a single accidental commit can create a significant security incident.
In this article, we will explore how to detect secrets in a Git repository, scan current files and historical commits, automate checks in CI/CD, and respond safely when credentials are exposed.
What Counts as a Secret in a Git Repository?
A secret is sensitive information that should not be exposed to unauthorized users or stored directly in source control without an appropriate protection mechanism.
Common examples include:
- Database usernames and passwords.
- Cloud provider access keys.
- API keys for external services.
- OAuth client secrets.
- Personal access tokens.
- Private SSH keys.
- TLS private keys.
- Signing certificates and passwords.
- Webhook signing secrets.
- Connection strings containing credentials.
- Authentication tokens stored in configuration files.
Consider the following example:
public sealed class PaymentService
{
private const string ApiKey =
"sk_live_example_secret_value";
public void ProcessPayment()
{
// Payment processing logic
}
}
This example illustrates the problem: a credential is embedded directly in application code.
Even if the value is intended only for local development, committing a real credential can expose it to anyone who can access the repository or its history.
A safer approach is to load the value from an environment variable or a managed secret store.
var apiKey = Environment.GetEnvironmentVariable(
"PAYMENT_API_KEY");
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new InvalidOperationException(
"The payment API key is not configured.");
}
This approach separates application code from the secret value. However, environment variables are not automatically secure in every environment. Their access, storage, logging, and injection into the application must also be controlled.
Why Checking Only the Latest Commit Is Not Enough
Git stores repository history. Removing a secret from a file changes the current version, but earlier commits may still contain the original content.
For example, imagine this sequence:
- A developer adds a database password to appsettings.json.
- The developer commits and pushes the change.
- A security review identifies the exposed password.
- The developer removes the password in another commit.
- The latest version no longer contains the value.
The repository may still contain the password in the earlier commit.
A basic search of the working directory will not necessarily find it because the current file no longer contains the value.
This is why a pre-release secret review should cover multiple areas:
- Current working-tree files.
- Staged changes.
- Untracked files that will be included in a release.
- Recent commits.
- Relevant repository history.
- Generated files and configuration artifacts.
- Build outputs and container images when applicable.
A complete review also considers branches, tags, pull requests, and other copies of the repository where the exposed value may remain accessible.
Search the Repository for Common Secret Patterns
A quick search can help identify obvious mistakes before running a dedicated scanner.
For example, on a system with grep installed, run:
grep -RInE \
--exclude-dir=.git \
--exclude-dir=node_modules \
--exclude-dir=bin \
--exclude-dir=obj \
'(password|passwd|secret|api[_-]?key|access[_-]?token)[[:space:]]*[:=]' .
This command searches files for common secret-related words followed by an assignment-like pattern.
It is useful as a preliminary check, but it is not a reliable secret detector on its own.
A legitimate configuration key may contain the word password without containing a real password. Conversely, a secret may use an unfamiliar variable name and escape the search.
Do not treat a search that returns no matches as proof that the repository contains no secrets.
Search for Private Keys
Private key files and embedded key blocks deserve special attention.
For example:
grep -RIn \
--exclude-dir=.git \
--exclude-dir=node_modules \
--exclude-dir=bin \
--exclude-dir=obj \
'BEGIN .*PRIVATE KEY' .
This may identify private key material embedded in files.
A matching line is a signal to investigate, not automatic proof of a security incident. Test fixtures and documentation may contain intentionally invalid examples.
Also remember that some private key formats do not use the exact text pattern shown above.
Use a Dedicated Secret Scanner
Dedicated secret-scanning tools can detect patterns associated with known credential formats, identify suspicious strings, and inspect Git history.
They often provide more useful results than generic text searches because they understand common token formats and can scan commits as well as files.
A practical workflow uses three stages:
- Scan the repository locally before committing.
- Scan proposed changes during code review or CI.
- Scan the repository and relevant history before release.
The specific tool should match the repository hosting platform, programming languages, compliance requirements, and CI/CD environment.
Example: Scan a Git Repository with Gitleaks
Gitleaks is a secret-scanning tool that can inspect repositories for potential secrets.
A typical repository scan can be started with:
gitleaks git .
To scan the current working directory using directory-scanning mode, use:
gitleaks dir .
The exact command options and behavior depend on the installed Gitleaks version. Check the version's command help and configure the scan to cover the files and Git history required by your release process.
A repository scan is useful because it can identify suspicious credential patterns that a basic grep command may miss.
However, scanners can produce false positives. They may also miss secrets that do not match known patterns or are encoded in unexpected ways.
Review findings carefully, and never paste a suspected production secret into an external issue or chat merely to investigate it.
Scan Git History, Not Just Current Files
Historical scanning is important because a secret can remain in an earlier commit after being removed from the latest source code.
Start by reviewing recent commits:
git log --oneline -n 20
To inspect the changes introduced by a particular commit:
git show <commit-id>
These commands are useful for manual review, but they are not substitutes for a dedicated historical secret scan.
Use a scanner that explicitly supports Git history and configure it to examine the required commit range. Confirm whether the selected mode scans the full repository history or only a subset.
For example, if the scanner supports Git repository scanning, run it against the repository and review the reported commit references.
A full history scan may identify secrets that are no longer present in the working tree.
What If a Secret Appears in Git History?
Do not assume that removing the file or editing the latest commit solves the problem.
First, revoke or rotate the exposed credential. Then assess the scope of access, review relevant logs, and determine whether unauthorized activity occurred.
If necessary, remove the sensitive material from repository history using an appropriate history-rewriting process. Coordinate this work with repository owners because rewriting history changes commit identifiers and can disrupt branches, tags, forks, and active developer clones.
History rewriting also cannot guarantee that every copy has been removed. Other clones, forks, caches, or downloaded archives may still contain the original commit.
Credential rotation is therefore essential even when repository history is cleaned up.
Protect Secrets in a .NET Application
.NET applications commonly use configuration providers to load settings from different sources.
During local development, developers may use User Secrets for values that should not be committed to source control. In production, credentials can be supplied through a managed secret store or a securely configured deployment environment.
Use User Secrets for Local Development
For a .NET project, initialize User Secrets with:
dotnet user-secrets init
Add a development secret:
dotnet user-secrets set \
"Payment:ApiKey" \
"replace-with-a-development-value"
The application can retrieve the value through configuration:
var apiKey = builder.Configuration[
"Payment:ApiKey"];
if (string.IsNullOrWhiteSpace(apiKey))
{
throw new InvalidOperationException(
"Payment API credentials are missing.");
}
User Secrets stores development values outside the project directory by default. It is intended for development, not as a production-grade secret vault.
Avoid committing actual credentials in launch settings, configuration files, or sample files. If an example configuration requires a key, use a clearly invalid placeholder.
Use a Secret Store in Production
Production applications should obtain credentials through an appropriate secret-management mechanism.
Depending on the deployment environment, this may be a managed cloud secret service, a container platform's secret integration, or another approved secrets manager.
The application should receive only the credentials it needs, and those credentials should have the minimum permissions required for the task.
Do not assume that moving a secret to an environment variable automatically provides encryption, access control, or safe logging. Secure the mechanism that supplies the value and restrict who can inspect it.
Automate Secret Scanning in CI/CD
Manual checks are useful, but they can be forgotten when a release deadline approaches.
Integrating secret scanning into CI/CD makes it easier to detect exposed credentials before the application reaches production.
A typical pipeline can include:
- A scan of changes before merging.
- A repository or commit-history scan at appropriate intervals.
- A mandatory security check before release.
- A policy for handling confirmed findings.
- A process for approving documented false positives.
For example, a CI job might execute:
gitleaks git .
If the scanner detects a finding, the pipeline should fail according to the tool's exit behavior and the team's security policy.
For a production pipeline, pin the scanner to a reviewed version, configure its rules explicitly, and verify that the scan covers the intended files and commit range.
Do not configure the pipeline to ignore all findings simply to keep releases moving. Instead, investigate each finding and document narrowly scoped exceptions where necessary.
Prevent Secrets from Entering Git
Secret scanning is more effective when combined with prevention.
Developers can install a pre-commit hook or use a tool that checks staged changes before creating a commit. Repository hosting platforms may also provide push protection or secret-scanning features that block recognized credentials.
These controls help catch mistakes early, but they should not be the only defense. A developer may bypass a local hook, and not every secret format can be detected automatically.
Use local checks, repository-level controls, and CI scanning together.
Review Common Secret-Scanning Findings
Not every scanner finding represents an exposed credential. Review each result in context.
Finding | Possible explanation | Recommended action |
|---|
API key in source code | A real credential may have been committed. | Verify safely, revoke or rotate if exposed, and remove the secret from source control. |
Password in a test file | Could be a test-only value or a copied production credential. | Confirm that the value is synthetic and cannot authenticate to a real system. |
Private key block | May be a real private key or intentionally invalid test data. | Verify its purpose and exposure; rotate or replace it if genuine and compromised. |
Token in Git history | A previously committed credential may remain accessible. | Revoke the token and investigate its history and usage. |
Secret-like string in documentation | May be a placeholder or example. | Confirm that it is nonfunctional and clearly identified as an example. |
Scanner reports a known false positive | A legitimate value matches a detection rule. | Document a narrow exception and keep the relevant scan enabled. |
When reviewing findings, avoid reproducing the complete secret in logs, pull request comments, or ticket descriptions. Refer to the file, commit, and finding identifier instead.
Common Mistakes to Avoid
Removing a Secret Without Rotating It
Deleting a credential from the latest source code does not invalidate the credential itself.
If the value was exposed, revoke or rotate it and investigate whether it was used without authorization.
Scanning Only the Latest Commit
A secret may remain in historical commits, tags, or branches.
Use a scanner that supports the required history and configure it to scan the relevant scope.
Relying Only on Regular Expressions
Text searches can detect obvious mistakes, but they can miss unfamiliar token formats and produce false positives.
Use dedicated secret-scanning tools alongside manual review.
Ignoring Findings Because the Repository Is Private
Private repositories still have access risks. Credentials may be exposed to unauthorized collaborators, compromised accounts, logs, or downstream copies.
Treat genuine credentials as sensitive regardless of repository visibility.
Disabling Scanning to Avoid Pipeline Failures
Repeated false positives should lead to better rules and narrowly scoped exceptions, not the removal of security checks.
A pipeline that always passes because every finding is ignored provides little protection.
Best Practices Before a Release
Use the following checklist before releasing an application:
- Scan current files and staged changes.
- Scan the required Git history and commit range.
- Review configuration files, scripts, test fixtures, and generated artifacts.
- Check for private keys, access tokens, connection strings, and cloud credentials.
- Verify whether findings are genuine without exposing the full values.
- Revoke or rotate any credential that may have been exposed.
- Investigate relevant access logs when a real secret has been committed.
- Remove exposed values from source code and replace them with secure configuration mechanisms.
- Clean repository history when appropriate, while recognizing that copies may remain elsewhere.
- Run secret scanning automatically in CI/CD.
- Use push protection or pre-commit checks where available.
- Document and narrowly scope approved false-positive exceptions.
- Recheck release artifacts and container images when secrets could have been copied into them.
The objective is to prevent secrets from entering source control, detect accidental exposure quickly, and respond correctly when a credential is compromised.
Advantages and Limitations of Secret Scanning
Aspect | Advantage | Limitation |
|---|
Automated detection | Finds many recognizable credential patterns quickly. | Cannot reliably detect every possible secret. |
Historical scanning | Can identify secrets removed from the latest version. | Other clones, forks, and downloaded copies may retain exposed data. |
CI/CD integration | Helps block releases containing detected secrets. | Requires ongoing rule maintenance and careful handling of false positives. |
Developer feedback | Finds mistakes earlier in the development process. | Local checks can be bypassed unless supported by additional controls. |
Incident response | Provides commit and file references that help investigations. | Detection alone does not revoke credentials or prove whether they were misused. |
Summary
A Git repository can contain exposed credentials even when the latest version of the code looks clean. Secrets may remain in historical commits, configuration files, scripts, test data, and build artifacts.
Start with a repository scan, but do not rely exclusively on text searches or the latest commit. Use a dedicated secret-scanning tool that covers the required repository history, and integrate scanning into CI/CD so that future mistakes are detected before release.
If a real credential has been exposed, revoke or rotate it immediately, investigate possible misuse, and remove the sensitive value from the repository where appropriate. History rewriting can reduce further exposure, but it cannot guarantee that every existing copy has been erased.
The most effective approach combines secure configuration, automated detection, prevention controls, and a clear incident-response process. This makes accidental secret exposure less likely and helps teams respond quickly when it happens.
Join the conversation! Your thoughts help the community grow.