The most expensive line of code in many Java applications is a single string: a database password, an API key, or a signing secret committed to application.properties two years ago and never rotated. Hardcoded credentials are consistently among the top findings when static analysis runs against enterprise Java codebases, and they have a nasty property that other vulnerabilities don't — they survive in git history long after someone "removes" them, sitting in every clone and fork forever. Secrets management is the discipline of making sure a credential is never in your source, never in your image, and never valid longer than it needs to be.
Where do secrets leak in Java applications?
Secrets leak from four predictable places: source files and config (application.properties, application.yml, web.xml), baked-in container image layers, application logs, and git history. The last two are the sneakiest. A log.info("connecting with " + connectionString) can dump a password into a log aggregator that a dozen teams can read, and a credential committed once persists in history even after you delete the line, because git keeps every version. Effective secrets management addresses all four, not just the obvious source-file case.
Externalize configuration — don't hardcode
The first rule is that a secret should never have a default value in code or a committed config file. In Spring Boot, reference secrets through externalized configuration that's populated at runtime from the environment or a secrets provider, not from a checked-in file:
@Component
public class DbConfig {
// Injected at runtime; no default, so a missing value fails fast and loudly
@Value("${DB_PASSWORD}")
private String dbPassword;
}
Committing DB_PASSWORD=changeme to application.properties for "convenience" defeats the whole exercise. Keep secret-bearing profiles out of the repository entirely.
Prefer a secrets manager over environment variables
Environment variables are a step up from hardcoding, but they leak easily — into child processes, crash dumps, and /proc, and they encourage long-lived static credentials. A dedicated secrets manager (HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) is the stronger pattern because it centralizes access control, auditing, and rotation. The high-value capability is short-lived, dynamically generated credentials — for example, Vault can mint a database username/password that's valid for an hour, so a leaked credential is worthless soon after it's exposed:
// Fetch a short-lived secret at startup from the secrets manager, then use it
String dbPassword = secretsClient.getSecret("db/prod/password");
// The credential is leased and auto-expires; rotation is the vault's job
Fetch at runtime; never bake the value into a Docker layer, because image history preserves it even after redeployment.
Protect keystores and signing material
Java keystores (PKCS12 is the modern default; the older JKS format is legacy) hold private keys and certificates and must themselves be protected. Don't commit keystores to source control, don't reuse the default changeit password, and store the keystore password in your secrets manager just like any other credential. For application-level encryption of stored secrets, a library like Jasypt can encrypt property values, but that only moves the problem — the master key still needs to live in a vault, not in the repo.
Detect what's already leaked
You almost certainly have secrets in your history already. Scan for them and remediate:
- Run a secret scanner (
gitleaks,detect-secrets, or your platform's native scanner) across the full history, not just the working tree. - When you find a live secret, rotate it first — deleting the commit does not invalidate an exposed credential. Removing it from history (with
git filter-repoor BFG) is cleanup, not remediation. - Add a pre-commit hook and a CI gate so new secrets are caught before they merge.
Keep secrets out of logs and error responses
The leak path teams most often forget is observability. A stack trace that includes a connection URL, a debug log that prints a request header carrying an API key, or an exception message echoed back to a client can each expose a live credential to whoever can read logs — frequently a much wider audience than whoever can read source. Establish a rule that secret-bearing objects never go through string concatenation into a logger, wrap connection and credential types so their toString() returns a redacted placeholder, and configure your logging framework to mask known secret patterns. In production, disable verbose error responses so an unhandled exception returns a generic message rather than a stack trace. These controls cost almost nothing and close a category of leak that source scanning alone will never catch, because the secret was correctly externalized — it just got printed at runtime.
Secrets management checklist
| Control | Practice | Prevents |
|---|---|---|
| Source | No secrets or defaults in code/config | Direct source leaks |
| Runtime | Fetch from a secrets manager at startup | Static, long-lived creds |
| Rotation | Short-lived / dynamic credentials | Long exposure windows |
| Images | Never bake secrets into layers | Image-history leaks |
| Logging | Redact secrets from logs and errors | Log-aggregator exposure |
| History | Scan repo history; rotate then scrub | Persistent git leaks |
How Safeguard helps
Secrets hygiene is a supply-chain problem in disguise: a leaked credential in your repository is just as exploitable as a vulnerable dependency, and it often hides in the same places your scanners already look. Safeguard integrates secret detection into the same pipeline scan that runs software composition analysis, so hardcoded credentials and vulnerable dependencies surface in one workflow rather than two disconnected tools. The Safeguard CLI runs those checks pre-commit and in CI, catching a secret before it ever reaches a shared branch, and Griffin AI helps triage findings by explaining exposure and impact in plain language so teams rotate the credentials that actually matter first. For teams standardizing security tooling across many Java services, the pricing page lays out how per-project scanning scales.
Create a free account at app.safeguard.sh/register, and see the secret-scanning configuration at docs.safeguard.sh.