Skip to content

feat(scan): support self-hosted cloud-pricing-api via INFRACOST_CLI_PRICING_API_KEY - #197

Merged
liamg merged 2 commits into
mainfrom
fix-492-self-hosted-pricing-api-key
Jul 30, 2026
Merged

feat(scan): support self-hosted cloud-pricing-api via INFRACOST_CLI_PRICING_API_KEY#197
liamg merged 2 commits into
mainfrom
fix-492-self-hosted-pricing-api-key

Conversation

@liamg

@liamg liamg commented Jul 30, 2026

Copy link
Copy Markdown
Member

Fixes FIX-492: CLI v2 always sends the OAuth access token as X-Api-Key, which a self-hosted cloud-pricing-api rejects (it does an exact string match against SELF_HOSTED_INFRACOST_API_KEY), and there was no way to run without reaching Infracost Cloud. Some customers only pay for cost estimates and run in networks where only their own services are reachable.

Setting the new INFRACOST_CLI_PRICING_API_KEY env var (paired with the existing INFRACOST_CLI_PRICING_ENDPOINT) switches the CLI into self-hosted pricing mode:

  • the static key is sent to provider plugins for pricing auth and always wins over the OAuth token
  • login, org resolution, RunParameters, telemetry, cached-token JWKS validation, and the update check are all skipped — nothing but pricing traffic leaves the machine
  • providers receive empty (non-nil) policy slices so no FinOps/tag policies are evaluated (nil means "evaluate all built-ins"); this also overrides INFRACOST_CLI_USE_ALL_LOCAL_POLICIES
  • the MCP session gate passes tools through without auth; Cloud-only commands fail with an actionable error

Also per the ticket: the events client gets a 10s timeout in all modes (it previously had none and could hang a command on a blocked network), and the legacy INFRACOST_PRICING_API_ENDPOINT env var is now honored with a rename warning instead of silently pricing against pricing.api.infracost.io.

Verified end-to-end against a logging mock: the scan completes with no login, the pricing endpoint receives the static key in X-Api-Key, and endpoints standing in for the dashboard/events APIs receive zero requests.

…RICING_API_KEY

Setting INFRACOST_CLI_PRICING_API_KEY switches the CLI into self-hosted
pricing mode, restoring the legacy endpoint+key setup for users who run
their own cloud-pricing-api and have no route to Infracost Cloud:

- The static key is sent to provider plugins as the pricing API key and
  always wins over the OAuth access token (which a self-hosted pricing
  API cannot validate — it does an exact string match on x-api-key).
- Login, org resolution, and the RunParameters fetch are skipped; the
  scan runs against zero-value run parameters.
- Providers receive empty (non-nil) policy slices so no FinOps or tag
  policies are evaluated (nil would mean "evaluate all built-ins"),
  overriding INFRACOST_CLI_USE_ALL_LOCAL_POLICIES.
- Telemetry becomes a no-op and cached-token validation (a JWKS fetch)
  is disabled so nothing else leaves the machine; the update check
  against releases.infracost.io is skipped too.
- The MCP session gate passes tools through without auth/org in this
  mode; Cloud-only tools fail downstream with an actionable error.
- The events client also gets a 10s request timeout regardless of mode
  so fire-and-forget telemetry can't hang a command on a blocked
  network.
- The legacy INFRACOST_PRICING_API_ENDPOINT env var is honored (with a
  rename warning) when nothing else sets a pricing endpoint, so
  self-hosted users migrating from the legacy CLI don't silently price
  against pricing.api.infracost.io.

Fixes FIX-492
@liamg liamg self-assigned this Jul 30, 2026
@liamg
liamg marked this pull request as ready for review July 30, 2026 15:21
@liamg
liamg requested review from a team and aliscott July 30, 2026 15:22
@liamg
liamg marked this pull request as draft July 30, 2026 15:23
@liamg
liamg marked this pull request as ready for review July 30, 2026 15:23
Comment thread internal/config/config.go Outdated
// (login, dashboard, telemetry) is disabled — policies, guardrails, budgets,
// usage defaults and config templates are skipped. Pair with
// PricingEndpoint to point at the self-hosted instance.
PricingAPIKey string `env:"INFRACOST_CLI_PRICING_API_KEY" flag:"pricing-api-key;hidden" usage:"API key for a self-hosted Cloud Pricing API; setting it disables Infracost Cloud features (policies, guardrails, budgets)"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PricingAPIKey and PricingEndpoint are independent, so setting only INFRACOST_CLI_PRICING_API_KEY leaves the endpoint at its default and sends the self-hosted key as X-Api-Key to pricing.api.infracost.io. Should we error, or at least warn loudly, when this is set and the endpoint is still the default?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with a hard error in c68e32b: scan/price now fail fast with "set INFRACOST_CLI_PRICING_ENDPOINT to your self-hosted Cloud Pricing API" when the key is set but the endpoint is still the default. The combination can never work (the SaaS only accepts OAuth tokens), so a warning would just delay the 403.

Comment thread internal/config/config.go Outdated
// (login, dashboard, telemetry) is disabled — policies, guardrails, budgets,
// usage defaults and config templates are skipped. Pair with
// PricingEndpoint to point at the self-hosted instance.
PricingAPIKey string `env:"INFRACOST_CLI_PRICING_API_KEY" flag:"pricing-api-key;hidden" usage:"API key for a self-hosted Cloud Pricing API; setting it disables Infracost Cloud features (policies, guardrails, budgets)"`

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registering this as a flag puts the key in argv, so it shows up in ps and shell history. It's the only credential in this config exposed that way, since AuthenticationToken is env-only. Do you think we should drop the flag and keep it env-only?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess we could remove it - I was thinking people would do --pricing-api-key $API_KEY or whatever, but let's remove the footgun.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed in c68e32b — env-only now, matching INFRACOST_CLI_AUTHENTICATION_TOKEN.

Comment thread internal/config/config.go Outdated
// Honor the legacy name when nothing else set an endpoint so self-hosted
// users migrating from the legacy CLI don't silently price against
// pricing.api.infracost.io.
if legacy := os.Getenv("INFRACOST_PRICING_API_ENDPOINT"); legacy != "" && config.PricingEndpoint == "https://pricing.api.infracost.io" {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This tests the resolved value rather than whether the new var was actually set, so someone who explicitly exports INFRACOST_CLI_PRICING_ENDPOINT=https://pricing.api.infracost.io while the legacy var is still in their shell gets silently repointed at the legacy value. Should this key off whether the new var was set instead?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c68e32b: the fallback now requires the new env var to be unset (checked directly) as well as the resolved value being the default, with a test for the explicit-default case.

Comment thread internal/config/config.go
// pricing.api.infracost.io.
if legacy := os.Getenv("INFRACOST_PRICING_API_ENDPOINT"); legacy != "" && config.PricingEndpoint == "https://pricing.api.infracost.io" {
logging.Warnf("INFRACOST_PRICING_API_ENDPOINT was renamed to INFRACOST_CLI_PRICING_ENDPOINT; using the legacy value %s — please update your configuration", legacy)
config.PricingEndpoint = legacy

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we handle the legacy key here too? Honoring the old endpoint but not INFRACOST_API_KEY seems a bit confusing.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in c68e32b, but gated: INFRACOST_API_KEY is only adopted when INFRACOST_PRICING_API_ENDPOINT is also set, which is unambiguously a legacy self-hosted setup. On its own it's too ambiguous — it lingers in CI environments for the runner flow, and adopting it there would silently flip those runs into self-hosted mode and disable Cloud features.

Comment thread internal/cmds/policies.go Outdated
Dashboard: cfg.Dashboard,
Currency: cfg.Currency,
PricingEndpoint: cfg.PricingEndpoint,
PricingAPIKey: cfg.PricingAPIKey,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this never gets read. ListPolicies doesn't touch s.PricingAPIKey, only Scan does at scanner.go:327 and :351. Could this go?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — removed in c68e32b. ListPolicies never prices anything, so the key is only threaded where Scan runs.

…t endpoint, legacy INFRACOST_API_KEY

- Drop the --pricing-api-key flag: it put the credential in argv (visible
  to ps and shell history); env-only now, matching AuthenticationToken.
- Error out of scan/price when the key is set but the pricing endpoint
  still points at pricing.api.infracost.io, which only accepts OAuth
  tokens — every lookup would 403 with a confusing provider error.
- Key the legacy INFRACOST_PRICING_API_ENDPOINT fallback off whether the
  new var was set, not the resolved value, so exporting the new var as
  the default endpoint isn't silently repointed by a stale legacy var.
- Adopt legacy INFRACOST_API_KEY as the pricing key, but only when
  INFRACOST_PRICING_API_ENDPOINT is also set (unambiguously a legacy
  self-hosted setup); alone it lingers in CI envs for the runner flow
  and must not flip the CLI into self-hosted mode.
- Remove the unread PricingAPIKey from the policies command's scanner.
@liamg
liamg merged commit 82fdde7 into main Jul 30, 2026
5 checks passed
@liamg
liamg deleted the fix-492-self-hosted-pricing-api-key branch July 30, 2026 17:51
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