Add companion TSIG API and BindTSIGAPI CRD
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

The vault-plugin-secrets-bind-tsig plugin needs an HTTP endpoint that
creates, reads, rotates and deletes TSIG keys on its behalf, decoupling
Vault from direct Kubernetes API access. This adds that companion API and
lets the operator deploy it declaratively.

- Add BindTSIGAPI CRD: creating one makes the operator reconcile a
  Deployment, Service, ConfigMap (env vars), token Secret and namespaced
  RBAC for the companion API. Spec covers image, replicas, port,
  targetNamespace, tokenSecretName, extra env, service exposure and
  resources.
- Generate the master access token Secret only when absent, so a
  VaultStaticSecret may pre-seed/overwrite it; the operator does not own it.
- Add the companion API server (internal/tsigapi): bearer-auth HTTP
  contract POST /v1/keys, GET/DELETE /v1/keys/{name}, POST
  /v1/keys/{name}/rotate, backed by BindTSIGKey custom resources the
  operator reconciles into key material.
- Add cmd/tsigapi entrypoint and Dockerfile.tsigapi (distroless).
- Wire the reconciler into setup, regenerate CRDs/RBAC/deepcopy, and add
  Woodpecker build (PR dry-run) and release (tag push) steps for the
  bind-tsig-api image.
- Cover the API server with auth and key-lifecycle unit tests.
This commit is contained in:
2026-07-11 12:44:13 +10:00
parent 49df29a072
commit 53db084c2d
14 changed files with 1657 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
package v1alpha1
import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// BindTSIGAPISpec configures the companion TSIG API that the operator deploys.
// The API exposes an HTTP contract (used by vault-plugin-secrets-bind-tsig) for
// creating, rotating and deleting TSIG keys; it does so by managing BindTSIGKey
// custom resources, which the operator then reconciles into key material.
type BindTSIGAPISpec struct {
// Image is the companion API container image.
// +kubebuilder:default="git.unkin.net/unkin/bind-tsig-api:latest"
// +optional
Image string `json:"image,omitempty"`
// ImagePullPolicy for the API container.
// +optional
ImagePullPolicy corev1.PullPolicy `json:"imagePullPolicy,omitempty"`
// Replicas of the API. Defaults to 1.
// +kubebuilder:default=1
// +optional
Replicas int32 `json:"replicas,omitempty"`
// Port the API listens on. Defaults to 8443.
// +kubebuilder:default=8443
// +optional
Port int32 `json:"port,omitempty"`
// TargetNamespace is where the API creates BindTSIGKey resources. Defaults
// to the API's own namespace.
// +optional
TargetNamespace string `json:"targetNamespace,omitempty"`
// TokenSecretName holds the master access token clients present to the API.
// The operator generates a token if the Secret does not exist, so a
// VaultStaticSecret may pre-seed it instead. Defaults to "<name>-token".
// +optional
TokenSecretName string `json:"tokenSecretName,omitempty"`
// Env are extra environment variables rendered into the API ConfigMap.
// +optional
Env map[string]string `json:"env,omitempty"`
// Service controls how the API is exposed (defaults to ClusterIP).
// +optional
Service ClusterServiceSpec `json:"service,omitempty"`
// Resources for the API container.
// +optional
Resources corev1.ResourceRequirements `json:"resources,omitempty"`
}
// BindTSIGAPIStatus reports observed API state.
type BindTSIGAPIStatus struct {
// +optional
Phase string `json:"phase,omitempty"`
// +optional
ReadyReplicas int32 `json:"readyReplicas,omitempty"`
// Endpoint is the in-cluster URL clients (Vault) use to reach the API.
// +optional
Endpoint string `json:"endpoint,omitempty"`
// TokenSecret is the Secret holding the master access token.
// +optional
TokenSecret string `json:"tokenSecret,omitempty"`
// +optional
ObservedGeneration int64 `json:"observedGeneration,omitempty"`
// +optional
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty"`
}
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
// +kubebuilder:resource:shortName=btapi
// +kubebuilder:printcolumn:name="Endpoint",type=string,JSONPath=`.status.endpoint`
// +kubebuilder:printcolumn:name="Ready",type=integer,JSONPath=`.status.readyReplicas`
// +kubebuilder:printcolumn:name="Phase",type=string,JSONPath=`.status.phase`
// BindTSIGAPI deploys the companion TSIG API. Creating one makes the operator
// reconcile a Deployment, Service, ConfigMap, token Secret and RBAC for it.
type BindTSIGAPI struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec BindTSIGAPISpec `json:"spec,omitempty"`
Status BindTSIGAPIStatus `json:"status,omitempty"`
}
// +kubebuilder:object:root=true
// BindTSIGAPIList contains a list of BindTSIGAPI.
type BindTSIGAPIList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []BindTSIGAPI `json:"items"`
}
func init() {
SchemeBuilder.Register(&BindTSIGAPI{}, &BindTSIGAPIList{})
}
+105
View File
@@ -581,6 +581,111 @@ func (in *BindPolicyStatus) DeepCopy() *BindPolicyStatus {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BindTSIGAPI) DeepCopyInto(out *BindTSIGAPI) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
in.Status.DeepCopyInto(&out.Status)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindTSIGAPI.
func (in *BindTSIGAPI) DeepCopy() *BindTSIGAPI {
if in == nil {
return nil
}
out := new(BindTSIGAPI)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BindTSIGAPI) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BindTSIGAPIList) DeepCopyInto(out *BindTSIGAPIList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]BindTSIGAPI, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindTSIGAPIList.
func (in *BindTSIGAPIList) DeepCopy() *BindTSIGAPIList {
if in == nil {
return nil
}
out := new(BindTSIGAPIList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *BindTSIGAPIList) DeepCopyObject() runtime.Object {
if c := in.DeepCopy(); c != nil {
return c
}
return nil
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BindTSIGAPISpec) DeepCopyInto(out *BindTSIGAPISpec) {
*out = *in
if in.Env != nil {
in, out := &in.Env, &out.Env
*out = make(map[string]string, len(*in))
for key, val := range *in {
(*out)[key] = val
}
}
in.Service.DeepCopyInto(&out.Service)
in.Resources.DeepCopyInto(&out.Resources)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindTSIGAPISpec.
func (in *BindTSIGAPISpec) DeepCopy() *BindTSIGAPISpec {
if in == nil {
return nil
}
out := new(BindTSIGAPISpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BindTSIGAPIStatus) DeepCopyInto(out *BindTSIGAPIStatus) {
*out = *in
if in.Conditions != nil {
in, out := &in.Conditions, &out.Conditions
*out = make([]v1.Condition, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BindTSIGAPIStatus.
func (in *BindTSIGAPIStatus) DeepCopy() *BindTSIGAPIStatus {
if in == nil {
return nil
}
out := new(BindTSIGAPIStatus)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *BindTSIGKey) DeepCopyInto(out *BindTSIGKey) {
*out = *in