---
id: w7/m5/t002
title: Operator — project ipAllowList onto the Valkey Traefik TCP/SNI route
worker: worker7
status: done
estimate: 45m
depends_on: [w7/m5/t001]
---

## Objective

Make the new `KeyValue.spec.ipAllowList` real: when a KeyValue is public (`BEX_KV_DOMAIN` set + `Public`), the operator attaches a Traefik TCP `ipAllowList` middleware to the Valkey SNI route so only the listed source CIDRs can connect — the exact mechanism managed Postgres uses for its external endpoint.

## Context

- `lego/backend/internal/postgres/access.go` — the reference implementation: builds the Traefik TCP `ipAllowList` middleware from `spec.ipAllowList` and binds it to the SNI route. (Note: it lives under backend today; the KeyValue equivalent belongs with the KeyValue reconciler.)
- `lego/operator/internal/controller/keyvalue_controller.go` — the Valkey reconciler that builds the internal Service + optional public Traefik TCP/SNI route (`BEX_KV_DOMAIN`, docs/ADR021-keyvalue-management.md).
- Empty allowlist ⇒ no middleware (open external endpoint), preserving current behavior.

## Steps

1. Locate where `keyvalue_controller.go` builds the public SNI route (the `BEX_KV_DOMAIN` path).
2. When `spec.ipAllowList` is non-empty, generate + attach a Traefik TCP `ipAllowList` middleware with those CIDRs (mirror the Postgres `access.go` construction; keep it TCP/SNI, not HTTP).
3. Ensure a later empty-list update removes the middleware (idempotent CreateOrUpdate / delete-on-empty).
4. `cd lego/operator && go build ./...`.

## Files

- `lego/operator/internal/controller/keyvalue_controller.go`
- (reference only) `lego/backend/internal/postgres/access.go`

## Acceptance criteria

- [ ] A public KeyValue with a non-empty `ipAllowList` gets a Traefik TCP `ipAllowList` middleware on its SNI route limited to those CIDRs.
- [ ] Emptying the list removes the restriction; internal-only KV (no public route) is unaffected.

## Out of scope

- REST/GraphQL/MCP surfacing (t003); dashboard (t004).
