Cloud
Cloudbeta

Secrets

Give a managed gateway the API tokens, keys, and passwords its config must not carry. Register a value per gateway with mcpg cloud secret set, reference it as ${secret.NAME}, and rotate it with one command — no publish, no restart, and the platform never shows a value back.

A published config is the whole gateway: every backend, every plugin, every credential those need. Most of those credentials must not sit in a file you version and hand around, and a managed gateway cannot read your shell environment the way a gateway on your own laptop can — the pod's environment belongs to the platform, so publishing refuses ${env.…} outright.

A secret is the answer: a named value you register for one gateway, which the platform delivers to that gateway and nothing else, and which the config references by name as ${secret.NAME}. The value is encrypted at rest, never returned by any API, and reaches the gateway as a file it reads at config load — not as an environment variable.

Register a secret, reference it from the config

Secrets are registered per gateway name, in the org, workspace, and environment your context names. Register before or after the first publish — the order does not matter, the publish that references the key is what needs it.

bash
# the value comes from stdin (preferred), from an environment variable,
# or — least preferred — inline, where it lands in your shell history
printf '%s' "$GITHUB_TOKEN" | mcpg cloud secret set edge GITHUB_TOKEN
mcpg cloud secret set edge GITHUB_TOKEN --from-env GITHUB_TOKEN
mcpg cloud secret set edge GITHUB_TOKEN --value ghp_…

mcpg cloud secret list edge            # keys, when, by whom — never a value
mcpg cloud secret unset edge GITHUB_TOKEN

A key is an identifier: letters, digits, and underscores, not starting with a digit, at most 64 characters, case-sensitive — ^[A-Za-z_][A-Za-z0-9_]{0,63}$. A value is at least one byte and at most 64 KiB, and it is stored exactly as sent: the CLI strips the trailing newline a terminal appends to stdin (that is the terminal's, not the secret's) and nothing else.

Reference the key from the config with ${secret.KEY}. It resolves anywhere a string does — a field on its own, or inside a longer string:

yaml
plugins:
  - name: github
    kind: github
    spec:
      token: ${secret.GITHUB_TOKEN}

mcp:
  capabilities:
    tools:
      - name: search
        backend:
          kind: http
          url: https://api.example.com/search
          headers:
            authorization: "Bearer ${secret.SEARCH_API_KEY}"

Then publish as usual. Publishing checks every ${secret.KEY} against the keys registered for that gateway name: a key that is not registered is refused (422, violation unregistered_secret, naming the token) before anything is provisioned, so a typo in the config or a forgotten secret set is caught at publish time — never at the first call.

Route a secret through the config, not through the plugin's own environment. A plugin that would read GITHUB_TOKEN from the process environment on your laptop never sees a registered secret: on a managed gateway the value is a file, and the config is the only thing that reads it. Put the reference where the plugin's config takes the value (spec.token: ${secret.GITHUB_TOKEN}).

Live updates and rotation

On a running gateway, a secret set or secret unset is live within about a minute — no publish, no restart. The platform rewrites the gateway's secret volume, the gateway notices the change and reloads its config: every plugin re-initialises with the new values, and client sessions survive the reload.

Before the gateway's first publish there is nothing to deliver to yet; the value is stored, and the first publish carries it.

Rotating a leaked or expired credential under incident is therefore one command:

bash
printf '%s' "$NEW_TOKEN" | mcpg cloud secret set edge GITHUB_TOKEN
# ✓ edge: GITHUB_TOKEN set — live (the gateway reloads within about a minute)

The gateway reloads with the new value. The config does not change, no new version is recorded, and no pod restarts. A value that a config referenced and that you unset is a config-load error on the next reload — the gateway keeps the previous config live and reports the key name — so unset a key after the config has stopped referencing it.

Each set and unset reports how the change was delivered:

DeliveryMeaning
livePushed to the running gateway; it picks the value up within about a minute.
pending_publishThe gateway is not provisioned yet. The value is stored and the next publish carries it.
failedThe value is stored, but the push to the running gateway was refused; the message carries the reason. Retry the command, or publish the current config to carry it.

And secret list reports one state for the set as a whole. The gateway reports a fingerprint of the secrets it loaded; the platform compares it with the set you registered. A fingerprint crosses, never a value.

StateMeaning
liveThe gateway has loaded the current registered values.
pendingThe gateway has not loaded the current registered values yet — a change made moments ago, or a gateway that has not been published since.
unknownThe platform has no report yet: the gateway is not provisioned, or has not enrolled.

What the platform never does

  • Return a value. No API, no CLI command, no console view, no log line shows a secret once it is set. A listing is keys and bookkeeping. If you need to know what a secret is, you need the source you set it from.
  • Capture a value in a config version. A version records the config, which holds the reference and not the value; a rollback keeps the secrets that are registered now. Rotating a secret is not a config change and records no version.
  • Share a secret between gateways. A secret belongs to one gateway name in one org, workspace, and environment. GITHUB_TOKEN on edge and GITHUB_TOKEN on staging are two secrets, registered twice.
  • Put a secret in the environment. The value is a file the gateway reads at config load, so ${env.…}, env://, and file:// stay refused for every published config, and a plugin's own environment-variable discovery never finds it.
  • Store it in the clear. Values are encrypted at rest under the platform's key, bound to the gateway they were registered for, and every set and unset is audited by key name.

Secrets in CI

A deploy pipeline needs a member credential. A service token (mcpg cloud service-token create, exported as MCPG_CLOUD_TOKEN) works for secret set, secret list, secret unset, and publish alike, so the credential that deploys a config is the one that supplies what the config references.

Two questions look alike and are answered in different places:

  • Is the key well-formed? A key outside ^[A-Za-z_][A-Za-z0-9_]{0,63}$ can never be registered, so a ${secret.…} that names one can never be published. That is a property of the file, and it belongs in a pull-request check that greps the diff.

  • Is the key registered? Only the platform knows. Ask it with a dry run against the live coordinates — it runs every gate a publish runs and reserves nothing:

    bash
    mcpg cloud publish edge --config gw.yaml --dry-run
    # ✓ 'edge' would update in place (2 replica(s), size s, gateway …)
    #   config sha256:   …
    #   secrets:         GITHUB_TOKEN, SEARCH_API_KEY
    #   nothing was reserved or provisioned
    

Never keep a local list of registered keys — in a repository, a wiki, or a pipeline variable. A list drifts from the thing it describes, and a drifted list fails green. The platform is the list; secret list and the dry run read it.

In the console

The gateway page in the control-plane console has a Secrets tab: the registered keys with when and by whom each was last set, and one state badge for the set (live / pending / unknown). Add or replace a secret with a key and a write-only value field — the value is sent once and cleared from the form — and remove one after a confirmation. Values are never shown, in this tab or anywhere else.

Reference

CLIRESTNotes
mcpg cloud secret set <gateway> KEY (stdin | --from-env VAR | --value V)PUT …/gateways/{name}/secrets/{KEY} body {"value": "…"}Set or replace. Reply carries delivery.state: live | pending_publish | failed.
mcpg cloud secret list <gateway>GET …/gateways/{name}/secretsKeys with created_by / updated_at, plus the set state: live | pending | unknown. Never a value.
mcpg cloud secret unset <gateway> KEYDELETE …/gateways/{name}/secrets/{KEY}Remove. Same delivery reply as set; 404 when the key is not registered.
mcpg cloud publish <gateway> --config f --dry-runPOST …/gateways/validatePrints secrets: … — the registered keys the config may reference.

is /v1/orgs/{org}/workspaces/{ws}/environments/{env}. The routes are member-gated like publish. A malformed key is 422 invalid_secret_key, an empty value 422 empty_secret_value, a value over 64 KiB 413 secret_value_too_large.

Where to go next