Skip to content

feat: allocate a network's fabric identity - #12

Merged
scotwells merged 5 commits into
mainfrom
feat/fabric-identity-allocation
Aug 27, 2026
Merged

feat: allocate a network's fabric identity#12
scotwells merged 5 commits into
mainfrom
feat/fabric-identity-allocation

Conversation

@scotwells

@scotwells scotwells commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

A network placed in two locations is two networks to the fabric. Each location draws its own identifier at random and checks it only against its own cell, so the two never match and neither imports the other routes. This allocates the identity once per network and carries it to the cells where the network is required.

The identity is drawn from an address pool that is never routed, so uniqueness, exhaustion accounting and audit come from an allocator that already solves them. It is never reissued: when a network is gone the object is collected and the allocation is retained.

Reading networks needs datum-cloud/network-services-operator#411, which mirrors them onto the federation hub beside the contexts already there.

Enumerability

This makes the identifier guessable where today it is random, because the address service offers no randomised allocation strategy. That matters because the component consuming it authorises nothing. Its authorizer is a permissive stub, and its own code notes that a tenant able to name another tenant VPC could redirect or intercept their traffic.

It is also thinner than it looks. The fabric truncates the identifier to 32 bits on the way into the Route Target, so today wider random value already has only 32 bits of effective entropy. A number that narrow cannot be a secret. The fix is to authorise the identifier rather than hide it, and that is not in this change.

Deployment

The central controller ships with manifests for the first time, following the split this platform already uses for a component that runs centrally rather than in a cell.

It reaches the hub as a second cluster and only the leader election lease is local. An unreachable hub has to cost this component its work. It does not have to cost it its identity.

Related: datum-cloud/enhancements#870

A network's identity on the fabric is drawn per location, at random, and
checked only against the VPCs in the cell that drew it. Two locations of
one network therefore get unrelated values by construction. The Route
Target is derived from that value, so neither location imports the
other's routes, and the VRF device is named from it, so a service
spanning locations needs two devices where one socket binds to one. Two
networks that collide do not fail closed: they merge, and tenant traffic
crosses between them.

A network is one network, so the value that says so cannot be decided in
any one location. This adds a central component that allocates it once
per network and carries it to the cells where the network is required.

Key changes:
- Add NetworkFabricIdentity, one per Network, spec-only, and a central
  fabric-identity-controller that allocates and publishes it
- Draw the identity from an IPAM block in a platform-owned tenancy and
  read it from the block's index, so uniqueness, exhaustion accounting
  and audit come from an allocator that already solves them
- Drive everything from NetworkContexts, which name the network, carry
  its UID and give the locations it is required in
- Mark each identity with the locations that need it and keep one policy
  per location, so the policy count follows locations, not networks
- Collect the object when nothing requires it, while retaining the
  allocation forever so an identity is never reissued
- Take an IPAM dependency, porting the per-tenancy client seam rather
  than inventing a second pattern

The identity is 32 bits because that is what survives into the Route
Target, and is never zero and never moved.
Naming the identifier claim from the network's UID meant reaching for a
UID that nothing else needed, which forced a field onto NetworkContext
purely to carry it.

Keying on namespace and name removes that without giving up what
retention is for. An identifier is still never released, so it can never
return to the pool where the allocator could hand it to an unrelated
network. A claim that finds its own retained allocation adopts it rather
than failing.

The cost is that a network deleted and recreated under the same name in
the same namespace inherits the identifier it had before. That is
confined to one name in one namespace, which is a far narrower risk than
reissue to anything at all.

Key changes:
- Derive the claim name from namespace and name, delimited by a dot,
  since a namespace cannot contain one and a dash would be ambiguous
- Adopt a retained allocation on conflict, reading the block back from
  the allocation the refusal names
- Drop networkRef.uid, which would document the opposite of the rule
The fabric identity controller was driven entirely by NetworkContexts,
which was a reachability compromise: this component has no multi-cluster
runtime, so it could not watch a Network where one lives. It could
therefore never tell a network that had been deleted from one that
happens to be required nowhere right now, and it collected the identity
in both cases. A live network whose last context drained lost the value
the fabric knows it by, and got it back only because the allocation is
retained.

The Network is now mirrored onto the federation hub alongside its
contexts, so the identity's own lifecycle follows the network and only
the placement follows the contexts.

Key changes:
- Key allocation on the Network existing rather than on a context
  appearing, and collection on the Network being gone
- Keep the identity of a live network required nowhere, withdrawing only
  its placement, so the next location it reaches is served at once
- Treat a Network on its way out as still present: the VRF device is
  named from the identity and the Route Target derives from it, so a
  teardown in flight still needs it
- Read the Network and its contexts from the hub as copies, never their
  UID, since a copy is a different object from its source
- Add a fabric-identity overlay and component so the central controller
  is actually deployed, with the hub and IPAM credentials mounted
  non-optional
- Install the NetworkFabricIdentity CRD, which was generated but never
  added to the CRD kustomization
The controller ran its manager against the federation hub, on the
grounds that everything it reads and writes is there. That put a
constant lease write on a control plane everything else federates
through, and tied this component's leadership to that plane being
reachable: a hub hiccup would churn leadership and restart the
controller.

An unreachable hub has to cost this component its work. It does not have
to cost it its identity.

Key changes:
- Run the manager against the cluster the component is scheduled on and
  reach the hub as a second cluster, the way the cell manager does
- Source both watches from the hub's cache, since that is where the
  mirrored networks and contexts live
- Refuse to start without a hub to watch, rather than come up watching a
  plane holding none of the objects the controller exists for
- Declare the lease and event access the local ServiceAccount now needs,
  which its binding already covers
A cluster carries a cache, so the manager runs it in the cache group
rather than the leader election group. It starts on every replica ahead
of any election and startup blocks until its cache has synced, which is
already the property a probe would assert.
@scotwells
scotwells marked this pull request as ready for review August 27, 2026 13:03
@scotwells
scotwells requested a review from a team as a code owner August 27, 2026 13:03
@scotwells
scotwells requested review from privateip and savme and removed request for savme August 27, 2026 13:03
@scotwells
scotwells merged commit 8bfeb9c into main Aug 27, 2026
8 checks passed
@scotwells
scotwells deleted the feat/fabric-identity-allocation branch August 27, 2026 13:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants