Files
vault-plugin-secrets-bind-tsig/backend.go
T
unkinben b4b8915d3e
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Scaffold the bind-tsig secrets engine
A Vault/OpenBao secrets engine that manages BIND TSIG keys via the
bind-operator companion API (Vault -> HTTP API -> BindTSIGKey CRs).

- backend + cmd entry point (plugin.ServeMultiplex), modelled on
  vault-plugin-secrets-litellm
- config path: companion API url/token/tls + defaults
- static-roles/static-creds: stable named key with managed rotation
- roles/creds: dynamic, lease-bound keys (revoke deletes the CR)
- tsig_key secret type with revoke/renew
- HTTP client for the companion API contract (/v1/keys CRUD + rotate)
- Makefile, Woodpecker CI (pre-commit/build/test + tag release RPMs),
  nfpm packaging (vault + openbao flavours)
- e2e: mock companion API + Vault + OpenBao in docker-compose, full
  lifecycle per engine; unit tests for the dynamic + static flows
2026-07-15 21:29:33 +10:00

126 lines
3.1 KiB
Go

// Package bindtsig implements a Vault / OpenBao secrets engine that manages
// BIND TSIG keys through the bind-operator's companion API. Static roles expose
// a named key with managed rotation; dynamic roles mint unique, lease-bound
// keys. The companion API owns the BindTSIGKey CRs; the operator reconciles
// them into the actual key material.
package bindtsig
import (
"context"
"errors"
"strings"
"sync"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/logical"
)
// errBackendNotConfigured is returned when a credential is requested before the
// companion API connection has been configured.
var errBackendNotConfigured = errors.New("bind-tsig backend not configured; write config first")
type bindTSIGBackend struct {
*framework.Backend
lock sync.RWMutex
client *apiClient
}
// Factory returns a configured bind-tsig secrets backend.
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := backend()
if err := b.Setup(ctx, conf); err != nil {
return nil, err
}
return b, nil
}
func backend() *bindTSIGBackend {
b := &bindTSIGBackend{}
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),
BackendType: logical.TypeLogical,
PathsSpecial: &logical.Paths{
SealWrapStorage: []string{configStoragePath},
},
Paths: framework.PathAppend(
[]*framework.Path{
pathConfig(b),
pathRole(b),
pathRolesList(b),
pathCredentials(b),
pathStaticRole(b),
pathStaticRolesList(b),
pathStaticCredentials(b),
},
),
Secrets: []*framework.Secret{
b.tsigKeySecret(),
},
Invalidate: b.invalidate,
WALRollback: nil,
}
return b
}
// reset drops the cached API client so it is rebuilt from storage on the next
// request. Called when the config changes.
func (b *bindTSIGBackend) reset() {
b.lock.Lock()
defer b.lock.Unlock()
b.client = nil
}
// invalidate clears the cached client when the config is written from another
// cluster node.
func (b *bindTSIGBackend) invalidate(_ context.Context, key string) {
if key == configStoragePath {
b.reset()
}
}
// getClient returns a cached companion-API client, building one from stored
// config if necessary.
func (b *bindTSIGBackend) getClient(ctx context.Context, s logical.Storage) (*apiClient, error) {
b.lock.RLock()
if b.client != nil {
defer b.lock.RUnlock()
return b.client, nil
}
b.lock.RUnlock()
b.lock.Lock()
defer b.lock.Unlock()
if b.client != nil {
return b.client, nil
}
config, err := getConfig(ctx, s)
if err != nil {
return nil, err
}
if config == nil {
return nil, errBackendNotConfigured
}
client, err := newClient(config)
if err != nil {
return nil, err
}
b.client = client
return b.client, nil
}
const backendHelp = `
The bind-tsig secrets engine manages BIND TSIG keys via the bind-operator
companion API.
Static roles expose a stable, named key whose material Vault rotates on a
schedule (the key name never changes, so zone allow-update clauses stay valid).
Dynamic roles mint a unique key per request, bound to a Vault lease and deleted
on revocation.
`