Skip to content

Redact credential headers from machines API errors - #90

Open
simoncocking wants to merge 1 commit into
phoenixframework:mainfrom
simoncocking:redact-api-token-in-boot-errors
Open

Redact credential headers from machines API errors#90
simoncocking wants to merge 1 commit into
phoenixframework:mainfrom
simoncocking:redact-api-token-in-boot-errors

Conversation

@simoncocking

@simoncocking simoncocking commented Aug 14, 2026

Copy link
Copy Markdown

The problem

FLAME.FlyBackend.http_post!/3 interpolates its request headers into the errors it raises, and those headers carry the Fly API token as a bearer credential:

headers: [
  {"Content-Type", "application/json"},
  {"Authorization", "Bearer #{state.token}"}
]

So every failed machine create raises a message containing the token, which then lands in logs and error trackers:

failed POST https://api.machines.dev/v1/apps/app/machines with
{:failed_connect, [{:to_address, {~c"api.machines.dev", 443}}, {:inet, [:inet], :econnrefused}]}
[{~c"Content-Type", "application/json"}, {~c"Authorization", "Bearer <FLY_API_TOKEN>"}]

Both raise clauses are affected — the non-2xx one and the transport-error one. This is easy to hit in practice: a rate limit, a capacity failure past the retry budget, or a TLS/DNS problem is enough. The 0.5.4 Let's Encrypt chain-depth issue was one such case, where every pool boot failed the handshake and raised.

This also defeats an existing intent in the module — the struct already carries

@derive {Inspect, only: [:host, :init, :cpu_kind, ...]}

which deliberately keeps :token out of inspected state. The raise path routes around it.

The change

Redact the values of headers that carry credentials before interpolating them, keeping the header names, since those are the useful part when debugging a failed request:

[{~c"Content-Type", "application/json"}, {~c"Authorization", "[REDACTED]"}]

proxy-authorization is covered too, so a future header addition doesn't reintroduce the leak.

Happy to adjust the approach — dropping the headers from the message entirely is also reasonable if you'd rather not carry the helper.

Test

test/fly_backend_test.exs gains a hermetic case that points :host at a closed local port, so the POST fails and remote_boot/1 raises, then asserts the token is absent and the header name is still present. It fails on main with the leaked token in the message and passes with the fix.

`http_post!/3` interpolates the request headers into the errors it raises,
and those headers carry `{"Authorization", "Bearer " <> token}`. Every
failed machine create therefore writes the Fly API token into logs and
error trackers:

    failed POST https://api.machines.dev/v1/apps/app/machines with
    {:failed_connect, ...} [{~c"Content-Type", "application/json"},
    {~c"Authorization", "Bearer <FLY_API_TOKEN>"}]

This defeats the `@derive {Inspect, only: [...]}` on the struct, which
already keeps `:token` out of inspected state.

Keep the header names, which are what you want when debugging a failed
request, and drop the values of the ones that carry credentials.
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