From 38ef6c4a09f0bb7456f1776751695da6a01cea6f Mon Sep 17 00:00:00 2001 From: Ben Vincent Date: Sun, 12 Jul 2026 19:12:39 +1000 Subject: [PATCH] Bump bind-operator to v0.2.0 and deploy companion TSIG API (#249) ## Why bind-operator v0.2.0 adds the `BindTSIGAPI` CRD and a companion API that `vault-plugin-secrets-bind-tsig` calls to create, rotate and delete TSIG keys (it does so by managing `BindTSIGKey` resources, which the operator reconciles into key material). This rolls the operator forward and deploys an API instance so Vault never talks to the Kubernetes API directly. ## Changes - Bump the operator image (`bind-system/deployment.yaml`) and the pulled CRD bundle URL (`bind-system/kustomization.yaml`) to `v0.2.0`. - Broaden the operator ClusterRole (`bind-system/rbac.yaml`) with `deployments`, `serviceaccounts` and `roles`/`rolebindings`, so the `BindTSIGAPI` reconciler can create the API Deployment and its namespaced Role/RoleBinding. - Add a `BindTSIGAPI` (`bind-tsig-api`) in `bind-internal`; the operator reconciles it into a Deployment, Service, ConfigMap, master-token Secret and RBAC. Keys are created in `bind-internal`, alongside the authoritative cluster and its existing keys. - Add the generated kubeconform schema for `BindTSIGAPI`. ## Notes - The master access token Secret (`bind-tsig-api-token`) is generated by the operator when absent; the operator does not own it, so a `VaultStaticSecret` can later pre-seed/overwrite it to source the token from Vault. - Validated: both overlays render (`kubectl kustomize`) and pass `kubeconform` (bind-internal 56/56 valid); pre-commit clean. ## Follow-up - Point `vault-plugin-secrets-bind-tsig` config at `http://bind-tsig-api.bind-internal.svc:8443`. Reviewed-on: https://git.unkin.net/unkin/argocd-apps/pulls/249 Co-authored-by: Ben Vincent Co-committed-by: Ben Vincent --- apps/base/bind-internal/kustomization.yaml | 1 + .../bind-internal/tsig-api/kustomization.yaml | 6 + .../base/bind-internal/tsig-api/tsig-api.yaml | 27 +++ apps/base/bind-system/deployment.yaml | 2 +- apps/base/bind-system/kustomization.yaml | 2 +- apps/base/bind-system/rbac.yaml | 10 +- .../bind.unkin.net/bindtsigapi_v1alpha1.json | 207 ++++++++++++++++++ 7 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 apps/base/bind-internal/tsig-api/kustomization.yaml create mode 100644 apps/base/bind-internal/tsig-api/tsig-api.yaml create mode 100644 schemas/bind.unkin.net/bindtsigapi_v1alpha1.json diff --git a/apps/base/bind-internal/kustomization.yaml b/apps/base/bind-internal/kustomization.yaml index 3713db0..112b5d8 100644 --- a/apps/base/bind-internal/kustomization.yaml +++ b/apps/base/bind-internal/kustomization.yaml @@ -7,3 +7,4 @@ resources: - authoritative - resolvers - externaldns + - tsig-api diff --git a/apps/base/bind-internal/tsig-api/kustomization.yaml b/apps/base/bind-internal/tsig-api/kustomization.yaml new file mode 100644 index 0000000..4979fc0 --- /dev/null +++ b/apps/base/bind-internal/tsig-api/kustomization.yaml @@ -0,0 +1,6 @@ +--- +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - tsig-api.yaml diff --git a/apps/base/bind-internal/tsig-api/tsig-api.yaml b/apps/base/bind-internal/tsig-api/tsig-api.yaml new file mode 100644 index 0000000..9b09541 --- /dev/null +++ b/apps/base/bind-internal/tsig-api/tsig-api.yaml @@ -0,0 +1,27 @@ +--- +# Companion TSIG API. The operator reconciles this into a Deployment, Service, +# ConfigMap, master-token Secret and namespaced RBAC. vault-plugin-secrets-bind-tsig +# calls it to create/rotate/delete TSIG keys, which it does by managing +# BindTSIGKey resources in this namespace (the operator reconciles the material). +# +# The master access token Secret (bind-tsig-api-token) is generated by the +# operator if absent; a VaultStaticSecret may later pre-seed/overwrite it so the +# token is sourced from Vault rather than generated in-cluster. +apiVersion: bind.unkin.net/v1alpha1 +kind: BindTSIGAPI +metadata: + name: bind-tsig-api + namespace: bind-internal +spec: + image: git.unkin.net/unkin/bind-tsig-api:v0.2.0 + replicas: 1 + port: 8443 + # targetNamespace defaults to this resource's namespace (bind-internal), where + # the authoritative cluster and its keys live. + resources: + requests: + cpu: 25m + memory: 64Mi + limits: + cpu: 250m + memory: 128Mi diff --git a/apps/base/bind-system/deployment.yaml b/apps/base/bind-system/deployment.yaml index 69e77b6..cc4387b 100644 --- a/apps/base/bind-system/deployment.yaml +++ b/apps/base/bind-system/deployment.yaml @@ -21,7 +21,7 @@ spec: runAsNonRoot: true containers: - name: operator - image: git.unkin.net/unkin/bind-operator:v0.1.5 + image: git.unkin.net/unkin/bind-operator:v0.2.0 args: - --metrics-bind-address=:8080 - --health-probe-bind-address=:8081 diff --git a/apps/base/bind-system/kustomization.yaml b/apps/base/bind-system/kustomization.yaml index 9f036fe..20033dc 100644 --- a/apps/base/bind-system/kustomization.yaml +++ b/apps/base/bind-system/kustomization.yaml @@ -6,6 +6,6 @@ resources: - namespace.yaml # CRDs are pulled from the bind-operator repo at the matching tag rather than # vendored here, so they never drift from the operator. - - https://git.unkin.net/unkin/bind-operator/raw/tag/v0.1.5/config/crd/install.yaml + - https://git.unkin.net/unkin/bind-operator/raw/tag/v0.2.0/config/crd/install.yaml - rbac.yaml - deployment.yaml diff --git a/apps/base/bind-system/rbac.yaml b/apps/base/bind-system/rbac.yaml index 628c3ed..37946c9 100644 --- a/apps/base/bind-system/rbac.yaml +++ b/apps/base/bind-system/rbac.yaml @@ -23,7 +23,15 @@ rules: resources: ["pods/exec"] verbs: ["create", "get"] - apiGroups: ["apps"] - resources: ["statefulsets"] + resources: ["statefulsets", "deployments"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # The BindTSIGAPI reconciler deploys the companion API: a ServiceAccount plus + # a namespaced Role/RoleBinding granting it access to BindTSIGKey + Secrets. + - apiGroups: [""] + resources: ["serviceaccounts"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + - apiGroups: ["rbac.authorization.k8s.io"] + resources: ["roles", "rolebindings"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: [""] resources: ["events"] diff --git a/schemas/bind.unkin.net/bindtsigapi_v1alpha1.json b/schemas/bind.unkin.net/bindtsigapi_v1alpha1.json new file mode 100644 index 0000000..e86859c --- /dev/null +++ b/schemas/bind.unkin.net/bindtsigapi_v1alpha1.json @@ -0,0 +1,207 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "metadata": { + "type": "object" + }, + "spec": { + "properties": { + "env": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "image": { + "default": "git.unkin.net/unkin/bind-tsig-api:latest", + "type": "string" + }, + "imagePullPolicy": { + "type": "string" + }, + "port": { + "default": 8443, + "format": "int32", + "type": "integer" + }, + "replicas": { + "default": 1, + "format": "int32", + "type": "integer" + }, + "resources": { + "properties": { + "claims": { + "items": { + "properties": { + "name": { + "type": "string" + }, + "request": { + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "name" + ], + "x-kubernetes-list-type": "map" + }, + "limits": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "type": "object" + }, + "requests": { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$", + "x-kubernetes-int-or-string": true + }, + "type": "object" + } + }, + "type": "object" + }, + "service": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + }, + "externalTrafficPolicy": { + "enum": [ + "Cluster", + "Local" + ], + "type": "string" + }, + "loadBalancerIP": { + "type": "string" + }, + "type": { + "enum": [ + "ClusterIP", + "LoadBalancer", + "NodePort" + ], + "type": "string" + } + }, + "type": "object" + }, + "targetNamespace": { + "type": "string" + }, + "tokenSecretName": { + "type": "string" + } + }, + "type": "object" + }, + "status": { + "properties": { + "conditions": { + "items": { + "properties": { + "lastTransitionTime": { + "format": "date-time", + "type": "string" + }, + "message": { + "maxLength": 32768, + "type": "string" + }, + "observedGeneration": { + "format": "int64", + "minimum": 0, + "type": "integer" + }, + "reason": { + "maxLength": 1024, + "minLength": 1, + "pattern": "^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$", + "type": "string" + }, + "status": { + "enum": [ + "True", + "False", + "Unknown" + ], + "type": "string" + }, + "type": { + "maxLength": 316, + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$", + "type": "string" + } + }, + "required": [ + "lastTransitionTime", + "message", + "reason", + "status", + "type" + ], + "type": "object" + }, + "type": "array", + "x-kubernetes-list-map-keys": [ + "type" + ], + "x-kubernetes-list-type": "map" + }, + "endpoint": { + "type": "string" + }, + "observedGeneration": { + "format": "int64", + "type": "integer" + }, + "phase": { + "type": "string" + }, + "readyReplicas": { + "format": "int32", + "type": "integer" + }, + "tokenSecret": { + "type": "string" + } + }, + "type": "object" + } + }, + "type": "object" +}