Skip to content

update_service silently writes literal "[REDACTED]" as real env values on round-trip #4

Description

@mrq1911

Summary

update_service will silently write the literal string "[REDACTED]" as a real environment-variable value if the caller round-trips a spec obtained from get_service/list_services without modification — corrupting every secret-looking var on that service. This is a natural, easy-to-fall-into workflow: fetch the current spec, tweak one field (e.g. add a healthcheck), write it back.

Root cause

  • sanitizeService/sanitizeEnvVars (src/sanitize.ts) redact matching env vars to "[REDACTED]" on every read (get_service, list_services).
  • restoreRedactedValues() exists to undo this — but only operates on compose YAML strings, used exclusively by the stack-update path.
  • update_service's handler (src/tools/services.ts) passes the caller-supplied spec.variables (a structured {name, value}[] array, not YAML) straight to client.updateService(id, spec) with no restoration step at all.
  • update_service_env is safe by a different mechanism — it fetches the live service server-side and only mutates the specific named vars from set/remove, so a caller never needs to round-trip a redacted value through it.

Net effect: update_service is the one write path with zero protection against this, despite being the most natural pairing with get_service for "fetch, modify one field, write back" workflows (e.g. adding a healthcheck, changing deploy.replicas, etc.).

Reproduction

  1. get_service({id}) → returns a spec with variables: [{name: "RPC_URL", value: "[REDACTED]"}, ...].
  2. Add/change an unrelated field (e.g. healthcheck), leave variables as returned.
  3. update_service({id, spec}).
  4. Every redacted var is now literally the string "[REDACTED]" in the running container's environment. In our case this took down a production service (Error: Endpoint should start with 'ws://', received '[REDACTED]').

Suggested fix

Give update_service the same protection as the compose path: before forwarding to client.updateService, diff the incoming spec.variables against a fresh client.getService(id) fetch and restore any var whose value is exactly "[REDACTED]" from the live value (mirroring restoreRedactedValues, but for the structured {name, value}[] shape instead of YAML). Doing this unconditionally (not just when redact !== "none") is safest, since a caller may have gotten the spec from an earlier get_service call in a different redact mode.

Recovery in our case was via rollback_service, which worked — but only because Swarm had a previous good revision to roll back to.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions