feat(scan): support self-hosted cloud-pricing-api via INFRACOST_CLI_PRICING_API_KEY - #197
Conversation
…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
| // (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)"` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| // (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)"` |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Removed in c68e32b — env-only now, matching INFRACOST_CLI_AUTHENTICATION_TOKEN.
| // 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" { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
Should we handle the legacy key here too? Honoring the old endpoint but not INFRACOST_API_KEY seems a bit confusing.
There was a problem hiding this comment.
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.
| Dashboard: cfg.Dashboard, | ||
| Currency: cfg.Currency, | ||
| PricingEndpoint: cfg.PricingEndpoint, | ||
| PricingAPIKey: cfg.PricingAPIKey, |
There was a problem hiding this comment.
Looks like this never gets read. ListPolicies doesn't touch s.PricingAPIKey, only Scan does at scanner.go:327 and :351. Could this go?
There was a problem hiding this comment.
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.
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 againstSELF_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_KEYenv var (paired with the existingINFRACOST_CLI_PRICING_ENDPOINT) switches the CLI into self-hosted pricing mode:RunParameters, telemetry, cached-token JWKS validation, and the update check are all skipped — nothing but pricing traffic leaves the machineINFRACOST_CLI_USE_ALL_LOCAL_POLICIESAlso 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_ENDPOINTenv var is now honored with a rename warning instead of silently pricing againstpricing.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.