Skip to content

Credentials & vault

The credentials subsystem gives every component an encrypted-at-rest local vault of named, versioned, opaque-byte secrets. It is offline-first: it runs entirely from the local vault and can optionally be seeded and refreshed from AWS Secrets Manager. The on-disk format is byte-identical across all four SDKs, so a vault written by one language opens in any other.

The subsystem is opt-in. The getCredentials() accessor returns nothing unless a credentials section is present in your component config (and, in Rust, unless the credentials cargo feature is also built). Keys you read and write are transparently namespaced by {ThingName}/{ComponentName}, so two components never collide.

Add a credentials section to your component config. The minimal form turns on a local-only vault with a file KeyProvider; this same JSON is read identically by all four SDKs.

{
"component": { "name": "com.example.MyComponent" },
"credentials": {
"vault": {
"path": "vault",
"keyProvider": { "type": "file" },
"keepVersions": 2,
"cacheTtlSecs": 300
},
"audit": { "enabled": true }
}
}

Defaults if you omit fields: vault.path = vault, keyProvider.type = file (or env on KUBERNETES — see KeyProviders), keepVersions = 2, cacheTtlSecs = 300, audit.enabled = true, and no central sync. The vault.path supports template variables such as {ThingName}.

Rust has a double gate — the credentials config section and the matching cargo feature. Without the credentials cargo feature the gg.credentials() accessor is compiled out entirely: the method is absent, so a call site referencing it will not compile. The None return applies only when the feature is built but no credentials config section is present.

[dependencies]
ggcommons = { version = "*", features = ["credentials"] }
# add "credentials-aws" for kms / greengrass KeyProviders and central AWS Secrets Manager sync,
# and "credentials-pkcs11" for the pkcs11 (HSM/TPM) provider.

Java, Python, and TypeScript have no compile-time gate; their optional providers (KMS, PKCS#11) load their dependencies lazily at runtime.

The accessor returns the service when configured, and a per-language “absent” value when there is no credentials section. Always guard for absent before using it.

// returns CredentialService, or null when there is no `credentials` section
CredentialService creds = gg.getCredentials();
if (creds == null) return;

get(name) returns the latest version as a Secret (or absent). The Secret exposes its bytes plus metadata (name, version, createdMs, source, contentType, labels). Convenience getters — getBytes / getString / getJson — skip straight to the decoded value.

Optional<Secret> s = creds.get("db/password");
s.ifPresent(v -> LOGGER.info("bytes={} source={}", v.bytes().length, v.source()));
// Decode straight to a value:
Optional<byte[]> raw = creds.getBytes("db/password");
Optional<String> pw = creds.getString("db/password");
Optional<JsonElement> cfg = creds.getJson("svc/config");
// A specific version, existence, listing, and version history:
Optional<Secret> v1 = creds.getVersion("db/password", "1");
boolean present = creds.exists("db/password");
List<SecretMeta> all = creds.list(""); // prefix filter; "" = everything
List<String> history = creds.versions("db/password");

Four convenience views parse a secret whose value is canonical JSON into a typed object. They are present-or-absent for a missing secret, and throw on the wrong shape (a CredentialException in Java, CredentialError in Python/TypeScript, a serde deserialization error in Rust).

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
Optional<BasicAuth> ba = creds.getBasicAuth("svc/login");
ba.ifPresent(a -> connect(a.username(), a.password()));
Optional<AwsCredentials> aws = creds.getAwsCredentials("aws/uploader");
Optional<TlsBundle> tls = creds.getTlsBundle("mtls/broker");
Optional<KafkaSasl> sasl = creds.getKafkaSasl("kafka/ingest");
// records expose accessKeyId(), certPem(), mechanism(), etc.

put(name, value) stores bytes and returns the new version id. It keeps the last keepVersions versions (default 2). Optional metadata — ttlSecs, labels, contentType — is passed as a PutOptions object in Java/Rust/TypeScript, and as keyword arguments in Python.

creds.put("db/password", "s3cr3t".getBytes(StandardCharsets.UTF_8));
creds.putString("db/password", "s3cr3t"); // Java-only string convenience
// with metadata:
String version = creds.put(
"api/token",
token,
new PutOptions().ttlSecs(3600).contentType("text/plain"));

PutOptions is a mutable fluent class (ttlSecs / labels / contentType setters; public source / centralVersionId fields). putString exists only in Java.

Delete a secret (all versions) with delete(name), which returns whether anything was removed.

The vault encrypts 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", and 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 KEK from an env var / mounted Secret — the offline software KEK, default on KUBERNETES 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
{
"credentials": {
"vault": {
"keyProvider": { "type": "env", "envVar": "GGCOMMONS_VAULT_KEK" }
}
}
}

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 — a vault wrapped with env opens under a file provider with the same KEK and vice versa.

Terminal window
# A 32-byte KEK, base64-encoded, in the default variable name:
export GGCOMMONS_VAULT_KEK="$(head -c 32 /dev/urandom | base64)"

Set credentials.central.type to awsSecretsManager to seed and refresh the local vault from AWS Secrets Manager. With type = none (the default) the vault is local-only. Listed secrets are pulled on startup (bootstrapOnStart, default true) and every refreshIntervalSecs (default 300). Each sync.secrets entry 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.

{
"credentials": {
"central": {
"type": "awsSecretsManager",
"region": "us-east-1",
"refreshIntervalSecs": 300,
"bootstrapOnStart": true,
"sync": {
"secrets": [
"db/password",
{ "name": "shared/ca", "from": "fleet/prod/ca-bundle" }
]
}
}
}
}

Call refresh() to force an immediate pull (it is a no-op when central sync is not configured).

creds.refresh(); // synchronous; blocks until the pull completes

An unknown central.type is rejected at startup in every language.

Any config element can reference a vault secret instead of embedding it, so messaging/streaming config never carries 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.

{
"streaming": {
"sinks": {
"kinesis": {
"accessKeyId": { "$secret": "aws/uploader", "field": "accessKeyId" },
"secretAccessKey": { "$secret": "aws/uploader", "field": "secretAccessKey" }
}
}
}
}

The SDK resolves these against the credential service. In Java and TypeScript the resolver returns a new (deep-copied) structure and never mutates the input; in Python and Rust it mutates the passed structure in place.

Every get / getVersion / put / delete is recorded to a dedicated audit logger — operation, name, version, source, and outcome, but never the value. The audit log is on by default; disable it with credentials.audit.enabled = false.

{ "credentials": { "audit": { "enabled": false } } }

The default sink writes to a per-language logger so you can route or filter it independently:

Language Audit logger / target
Java com.mbreissi.ggcommons.credentials.audit
Python ggcommons.credentials.audit
Rust ggcommons::credentials::audit (tracing target)
TypeScript the LogAuditSink logger

A Secret redacts its value from the default string form in every language, so accidentally logging the object does not leak the secret:

  • JavatoString() renders Secret{name=.., version=.., bytes=<N redacted>}.
  • Python__repr__ redacts.
  • Rust — a custom Debug redacts, and the bytes live in a Zeroizing buffer wiped on drop.
  • TypeScript — both toString() and toJSON() redact, so JSON.stringify(secret) is safe too.

This protects only the default representations. Calling secret.bytes() (or getString etc.) and logging the result yourself still leaks — don’t.