Documentation
¶
Overview ¶
Package configazurekeyvault contributes secrets from Azure Key Vault as a first-class config layer: precedence, per-key merge, provenance and safe hot-reload, exactly like a file layer — but marked sensitive, so the core refuses to write a value sourced from here into a plainer layer beneath.
Key Vault names allow only letters, digits and hyphens, so unlike every other backend this family adapts there is no hierarchy to map: a secret's name becomes a config key verbatim, hyphens and all. Structure comes from the other mode instead, where one secret holds a whole document.
Reading a vault costs one listing plus one request per secret, because the service's listing carries metadata without values. That is the service's shape, not this adapter's, and it is why the poll interval defaults to minutes rather than seconds.
The vault is reached through the narrow SecretsAPI interface, which Wrap adapts from a configured *azsecrets.Client. Injecting the client keeps every credential and vault-URL decision with the consumer — the adapter never authenticates — and lets the whole unit suite run against a fake.
See the config-azure-keyvault spec for the decisions behind all of this.
Index ¶
- Constants
- Variables
- func FromClient(client *azsecrets.Client, opts ...Option) config.Backend
- func FromClientSecret(client *azsecrets.Client, name string, codec config.Codec, opts ...Option) config.Backend
- func FromCredential(cred azcore.TokenCredential, vaultURL string, opts ...Option) (config.Backend, error)
- func FromCredentialSecret(cred azcore.TokenCredential, vaultURL, name string, codec config.Codec, ...) (config.Backend, error)
- func New(api SecretsAPI, opts ...Option) config.Backend
- func NewSecret(api SecretsAPI, name string, codec config.Codec, opts ...Option) config.Backend
- type Fitness
- type Option
- type Properties
- type Secret
- type SecretsAPI
Constants ¶
const DefaultPollInterval = 5 * time.Minute
DefaultPollInterval is how often the backend polls for change when the consumer does not say otherwise.
Five minutes, deliberately slower than the rest of this family. A poll re-reads the vault, and the service has no batch read — so a sixty-second cadence over a fifty-secret vault would be fifty-one requests a minute, indefinitely.
const SourceKind = config.SourceKind("azure-keyvault")
SourceKind is what a layer from this backend reports as, so provenance can name Key Vault as the origin of a value.
Variables ¶
var ( // ErrNoCredential reports a nil credential supplied to a FromCredential rung. // // It covers the TYPED nil too, which is the one worth having: a nil // *azidentity.DefaultAzureCredential carried in an [azcore.TokenCredential] // interface compares unequal to nil, so without this guard it would be // accepted here and panic at the first request, far from the mistake. ErrNoCredential = errors.NewSentinel("configazurekeyvault.no_credential", "no Azure credential supplied; build one with azidentity, or use the ambient rung") // ErrNoVaultURL reports a rung called with no vault URL. // // Unlike an AWS region there is nothing ambient to fall back on: an Azure // credential names a principal and carries no endpoint, so the vault must be // named explicitly however the credential was obtained. ErrNoVaultURL = errors.NewSentinel("configazurekeyvault.no_vault_url", "no vault URL supplied; pass https://<name>.vault.azure.net/") )
Functions ¶
func FromClient ¶
FromClient returns a backend reading a Key Vault through a configured client — the convenience path over New and Wrap.
client must already carry the vault URL and a credential. This adapter never authenticates and never resolves a credential: managed identity, workload identity, a service principal or DefaultAzureCredential all work here precisely because the adapter knows about none of them, and azidentity stays in your dependency graph rather than every consumer's.
func FromClientSecret ¶
func FromClientSecret( client *azsecrets.Client, name string, codec config.Codec, opts ...Option, ) config.Backend
FromClientSecret returns a backend reading ONE secret, whose value is a whole document, through a configured client — the convenience path over NewSecret and Wrap. The same client rules as FromClient apply.
func FromCredential ¶ added in v0.3.0
func FromCredential( cred azcore.TokenCredential, vaultURL string, opts ...Option, ) (config.Backend, error)
FromCredential builds the Key Vault client from a credential the caller holds — a client secret, a certificate, a workload identity — and returns a backend over the vault.
azsecrets.NewClient does no I/O: it errors only on a malformed vault URL, so this rung constructs eagerly and reaching the vault stays deferred to Load, which has a context to bound it.
Where the ambient rung is ¶
Deliberately NOT here. Resolving the ambient Azure identity chain costs this module seven further dependencies — azidentity, MSAL, golang-jwt, uuid, pkg/browser and the rest — and charging every consumer for an identity graph they may never use would spend the dependency-footprint guarantee this adapter states in its own test.
It lives in the ambient subpackage instead, over go/azureclient:
import kvambient "gitlab.com/phpboyscout/go/config-azure-keyvault/ambient" b, err := kvambient.Default(ctx, "https://my-vault.vault.azure.net/")
To share ONE resolved credential across several adapters, resolve it with go/azureclient and pass it here (spec 0012 L-4, L-5).
func FromCredentialSecret ¶ added in v0.3.0
func FromCredentialSecret( cred azcore.TokenCredential, vaultURL, name string, codec config.Codec, opts ...Option, ) (config.Backend, error)
FromCredentialSecret is FromCredential for the single-secret shape: one secret whose payload is a whole document, decoded through the supplied codec.
func New ¶
func New(api SecretsAPI, opts ...Option) config.Backend
New returns a backend contributing a Key Vault's secrets as one config layer, each secret's name becoming a key verbatim.
api is the injected client: a fake in tests, a Wrap-ped *azsecrets.Client in production.
func NewSecret ¶
NewSecret returns a backend contributing ONE secret, whose value is a whole document, as a config layer.
This is how structure is expressed against a store whose names cannot carry it: the tree comes from the document rather than from the key space. It also costs a single request, where reading the vault costs one per secret.
The codec is a parameter rather than an option because it is not optional here — a secret's value is one opaque string, so without something to decode it there is no tree to contribute, and an option would only move that failure to run time.
The "fit to use" rules still apply: a disabled, expired or managed document secret is reported absent rather than served.
Types ¶
type Fitness ¶
type Fitness struct {
// Enabled reports whether the service will return the value at all.
Enabled bool
// Managed reports a secret backing a Key Vault certificate, holding its
// PFX or PEM rather than configuration.
Managed bool
// Expires is the secret's expiry, if one is set.
Expires *time.Time
}
Fitness is what the skip rules read: whether a secret is one the layer should carry at all.
It is the same set in both modes because it is a property of the secret, not of how it was read — a listing reports it, and so does a get.
type Option ¶
type Option func(*backend)
Option configures a backend.
func WithNamePrefix ¶
WithNamePrefix reads only the secrets whose names begin with prefix.
The filter is applied after the listing, because the service offers no server-side name filter. So it cannot save the listing itself — but it does avoid fetching every secret in a vault shared with other applications, which is the cost that grows.
The prefix is not stripped: a Key Vault name is a config key verbatim, and removing part of it here would reintroduce, through the back door, exactly the name-rewriting this adapter refuses to do.
func WithPollInterval ¶
WithPollInterval sets how often the backend polls for change. The default is DefaultPollInterval.
func WithValueCodec ¶
WithValueCodec decodes each secret's value through codec in the flat mode: a value that decodes to a mapping becomes a subtree under the secret's own name, and a value the codec rejects — a bare password — stays a scalar string, so a vault mixing plain secrets and documents reads correctly.
The name is still the key either way: this adds structure beneath a key, never structure between keys, which the naming rules forbid.
type Properties ¶
type Properties struct {
Name string
Version string
ContentType string
// Fitness is what the skip rules read (see wanted).
Fitness
// NotBefore is the secret's start date, if one is set. It is carried for
// completeness and deliberately NOT acted on — see wanted.
NotBefore *time.Time
}
Properties is a secret's metadata, as a listing returns it — everything the adapter needs to decide whether a secret is worth fetching.
type Secret ¶
type Secret struct {
// Name is the secret's name, which is also its config key.
Name string
// Value is the secret's value. Key Vault returns secrets as strings.
Value string
// Version identifies this revision — the marker the poll compares.
Version string
// ContentType is the free-text hint the vault carries, surfaced for a
// consumer to act on and never used to choose a decoder: it is a hint, not
// a contract, so decoding on it would be guessing at a format the service
// does not enforce.
ContentType string
// Fitness carries the attributes the skip rules read. A get returns them
// alongside the value, so document mode can apply the same rule as the
// flat mode without a second request.
Fitness Fitness
}
Secret is one secret's current value.
type SecretsAPI ¶
type SecretsAPI interface {
// Get reads one secret's current value. It returns a nil Secret and a nil
// error when the secret does not exist, so absence is not an error.
Get(ctx context.Context, name string) (*Secret, error)
// List returns every secret's metadata. Deliberately not its value: the
// service's listing does not carry one, and an interface that pretended
// otherwise would hide the per-secret cost from the code above it.
List(ctx context.Context) ([]Properties, error)
}
SecretsAPI is the slice of Key Vault this adapter uses, behind an interface it owns so a fake drives the unit suite and the real client is adapted by Wrap.
func Wrap ¶
func Wrap(client *azsecrets.Client) SecretsAPI
Wrap adapts a real Key Vault client to the narrow SecretsAPI interface.