Skip to content

sugarfreebytes/spring-keyring-config

Repository files navigation

spring-keyring-config

An OS keyring-backed Spring Boot ConfigData provider. Load secrets from the host operating system's native credential store (macOS Keychain, Windows Credential Manager, Linux Secret Service) and reference them with ${keyring@…} placeholders — the same reference-by-placeholder model as Spring Cloud GCP Secret Manager's sm@ secrets, activated through a standard spring.config.import.

When to use this (and when not to)

A local-development tool for an interactive developer machine. It reads secrets from the per-user OS keyring and resolves them with Spring's ${keyring@…} syntax — so no plaintext secret file sits on disk for a tool, backup, or coding agent to read. Unlike a gitignored .env, nothing is written to a file at all; unlike a shell injector (op run / direnv / aws-vault), there's no wrapper to launch the app under. The trade: it is Spring-only, OS-specific, and each secret is stored by hand (see Storing a secret). If you already inject secrets shell-wide across non-JVM tools, that may suit you better.

It is not a production secrets manager like Spring Cloud Vault or AWS/GCP Secrets Manager. CI, containers, and headless or shared environments typically have no unlocked per-user keyring, so a keyring@… reference normally fails with KeyringAccessException — omit the keyring: import from every non-local profile.

Coordinates

com.sugarfreebytes:spring-keyring-config:0.0.1

Requirements

  • Java 17+

  • Spring Boot 3.2+

  • Windows only: add net.java.dev.jna:jna-platform to your build. It is optional and not pulled in transitively, so macOS/Linux consumers never get it. Without it on Windows, startup fails with a clear message.

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

Usage

Import the keyring: scheme and reference secrets with keyring@:

spring:
  config:
    import: "keyring:"

# reference form:  ${keyring@<service>}  or  ${keyring@<service>/<account>}
api-key: "${keyring@openai-api-key}"             # service only
db-password: "${keyring@db-reporting/app-user}"  # service = db-reporting, account = app-user

The first / splits service from account. Omit the account (${keyring@<service>}) to match by service alone.

Behavior

  • There are no configuration properties. Behavior is fixed.
  • A referenced-but-missing secret fails (KeyringMissingSecretException) rather than resolving to empty. The failure fires when the reference is first resolved (normally during startup, as the value is bound), not necessarily during the ConfigData load phase.
  • A default makes a reference optional. Supply Spring's usual ${…:default} fallback — ${keyring@db-pass:dev-only} — and an absent secret resolves to the default instead of failing. The first : separates the reference from the default, so a default may itself contain :.
  • A default covers an absent secret, not an unqueryable store. The default applies only when the store was queried and held no such secret. If the store itself cannot be reached (tool missing, keyring locked, access denied, unsupported OS), the reference throws KeyringAccessException even when a default is present — an unreachable store is a hard error, not a missing value. So ${keyring@db-pass:dev-only} does not make startup succeed on a host with no keyring; to disable keyring in such an environment, omit the keyring: import from that profile.
  • A store that cannot be queried (tool missing, keyring locked, access denied) or an unsupported OS throws KeyringAccessException. To disable keyring in an environment, omit the keyring: import from that profile.
  • Secrets are returned verbatim — within two limits.
    • Store them as UTF-8 text. A value ending in a newline, or a non-text (binary) value, cannot be read back faithfully on macOS (the security CLI renders non-text bytes as a hex string).
    • A secret containing a literal ${…} sequence is rejected at resolution (KeyringInterpolatableSecretException). A resolved value is handed back to Spring's placeholder resolver, which recursively resolves placeholders in resolved values, so such a secret would be re-interpolated against the Environment rather than passed through literally — standard Spring behavior, shared by every property source, and on Boot 3.2 (whose resolver has no escape character) not suppressible. Rather than return a changed value or let startup fail later on an unrelated placeholder, the provider fails loud with the offending reference named.
  • Reads are uncached and never logged. Each ${keyring@…} reference queries the store afresh — on macOS and Linux a short-lived CLI subprocess (30 s timeout). Spring may resolve a given placeholder more than once (binding passes, @Value re-resolution), so N references resolved M times means N×M lookups on the resolution path. A consumer that binds a resolved value (a DataSource, @ConfigurationProperties) holds it in memory for the run.

Storing a secret

This library only reads; populate the store with each OS's native tooling. The service/account you store under must match the keyring@service/account you reference.

macOS — Keychain, via the security CLI:

security add-generic-password -s openai-api-key -a "$USER" -w
# -w with no value prompts; or: -w <secret>

Linux — a Secret Service provider (e.g. GNOME Keyring), via libsecret's secret-tool. Secrets are matched by service/account attributes:

secret-tool store --label="openai api key" service openai-api-key account "$USER"
# secret-tool reads the value from stdin

Windows — Credential Manager. The account is folded into the target as service/account:

cmdkey /generic:openai-api-key/%USERNAME% /user:%USERNAME% /pass:<secret>

Design decisions

  • keyring@…, not keyring://… — a : is Spring's ${name:default} separator, so a :// scheme would split at the scheme colon and never reach this source. (Spring Cloud GCP made the same sm://sm@ move.)
  • A missing secret fails (see Behavior) rather than resolving to empty — a reference asserts the secret exists, so resolving silently to empty would bury a misconfiguration as a downstream failure. Same stance as Spring Cloud GCP / AWS Secrets Manager.
  • An unreachable store or unsupported OS is a hard error (see Behavior), not a silent skip — a silent no-op would let an app boot without the secrets it requires. Disable it where it can't run by omitting the import, keeping the choice explicit.
  • Reads are uncached (see Behavior) — the provider is a lookup against the OS keyring, not a second store of secrets; a cache would make it one. The OS keyring stays the single secure store.
  • It answers only keyring@… references — a reference-only property source (the same model as Spring Cloud GCP Secret Manager's sm@), it exposes no property names of its own and returns nothing for any other key. So it never takes part in @ConfigurationProperties binding, can't override or collide with the rest of your configuration, and secrets never surface as named properties (e.g. in the /env actuator endpoint) — each is reachable only through an explicit ${keyring@…} reference.

License

Apache-2.0.

About

A Spring config library that reads secrets from your OS keyring via ${keyring@…} placeholders — a local-dev alternative to a plaintext .env.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages