Use memory for fixture credentials in tests and local development. For a deployed service, choose the cloud store your organization operates, or Vault when you already manage its KV v2 engine and authentication. Configure the workload’s identity and least-privilege policy explicitly: a default credential chain can find credentials, but it does not prove they are the intended identity or grant secret access.
The examples assume your application module already depends on
arkoren.dev/core. ctx is a startup or operation context. Check construction
errors before wrapping a driver; secret.New neither validates nor starts it.
Use a successful credential read to check access,
then separately verify the credential with the system that accepts it.
Compatibility Matrix
This comparison covers Arkoren’s secret contract, not the provider’s whole administration API. Versioned reads select a retained concrete version within one logical secret; they do not reserve its name or retain history.
| Driver | Versioned Reads | Arbitrary Bytes | Shared Storage | Permanent Delete | Native Access | Observed Mutations |
|---|---|---|---|---|---|---|
| Memory | Yes | Yes | No | Yes | No | Yes |
| AWS Secrets Manager | Yes | Yes | Yes | Yes | Yes | |
| Azure Key Vault | Yes | Yes | Yes | Yes | ||
| Google Cloud Secret Manager | Yes | Yes | Yes | Yes | Yes | Yes |
| HashiCorp Vault | Yes | Yes | Yes |
Shared storage means processes address the same provider resource and have permission to access it; memory is private to one process. Permanent deletion describes removal through the contract, not erasure of provider backups or application copies. Never use deletion as a rotation step.
Observed mutations are the facade’s payload-free telemetry and optional notifications. Native access and raw driver calls bypass them. All drivers support the facade’s typed JSON definitions and literal namespaces, subject to their own naming and payload rules. A namespace does not create provider resources or permissions.
Memory
The bundled memorysecret driver keeps a version history in process memory.
Use synthetic values only: it is neither durable storage nor a protected vault.
Connecting
Seed a reporting credential for an isolated test:
import (
"arkoren.dev/core/service/secret"
"arkoren.dev/core/service/secret/memorysecret"
)
driver := memorysecret.New(func(fixtures *memorysecret.Builder) {
fixtures.Values(map[string][]byte{
"reporting-client": []byte(`{"account":"local","token":"fixture"}`),
})
})
secrets := secret.New(driver)
Each seed starts at version "1". Values replaces and copies the map and
byte slices; nil or empty clears the seeds, and omitting it starts empty. A
retained construction draft cannot change the published store. There are no
start/stop hooks, native clients, timeout settings, or retry settings.
Use the typed credential read with this fixture.
The returned fields are local and fixture; do not print real credentials in
an equivalent production read.
Storage Limits
Operations are concurrency-safe and copy payload bytes at their boundaries.
Already-canceled contexts change nothing; missing names or versions wrap
secret.ErrNotFound. Process exit loses all history. Deletion removes a name’s
history immediately, and recreating it starts again at version "1"; an old
name/version selector can therefore select different contents.
See the memorysecret API reference.
AWS Secrets Manager
Choose AWS Secrets Manager for workloads with an explicitly configured AWS
identity. Runtime reads need secretsmanager:GetSecretValue on the required
resources; customer-managed encryption keys can require KMS permissions too.
Keep create, write, and delete permissions on provisioning identities.
Connecting
From your application module, install the driver:
go get arkoren.dev/driver/aws
The AWS SDK discovers credentials and region through its standard sources, including environment, shared configuration, and workload roles. Override the region when the deployment does not supply it:
import (
"arkoren.dev/core/service/secret"
"arkoren.dev/driver/aws/awssecret"
)
driver, err := awssecret.New(ctx, func(provider *awssecret.Builder) {
provider.Region("eu-west-1")
})
secrets := secret.New(driver)
Construction loads SDK configuration; it does not test secret access. No driver
lifecycle hooks are required. Use the startup read
with a provisioned name such as reporting-token.
Provider Settings
Credentials supplies a shared credential provider instead of the default
chain; Endpoint overrides the service URL. Bound calls with operation contexts.
For transport, retries, and timeouts beyond those settings, configure a native
SDK client. The deprecated Name builder method is a compatibility no-op; it
does not select a default secret or namespace.
The driver accepts AWS secret names or supported identifiers. Reads accept text
or binary values; writes through Arkoren use binary payloads. Explicit
Reference.Version is an AWS VersionId, not a movable staging label. Missing
and pending-deletion secrets map to secret.ErrNotFound.
Delete schedules deletion using AWS’s recovery window and does not immediately
free the name. A later same-name resource can have a different ARN. Keep the
logical secret and requested version retained for pinned reads.
Native Access
If your application already owns an aws.Config named config, wrap its client:
import "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
driver := awssecret.NewFrom(secretsmanager.NewFromConfig(config))
native := driver.Client()
native provides SDK operations such as describing secrets. The driver does not
copy or close it. Client() returns nil for a nonnative test double. Native
operations bypass contract error mapping and facade observations.
The reporting-only awssecret.Config masks credential-provider contents by
type without retrieving credentials or invoking their formatter. Its endpoint
and name text, native objects, and returned errors are not sanitized. Keep
credentials out of endpoint URLs and ordinary diagnostics.
Local Checks
With Docker, run an isolated MiniStack instance bound only to loopback. These credentials are synthetic and must never be used as production credentials:
docker run --rm --name arkoren-secrets-aws -p 127.0.0.1:4566:4566 -e AWS_ACCESS_KEY_ID=000000000000 -e AWS_SECRET_ACCESS_KEY=test -e AWS_DEFAULT_REGION=us-east-1 ministackorg/ministack:1.5.8
After readiness, use this alternative construction in your local application:
import "github.com/aws/aws-sdk-go-v2/credentials"
driver, err := awssecret.New(ctx, func(provider *awssecret.Builder) {
provider.Region("us-east-1")
provider.Endpoint("http://localhost:4566")
provider.Credentials(credentials.NewStaticCredentialsProvider("000000000000", "test", ""))
})
secrets := secret.New(driver)
Run the disposable version check with this facade.
It verifies a synthetic write and pinned read, not AWS IAM permissions. Stop the
container with docker stop arkoren-secrets-aws; --rm discards its data.
See the awssecret API reference.
Azure Key Vault
Choose Key Vault when you have a vault and an Azure identity with secret access, such as the Key Vault Secrets User role for runtime reads. Grant provisioning permissions separately; default credential discovery is not an access check.
Connecting
Install the driver from your application module:
go get arkoren.dev/driver/azure
Supply the vault’s absolute HTTPS URL. The driver uses Azure’s default credential chain, including supported developer and managed identities:
import (
"arkoren.dev/core/service/secret"
"arkoren.dev/driver/azure/azuresecret"
)
driver, err := azuresecret.New(ctx, "https://my-vault.vault.azure.net/")
secrets := secret.New(driver)
Construction validates the URL and creates SDK objects without contacting the
vault. There are no builder settings or driver lifecycle hooks. Use a
startup read to verify access to the provisioned
name reporting-token.
Provider Limits
Names are flat and allow letters, digits, and hyphens, not slash paths. Values
must be valid UTF-8; invalid bytes fail locally before a native write. Encode
binary material, for example as base64, and decode it after reading. Missing
names and versions wrap secret.ErrNotFound.
Update first reads for existence and then writes, so it requires read and write
permission. It has no native version precondition and is not an atomic
compare-and-set operation. Already-canceled operations fail before local UTF-8
or SDK parameter validation without a provider request.
Delete follows the vault’s soft-delete retention. Writing a soft-deleted name
wraps azuresecret.ErrSoftDeleted; recovery restores the retained resource.
Do not automatically purge it to make provisioning succeed: purge is irreversible
and can be blocked by purge protection. Each value write creates a new version;
metadata and availability can still change independently.
Native Access
For custom credentials, transport, retry, or timeout policy, use an existing
azcore.TokenCredential named credential and your HTTPS vaultURL:
import "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets"
native, err := azsecrets.NewClient(vaultURL, credential, nil)
driver := azuresecret.NewFrom(native)
Replace nil with SDK client options when needed. driver.Client() returns the
shared client for recovery and other administration outside the contract; the
driver neither copies nor closes it. Keep SDK body logging and native error
details out of ordinary diagnostics. No offline emulator setup is provided by
this driver; use memory for fixture tests.
See the azuresecret API reference.
Google Cloud Secret Manager
Choose Secret Manager when your project has its API enabled and the runtime identity has Secret Manager Secret Accessor permission on the required secrets. Creating resources, adding versions, and deleting require separate permissions.
Connecting
Install the driver from your application module:
go get arkoren.dev/driver/gcp
The SDK uses Application Default Credentials. Configure the project for short
names such as reporting-token:
import (
"arkoren.dev/core/service/secret"
"arkoren.dev/driver/gcp/gcpsecret"
)
driver, err := gcpsecret.New(ctx, func(provider *gcpsecret.Builder) {
provider.Project("my-project")
})
secrets := secret.New(driver)
Register driver.Stop with application cleanup
immediately after successful construction. It owns the native gRPC connection;
stop consumers before releasing it. The construction context controls SDK
construction and credential discovery, not future calls. Verify permissions with
the startup read.
New installs a discard logger for SDK request/response and credential logging,
even if GOOGLE_SDK_GO_LOGGING_LEVEL=debug is set. This does not disable the
facade’s separate payload-free observations.
Provider Settings
Project defaults to empty and is not inferred from credentials. Either set it
for unqualified secret IDs or use full names such as
projects/my-project/secrets/reporting-token. Fully qualified names keep their
explicit project. Slashes belong to that resource path, not inside a secret ID.
Use operation contexts to bound calls; native client settings own retry and
timeout policy.
Put creates an absent secret with automatic replication and then adds a
version. A failed version write can leave an empty resource. Update only adds
a version to an existing resource. Empty version selects latest; concrete
versions returned by the driver are strings such as "1", not resource paths.
Missing resources or versions wrap secret.ErrNotFound.
Delete removes the resource and its versions. Recreation can reuse numeric
version selectors; a movable alias such as latest never pins contents.
Native Access
If you configure SDK endpoint, credentials, retries, or transport yourself,
NewFrom preserves that client’s logging policy.
SDK debug logging can include secret payloads, including when enabled by
GOOGLE_SDK_GO_LOGGING_LEVEL. Configure a discard logger before supplying a native client, after other client options so that it takes precedence.
With existing options of type []option.ClientOption, construct the client:
import (
"log/slog"
secretmanager "cloud.google.com/go/secretmanager/apiv1"
"google.golang.org/api/option"
)
clientOptions := append(options, option.WithLogger(slog.New(slog.DiscardHandler)))
native, err := secretmanager.NewClient(ctx, clientOptions...)
After checking the error, wrap it with the naming policy:
driver := gcpsecret.NewFrom(native, func(provider *gcpsecret.Builder) {
provider.Project("my-project")
})
driver.Client() exposes native version administration. Stop closes supplied
clients too: keep one cleanup owner and do not share the client with consumers
that outlive this driver. Native close has no context parameter, so an expired
Stop context still initiates release but cannot bound SDK connection/resolver
teardown. Use memory for offline fixtures; this driver supplies no emulator
setup.
See the gcpsecret API reference.
HashiCorp Vault
Choose Vault when you operate a KV v2 mount and an authentication policy for your application. Runtime readers need access to its data paths; provisioning tools need appropriate write and metadata permissions too. This driver does not use Vault’s dynamic-secret engines.
Connecting
Install the driver from your application module:
go get arkoren.dev/driver/vault
For managed login, supply an existing Vault SDK api.AuthMethod named
loginMethod. For example, the separate
Kubernetes auth SDK
requires a Vault role bound to the workload’s service account and namespace.
Configure that role and method before constructing the driver:
import (
"arkoren.dev/core/service/secret"
"arkoren.dev/driver/vault/vaultsecret"
)
driver, err := vaultsecret.New(func(provider *vaultsecret.Builder) {
provider.Address("https://vault.internal:8200")
provider.Auth(loginMethod)
})
secrets := secret.New(driver)
Construction performs SDK validation but no login. Register driver.Stop as
cleanup and driver.Start as startup through the
application lifecycle. Register
credential loading after Start succeeds. With managed authentication, operations
before successful Start or after Stop return vaultsecret.ErrNotStarted.
Authentication Lifetime
Mount defaults to secret; Address follows SDK defaults including
VAULT_ADDR. The SDK can load VAULT_TOKEN; a nonempty Token setting overrides
it. Static tokens are never renewed by the driver. With Auth, successful Start
replaces the token with the login result.
Renewable login tokens are renewed in the background. When renewal ends, the
driver logs in again, retrying with exponential waits from one second to one
minute. A nonrenewable login token is not automatically refreshed. Repeated
Start while ready is a no-op; malformed native renewability fails Start.
Logger defaults to slog.Default() and reports failed stages and retry delays,
not native diagnostics, tokens, or response bodies. Returned errors are not
sanitized.
Start and Stop serialize. Call Stop after Start returns; it cannot cancel a blocked authentication callback. Stop signals managed renewal and waits for its owner and native watcher within the caller’s wait budget. An expired context still signals cleanup, but an in-flight native renewal RPC depends on SDK timeouts and transport cooperation. Ownership remains tracked after an early return; restart is rejected until that worker drains, and a later Stop can wait again. The native HTTP transport is not closed by the driver.
JSON Credentials
Vault requires a non-null JSON object. With a provisioned reporting-client
object, use the typed read. String would return
the entire JSON object as text, not its token field. Names are KV v2 paths
relative to the selected mount; do not add the mount’s API envelope yourself.
Reads re-encode objects, so whitespace and member order can change. Writes
reject numbers whose value changes under native float64 JSON serialization,
including nested numbers. Store exact identifiers or decimals as JSON strings;
the driver does not convert them for you. For example, a provisioning tool can
encode this input with encoding/json/v2 before writing it:
type PartnerCredential struct {
AccountID string `json:"account_id"`
Token string `json:"token"`
}
input := PartnerCredential{AccountID: "9007199254740993", Token: "fixture"}
Accepted numeric tokens remain numbers and are sent unchanged; equivalent
notations such as 1e3 and 1000 are accepted. Existing data is not migrated,
and reads cannot recover precision lost by an earlier writer or backend.
Version Limits
Explicit selectors must be positive decimal versions. Empty selects current;
zero, negative, or unparseable selectors wrap secret.ErrNotFound. Missing,
deleted, or destroyed payloads also wrap that sentinel. Already-canceled calls
fail before version/JSON validation and provider requests.
Update reads the current version and uses its number as a native CAS
precondition, requiring read and write access. Concurrent changes fail without a
driver-level retry; detected absence after CAS rejection wraps
secret.ErrNotFound. Metadata deletion and recreation can restart at version
1, so this is not a cross-recreation identity check. Native SDK retry policy
remains unchanged. Delete permanently deletes metadata and all history, not
just the latest value through Vault’s soft-delete API.
Native Access
If you own authentication, logging, transport, and token renewal, wrap your
existing *api.Client named native from github.com/hashicorp/vault/api:
driver := vaultsecret.NewFrom(native.KVv2("application"))
versions := driver.Client()
versions is the shared native *api.KVv2, usable for version administration.
Client() is nil for nonnative doubles. This path has no managed login or
renewal; Start/Stop are no-ops, as they are for static-token drivers. You retain
transport cleanup. Native calls bypass payload validation, error mapping, and
facade observation. The reporting-only vaultsecret.Config masks its static
Token, not Address or a shared Auth formatter.
Local Checks
Run an isolated development server with Docker. Its root token and HTTP transport are intentionally insecure: bind loopback and use only synthetic secrets.
docker run --rm --name arkoren-secrets-vault -p 127.0.0.1:8200:8200 hashicorp/vault server -dev -dev-root-token-id=dev -dev-listen-address=0.0.0.0:8200
After readiness, the server has a secret KV v2 mount. Use this alternative
construction for the disposable check:
driver, err := vaultsecret.New(func(provider *vaultsecret.Builder) {
provider.Address("http://localhost:8200")
provider.Token("dev")
})
secrets := secret.New(driver)
Run the JSON version check. A successful pinned
read checks this local mount, not production authentication or TLS policy. Stop
the server with docker stop arkoren-secrets-vault; --rm discards its data.
See the vaultsecret API reference.
For shared reads and writes, see the
secret API reference.