Redact credential-bearing base-url path segments before they reach logs or error strings #93

Open
opened 2026-08-22 22:01:50 +00:00 by coilyco-ops · 1 comment
Member

Handed over from coilyco-flight-deck/infrastructure#708, where a Telegram bot credential was exposed for 22 days in notification-failure log output and had to be rotated.

What happened

The signoz-telegram mapper is configured with a base-url that is the credential:

TELEGRAM_API_BASE_URL: "https://api.telegram.org/bot{{ .botToken }}"

Telegram carries its bot token in the URL path rather than a header or a query parameter, and base-url takes a single env value, so the deploy side has no way to supply the token separately. From the manifest's own comment in coilyco-bridge/deploy:

the guardfile's base-url needs the token already interpolated. The chart maps one env var to one SSM path and cannot compose, so this declares the Secret directly.

That is a reasonable workaround given what beaver offers today. The consequence is that any code path reporting a failing request by echoing its URL emits the credential verbatim, which is exactly what happened.

Why rotation did not fix it

Rotation replaces the burned value. It does nothing about the mechanism, so the next notification failure burns the new credential the same way. infrastructure#708 should not close until this lands.

Worth noting for whoever picks this up: the failure path is the leak, so it cannot be safely exercised against a live credential. Reproduce against a throwaway token.

Two candidate fixes

  1. Path composition. Let a guardfile compose a path segment from its own env var, so the token is supplied separately and assembled at call time. auth query-param already exists for the query case, so this is the path-shaped sibling of a pattern beaver has.
  2. Redaction. Mask a /bot<id>:<secret> style segment in any URL before it reaches a log line, error string, or trace attribute.

Recommend doing 2 regardless, and 1 as well if it is cheap. Redaction is the stronger guarantee because it holds even when a future caller reintroduces a credential-bearing base URL by configuration, which is precisely how this arose. Composition alone leaves the same trap open for the next integration whose API puts a secret in the path.

A general form is probably better than a Telegram-specific pattern: treat any base-url-derived path segment as sensitive by default, since an operator only puts a secret in a base URL when the upstream API forces it.

Acceptance

A deliberately failed upstream call produces a log line, error string, and trace attribute containing a redacted path segment and no token material. Cover the success path too, so a future change that starts logging request URLs on success does not reopen this.

Context

  • Exposure and rotation: coilyco-flight-deck/infrastructure#708
  • Consumer wiring: services/signoz-telegram/ in coilyco-bridge/deploy, kai-server-externalsecret.yml and signoz-telegram.mcp.kdl
  • Callers today: SigNoz alert routing on ser8 and kai-server, plus Forgejo CI failure alerts via /api/create_alert
Handed over from `coilyco-flight-deck/infrastructure#708`, where a Telegram bot credential was exposed for 22 days in notification-failure log output and had to be rotated. ## What happened The `signoz-telegram` mapper is configured with a `base-url` that **is** the credential: ``` TELEGRAM_API_BASE_URL: "https://api.telegram.org/bot{{ .botToken }}" ``` Telegram carries its bot token in the URL path rather than a header or a query parameter, and `base-url` takes a single env value, so the deploy side has no way to supply the token separately. From the manifest's own comment in `coilyco-bridge/deploy`: > the guardfile's base-url needs the token already interpolated. The chart maps one env var to one SSM path and cannot compose, so this declares the Secret directly. That is a reasonable workaround given what beaver offers today. The consequence is that **any code path reporting a failing request by echoing its URL emits the credential verbatim**, which is exactly what happened. ## Why rotation did not fix it Rotation replaces the burned value. It does nothing about the mechanism, so the next notification failure burns the new credential the same way. `infrastructure#708` should not close until this lands. Worth noting for whoever picks this up: the failure path is the leak, so it cannot be safely exercised against a live credential. Reproduce against a throwaway token. ## Two candidate fixes 1. **Path composition.** Let a guardfile compose a path segment from its own env var, so the token is supplied separately and assembled at call time. `auth query-param` already exists for the query case, so this is the path-shaped sibling of a pattern beaver has. 2. **Redaction.** Mask a `/bot<id>:<secret>` style segment in any URL before it reaches a log line, error string, or trace attribute. **Recommend doing 2 regardless, and 1 as well if it is cheap.** Redaction is the stronger guarantee because it holds even when a future caller reintroduces a credential-bearing base URL by configuration, which is precisely how this arose. Composition alone leaves the same trap open for the next integration whose API puts a secret in the path. A general form is probably better than a Telegram-specific pattern: treat any base-url-derived path segment as sensitive by default, since an operator only puts a secret in a base URL when the upstream API forces it. ## Acceptance A deliberately failed upstream call produces a log line, error string, and trace attribute containing a redacted path segment and no token material. Cover the success path too, so a future change that starts logging request URLs on success does not reopen this. ## Context * Exposure and rotation: `coilyco-flight-deck/infrastructure#708` * Consumer wiring: `services/signoz-telegram/` in `coilyco-bridge/deploy`, `kai-server-externalsecret.yml` and `signoz-telegram.mcp.kdl` * Callers today: SigNoz alert routing on ser8 and kai-server, plus Forgejo CI failure alerts via `/api/create_alert`
Author
Member

Correcting the framing I filed this under. This is hardening, not the cause.

I filed this believing beaver's URL-as-credential shape produced the infrastructure#708 exposure. Checking SigNoz retention afterwards showed otherwise. All 36 retained token-bearing log records over 30 days come from one container:

fleet-reachability / gatus    36

Zero from signoz-telegram. Gatus keeps its own direct Telegram path, independent of this mapper by design, and it is the unit that logged the credential. Beaver has not been observed leaking anything.

What stands unchanged is the shape: this mapper is configured with a base-url that is itself a credential, because Telegram puts its token in the path and base-url takes one env value. That is a live trap even though it has not sprung. A caller who adds request-URL logging on an error path turns it into the same incident.

So this stays open and the acceptance criteria are unchanged, but the priority is defence in depth rather than incident remediation. The actual infrastructure#708 fix is a SigNoz ingest-side redaction pipeline, which covers Gatus and any future unit that logs a credential-bearing URL, including this one if it ever starts.

**Correcting the framing I filed this under. This is hardening, not the cause.** I filed this believing beaver's URL-as-credential shape produced the `infrastructure#708` exposure. Checking SigNoz retention afterwards showed otherwise. All 36 retained token-bearing log records over 30 days come from one container: ``` fleet-reachability / gatus 36 ``` Zero from `signoz-telegram`. Gatus keeps its own direct Telegram path, independent of this mapper by design, and it is the unit that logged the credential. **Beaver has not been observed leaking anything.** What stands unchanged is the shape: this mapper is configured with a `base-url` that is itself a credential, because Telegram puts its token in the path and `base-url` takes one env value. That is a live trap even though it has not sprung. A caller who adds request-URL logging on an error path turns it into the same incident. So this stays open and the acceptance criteria are unchanged, but the priority is defence in depth rather than incident remediation. The actual `infrastructure#708` fix is a SigNoz ingest-side redaction pipeline, which covers Gatus and any future unit that logs a credential-bearing URL, including this one if it ever starts.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/mcp-beaver#93
No description provided.