spillway

Replicate Kubernetes Secrets and ConfigMaps across namespaces — via annotations or declarative profiles.

$ helm install spillway oci://ghcr.io/kroy-the-rabbit/charts/spillway --version 0.4.4 --namespace spillway-system --create-namespace
One annotation.
Every tenant
gets a copy.

You have a Secret in platform and every one of your tenant namespaces needs a copy. Copy-pasting YAML doesn't scale, and a sync script is exactly as fun as it sounds.

Spillway is a Kubernetes controller that watches your cluster and handles creation, updates, and cleanup automatically. Annotate a single source object, or use a SpillwayProfile to manage entire sets declaratively — no annotations on the sources required.

Requires Kubernetes 1.25+ and Helm 3.10+. The SpillwayProfile CRD is installed by the chart. No admission webhooks, no cluster-wide pre-configuration.

01

Annotation-driven replication

Add one annotation to any Secret or ConfigMap. Target by explicit name, glob pattern (team-*), the value all, or a Kubernetes label selector. Combine freely. Exclusions always win over includes.

02

SpillwayProfile CRD v0.3+

Define replication policy as a first-class resource, independent of the source objects. Platform teams control what gets replicated and where without touching any existing Secrets or ConfigMaps. Ideal for GitOps workflows.

03

Key projection v0.3+

Whitelist or blacklist specific data keys per replica. Use include-keys to give a tenant only the keys they need, or exclude-keys to strip internal fields. Works on both annotation-driven and profile replicas.

04

Replica TTL v0.3+

Set a lifetime with a Go duration (e.g. 24h). Expired replicas are permanently deleted — not recreated. The expired namespace is recorded on the source. Remove that entry to re-enable replication. Perfect for time-limited maintenance grants.

05

Namespace consent v0.3+

Annotate a target namespace to declare what sources it accepts, using Kind/namespace/name format with * wildcards. Absent annotation means accept all — existing clusters are unaffected by default.

Annotate a Secret or ConfigMap

One annotation — Spillway reconciles continuously. Changes to the source propagate to every replica. Deletions clean up all replicas automatically.

Glob patterns match namespace names at reconcile time. Label selectors resolve live against the cluster — new namespaces that gain a matching label are picked up on the next sync.

secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: shared-api-token
  namespace: platform
  annotations:
    spillway.kroy.io/replicate-to: "team-*,prod"
    spillway.kroy.io/include-keys: "token"
type: Opaque
stringData:
  token: hunter2
  internal-notes: do not distribute

Label selectors and TTL

Target dynamically by namespace label. Combine with a TTL to create time-limited replicas that expire and self-destruct — no manual cleanup required.

maintenance-grant.yaml
  annotations:
    spillway.kroy.io/replicate-to-matching: "env=prod"
    spillway.kroy.io/exclude-namespaces: "team-dev"
    spillway.kroy.io/replica-ttl: "24h"

Policy without touching the source

A SpillwayProfile separates replication policy from the objects being replicated. Platform teams own the profile; application teams own their Secrets and ConfigMaps. Neither needs to know about the other.

Profiles support key projection, namespace exclusions, label selectors, and the accept-from consent check — everything the annotation path supports, unified in one resource.

Deleting the profile removes all replicas it owns. It will never touch annotation-managed replicas.

platform-profile.yaml
apiVersion: spillway.kroy.io/v1alpha1
kind: SpillwayProfile
metadata:
  name: platform-secrets
  namespace: platform
spec:
  targetNamespaces:
    - "team-*"
  targetSelector:
    matchLabels:
      env: prod
  excludeNamespaces:
    - team-dev
  sources:
    - kind: Secret
      name: shared-api-token
      includeKeys: ["token"]
    - kind: ConfigMap
      name: app-config
      excludeKeys: ["internal-notes"]

Tenant-side allowlisting

A namespace can declare exactly which sources it accepts. Format is Kind/namespace/name with * wildcards in any segment. Spillway checks this before creating or updating any replica.

Absent annotation means accept from all sources — existing clusters are unaffected by default. Add it to any namespace that should be more selective.

payments-ns.yaml
apiVersion: v1
kind: Namespace
metadata:
  name: payments
  annotations:
    # any Secret from platform, one specific ConfigMap
    spillway.kroy.io/accept-from: "Secret/platform/*,\
      ConfigMap/ops/app-config"

    # or: accept everything from platform
    # spillway.kroy.io/accept-from: "*/platform/*"
annotation  (prefix: spillway.kroy.io/) description
replicate-to Comma-separated targets. Accepts all, globs like team-*, or explicit names. Mix freely.
replicate-to-matching Label selector targeting, e.g. env=prod. Resolved live at reconcile time.
exclude-namespaces Comma-separated exclusions, same syntax as replicate-to. Always wins over includes.
include-keys Data key whitelist. Only listed keys are copied. Mutually exclusive with exclude-keys.
exclude-keys Data key blacklist. Listed keys are stripped from all replicas.
replica-ttl Go duration (e.g. 24h). Replicas are permanently deleted when the TTL elapses. The expired namespace is recorded in expired-namespaces on the source — remove that entry to re-enable.
force-adopt Set to "true" to claim ownership of a pre-existing unmanaged object. Annotation-driven replication only.
accept-from(on Namespace) Comma-separated allowlist in Kind/namespace/name format. Wildcards accepted in each segment. E.g. Secret/platform/*, */platform/*, all.

kube-system is excluded by default when using all or wildcards — explicitly naming it overrides the protection. The SpillwayProfile CRD is installed automatically (installCRDs: true). Set it to false if you manage CRDs separately via GitOps.

helm upgrade spillway oci://ghcr.io/kroy-the-rabbit/charts/spillway \
  --version 0.4.4 \
  --namespace spillway-system \
  --reuse-values