Skip to content

feat(security): the tightest credential each provider can be given, stated per provider - #96

Merged
getnable merged 3 commits into
mainfrom
feat/least-privilege-scopes
Aug 8, 2026
Merged

feat(security): the tightest credential each provider can be given, stated per provider#96
getnable merged 3 commits into
mainfrom
feat/least-privilege-scopes

Conversation

@getnable

@getnable getnable commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Asked on r/selfhosted: can the scope be reduced down to billing? For some providers, exactly. For others the honest answer is no. The useful thing is saying which is which, because a table that grades everything "least privilege" is wrong in the one place it matters.

connector_scopes.py is that table. Every entry carries a grade:

grade meaning
scoped The provider has a billing-specific permission and nable uses exactly it. A leaked credential reads billing and nothing else.
role No per-key scope, but the key can sit behind a limited role or user.
account No narrower option exists. The credential is as powerful as the account it was minted from.

Twilio: account → scoped, and this one is code

nable asked for the Account SID and Auth Token, a credential that can send messages, buy numbers and spend money, in order to read a usage report. Twilio supports Restricted API keys on the Usage API, so nable now takes TWILIO_API_KEY / TWILIO_API_SECRET and prefers them when both are present.

The Auth Token still works, so nobody's setup breaks because a better option now exists. The account lookup is not covered by a Usage-only key and degrades to a default label, which costs a display name and nothing else.

The ambient scan learned to see that credential too. It matched on one fixed key set per provider, so a machine holding only the safer Twilio credential would not have been found: the scan would have quietly punished the better choice.

Datadog: account → scoped, no code needed

An unscoped application key inherits everything you can do in Datadog. usage_read and billing_read reduce it to cost. The setup hint now says so, and every hint interpolates the manifest's permission string rather than retyping it, so what a user is told and what the trust page publishes cannot drift.

Databricks: still account, and the gap is recorded

The billable-usage download API is account-admin only. system.billing.usage can be read with three grants instead, and nable does not query it over SQL yet. That ships as a gap field, renders in the output, and is asserted by a test. A manifest that only records good news is marketing.

The rest

Also graded honestly rather than talked up: OpenAI and Anthropic admin keys (the cost endpoints are admin-gated, and those keys can manage members), Vercel (team scope and an expiry, no per-permission scopes), New Relic (the key inherits its user's role), Snowflake (a dedicated role reaching two metering views), MongoDB Atlas and Cloudflare (billing-only by construction).

Surface

nable connect --scopes

Prints the whole table before any credential is read, worst grade first, because bad news should not be buried under the good news.

Tests

15 guards, 57 cases: every registered billing source has an entry, no entry documents a source that does not exist, scoped is never spent on a hand-wave like "no billing-only scope", the broad ones stay labelled broad, the Databricks gap survives, the setup hints still quote the manifest verbatim, and the Twilio credential swap does not move the account SID out of the URL path.

Full suite green at 2254 passed, 2 skipped.

🤖 Generated with Claude Code

getnable and others added 3 commits August 8, 2026 18:29
…tated per provider

Asked on r/selfhosted: can the scope be reduced down to billing? For some
providers, exactly. For others the honest answer is no. The useful thing is
saying which is which, because a table that grades everything "least privilege"
is wrong in the one place it matters.

connector_scopes.py is that table, and every entry carries a grade:

  scoped   The provider has a billing-specific permission and nable uses exactly
           it. A leaked credential reads billing and nothing else.
  role     No per-key scope, but the key can sit behind a limited role or user.
  account  No narrower option exists. The credential is as powerful as the
           account it was minted from. Said plainly.

Twilio moves from account to scoped, and that one is code rather than
documentation. nable asked for the Account SID and Auth Token, a credential that
can send messages, buy numbers and spend money, to read a usage report. Twilio
supports Restricted API keys on the Usage API, so nable now takes
TWILIO_API_KEY / TWILIO_API_SECRET and prefers them when both are present. The
Auth Token still works, so nobody's setup breaks because a better option exists.
The account lookup is not covered by a Usage-only key and degrades to a default
label, which costs a display name and nothing else.

The ambient scan learned to see that credential. It matched on one fixed key set
per provider, so a machine holding only the safer Twilio credential would not
have been found: the scan would have quietly punished the better choice.

Datadog moves from account to scoped without code. An unscoped application key
inherits everything you can do in Datadog; usage_read and billing_read reduce it
to cost. The setup hint now says so, and every hint interpolates the manifest's
permission string rather than retyping it, so what a user is told and what the
trust page publishes cannot drift.

Databricks is graded account and records a known gap: the billable-usage
download API is account-admin only, while system.billing.usage needs three
grants. nable does not query it over SQL yet. A manifest that only records good
news is marketing, so the gap ships in the open and renders in the output.

`nable connect --scopes` prints the whole table before any credential is read,
worst grade first.

15 guards, 57 cases. They pin that every registered billing source has an entry,
that "scoped" is never spent on a hand-wave, that the broad ones stay labelled
broad, and that the setup hints still quote the manifest verbatim. Full suite
green at 2254.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nner

CI's secret scan flagged six lines in the new test file. They are fixtures
("tok", "sec", "auth-token-value"), not secrets, but the scanner is right to
stop at an assignment named TWILIO_AUTH_TOKEN, and the fix belongs at the line
rather than in the baseline: a pragma says "this is fake" where a reader will
see it, while a baseline entry hides it behind a hash nobody opens.

The local suite could never have caught this. detect-secrets runs in CI only,
so a green pytest was never evidence about it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The previous pass annotated four of the eight fixture lines and I checked it
with a local run that exited 0 against a baseline CI does not have, so a green
exit proved nothing. Verified this time the way CI verifies: the same
detect-secrets-hook invocation, plus an assertion that the baseline file comes
out unmodified, because the hook rewrites it and then fails on the dirty tree.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@getnable
getnable force-pushed the feat/least-privilege-scopes branch from f923e95 to b203199 Compare August 8, 2026 23:29
@getnable
getnable merged commit 00d9eef into main Aug 8, 2026
5 checks passed
@getnable
getnable deleted the feat/least-privilege-scopes branch August 8, 2026 23:32
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