Skip to content

feat(model,api,taskworker): bootstrap the egress prober's credential without a human - #441

Merged
Ryanmello07 merged 1 commit into
urnetwork:mainfrom
Ryanmello07:feat/prober-credential-bootstrap-upstream
Aug 22, 2026
Merged

feat(model,api,taskworker): bootstrap the egress prober's credential without a human#441
Ryanmello07 merged 1 commit into
urnetwork:mainfrom
Ryanmello07:feat/prober-credential-bootstrap-upstream

Conversation

@Ryanmello07

Copy link
Copy Markdown
Contributor

Standing up an egress prober currently needs a person to create an account, authorise a balance code, and hand-carry a jwt into the prober's environment. This does that server-side on a schedule instead.

A taskworker task creates the prober's network, funds it, mints a client jwt and stores it; GET /network/prober-credential serves that jwt to the prober, authenticated with the same operator secret as the existing /network/provider-egress-* routes — one secret, one mechanism.

Idempotence is the load-bearing property, since the task re-arms every 6h forever. Four independent layers: a DB-level claim committed before any account is created (so a crash costs one bounded attempt, not an orphan account nothing can find); a hard cap of 5 creation attempts; a single-assignment write-back so a live identity can never be repointed; and per-step conditionals so a steady-state pass performs no writes at all. Tests cover each, and one of them caught a real orphan-network defect during development — a status flag reported success while an unreachable network sat in the table, and only count(*) FROM network saw it.

The seedphrase is deliberately discarded. This server holds the jwt signing keys, so the credential can be re-minted indefinitely from the stored ids; a seedphrase is a human login credential and no human logs into a machine-managed identity. Persisting it would put a root credential in postgres, recoverable from any dump, to enable a login nobody performs. The trade — the account is unrecoverable by a human by design — is documented at the discard site so nobody "fixes" it later.

ProberCredentialResult is narrow, but its doc comment says plainly that the narrowness is not the protection: the jwt itself carries network_id/user_id/network_name as readable claims, and holding it is enough to call /auth/regenerate-seedphrase. The operator secret is the actual gate. An earlier version of that comment claimed otherwise and was wrong.

Rebased onto dbbdaebc after #436; the db_migrations.go collision with it is resolved (blackhole first, then prober_identity, both before the competition block).

Migration placement — same caveat as #436. ApplyDbMigrationsUpTo iterates from the deployed version, so on an already-deployed database this entry is never reached. Run bringyourctl db audit --fix after deploy; the statement is IF NOT EXISTS and safe to apply out of band.

Build, vet and gofmt clean. Leakage-checked: no beta-only config.

…without a human

A deployment that had never had an operator hand-mint a prober credential had
no egress probing at all, and said nothing about it. The steps were: create a
network, POST /network/auth-client, paste the by_client_jwt into the prober's
environment. Miss them and every provider goes unprobed, which is
indistinguishable from a fleet that is simply healthy -- the failure is silent
in exactly the way that matters.

This makes the server do it. A task creates the prober's network and balance,
mints its client jwt, and re-mints before the jwt expires; a route hands that
credential to the prober. No human step remains between a fresh deployment and
a probing prober.

prober_identity exists to make a job that re-runs forever safe. It is the only
record that the account was already created, and it has to be, because looking
the network up by name cannot work: the seedphrase branch of NetworkCreate
ignores the requested name and generates a random one, so there is nothing to
search for on the next run. The singleton primary key makes "at most one prober
identity" a schema fact rather than a convention, so the create is an upsert
that collides with itself; create_attempts bounds it if the create keeps
failing. Re-mints re-auth the SAME client rather than accumulating one client
per refresh.

The credential route is deliberately on the same auth as the egress routes it
sits beside -- the operator secret, fail-closed when the vault resource is
missing. A route that hands out a credential is the strongest possible reason
not to invent a second, less-examined way in. Read the note on
ProberCredentialResult before touching it: the narrow response body is not the
protection. The jwt authenticates as the account, and holding it is enough to
regenerate the account's seedphrase -- the operator secret is the whole gate.

Both rejection branches log loudly, unlike the endpoints next door. A
credential endpoint that rejects everything produces a prober that probes
nothing, and that misreading has already cost this system eight hours; the log
answers "which side is misconfigured" before it is asked.

The migration sits before the competition control-plane block, so it does not
run on a database already past that index -- see the note above it, and run
`bringyourctl db audit --fix` after deploying to an existing database.
@Ryanmello07
Ryanmello07 merged commit a6b813a into urnetwork:main Aug 22, 2026
1 check passed
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.

1 participant