Skip to main content

spring-keyring-config: Spring Boot Secrets from Your OS Keyring (instead of .env)

· 5 min read
Ionuț Barangă
Software Engineer

Close-up of keys

Photo by George Becker on Pexels

Every Spring Boot project I touch eventually grows a .env, an application-local.yml, or some secrets.properties that I'm told to "just not commit". It's convenient: one file, all your local keys, sourced in seconds.

But here's the catch: that file is plaintext, and it sits on disk. Anything running as you can read it — a misbehaving build plugin, a backup daemon, or, increasingly, a coding agent you let loose in your repo. Putting a sensitive API key or a production-adjacent DB password in there is something I'm not comfortable doing on my dev workstation.

The Problem with the Local Secrets File​

The .env approach is tempting because it solves the problem instantly. You drop a key in a file, add the file to .gitignore, and move on.

The issues show up later:

  • It's plaintext on disk. No encryption, no access control beyond file permissions.
  • It's easy to leak. A stray git add -A, a .gitignore that doesn't match, a copied folder, a screen-shared terminal.
  • Everything running as you can read it. Your OS doesn't distinguish between you and a tool you ran. A hallucinating coding agent with shell access can cat it just as easily as you can.

Your operating system already ships a better place for this: a credential store. macOS has the Keychain, Windows has the Credential Manager, Linux has the Secret Service (GNOME Keyring and friends). They're encrypted, unlocked per-user, and meant exactly for this.

So the question I kept asking was: why is my secret in a file at all, when the OS already has somewhere safe to keep it?

spring-keyring-config​

That question is what spring-keyring-config answers.

A Spring Boot ConfigData provider that resolves ${keyring@…} references straight from the host OS credential store — so no plaintext secret file sits on disk for a tool, backup, or coding agent to read.

It plugs into Spring Boot's ConfigData mechanism as a reference-only property source. It doesn't dump your whole keyring into the environment; it only answers placeholders that explicitly ask for a keyring@ reference, and it reads them fresh each time — no caching, no copy living in memory longer than it needs to.

Under the hood it just talks to the native tools you already have:

OSBackendVia
macOSKeychainsecurity CLI
LinuxSecret Service (GNOME Keyring, etc.)secret-tool
WindowsCredential Managercmdkey

Requirements are modest: Java 17+ and Spring Boot 3.2+. It's licensed Apache-2.0.

Putting a Secret in the Keyring​

First, store the secret once, using the tool your OS already provides. No application code involved.

# macOS
security add-generic-password -s sensitive-api-key -w

# Linux
secret-tool store --label="sensitive api key" service sensitive-api-key
:: Windows
cmdkey /generic:sensitive-api-key /user:%USERNAME% /pass:<secret>

The secret now lives in the encrypted store, gated behind your login session. There's nothing to .gitignore.

Wiring It into Spring Boot​

Add the dependency:

implementation("com.sugarfreebytes:spring-keyring-config:0.0.1")

// Windows only
runtimeOnly("net.java.dev.jna:jna-platform:5.14.0")

Turn the provider on with a single config import:

spring:
config:
import: "keyring:"

Then reference your secrets the same way you reference any other property — except the value comes from the keyring, not from the file:

api-key: "${keyring@sensitive-api-key}" # service only
db-password: "${keyring@db-reporting/app-user}" # service/account

The service/account form maps onto the two-part identity those credential stores use, so you can keep several accounts under one logical service. And if you want a harmless fallback for a value you don't care about locally, defaults work as usual:

db-password: "${keyring@db-pass:dev-only}"

That's the whole integration. Your application.yml can be committed without a second thought — it contains references, not secrets.

Failing Loud, On Purpose​

One design choice worth calling out: the library is deliberately strict, because a secret that silently resolves to an empty string is worse than one that fails.

  • A missing entry with no default throws KeyringMissingSecretException instead of quietly becoming "".
  • A store that's unreachable, locked, or simply not there — a missing CLI, an unsupported OS — throws KeyringAccessException even when a default is present, so it can't masquerade as "use the fallback".
  • Secrets that contain ${…} are rejected (KeyringInterpolatableSecretException) rather than being re-interpolated into something you didn't intend.

You find out at startup that something is wrong, not three layers deep into a request.

Where It Fits (and Where It Doesn't)​

To be clear about the trade-off: this is a local-development tool. CI runners, containers, and headless or shared boxes typically have no unlocked per-user keyring to talk to — and that's by design. For those environments you still want a real secrets manager (Vault, cloud KMS, the platform's injected env vars).

What it replaces is the messy middle: the plaintext .env on your own machine — keeping your development keys in the vault your OS already runs instead of a file. That's the one job it's built for, and the only one it claims.

The code is on GitHub at sugarfreebytes/spring-keyring-config, with 0.0.1 already on Maven Central. It's early days, Apache-2.0, and feedback is welcome.