Credentials API
This is the method-by-method API reference for the credentials subsystem. For a task-oriented
walkthrough (enabling the vault, reading and writing secrets, KeyProviders, central sync), see the
Credentials guide. Every field, default, and validation rule for the
credentials config section lives in the config schema reference.
The subsystem is opt-in: the accessor returns an “absent” value unless a credentials config
section is present (and, in Rust, unless the credentials cargo feature is also built). Keys are
transparently namespaced by {ThingName}/{ComponentName}.
Accessor
Section titled “Accessor”Get the service from the SDK handle. The name and the absent value differ per language.
| Language | Method | Returns when configured | Returns when not configured |
|---|---|---|---|
| Java | getCredentials() |
CredentialService |
null |
| Python | get_credentials() |
CredentialService |
None |
| Rust | credentials() |
Some(Arc<dyn CredentialService>) |
None |
| TypeScript | credentials() |
CredentialService |
undefined |
The platform-profile default KEK provider (env on KUBERNETES) is applied at init but never
auto-enables credentials — only a credentials config section does.
Factory functions
Section titled “Factory functions”Most components get the service from the accessor above. The factory is for standalone use (no SDK
handle) or for building a namespaced service yourself. It builds a DefaultCredentialService from
the parsed credentials config.
// final class com.mbreissi.ggcommons.credentials.Credentials (static methods)static CredentialService open(JsonObject credentialsConfig);static CredentialService open(JsonObject credentialsConfig, String namespace);static CredentialService open(JsonObject credentialsConfig, String namespace, String defaultKeyProviderType);
// also exposes the KeyProvider builder used internally:static KeyProvider buildKeyProvider(JsonObject kp, String defaultKeyPath);static KeyProvider buildKeyProvider(JsonObject kp, String defaultKeyPath, String defaultType);# module ggcommons.credentials.configdef open_from_config(credentials_cfg: dict, namespace: str = "", default_key_provider: str | None = None) -> DefaultCredentialService: ...
def build_key_provider(kp: dict, default_key_path: str, default_type: str = "file"): ...// module ggcommons::credentials (re-exported from config.rs); requires the `credentials` featurepub fn open(config: &CredentialsConfig) -> Result<DefaultCredentialService>;pub fn open_namespaced(config: &CredentialsConfig, namespace: &str) -> Result<DefaultCredentialService>;pub fn open_namespaced_with_default(config: &CredentialsConfig, namespace: &str, default_kind: Option<&str>) -> Result<DefaultCredentialService>;Config is deserialized into a typed CredentialsConfig (serde camelCase) first, unlike the other
three languages, which take parsed JSON / a dict directly.
// module ggcommons/credentials/config — ASYNC (returns a Promise)async function openFromConfig( cfg?: CredentialsConfig, // default {} namespace?: string, // default "" defaultKeyProvider?: string,): Promise<DefaultCredentialService>;
async function buildKeyProvider( kp: KeyProviderConfig, vaultPath: string, defaultKeyPath: string, defaultType?: string,): Promise<BuiltKeyProvider>;The TypeScript factory and buildKeyProvider are async; the other three are synchronous.
CredentialService
Section titled “CredentialService”The public seam every component depends on. Every read first picks up cross-process vault changes (reload-if-changed) before returning.
| Capability | Description |
|---|---|
get |
Latest version as a Secret, or absent. |
getVersion |
A specific version by id, or absent. |
exists |
Whether the secret exists (latest version). |
list |
SecretMeta for all secrets whose name starts with a prefix ("" = all). |
versions |
The version-id history for one secret. |
put |
Store bytes, return the new version id. Keeps the last keepVersions (default 2). |
delete |
Remove all versions; returns whether anything was removed. |
refresh |
Force an immediate central pull (no-op without central sync). |
stats |
Non-sensitive CredentialStats for the metrics bridge. |
getBytes / getString / getJson |
Decode the latest value straight to bytes / UTF-8 / parsed JSON. |
| typed views | getAwsCredentials / getBasicAuth / getTlsBundle / getKafkaSasl — see Typed views. |
The per-language signatures follow.
public interface CredentialService { Optional<Secret> get(String name); Optional<Secret> getVersion(String name, String version); boolean exists(String name); List<SecretMeta> list(String prefix); List<String> versions(String name); String put(String name, byte[] value, PutOptions opts); boolean delete(String name);
default String put(String name, byte[] value); // PutOptions defaulted default void refresh(); default CredentialStats stats();
default Optional<byte[]> getBytes(String name); default Optional<String> getString(String name); default Optional<JsonElement> getJson(String name); default String putString(String name, String value); // Java only
default Optional<AwsCredentials> getAwsCredentials(String name); default Optional<BasicAuth> getBasicAuth(String name); default Optional<TlsBundle> getTlsBundle(String name); default Optional<KafkaSasl> getKafkaSasl(String name);}class CredentialService: def get(self, name: str) -> Optional[Secret]: ... def get_version(self, name: str, version: str) -> Optional[Secret]: ... def exists(self, name: str) -> bool: ... def list(self, prefix: str = "") -> List[SecretMeta]: ... def versions(self, name: str) -> List[str]: ... def put(self, name: str, value: bytes, **opts) -> str: ... # kwargs, not a PutOptions object def delete(self, name: str) -> bool: ... def refresh(self) -> None: ... def stats(self) -> CredentialStats: ...
def get_bytes(self, name: str) -> Optional[bytes]: ... def get_string(self, name: str) -> Optional[str]: ... def get_json(self, name): ... # parsed JSON, or None
def get_aws_credentials(self, name: str) -> Optional[AwsCredentials]: ... def get_basic_auth(self, name: str) -> Optional[BasicAuth]: ... def get_tls_bundle(self, name: str) -> Optional[TlsBundle]: ... def get_kafka_sasl(self, name: str) -> Optional[KafkaSasl]: ...Accepted put kwargs: ttl_secs, labels, content_type, source, central_version_id. An
unknown keyword raises TypeError. There is no putString and no PutOptions object in Python.
pub trait CredentialService: Send + Sync { fn get(&self, name: &str) -> Result<Option<Secret>>; fn get_version(&self, name: &str, version: &str) -> Result<Option<Secret>>; fn exists(&self, name: &str) -> Result<bool>; fn list(&self, prefix: &str) -> Result<Vec<SecretMeta>>; fn versions(&self, name: &str) -> Result<Vec<String>>; fn put(&self, name: &str, value: &[u8], opts: PutOptions) -> Result<String>; fn delete(&self, name: &str) -> Result<bool>;
fn refresh(&self) -> Result<()> { Ok(()) } // default fn stats(&self) -> CredentialStats { /* default */ }
fn get_bytes(&self, name: &str) -> Result<Option<Zeroizing<Vec<u8>>>>; fn get_string(&self, name: &str) -> Result<Option<String>>; fn get_json(&self, name: &str) -> Result<Option<serde_json::Value>>;
fn get_aws_credentials(&self, name: &str) -> Result<Option<AwsCredentials>>; fn get_basic_auth(&self, name: &str) -> Result<Option<BasicAuth>>; fn get_tls_bundle(&self, name: &str) -> Result<Option<TlsBundle>>; fn get_kafka_sasl(&self, name: &str) -> Result<Option<KafkaSasl>>;}Every method returns Result<..>. get_bytes returns the bytes in a Zeroizing buffer.
interface CredentialService { get(name: string): Secret | undefined; getVersion(name: string, version: string): Secret | undefined; exists(name: string): boolean; list(prefix?: string): SecretMeta[]; versions(name: string): string[]; put(name: string, value: Buffer, opts?: PutOptions): string; delete(name: string): boolean;
refresh(): Promise<void>; // the only async method stats(): CredentialStats;
getBytes(name: string): Buffer | undefined; getString(name: string): string | undefined; getJson(name: string): unknown | undefined;
getAwsCredentials(name: string): AwsCredentials | undefined; getBasicAuth(name: string): BasicAuth | undefined; getTlsBundle(name: string): TlsBundle | undefined; getKafkaSasl(name: string): KafkaSasl | undefined;}Reads are synchronous (Node is single-threaded — no in-process lock); only refresh() is async.
Secret
Section titled “Secret”The decrypted value plus its metadata. The bytes accessor returns the raw value; asString /
as_str decodes strict UTF-8; asJson / as_json parses JSON. The default string form redacts.
public final class Secret { String name(); String version(); long createdMs(); String source(); String contentType(); Map<String, String> labels(); byte[] bytes(); String asString(); // strict UTF-8 JsonElement asJson(); // toString() -> Secret{name=.., version=.., bytes=<N redacted>}}class Secret: name: str version: str labels: dict[str, str] created_ms: int source: str content_type: str
def bytes(self) -> bytes: ... def as_str(self) -> str: ... # strict UTF-8 def as_json(self): ... # __repr__ redacts the valuepub struct Secret { pub name: String, pub version: String, pub labels: BTreeMap<String, String>, pub created_ms: u64, pub source: String, pub content_type: String, // bytes: Zeroizing<Vec<u8>> (private) — wiped on drop}
impl Secret { pub fn bytes(&self) -> &[u8]; pub fn as_str(&self) -> Result<&str>; // strict UTF-8 pub fn as_json(&self) -> Result<serde_json::Value>; // custom Debug redacts the bytes}class Secret { readonly name: string; readonly version: string; readonly labels: Record<string, string>; readonly createdMs: number; readonly source: string; readonly contentType: string;
bytes(): Buffer; asString(): string; // strict UTF-8 asJson(): unknown; // both toString() and toJSON() redact — JSON.stringify(secret) is safe}PutOptions
Section titled “PutOptions”Optional metadata for put. Fields: ttlSecs / ttl_secs, labels, contentType /
content_type, source, centralVersionId / central_version_id.
// mutable fluent class; setters for ttlSecs/labels/contentType, public source/centralVersionId fieldsPutOptions opts = new PutOptions().ttlSecs(3600).contentType("text/plain");String version = creds.put("api/token", token, opts);# no PutOptions type — pass kwargs to put()version = creds.put("api/token", token, ttl_secs=3600, content_type="text/plain")// struct with public fields; ttl_secs: Option<u64>let version = creds.put("api/token", &token, PutOptions { ttl_secs: Some(3600), content_type: Some("text/plain".into()), ..Default::default()})?;// interface: ttlSecs?, labels?, contentType?, source?, centralVersionId?const version = creds.put("api/token", token, { ttlSecs: 3600, contentType: "text/plain" });SecretMeta and CredentialStats
Section titled “SecretMeta and CredentialStats”Both are value-free metadata. SecretMeta is what list and versions surface; CredentialStats
feeds the metrics bridge. Field names are camelCase (Java/TypeScript) or snake_case (Python/Rust),
but the shapes are identical.
SecretMeta—name,version,createdMs/created_ms,ttlSecs/ttl_secs(optional),source,labels.CredentialStats—secretCount/secret_count,lastSyncAgeMs/last_sync_age_ms(optional;null/Nonewhen there is no central sync),syncFailures/sync_failures,rotations.
Typed views
Section titled “Typed views”Four convenience getters parse a secret whose value is canonical JSON into a typed object. They are
present-or-absent for a missing secret and error on the wrong shape: a CredentialException
(Java) / CredentialError (Python, TypeScript), or a serde deserialization error in Rust.
The on-disk JSON keys are identical across all four languages:
| View | Canonical JSON keys |
|---|---|
| AWS credentials | accessKeyId, secretAccessKey, sessionToken?, expiry? |
| Basic auth | username, password |
| TLS bundle | certPem, keyPem, caPem? |
| Kafka SASL | mechanism? (default PLAIN), username, password |
The constructed object’s field casing differs: camelCase (Java records / TypeScript interfaces) vs
snake_case (Python dataclasses / Rust structs — e.g. access_key_id, cert_pem).
KeyProviders
Section titled “KeyProviders”The vault wraps each secret’s data-encryption key (DEK) under a key-encryption key (KEK) held by a
KeyProvider, selected by vault.keyProvider.type. The effective type is
explicit type ▸ platform default ▸ "file"; the platform default is env on KUBERNETES and file
everywhere else.
type |
KEK custodian | Key fields |
|---|---|---|
file (default) |
A local 32-byte keyfile | keyPath |
env |
A base64 32-byte KEK from an env var / mounted Secret | envVar (default GGCOMMONS_VAULT_KEK) |
kms / greengrass (aliases) |
AWS KMS via TES | kmsKeyId, region, endpointUrl |
pkcs11 |
HSM / TPM (wrap stays inside the token) | modulePath, tokenLabel, keyLabel, pinEnv |
The env provider reads a base64-encoded 32-byte KEK from the named variable (default
GGCOMMONS_VAULT_KEK), tolerates a trailing newline, and errors on unset / empty / bad-base64 /
wrong-length. It is crypto-identical to file given the same raw key.
The env KEK constant
Section titled “The env KEK constant”The default env-var name is exposed as a constant.
| Language | Constant | Value |
|---|---|---|
| Java | EnvKeyProvider.DEFAULT_ENV_VAR |
GGCOMMONS_VAULT_KEK |
| Python | EnvKeyProvider.DEFAULT_ENV_VAR |
GGCOMMONS_VAULT_KEK |
| Rust | keyprovider::DEFAULT_KEK_ENV_VAR |
GGCOMMONS_VAULT_KEK |
| TypeScript | (literal default in config.ts) |
GGCOMMONS_VAULT_KEK |
Rust feature gating
Section titled “Rust feature gating”Rust has a double gate: the service needs both the credentials cargo feature and a
credentials config section. The file and env providers are always available with the
credentials feature; kms / greengrass additionally require credentials-aws, and pkcs11
requires credentials-pkcs11. Building without the matching feature is a configuration error at open
time. Java, Python, and TypeScript have no compile-time gate — their optional KMS / PKCS#11
dependencies load lazily at runtime.
The KeyProvider extension seam
Section titled “The KeyProvider extension seam”Custom KEK custodians implement the KeyProvider interface (a narrow wrap/unwrap-DEK seam). The
custodian id is written to the vault’s KekInfo.provider; the KEK never lands on disk in plaintext.
public interface KeyProvider { String providerId(); // e.g. "file" KekInfo wrapDek(String vaultId, byte[] dek); byte[] unwrapDek(String vaultId, KekInfo kek);}class KeyProvider(ABC): @property @abstractmethod def provider_id(self) -> str: ... # e.g. "file" @abstractmethod def wrap_dek(self, vault_id: str, dek: bytes) -> dict: ... # returns the kek dict @abstractmethod def unwrap_dek(self, vault_id: str, kek: dict) -> bytes: ...Note provider_id is a property in Python (a method elsewhere).
pub trait KeyProvider: Send + Sync { fn provider_id(&self) -> &str; // e.g. "file" fn wrap_dek(&self, vault_id: &str, dek: &[u8; KEY_LEN]) -> Result<KekInfo>; fn unwrap_dek(&self, vault_id: &str, kek: &KekInfo) -> Result<Zeroizing<[u8; KEY_LEN]>>;}interface KeyProvider { providerId(): string; // e.g. "file" wrapDek(vaultId: string, dek: Buffer): KekInfo; unwrapDek(vaultId: string, kek: KekInfo): Buffer;}Central source and sync
Section titled “Central source and sync”Set credentials.central.type to seed and refresh the local vault from a central source.
central.typeis one ofnone(the default — local-only vault) orawsSecretsManager. An unknown value is rejected at open time in every language.- When
awsSecretsManager, a sync engine pullscentral.sync.secretsonbootstrapOnStart(defaulttrue) and everyrefreshIntervalSecs(default300). - Each
sync.secretsentry is a bare name (whose central id is the device-namespaced path) or{ "name": ..., "from": ... }to pull from an explicit or fleet-shared central id. refresh()forces an immediate pull (a no-op when central sync is not configured).
In Rust the central source requires the credentials-aws feature; configuring awsSecretsManager
without it returns an error at open time. Java, Python, and TypeScript load the AWS SDK lazily.
The Rust config types are CentralConfig, SyncSelect, and SyncEntry (all serde camelCase);
Java/Python/TypeScript read the same fields from the parsed config object.
Audit log
Section titled “Audit log”Every get / getVersion / put / delete is recorded through a pluggable AuditSink — the
operation, secret name, version, source, and outcome, but never the value. Auditing is on by
default; disable it with credentials.audit.enabled = false.
The audit event carries metadata only:
op—"get"|"put"|"delete"name— caller-facing secret name (transparent namespace stripped)version— version touched, or"-"when not applicable / not foundsource—"local"|"central"|"-"outcome—"hit"|"miss"|"ok"
The default LogAuditSink writes to a dedicated per-language logger so you can route or filter it
independently:
| Language | Types | Audit logger / target |
|---|---|---|
| Java | AuditSink, AuditEvent, LogAuditSink |
com.mbreissi.ggcommons.credentials.audit |
| Python | (audit module) |
ggcommons.credentials.audit |
| Rust | (audit module) |
ggcommons::credentials::audit (tracing target) |
| TypeScript | LogAuditSink, logSink |
the LogAuditSink logger |
In Java the sink is a one-method interface (void record(AuditEvent event)) and AuditEvent is a
record; only the target/logger name differs across languages.
$secret references
Section titled “$secret references”Resolves $secret references inside a config element so messaging / streaming config can reference
vault secrets without embedding plaintext. Use {"$secret":"name"} to substitute the secret’s whole
value as a string, or {"$secret":"name","field":"k"} to substitute field k of the secret’s JSON.
Resolution errors if the secret (or field) is absent.
// returns a deep COPY; the input is never mutatedJsonElement resolved = SecretRefs.resolve(JsonElement element, CredentialService creds);# mutates the passed structure IN PLACE (returns None;# a top-level "$secret" scalar resolves to a returned string)resolve_secret_refs(value, creds) -> None// mutates the passed Value IN PLACEfn resolve_secret_refs(value: &mut serde_json::Value, creds: &dyn CredentialService) -> Result<()>;// returns a deep CLONE; the input is never mutatedfunction resolveSecretRefs(value: unknown, creds: CredentialService): unknown;Example: read a secret
Section titled “Example: read a secret”A minimal read, guarding for the absent service first. (Verified against each SDK’s credentials test suite and dev guide.)
CredentialService creds = gg.getCredentials();if (creds == null) return; // no `credentials` section
creds.put("db/password", "s3cr3t".getBytes(StandardCharsets.UTF_8)); // put(name, byte[])Optional<Secret> s = creds.get("db/password");s.ifPresent(v -> LOGGER.info("ok bytes={} source={}", v.bytes().length, v.source()));
Optional<BasicAuth> ba = creds.getBasicAuth("svc/login"); // typed view (throws on wrong shape)creds = gg.get_credentials()if creds is None: return # no `credentials` section
creds.put("db/password", b"s3cr3t") # put(name, value, **opts)s = creds.get("db/password")if s is not None: logger.info("ok bytes=%d source=%s", len(s.bytes()), s.source)
cfg = creds.get_json("svc/config") # convenience JSON viewuse ggcommons::credentials::PutOptions;
let Some(creds) = gg.credentials() else { return Ok(()); }; // None when off / no section
creds.put("db/password", b"s3cr3t", PutOptions::default())?;if let Some(s) = creds.get("db/password")? { tracing::info!(bytes = s.bytes().len(), source = %s.source, "ok");}let pw = creds.get_string("db/password")?; // Option<String>Requires the credentials cargo feature (plus credentials-aws / credentials-pkcs11 for those
providers).
const creds = gg.credentials();if (!creds) return; // undefined when no `credentials` section
creds.put("db/password", Buffer.from("s3cr3t")); // put(name, Buffer, opts?)const s = creds.get("db/password");if (s) logger.info(`ok bytes=${s.bytes().length} source=${s.source}`);
const ba = creds.getBasicAuth("svc/login"); // typed view (throws on wrong shape)See also
Section titled “See also”- Credentials guide — enabling the vault, KeyProviders, central sync, and the security model, with task-oriented examples.
- Config schema reference — every
credentialsfield, default, and validation rule. - Parameters API — offline-first externalized config that reuses the credentials vault as an encrypted cache.