Kubernetes operator · Cloud network inventory

Your subnet inventory, maintained by the cloud itself.

Subnet Operator discovers VPCs and subnets across every account and region from resource tags, keeps them as Kubernetes objects and flags what is wrong. No more spreadsheets. Untagged networks are one click from being managed, and when you are ready it can also allocate subnets automatically.

Read-only by default Multi-account Prometheus-native Apache-2.0
0writes to your cloud unless you opt in
~10 sfrom an EC2 API call to the cluster, via EventBridge
N accountsone operator, hub-and-spoke via AssumeRole
3 cloudsAWS · GCP · Azure from 3.0
The problem

The network spreadsheet is always wrong.

Most IT teams track VPCs and subnets by hand: a shared sheet, a wiki page, a CMDB export. It goes stale on the first day, and the gaps show up at the worst possible moment.

Drift

Subnets are created by Terraform, consoles and scripts. Nobody updates the sheet, so it slowly turns into fiction.

Overlaps found too late

Two teams pick the same CIDR in different accounts. You find out when peering or Transit Gateway routing breaks.

No owner, no headroom

Who owns this subnet? Is it 90% full? Answering takes a ticket, three consoles and a guess.

And every tool that promises to fix this starts with "first, tag everything" — which is why the spreadsheet is still there.

The product

A minimal core that does one job well. Automation when you want it.

The inventory is useful on its own and safe to run anywhere: it only reads. Automatic management is a separate, opt-in layer on top of the same API.

CORE · READ-ONLY

Live network inventory

Everything the spreadsheet tried to be, generated from the cloud and kept current.

  • Discovers VPCs by tag across accounts and regions, including every subnet inside them
  • CIDR, AZ, free IPs, utilization, public/private, owner, environment, tier
  • Finds missing required tags and CIDR overlaps between VPCs, even across accounts
  • Shows what is not managed yet and takes it under management in one step: a ResourceImport tags the resource in AWS, nothing else
  • Updates within seconds from CloudTrail events, with a periodic full resync as a safety net
  • Prometheus metrics, kubectl, and a read-only Google Sheet mirror for people who still want a table
Only ec2:Describe* calls. Nothing in your cloud is ever modified. The Google Sheet mirror is written, never read back, so the old spreadsheet stops being something anyone maintains.
OPTIONAL · AUTOMATIC MANAGEMENT

Subnets on demand

Teams ask for capacity; the operator picks a free CIDR and creates correctly tagged subnets.

  • SubnetClaim: prefix length, zones, owner, environment, tier and the target network (VPC)
  • CIDR allocation by a built-in first-fit allocator over the VPC's free space, skipping existing subnets and other claims' reservations
  • Route-table association, the organization's tags, and a hs/claim tag that lets the operator adopt a subnet it created
  • "Allocate only" mode for Terraform-first teams: the CIDR is reserved, Terraform creates the subnet
  • Never deletes subnets and never touches Terraform-owned resources
Off until you pass --enable-writes (writes.enabled in the chart) and deploy a write role of its own, and only for the namespaces a scope allows. Tagging a resource needs nothing but ec2:CreateTags.
How it works

One operator, every account, near real time.

The operator runs in a hub cluster and reaches spoke accounts with short-lived AssumeRole credentials. Change events tell it which account and region to look at again.

SPOKE ACCOUNTS · EVERY REGION VPCs & subnets tag hs/managed=true hs/owner · env · tier CloudTrail EC2 API calls EventBridge rule network changes only roles: readonly · write HUB ACCOUNT Event bus org-wide PutEvents SQS queue SSE · DLQ · 1h retention EKS · SUBNET OPERATOR Event poller debounce 10s Scope controller Discovery ec2:Describe* sts:AssumeRole → Describe* OUTPUTS CRDs NetworkScope · Network Subnet Prometheus alerts · Grafana Google Sheet read-only mirror SubnetClaim (opt-in) allocate CIDR · create tagged subnet (--enable-writes)
01

Tag, or import

Mark VPCs with hs/managed=true and owners — by hand, in Terraform, or by importing what discovery found untagged. Tag policies keep the schema honest.

02

Scope

A NetworkScope lists accounts, regions and required tags.

03

Discover

The operator assumes a read-only role per account and mirrors what it finds.

04

React

CloudTrail events trigger a targeted resync of just the account/region that changed.

Onboarding

From untagged to managed in one step.

Discovery does not stop at the tag selector: it lists what it can see but nobody has claimed. Importing a resource applies the organization's tags in AWS — nothing else changes — and the inventory picks it up on the next CloudTrail event, by hand or by policy.

Unmanaged resources 3 found

Discovered, but not tagged for the operator.

ResourceAccountCIDR
vpcvpc-0f3e2a1b7c9d4e56terraform333333333333 · eu-central-110.90.0.0/16Import
subnetsubnet-04d1c2b3a4e5f607333333333333 · eu-central-110.30.4.0/24Import
subnetsubnet-0b2c3d4e5f6a7b89222222222222 · eu-west-110.20.40.0/22Import
Import subnet-04d1c2b3a4e5f607
Apply importCopy YAML
What gets applied
apiVersion: network.hypersurgery.dev/v1
kind: ResourceImport
spec:
  scopeRef: organization
  account: "333333333333"
  region: eu-central-1
  resourceID: subnet-04d1c2b3a4e5f607
  tags:
    hs/managed: "true"
    hs/owner: team-data
    hs/env: prod
    hs/tier: private

Nothing stays hidden

Every VPC and subnet in the accounts is discovered; the tag selector decides what is managed, not what is visible.

An import is a tag change

A ResourceImport object, applied by hand or through GitOps, becomes one ec2:CreateTags call. Tags stay the source of truth; the CRD is not a second spreadsheet.

Terraform-aware

For resources that carry managed-by=terraform the app warns and shows the ignore_changes snippet, so the next plan does not undo the import.

Narrow permissions, full audit

Importing needs nothing but ec2:CreateTags; who imported what, and when, is in the object, in a Kubernetes Event, in the operator's audit stream and in CloudTrail.

Try the import in the demo
Automatic mode

Or let policy do the importing.

Someone creates a subnet from the console at 5 pm on a Friday. The CloudTrail event that tells the operator about it also says who did it. With an auto-import policy on the scope, the operator derives the tags — from the creator's role, from the parent VPC, or from the account's defaults — and applies them within seconds. What it cannot resolve becomes an alert for a human instead of a silent gap.

  • Three sources for the tags, tried in order: the creating principal, the network (VPC) the subnet lives in, the account's defaults. Tags the resource already has always win
  • Skip rules for Terraform: by tag or by the principal that created it, so pipelines keep their own state clean
  • Dry run first. The operator still creates the ResourceImport objects — you review a day of them before switching to Apply
  • Nothing owner-less is tagged. If no rule yields an owner, the resource stays unmanaged and the alert fires
networkscope.yamlauto-import policy
spec:
  autoImport:
    mode: DryRun                  # Off | DryRun | Apply
    namespace: platform              # where its ResourceImports go
    fromCreator:
      - principalPrefix: "arn:aws:sts::222222222222:assumed-role/payments-"
        tags: { hs/owner: team-payments }
      - principalPrefix: "arn:aws:sts::333333333333:assumed-role/data-platform-"
        tags: { hs/owner: team-data }
    inheritFromNetwork: [hs/owner, hs/env]
    accountDefaults:
      - account: "333333333333"
        tags: { hs/env: prod }
    skip:
      - tagKey: managed-by
        tagValue: terraform
      - principalPrefix: "arn:aws:sts::222222222222:assumed-role/terraform-"
    requiredTags: [hs/owner]     # otherwise: leave unmanaged, alert
Alerting

Know the moment something appears that nobody owns.

The operator's metrics carry every unmanaged resource with its account and region, so an ordinary Prometheus rule turns a hand-made subnet into a Slack or Teams message a few seconds after CloudTrail sees it — with the import one click away.

Unmanaged resources Firing · 2 last 7 days
4321 MonTueWedThuFriSatSun alert fired 17:04
AccountRegionResourceAge
333333333333eu-central-1subnet-04d1c2b3a4e5f6074m
222222222222eu-west-1subnet-0b2c3d4e5f6a7b894m
AlertmanagerAPP17:04
🔥 FIRING · UnmanagedNetworkResource
2 new network resources without hs/managed in the last 10 minutes.
Resourcesubnet-04d1c2b3a4e5f607
Account / region333333333333 / eu-central-1
Created byassumed-role/ops-admin/s.okonkwo
Auto-importskipped · no owner rule matched
Import…Silence 24hGrafana
UnmanagedNetworkResource Firing
Subnet operator · scope organization
Resourcesubnet-0b2c3d4e5f6a7b89
Account222222222222 · eu-west-1
CIDR10.20.40.0/22
First seen17:04, 4 minutes ago
Import in dashboardOpen runbook
prometheusrule.yamlships with the chart
- alert: UnmanagedNetworkResource
  expr: increase(hs_unmanaged_resources_total[30m]) > 0
  for: 10m
  labels: { severity: warning }
  annotations:
    summary: "{{ $value }} new unmanaged {{ $labels.kind }} resources in {{ $labels.account }}/{{ $labels.region }}"
    runbook_url: "https://hypersurgery.dev/docs/#unmanaged"

- alert: AutoImportedResources          # info: a daily digest, not a page
  expr: increase(hs_auto_imports_total{result="applied"}[24h]) > 0
  labels: { severity: info }
alertmanager.yamlroute to Slack and Teams
receivers:
  - name: network-slack
    slack_configs:
      - channel: "#network-inventory"
        title: "{{ .CommonLabels.alertname }}"
        actions:
          - type: button
            text: "Import…"
            url: "https://hypersurgery.dev/dashboard/"
  - name: network-teams
    msteamsv2_configs:
      - webhook_url: "https://….webhook.office.com/…"
route:
  routes:
    - matchers: [alertname = UnmanagedNetworkResource]
      receiver: network-slack
      continue: true
    - matchers: [alertname = AutoImportedResources]
      receiver: network-teams

The chart's PrometheusRule ships all of it: the unmanaged-resource rule above, a daily digest of what the policy imported by itself, plus a subnet at 85% (configurable), a subnet with no free addresses, an unreachable or throttled account, overlapping network CIDRs, a stale inventory, a claim or import stuck for too long and an operator that is not running. Each has a runbook entry, and they all travel this route into Slack or Teams.

Features

Built like a cloud-native component, not a script.

Tag-driven discovery

Select VPCs by tags; every subnet inside is included, so untagged ones cannot hide.

Hub and spoke

One operator, many accounts. Role ARNs are checked against the declared account ID.

Event-driven

EventBridge → SQS, debounced, leader-only. Only the changed account/region is resynced.

Compliance findings

Missing required tags per subnet, surfaced in status and as metrics.

Overlap detection

CIDR overlaps across VPCs, GCP networks, accounts and projects, before peering or TGW routing fails.

Prometheus metrics

Free and total IPs, missing tags, overlaps, target health. Alert on 80% before it is 100%.

Terraform-friendly

Terraform-owned resources are observed, never mutated. Allocate-only mode keeps Terraform in charge.

Refused at apply time

An admission webhook rejects a claim that cannot be satisfied — a prefix that does not fit the VPC, an AZ from another region — with a readable message, instead of failing quietly in a reconcile loop.

Audit trail

Every import, allocation and policy decision leaves a Kubernetes Event on the object and one JSON line on a separate stream: who asked, what changed, and the tags before and after.

One-step onboarding

Untagged VPCs and subnets are listed, not hidden. Importing one applies the tags in AWS — with a warning when Terraform owns it.

API

Declare the scope. Read the result.

Group network.hypersurgery.dev, version v1: one API for every cloud, with a written compatibility promise (v1beta1, the API of 0.8 and 0.9, is still served, deprecated). Network and Subnet objects are written by the operator and owned by their scope.

networkscope.yamlyou write
apiVersion: network.hypersurgery.dev/v1
kind: NetworkScope
metadata:
  name: organization
spec:
  provider: AWS
  accounts:
    - id: "111111111111"          # hub, own credentials
    - id: "222222222222"
      aws:
        roleARN: arn:aws:iam::222222222222:role/aws-subnet-operator-readonly
  regions: [eu-central-1, eu-west-1]
  networkSelector:
    matchTags:
      hs/managed: "true"
  requiredSubnetTags: [hs/owner, hs/env, hs/tier]
  resyncInterval: 10m
subnet/subnet-0e4f5a6bthe operator writes
spec:
  provider: AWS
  id: subnet-0e4f5a6b
  networkID: vpc-0aa11bb2
  account: "111111111111"
  region: eu-central-1
status:
  cidrBlock: 10.20.2.0/24
  zone: eu-central-1b
  totalIPs: 251
  availableIPs: 31
  utilizationPercent: 87
  owner: team-payments
  tier: private
  missingTags: [hs/env]
  aws:
    public: false
Safety

Designed so you can install it on day one.

Read-only by default

The core uses three EC2 Describe calls. Write permissions live in a separate role you add later, if ever.

Never deletes cloud resources

Removing an object from the cluster never removes a subnet. Deletion stays a human decision.

Right account or nothing

Credentials are verified against the declared account ID, so a typo cannot report another account's networks.

Degrades gracefully

An unreachable account is reported, not erased. Its last known state stays until it syncs again.

Survives a node drain

Two replicas by default with a disruption budget and leader election, so draining the node holding the lease does not pause discovery.

Reads one namespace of Secrets

The credential Secret is read through a namespaced Role, not a cluster-wide permission. A compromised operator does not open every Secret you have.

Evidence

Every claim on this page is checkable.

These badges are live. They come from the runs themselves, not from a screenshot someone took on a good day.

CI Security scans CodeQL Coverage OpenSSF Scorecard Apache-2.0
On every changeUnit tests and envtest against a real API server, the Helm chart rendered and linted, and a check that the generated CRDs match the types.
End to endThe operator runs in a Kind cluster against a mock AWS, across two accounts, and is asked to discover, allocate and import for real.
Security scannersgovulncheck, gosec, Trivy and Semgrep on every pull request and again weekly, because vulnerability databases move when the code does not. CodeQL with the extended security queries.
Supply chainThird-party actions are pinned to commit SHAs, not tags. OpenSSF Scorecard runs in the open and publishes its result.
UpgradesEvery change is installed on top of the latest release in Kind and checked for anything lost on the way, the move of stored objects to the v1 API included.
Not yet provenEverything above runs against a mock (Moto). The operator is stable in what it promises — the v1 API and its compatibility rules — but it has not yet been run against a real AWS organization, and the Google Cloud provider of 2.0 and the Azure provider of 3.0, tested against in-repository fakes of the Google and Azure APIs, not against a real project or subscription. If you can run the read-only core in one, we would like to hear how it went; until someone has, this page will keep saying so.
Read

The longer version, with fewer bullet points.

Cover of the article I Must Break Your Spreadsheet
Article · 12 min

I Must Break Your Spreadsheet

Why the tags are the inventory and the table never was, what happens in the five minutes after somebody creates a subnet by hand, and what the alerts, the Grafana dashboard and the manifests actually look like.

Read the article
Roadmap

Where the project is today.

1

Read-only inventory

DONE

Multi-account, multi-region discovery; Network (VPC) and Subnet CRDs; compliance findings; CIDR overlaps; metrics.

2

Scale out

DONE

Near-real-time updates and the views people actually look at.

Kind + Moto e2eEventBridge → SQS eventsGoogle Sheet exportGrafana dashboardHelm chart
3

Automatic management (opt-in)

DONE

SubnetClaim with a built-in first-fit CIDR allocator, route-table association, allocate-only mode for Terraform; ResourceImport to take untagged networks under management with a single object, and a policy that does it by itself.

SubnetClaim + CIDR allocationAllocate-only modeOpt-in creation behind a write roleResourceImportAuto-import policy
4

Production hardening

ONE ITEM OPEN

Admission webhooks refuse an impossible claim at kubectl apply instead of a reconcile later; every import, allocation and policy decision leaves a Kubernetes Event and a line in a JSON audit stream; the chart runs two replicas with a disruption budget and reads credentials from one namespace, not the cluster. The image is public, multi-arch and signed with cosign keyless, so installing needs no credentials and you can check what you are about to run. The operator alerts on its own absence and on claims and imports that stay stuck, and a stopped leader hands its lease over in about a second and a half. A target that AWS throttles backs off on its own and alerts as throttled, not as down; the chart itself is signed on ghcr.io; and the dashboard can run in the cluster, reading it as whoever is looking. Every change is now tested as an upgrade from the last release, capacity is measured rather than estimated (400 account/region pairs), and a threat model came with fixes: the write role can touch only networks, auto-import never overwrites a tag somebody set, a scope can be limited to the namespaces allowed to write through it, and the audit trail records who really created a claim or an import. Still open: a conformance run against a real AWS organization.

Admission webhooksAudit trailHA defaultsNamespaced Secret accessSigned images · SBOM · provenanceAlerts that notice silenceFast leader hand-overThrottling backoffSigned chart on ghcr.ioLive dashboardUpgrade testsThreat modelNamespace-scoped writesReal-AWS conformance · needs an account
5

A cloud-neutral API

DONE · 0.8 · 0.9

The API moved to network.hypersurgery.dev, with the cloud as a field of the scope rather than part of every name, and the project became subnet-operator. 0.8 migrated existing objects itself, status included, so an upgrade did not lose a reservation; 0.9 dropped the old group, and will not run next to an object that was never migrated. Decided in ADR 0002.

6

1.0 · stable API

DONE · 1.0

The v1 API with a written compatibility promise, in which a scope that names no namespaces can be written through by none. v1beta1 stays served, deprecated, through a conversion webhook, and the operator moves what 0.9 stored to v1 on its own. The support policy applies in full, and every release also ships a signed OLM bundle. Stable means that promise: the conformance run against a real AWS organization is still open.

7

Google Cloud

DONE · 2.0

The same inventory, claims and imports for GCP projects, added to the v1 API without breaking it. A major release because the ownership tag keys become provider-dependent (hs/* stays on AWS, hs-* on GCP), with an upgrade guide, as the support policy provides for a new provider. Released in 2.0.0: discovery of VPC networks and subnetworks with their IP usage and Resource Manager tags; subnetworks created by claims and tags bound by imports, only ever added; GKE Workload Identity, Workload Identity Federation and a read and a write service account per project; and change events from Cloud Audit Logs through a log sink and Pub/Sub; and overlaps between GCP networks, peered ones flagged. Still open is a conformance run against a real Google Cloud project. The GCP guide.

DiscoverySubnetwork creation and tag bindingWorkload Identity · impersonationDocs and dashboardAudit log events via Pub/SubReal-project conformance · needs a project
8

Azure

DONE · 3.0

The same for Azure subscriptions, still on the v1 API. Azure subnets cannot carry tags, so their ownership is kept on the parent virtual network, one tag per subnet. A major release because it adds a provider, as the support policy provides; nothing breaks for AWS or Google Cloud. Released in 3.0.0: discovery of virtual networks and subnets with their address usage; subnets created by claims and ownership written by imports, only ever added, within the 50 tags a virtual network may carry; Microsoft Entra Workload ID with a read and a write identity per subscription; and change events from Event Grid through a Storage queue. All of it is tested against in-repository fakes of the Azure APIs. Still open is a conformance run against real subscriptions, and Azure's events do not say who created a resource, so creator rules of the auto-import policy do not apply there. The Azure guide.

DiscoverySubnet creation and ownership on the virtual networkWorkload ID · identities per subscriptionEvent Grid events via a Storage queueDocs and dashboardReal-subscription conformance · needs a subscription
Clouds

AWS, Google Cloud and Azure. Vendor-neutral by design.

One API group for every cloud, with the provider as a field: what all clouds share is a neutral field, what only one has lives in that provider's own member, so the model follows real platforms instead of a lowest common denominator.

Amazon Web Services

provider: AWS

VPCs, subnets, route tables, CloudTrail events, AssumeRole. Available now.

Google Cloud

provider: GCP

Global VPC networks, regional subnetworks with secondary ranges, Shared VPC host projects, Resource Manager tags, Workload Identity, audit log events through Pub/Sub. Available from 2.0.

Microsoft Azure

provider: Azure

Virtual networks and subnets across subscriptions, subnet ownership kept as tags on the virtual network, Microsoft Entra Workload ID, Event Grid events through a Storage queue. Available from 3.0; tested against fakes, not yet against real subscriptions.

CNCF

Why it belongs in the cloud-native landscape.

Kubernetes-nativeCRDs, controllers, status conditions and owner references. Built with kubebuilder and controller-runtime.
ObservablePrometheus metrics that feed straight into alerting and Grafana.
Vendor-neutralOne inventory model for AWS, Google Cloud and Azure, behind a provider interface every cloud plugs into. AWS, Google Cloud from 2.0 and Azure from 3.0.
ComposableWorks next to Terraform, Karpenter, the AWS Load Balancer Controller and Crossplane instead of replacing them.
OpenApache-2.0 licensed, open development and a public roadmap.

Looking for early adopters and maintainers

helm repo add hypersurgery https://charts.hypersurgery.dev

We are preparing the project for the CNCF Sandbox. The most useful help right now:

  • Platform teams who track networks in spreadsheets today and want to try the read-only core
  • A run of the read-only core against a real AWS organization, and a report of what it found
  • Feedback on the tag schema and the v1 API: it only grows now, so additions are still possible
  • Contributors for the Google Cloud and Azure providers